@ape-egg/vibe 1.9.0 → 1.9.5

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/llms.txt CHANGED
@@ -9,9 +9,12 @@ Vibe is a lightweight reactive library that uses Proxy-based state and MutationO
9
9
  **Key characteristics:**
10
10
  - Proxy-based reactive state (`window.$`)
11
11
  - Deep reactivity (nested mutations automatically trigger updates)
12
- - Surgical DOM updates (only affected elements re-render)
12
+ - Surgical DOM updates (only affected elements re-render; idempotent writes skip no-op DOM mutations)
13
+ - State batching (multiple `$.x = y` writes in the same microtask flush as one update)
13
14
  - MutationObserver for dynamic element tracking
14
- - Works with vanilla HTML - no special file format
15
+ - Works with vanilla HTML no special file format
16
+ - Component-local state via `component({...})` + `this.X`
17
+ - Component composition via `<component src>` with reactive props (objects/arrays/primitives) and slots
15
18
 
16
19
  ## Installation
17
20
 
@@ -23,19 +26,21 @@ npm install @ape-egg/vibe
23
26
 
24
27
  ```html
25
28
  <script type="module">
26
- import state from "@ape-egg/vibe";
27
- window.$ = state({ name: "World", count: 0 });
29
+ import vibe from "@ape-egg/vibe";
30
+ window.$ = vibe({ name: "World", count: 0 });
28
31
  </script>
29
32
 
30
- <h1>Hello, @[name]!</h1>
31
- <button onclick="$.count++">Clicked @[count] times</button>
33
+ <body vibe-fouc>
34
+ <h1>Hello, @[name]!</h1>
35
+ <button onclick="$.count++">Clicked @[count] times</button>
36
+ </body>
32
37
  ```
33
38
 
34
39
  ## Core Syntax
35
40
 
36
41
  ### Reactive Bindings
37
42
 
38
- Use `@[property]` syntax anywhere in HTML or CSS:
43
+ Use `@[expression]` syntax anywhere in HTML or CSS:
39
44
 
40
45
  ```html
41
46
  <!-- Text content -->
@@ -44,11 +49,15 @@ Use `@[property]` syntax anywhere in HTML or CSS:
44
49
  <!-- Expressions -->
45
50
  <div>@[firstName + ' ' + lastName]</div>
46
51
  <div>@[count * 2]</div>
52
+ <div>@[items.filter(x => x.active).length]</div>
47
53
 
48
54
  <!-- Attributes -->
49
55
  <input value="@[inputValue]">
50
56
  <button disabled="@[isLoading]">Submit</button>
51
57
 
58
+ <!-- Attribute names (name bindings) -->
59
+ <icon @[iconName]></icon>
60
+
52
61
  <!-- CSS -->
53
62
  <style>
54
63
  .box { background: @[themeColor]; }
@@ -65,7 +74,7 @@ State is accessed globally via `window.$`:
65
74
  // Read
66
75
  console.log($.firstName);
67
76
 
68
- // Write (triggers re-render)
77
+ // Write (triggers re-render — batched per microtask)
69
78
  $.firstName = "John";
70
79
 
71
80
  // Increment
@@ -97,9 +106,17 @@ With index:
97
106
  <!-- /each -->
98
107
  ```
99
108
 
109
+ The expression after `each` is any JavaScript that returns an array or array-like:
110
+
111
+ ```html
112
+ <!-- each items.filter(x => x.active) as item -->
113
+ <!-- each Array.from({length: 10}, (_, i) => i) as n -->
114
+ <!-- each Object.values(users) as user, i -->
115
+ ```
116
+
100
117
  ### Nested Iteration
101
118
 
102
- Use dot paths for nested arrays:
119
+ Use dot paths or any expression:
103
120
 
104
121
  ```html
105
122
  <!-- each categories as category -->
@@ -122,71 +139,128 @@ Use dot paths for nested arrays:
122
139
 
123
140
  Conditionals can be nested inside iterations and vice versa.
124
141
 
125
- ## Special Attributes
142
+ ## Components
143
+
144
+ ```html
145
+ <component src="/components/card.html" title="@[pageTitle]" theme="dark">
146
+ <p>This content replaces &lt;slot&gt;&lt;/slot&gt;</p>
147
+ </component>
148
+ ```
126
149
 
127
- ### Dehydrate
150
+ **Behavior:**
151
+ 1. Vibe finds `<component src="...">` elements (also `<div class="component" src="...">`)
152
+ 2. Fetches the source HTML
153
+ 3. Substitutes props (`@[title]` in the template becomes `@[pageTitle]`)
154
+ 4. Inserts slot content (between the tags) into `<slot></slot>`
155
+ 5. Removes `src`; the `<component>` wrapper remains as a boundary marker
128
156
 
129
- Skip reactive processing for an element and its children:
157
+ **Reactive props** (`title="@[pageTitle]"`) keep tracking the parent's state. **Static props** (`theme="dark"`) inline as literals. **Object/array props** through iteration scope (`<component card="@[card]">` inside `<!-- each cards as card -->`) are stashed in an internal registry so child templates can `@[card.name]` or `<!-- each card.abilities as a -->`.
158
+
159
+ **Two-way binding** falls out for free: a child component that writes `$.value = x` in an event handler has that `$.value` rewritten to the parent's bound path, so the parent's state actually updates.
160
+
161
+ **Slot content** that itself contains `<component src>` is also processed recursively.
162
+
163
+ ## Component-Local State
164
+
165
+ For state scoped to a single component (form drafts, toggles, derived view-state) instead of global `$`:
130
166
 
131
167
  ```html
132
- <code vibe-dehydrate>@[this] displays literally, not parsed</code>
168
+ <!-- /components/Counter.html -->
169
+ <script type="module">
170
+ import component from '@ape-egg/vibe/component';
171
+ component({ count: 0, increment() { this.count++; } });
172
+ </script>
173
+
174
+ <button onclick="this.increment()">Clicked @[this.count] times</button>
133
175
  ```
134
176
 
135
- Use cases:
136
- - Displaying `@[...]` syntax in documentation
137
- - Static content that shouldn't be reactive
138
- - Performance optimization for large static sections
177
+ How it works:
139
178
 
140
- ### Boolean Attributes
179
+ 1. `component({...})` generates a unique id (e.g. `_c0`) and stores state at `$[id]`
180
+ 2. The `<script>` and every following sibling get `data-vibe-component-id="<id>"`
181
+ 3. Inside that subtree, `@[this.X.Y]` is rewritten to `@[_c0.X.Y]`; `onclick="this.fn()"` becomes `onclick="$['_c0'].fn()"`; `$.this.x = v` (in event handlers) becomes `$['_c0'].x = v`
182
+ 4. When the component leaves the DOM, its bucket is freed automatically
141
183
 
142
- Attributes not in the value whitelist are removed when falsy:
184
+ Multi-segment paths, conditionals (`<!-- if this.editing -->`), and iterations (`<!-- each this.items as item -->`) all work. Global `$` and component-local `this.X` coexist within the same template.
185
+
186
+ ## Drop-In Components (Standalone Usage)
187
+
188
+ `component()` auto-boots the runtime, so a `<component>` block is a fully self-contained reactive unit. Drop it into any HTML page — no top-level `vibe(...)` call, no build step, no global state setup. Import directly from a CDN:
143
189
 
144
190
  ```html
145
- <button disabled="@[isLoading]">Submit</button>
146
- <!-- When isLoading is false, disabled attribute is removed entirely -->
191
+ <component>
192
+ <script type="module">
193
+ import component from 'https://esm.sh/@ape-egg/vibe/component.js';
194
+ component({ count: 0 });
195
+ </script>
196
+ <button onclick="this.count--">-</button>
197
+ <span>Count: @[this.count]</span>
198
+ <button onclick="this.count++">+</button>
199
+ </component>
147
200
  ```
148
201
 
149
- ## Events
202
+ Mechanism:
203
+ 1. `component({...})` claims the nearest unprocessed `<component>` (or `<div class="component">`) wrapper
204
+ 2. It calls `ensureBoot()` from `@ape-egg/vibe/boot`, which initializes `window.$`, parses the DOM, hydrates bindings, and starts the MutationObserver — exactly once per page, even if multiple drop-in blocks call `component()`
205
+ 3. `@[this.X]`, `onclick="this.fn()"`, `<!-- if this.X -->`, etc. resolve against that block's bucket
206
+
207
+ Multiple drop-in blocks on the same page each get their own state and run independently. They can still read each other's state via global `$['<id>']` if coordination is needed.
208
+
209
+ This makes vibe usable as a "sprinkle of reactivity" library: paste a snippet into a static site, blog post, CMS-rendered page, or third-party HTML, and it just works.
210
+
211
+ ## Lifecycle Hooks
212
+
213
+ ```javascript
214
+ $.on('ready', () => {}); // once, after initial parse + hydrate + components mounted
215
+ $.on('afterUpdate', (cur, prev) => {}); // every state change (batched per microtask)
216
+ $.on('afterDomMutation', () => {}); // after every MutationObserver batch
217
+ ```
218
+
219
+ Also: `await $.ready` resolves after boot, useful when calling code captured `window.$` before vibe finished initializing.
150
220
 
151
- Use standard inline event handlers:
221
+ ## Special Attributes
222
+
223
+ ### vibe-fouc
224
+
225
+ Add to an element (typically `<body>`) to hide it until Vibe's first hydration completes. Vibe removes the attribute when ready.
152
226
 
153
227
  ```html
154
- <button onclick="$.count++">Increment</button>
155
- <input oninput="$.text = this.value">
156
- <form onsubmit="event.preventDefault(); handleSubmit()">
228
+ <body vibe-fouc>
157
229
  ```
158
230
 
159
- ## Styling
231
+ Pair with `vibe.css` (`<link rel="stylesheet" href="@ape-egg/vibe/vibe.css">`) which targets `[vibe-fouc]` with `visibility: hidden`.
160
232
 
161
- ### CSS Bindings
233
+ ### vibe-dehydrate
162
234
 
163
- Reactive values work inside `<style>` tags:
235
+ Skip reactive processing for an element and its children:
164
236
 
165
237
  ```html
166
- <style>
167
- .box {
168
- background: @[backgroundColor];
169
- color: @[textColor];
170
- width: @[width]px;
171
- }
172
- </style>
238
+ <code vibe-dehydrate>@[this] displays literally, not parsed</code>
173
239
  ```
174
240
 
175
- ### Preventing FOUC
241
+ Use cases: documenting `@[...]` syntax, large static sections, content that must not be evaluated.
242
+
243
+ ### Boolean Attributes
176
244
 
177
- Hide content until hydration completes:
245
+ Pure-binding attributes that aren't in the value whitelist (e.g. `disabled`, `checked`, `active`) are added when truthy and removed when falsy:
178
246
 
179
247
  ```html
180
- <body style="visibility: hidden;">
248
+ <button disabled="@[isLoading]">Submit</button>
249
+ <!-- isLoading=false → attribute removed entirely -->
181
250
  ```
182
251
 
183
- Or use the included CSS:
252
+ ## Events
253
+
254
+ Use standard inline event handlers — they execute against `$` directly:
184
255
 
185
256
  ```html
186
- <link rel="stylesheet" href="@ape-egg/vibe/vibe.css">
187
- <body vibe-fouc>
257
+ <button onclick="$.count++">Increment</button>
258
+ <input oninput="$.text = this.value">
259
+ <form onsubmit="event.preventDefault(); handleSubmit()">
188
260
  ```
189
261
 
262
+ Inside a component, `this.X` references the component's bucket (rewritten at parse time); `this.value`, `this.checked`, `this.dataset` etc. still reach the DOM element since DOM properties are preserved.
263
+
190
264
  ## Dynamic Elements
191
265
 
192
266
  Elements added via JavaScript are automatically hydrated through MutationObserver:
@@ -200,33 +274,47 @@ document.body.appendChild(div);
200
274
 
201
275
  ## API Reference
202
276
 
203
- ### `state(initialState, afterUpdate?)`
277
+ ### `vibe(initialState, config?)`
204
278
 
205
- Creates reactive state and initializes the framework.
279
+ Initializes the framework and returns the reactive state proxy.
206
280
 
207
281
  ```javascript
208
- import state from "@ape-egg/vibe";
282
+ import vibe from "@ape-egg/vibe";
209
283
 
210
- window.$ = state(
284
+ window.$ = vibe(
211
285
  { count: 0, user: { name: "Alice" } },
212
- (newState, oldState) => {
213
- console.log("State updated:", newState);
214
- }
286
+ { debug: false }
215
287
  );
216
288
  ```
217
289
 
218
290
  **Parameters:**
219
- - `initialState` - Object containing initial state values
220
- - `afterUpdate` - Optional callback after each state change (receives read-only snapshots)
291
+ - `initialState` object containing initial state values
292
+ - `config` optional object. Currently supports `{ debug: boolean }`. A third positional argument can pass a target selector (defaults to `body`).
293
+
294
+ **Returns:** Reactive proxy. Assign it to `window.$` so inline event handlers and bindings can find it.
295
+
296
+ ### `component(initialState)`
297
+
298
+ Imported from `@ape-egg/vibe/component`. Registers a component-local state bucket; called from inside a `<script type="module">` in a component file. See [Component-Local State](#component-local-state).
221
299
 
222
- **Returns:** Proxy object for reactive state access
300
+ ### `$.on(event, callback)`
223
301
 
224
- ## Scoped Variables
302
+ Register a lifecycle listener. Events: `'ready'`, `'afterUpdate'`, `'afterDomMutation'`. Listeners registered before boot are queued and replayed once vibe is ready.
303
+
304
+ ### `$.ready`
305
+
306
+ A Promise that resolves once vibe has finished initial parse + hydrate + component loading.
307
+
308
+ ### `$.reconcile(el, html)` and `$.renderComponent(rawHtml, props, slot, opts)` (advanced)
309
+
310
+ Subtree reconciliation primitives used by `@ape-egg/vite-plugin-vibe` for surgical HMR. `$.reconcile` diffs `el`'s children against fresh `html` while preserving DOM identity, focus, and selection; vibe-managed regions (iterations, conditionals, components, slot pairs) are treated as opaque. Treat these as plumbing — shape may evolve.
311
+
312
+ ## Scoped Variables in Iterations
225
313
 
226
314
  Inside `<!-- each -->` blocks, these variables are available:
227
- - `item` (or custom name) - current array element
228
- - `index` (or custom name) - current index
229
- - Parent state remains accessible via `$`
315
+ - `item` (or custom name) current array element
316
+ - `index` (or custom name) current index
317
+ - Parent state remains accessible via `$` and via free identifiers (e.g. `tutorialProgress`)
230
318
 
231
319
  ```html
232
320
  <!-- each users as user, i -->
@@ -236,7 +324,7 @@ Inside `<!-- each -->` blocks, these variables are available:
236
324
 
237
325
  ### Scoped State (Internal)
238
326
 
239
- Each iteration instance stores a `scopedState` - a Proxy wrapper that provides access to both local variables (`item`, `index`) and global state. This enables reactive updates inside iterations:
327
+ Each iteration instance stores a `scopedState` Proxy that combines local variables (`item`, `index`) with global state. This lets bindings inside iterations react to global state changes:
240
328
 
241
329
  ```html
242
330
  <!-- each menuItems as item -->
@@ -246,50 +334,50 @@ Each iteration instance stores a `scopedState` - a Proxy wrapper that provides a
246
334
  <!-- /each -->
247
335
  ```
248
336
 
249
- When `tutorialProgress` changes, Vibe:
250
- 1. Finds affected elements inside iteration instances
251
- 2. Uses that instance's `scopedState` for evaluation
252
- 3. Can access both `item` (local) and `tutorialProgress` (global)
253
- 4. Updates only the affected elements without re-rendering the entire iteration
337
+ When `tutorialProgress` changes, vibe finds affected elements inside iteration instances, evaluates against that instance's `scopedState`, and updates only the changed elements without re-rendering the whole list.
254
338
 
255
339
  ## Architecture
256
340
 
257
- Vibe consists of these core modules:
341
+ Vibe consists of these core runtime modules (in `runtime/`):
258
342
 
259
- - **state.js** - Proxy-based reactive state container with recursive proxies for deep reactivity
260
- - **parse.js** - DOM parser that finds `@[...]` bindings
261
- - **link.js** - Maps elements to parsed tree nodes
262
- - **hydrate.js** - Updates DOM with current state values (supports scoped state for iterations)
263
- - **affected.js** - Determines which elements need updating (walks iteration instances with scoped state)
264
- - **iterate.js** - Array rendering with efficient diffing (stores scoped state per instance)
265
- - **conditionals.js** - Conditional block rendering
343
+ - **state.js** Proxy-based reactive state, recursive proxies for deep reactivity, batched flush via `queueMicrotask`
344
+ - **parse.js** DOM parser that finds `@[...]` bindings, iteration/conditional comments, and `<component src>` elements
345
+ - **manifest.js** Maps DOM elements to parsed-tree nodes (was `link.js` in older versions)
346
+ - **hydrate.js** Updates DOM with current state values; idempotent (skips no-op writes)
347
+ - **affected.js** Determines which bindings need re-evaluation when state changes
348
+ - **iterate.js** Array rendering with diffing; bulk-replacement fast path; iteration-prop registry for non-primitive component props
349
+ - **conditionals.js** Conditional block rendering; preserves both branches in the manifest for reactive switching
350
+ - **component.js** — `<component src>` fetching, prop substitution, slot inlining, component-local state setup, `this.X` rewriting
351
+ - **reconcile.js** — Subtree reconciliation primitive (`$.reconcile`)
352
+ - **cleanup.js** — `vibe-fouc` release once a subtree is fully processed
266
353
 
267
354
  ## How It Works
268
355
 
269
356
  ```
270
- 1. state() initializes the Proxy and framework
271
- 2. parse.js scans DOM for @[...], <!-- each -->, <!-- if -->
272
- 3. link.js maps elements to the parsed tree
357
+ 1. vibe(initialState) creates the Proxy and starts the runtime
358
+ 2. parse.js scans DOM for @[...], <!-- each -->, <!-- if -->, <component src>
359
+ 3. manifest.js maps elements parsed tree
273
360
  4. hydrate.js replaces bindings with values
274
361
  5. iterate.js renders <!-- each --> loops
275
362
  6. conditionals.js renders <!-- if --> blocks
276
- 7. MutationObserver watches for new elements
277
- 8. On state change: affected.js finds changed elements → hydrate.js updates them
363
+ 7. component.js fetches <component src> templates, substitutes props, inlines slots
364
+ 8. MutationObserver watches for new/removed elements; affected.js + hydrate.js handle state changes
278
365
  ```
279
366
 
280
367
  ## Current Limitations
281
368
 
282
- - **No computed values**: Derived state must be calculated manually
283
- - **No two-way binding sugar**: Must wire input events manually
284
- - **Expression security**: `new Function()` evaluation - don't bind untrusted input
369
+ - **No computed values primitive**: derived state can be done with `Object.defineProperty($, 'x', { get })` or with `afterUpdate` listeners; a first-class `computed` API isn't shipped
370
+ - **Expression security**: `new Function()` evaluation don't bind untrusted input
371
+ - **HTML binding**: `@[expr]` always sets `textContent` (escape-safe). A first-class opt-in for trusted HTML is planned but not finalized
285
372
 
286
373
  ## Best Practices
287
374
 
288
- 1. **Initialize state before DOM**: Place `<script>` in `<head>` or before reactive elements
289
- 2. **Use dehydrate for docs**: When showing `@[...]` syntax examples
290
- 3. **Prevent FOUC**: Use `visibility: hidden` on body until hydration
291
- 4. **Keep expressions simple**: Complex logic belongs in JavaScript, not templates
292
- 5. **Mutate freely**: Deep reactivity means `$.user.name = "New"` just works - no spread operators needed
375
+ 1. **Initialize state in `<head>`** before reactive elements; pair with `vibe-fouc` on `<body>`
376
+ 2. **Mutate freely** deep reactivity means `$.user.name = "New"` just works; no spread operators needed
377
+ 3. **Use lifecycle hooks instead of `setTimeout` / `requestAnimationFrame`** when you need to wait for vibe to finish work
378
+ 4. **Use `component({...})` for state that's local to one piece of UI** (form draft, toggle); keep cross-cutting state on global `$`
379
+ 5. **Use `vibe-dehydrate` when you need to display `@[...]` literally** (docs, code examples)
380
+ 6. **Keep expressions simple** — complex logic belongs in JavaScript; templates should read like HTML
293
381
 
294
382
  ## Browser Support
295
383
 
@@ -298,6 +386,10 @@ Modern browsers with:
298
386
  - MutationObserver
299
387
  - ES Modules
300
388
 
389
+ ## Related Packages
390
+
391
+ - **`@ape-egg/vite-plugin-vibe`** — Vite plugin with surgical component HMR (template-only edits reconcile in place; script edits re-mount)
392
+
301
393
  ## Resources
302
394
 
303
395
  - **Homepage**: https://vibe.korte.kim
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@ape-egg/vibe",
3
- "version": "1.9.0",
3
+ "version": "1.9.5",
4
4
  "type": "module",
5
5
  "description": "Runtime-first reactivity with optional compiler",
6
6
  "main": "index.js",
7
7
  "homepage": "https://vibe.korte.kim",
8
8
  "exports": {
9
9
  ".": "./index.js",
10
+ "./boot": "./boot.js",
10
11
  "./component": "./component.js",
11
12
  "./runtime": "./runtime/index.js",
12
13
  "./compiler": "./compiler/bin/vibe-compile.js"