···11+# Pin Scoping
22+33+Element references via `data-volt-pin` require a scoping strategy to avoid name collisions and provide predictable access patterns.
44+This document explores three approaches that were considered
55+66+## Current Implementation: [data-volt] Root Scoping
77+88+**How it works:**
99+1010+- Each `[data-volt]` root has its own pin registry
1111+- Pins are isolated to their scope
1212+- `$pins.name` accesses pins within the current root
1313+1414+**Example:**
1515+1616+```html
1717+<div data-volt>
1818+ <input data-volt-pin="username" />
1919+ <button data-volt-on-click="$pins.username.focus()">Focus</button>
2020+</div>
2121+2222+<div data-volt>
2323+ <input data-volt-pin="username" /> <!-- Different registry -->
2424+ <button data-volt-on-click="$pins.username.focus()">Focus</button>
2525+</div>
2626+```
2727+2828+**Pros:**
2929+3030+- Predictable: Each root is isolated
3131+- No name collision risk across roots
3232+- Aligns with current scope model (one scope per root)
3333+- Simple to implement and reason about
3434+3535+**Cons:**
3636+3737+- Can't share pins across roots (must use global state instead)
3838+- No sub-scoping within a root for component-like patterns
3939+4040+**Use Cases:**
4141+4242+- Simple applications with clear root boundaries
4343+- When each `[data-volt]` represents a distinct feature/component
4444+4545+---
4646+4747+## Alternative 1: Explicit Scope Boundaries (data-volt-scope)
4848+4949+**How it works:**
5050+5151+- Introduce `data-volt-scope` attribute to create nested scopes
5252+- Pins registered within a scope are isolated to that scope and its descendants
5353+- `$pins` searches up the scope chain
5454+5555+**Example:**
5656+5757+```html
5858+<div data-volt>
5959+ <div data-volt-scope="form1">
6060+ <input data-volt-pin="username" />
6161+ <button data-volt-on-click="$pins.username.focus()">Focus</button>
6262+ </div>
6363+6464+ <div data-volt-scope="form2">
6565+ <input data-volt-pin="username" /> <!-- Different scope -->
6666+ <button data-volt-on-click="$pins.username.focus()">Focus</button>
6767+ </div>
6868+</div>
6969+```
7070+7171+**Pros:**
7272+7373+- Fine-grained control over scope boundaries
7474+- Supports nested component patterns
7575+- Can isolate widgets within a larger root
7676+7777+**Cons:**
7878+7979+- More complex: requires scope hierarchy tracking
8080+- Additional attribute to learn
8181+- Lookup complexity (walking scope chain)
8282+- Breaks current 1:1 scope-to-root model
8383+8484+**Use Cases:**
8585+8686+- Large applications with reusable sub-components
8787+- When you need multiple isolated widgets within one root
8888+- Form libraries with nested fieldsets
8989+9090+**Implementation Complexity:**
9191+9292+- Requires scope hierarchy (parent references)
9393+- WeakMap must track scope chains
9494+- Pin lookup becomes recursive
9595+9696+## Alternative 2: Global Document-Wide Registry
9797+9898+**How it works:**
9999+100100+- Single global pin registry for entire document
101101+- All pins accessible from any scope
102102+- Names must be unique across the entire page
103103+104104+**Example:**
105105+106106+```html
107107+<div data-volt>
108108+ <input data-volt-pin="username" />
109109+</div>
110110+111111+<div data-volt>
112112+ <!-- Can access pins from other roots -->
113113+ <button data-volt-on-click="$pins.username.focus()">Focus</button>
114114+</div>
115115+```
116116+117117+**Pros:**
118118+119119+- Simplest to understand: flat namespace
120120+- Easy to share element references across roots
121121+- No scoping complexity
122122+123123+**Cons:**
124124+125125+- High risk of name collisions
126126+- No isolation between roots (breaks encapsulation)
127127+- Debugging becomes harder (where is this pin defined?)
128128+- Not composable (can't have two instances of same component)
129129+130130+**Use Cases:**
131131+132132+- Prototypes and simple pages
133133+- Single-page applications with unique IDs everywhere
134134+- When cross-root communication is primary goal
135135+136136+**Implementation Complexity:**
137137+138138+- Simple: single Map instead of per-scope maps
139139+- No WeakMap needed
140140+141141+## Comparison Table
142142+143143+| Aspect | Root Scoping (Current) | Explicit Scopes | Global |
144144+|--------|------------------------|-----------------|--------|
145145+| Isolation | Per root | Per scope boundary | None |
146146+| Name Collisions | Safe within root | Safe within scope | High risk |
147147+| Complexity | Low | Medium | Very Low |
148148+| Cross-root Access | Not supported | Not supported | Supported |
149149+| Nested Components | Not supported | Supported | Not needed |
150150+| Implementation | Simple | Complex | Trivial |
151151+| Composability | Good | Excellent | Poor |
152152+153153+## Decision Rationale
154154+155155+**Current implementation uses Root Scoping** for the following reasons:
156156+157157+1. **Aligns with existing architecture**: VoltX already uses one scope per `[data-volt]` root
158158+2. **Simplicity**: No additional concepts or attributes to learn
159159+3. **Good enough**: Most use cases don't require nested scopes
160160+4. **Future extensibility**: Can add `data-volt-scope` later if needed (additive change)
161161+162162+**When to reconsider:**
163163+164164+- If users frequently request nested component isolation
165165+- If framework adds first-class component system
166166+- If cross-root pin access becomes a common need (could add `data-volt-pin-global`)
167167+168168+## Migration Path
169169+170170+If we later adopt Alternative 1 (Explicit Scopes):
171171+172172+1. Keep current behavior as default
173173+2. Add `data-volt-scope` for opt-in nested scopes
174174+3. Update metadata to track parent scopes
175175+4. Modify `getPin()` to walk scope chain
176176+177177+This would be backward compatible since existing code without `data-volt-scope` would continue to work.
+527
docs/global-state.md
···11+# Global State
22+33+VoltX provides built-in global state management through special variables and the global store. These features enable sharing state across components, accessing metadata, and coordinating behavior without external dependencies.
44+55+## Overview
66+77+Every Volt scope automatically receives special variables (prefixed with `$`) that provide access to:
88+99+- **Global Store** - Shared reactive state across all scopes
1010+- **Scope Metadata** - Information about the current reactive context
1111+- **Element References** - Access to pinned DOM elements
1212+- **Utility Functions** - Helper functions for common tasks
1313+1414+## Special Variables
1515+1616+### `$store`
1717+1818+Access globally shared reactive state across all Volt roots.
1919+2020+**Declarative API:**
2121+2222+```html
2323+<!-- Define global store -->
2424+<script type="application/json" data-volt-store>
2525+{
2626+ "theme": "dark",
2727+ "user": { "name": "Alice" }
2828+}
2929+</script>
3030+3131+<!-- Use in any Volt root -->
3232+<div data-volt>
3333+ <p data-volt-text="$store.get('theme')"></p>
3434+ <button data-volt-on-click="$store.set('theme', 'light')">Toggle</button>
3535+</div>
3636+```
3737+3838+**Programmatic API:**
3939+4040+```typescript
4141+import { registerStore, getStore } from 'voltx.js';
4242+4343+// Register store with signals or raw values
4444+registerStore({
4545+ theme: signal('dark'),
4646+ count: 0 // Auto-wrapped in signal
4747+});
4848+4949+// Access store
5050+const store = getStore();
5151+store.set('count', 5);
5252+console.log(store.get('count')); // 5
5353+```
5454+5555+**Methods:**
5656+5757+- `$store.get(key)` - Get signal value
5858+- `$store.set(key, value)` - Update signal value
5959+- `$store.has(key)` - Check if key exists
6060+- `$store[key]` - Direct signal access
6161+6262+### `$origin`
6363+6464+Reference to the root element of the current reactive scope.
6565+6666+```html
6767+<div data-volt id="app-root">
6868+ <p data-volt-text="'Root ID: ' + $origin.id"></p>
6969+ <!-- Displays: "Root ID: app-root" -->
7070+</div>
7171+```
7272+7373+### `$scope`
7474+7575+Direct access to the raw scope object containing all signals and context.
7676+7777+```html
7878+<div data-volt data-volt-state='{"count": 0}'>
7979+ <p data-volt-text="Object.keys($scope).length"></p>
8080+ <!-- Shows number of scope properties -->
8181+</div>
8282+```
8383+8484+### `$pins`
8585+8686+Access DOM elements registered with `data-volt-pin`.
8787+8888+```html
8989+<div data-volt>
9090+ <input data-volt-pin="username" />
9191+ <input data-volt-pin="password" type="password" />
9292+9393+ <button data-volt-on-click="$pins.username.focus()">
9494+ Focus Username
9595+ </button>
9696+9797+ <button data-volt-on-click="$pins.password.value = ''">
9898+ Clear Password
9999+ </button>
100100+</div>
101101+```
102102+103103+**Notes:**
104104+105105+- Pins are scoped to their root element
106106+- Each root maintains its own pin registry
107107+- Pins are accessible immediately after registration
108108+109109+### `$pulse(callback)`
110110+111111+Defers callback execution to the next microtask, ensuring DOM updates have completed.
112112+113113+```html
114114+<div data-volt data-volt-state='{"count": 0}'>
115115+ <button data-volt-on-click="count.set(count.get() + 1); $pulse(() => console.log('Updated!'))">
116116+ Increment
117117+ </button>
118118+</div>
119119+```
120120+121121+**Use Cases:**
122122+123123+- Run code after DOM updates
124124+- Coordinate async operations
125125+- Batch multiple updates
126126+127127+### `$uid(prefix?)`
128128+129129+Generates unique, deterministic IDs within the scope.
130130+131131+```html
132132+<div data-volt>
133133+ <input data-volt-bind:id="$uid('field')" />
134134+ <!-- id="volt-field-1" -->
135135+136136+ <input data-volt-bind:id="$uid('field')" />
137137+ <!-- id="volt-field-2" -->
138138+139139+ <input data-volt-bind:id="$uid()" />
140140+ <!-- id="volt-3" -->
141141+</div>
142142+```
143143+144144+**Notes:**
145145+146146+- IDs are unique within the scope
147147+- Counter increments on each call
148148+- Different scopes have independent counters
149149+150150+### `$arc(eventName, detail?)`
151151+152152+Dispatches a CustomEvent from the current element.
153153+154154+```html
155155+<div data-volt data-volt-on-user:save="console.log('Saved:', $event.detail)">
156156+ <button data-volt-on-click="$arc('user:save', { id: 123, name: 'Alice' })">
157157+ Save User
158158+ </button>
159159+</div>
160160+```
161161+162162+**Event Properties:**
163163+164164+- `bubbles: true` - Event bubbles up the DOM
165165+- `composed: true` - Crosses shadow DOM boundaries
166166+- `cancelable: true` - Can be prevented
167167+- `detail` - Custom data payload
168168+169169+### `$probe(expression, callback)`
170170+171171+Observes a reactive expression and calls a callback when dependencies change.
172172+173173+```html
174174+<div data-volt data-volt-state='{"count": 0}' data-volt-init="$probe('count', v => console.log('Count:', v))">
175175+ <button data-volt-on-click="count.set(count.get() + 1)">Increment</button>
176176+ <!-- Logs: "Count: 0" immediately, then "Count: 1", "Count: 2", etc. -->
177177+</div>
178178+```
179179+180180+**Parameters:**
181181+182182+- `expression` (string) - Reactive expression to observe
183183+- `callback` (function) - Called with expression value on changes
184184+185185+**Returns:**
186186+187187+- Cleanup function to stop observing
188188+189189+**Example:**
190190+191191+```html
192192+<div data-volt
193193+ data-volt-state='{"x": 0, "y": 0}'
194194+ data-volt-init="const cleanup = $probe('x + y', sum => console.log('Sum:', sum))">
195195+196196+ <button data-volt-on-click="x.set(x.get() + 1)">+X</button>
197197+ <button data-volt-on-click="y.set(y.get() + 1)">+Y</button>
198198+199199+ <!-- Logs: "Sum: 0" initially, then on every change -->
200200+</div>
201201+```
202202+203203+## `data-volt-init`
204204+205205+Run initialization code once when an element is mounted.
206206+207207+**Basic Usage:**
208208+209209+```html
210210+<div data-volt
211211+ data-volt-state='{"initialized": false}'
212212+ data-volt-init="initialized.set(true)">
213213+214214+ <p data-volt-text="initialized"></p>
215215+ <!-- Displays: true -->
216216+</div>
217217+```
218218+219219+**Setting Up Observers:**
220220+221221+```html
222222+<div data-volt
223223+ data-volt-state='{"count": 0, "log": []}'
224224+ data-volt-init="$probe('count', v => log.push(v))">
225225+226226+ <button data-volt-on-click="count.set(count.get() + 1)">Increment</button>
227227+ <p data-volt-text="log.join(', ')"></p>
228228+ <!-- Displays: "0, 1, 2, ..." -->
229229+</div>
230230+```
231231+232232+**Accessing Special Variables:**
233233+234234+```html
235235+<div data-volt
236236+ id="main"
237237+ data-volt-state='{"rootId": ""}'
238238+ data-volt-init="rootId.set($origin.id)">
239239+240240+ <p data-volt-text="rootId"></p>
241241+ <!-- Displays: "main" -->
242242+</div>
243243+```
244244+245245+## Global Store Patterns
246246+247247+### Shared Application State
248248+249249+```html
250250+<!-- Define global state once -->
251251+<script type="application/json" data-volt-store>
252252+{
253253+ "theme": "light",
254254+ "user": null,
255255+ "authenticated": false
256256+}
257257+</script>
258258+259259+<!-- Header component -->
260260+<div data-volt>
261261+ <div data-volt-class="$store.get('theme')">
262262+ <button data-volt-on-click="$store.set('theme', $store.get('theme') === 'light' ? 'dark' : 'light')">
263263+ Toggle Theme
264264+ </button>
265265+ </div>
266266+</div>
267267+268268+<!-- User profile -->
269269+<div data-volt>
270270+ <div data-volt-if="$store.get('authenticated')">
271271+ <p data-volt-text="'Welcome, ' + $store.get('user').name"></p>
272272+ </div>
273273+</div>
274274+```
275275+276276+### Cross-Component Communication
277277+278278+```html
279279+<script type="application/json" data-volt-store>
280280+{
281281+ "selectedId": null,
282282+ "items": []
283283+}
284284+</script>
285285+286286+<!-- Item list -->
287287+<div data-volt>
288288+ <div data-volt-for="item in $store.get('items')">
289289+ <button data-volt-on-click="$store.set('selectedId', item.id)" data-volt-text="item.name"></button>
290290+ </div>
291291+</div>
292292+293293+<!-- Item details -->
294294+<div data-volt>
295295+ <div data-volt-if="$store.get('selectedId')">
296296+ <p data-volt-text="'Selected: ' + $store.get('selectedId')"></p>
297297+ </div>
298298+</div>
299299+```
300300+301301+### Persistent Global State
302302+303303+```typescript
304304+import { registerStore, getStore } from 'voltx.js';
305305+import { registerPlugin, persistPlugin } from 'voltx.js';
306306+307307+// Register persist plugin
308308+registerPlugin('persist', persistPlugin);
309309+310310+// Initialize store with persisted values
311311+const saved = localStorage.getItem('app-store');
312312+const initialState = saved ? JSON.parse(saved) : { theme: 'light', user: null };
313313+314314+registerStore(initialState);
315315+316316+// Save on changes
317317+const store = getStore();
318318+const originalSet = store.set.bind(store);
319319+store.set = (key, value) => {
320320+ originalSet(key, value);
321321+ localStorage.setItem('app-store', JSON.stringify({
322322+ theme: store.get('theme'),
323323+ user: store.get('user')
324324+ }));
325325+};
326326+```
327327+328328+## Best Practices
329329+330330+### Use `$store` for Shared State
331331+332332+Global state should live in `$store`:
333333+334334+```html
335335+<!-- Good: Shared theme in store -->
336336+<script type="application/json" data-volt-store>
337337+{ "theme": "dark" }
338338+</script>
339339+340340+<div data-volt>
341341+ <p data-volt-class="$store.get('theme')">Content</p>
342342+</div>
343343+```
344344+345345+### Use `$pins` for Element Access
346346+347347+Access DOM elements through pins instead of `querySelector`:
348348+349349+```html
350350+<!-- Good: Using pins -->
351351+<div data-volt>
352352+ <input data-volt-pin="username" />
353353+ <button data-volt-on-click="$pins.username.focus()">Focus</button>
354354+</div>
355355+356356+<!-- Avoid: Manual querySelector -->
357357+<div data-volt>
358358+ <input id="username" />
359359+ <button data-volt-on-click="document.querySelector('#username').focus()">Focus</button>
360360+</div>
361361+```
362362+363363+### Use `data-volt-init` for Setup
364364+365365+Initialize observers and one-time setup in `data-volt-init`:
366366+367367+```html
368368+<div data-volt
369369+ data-volt-state='{"count": 0}'
370370+ data-volt-init="$probe('count', v => console.log('Count:', v))">
371371+372372+ <!-- Component content -->
373373+</div>
374374+```
375375+376376+### Scope Pin Names Appropriately
377377+378378+Use descriptive pin names and avoid collisions:
379379+380380+```html
381381+<!-- Good: Descriptive names -->
382382+<div data-volt>
383383+ <input data-volt-pin="searchInput" />
384384+ <input data-volt-pin="filterInput" />
385385+</div>
386386+387387+<!-- Avoid: Generic names that might collide -->
388388+<div data-volt>
389389+ <input data-volt-pin="input" />
390390+ <input data-volt-pin="input2" />
391391+</div>
392392+```
393393+394394+### Clean Up Observers
395395+396396+Always clean up `$probe` observers when no longer needed:
397397+398398+```html
399399+<div data-volt
400400+ data-volt-state='{"active": true}'
401401+ data-volt-init="const cleanup = $probe('active', v => console.log(v))">
402402+403403+ <button data-volt-on-click="active.set(false); cleanup()">Deactivate</button>
404404+</div>
405405+```
406406+407407+## Examples
408408+409409+### Todo App with Global State
410410+411411+```html
412412+<script type="application/json" data-volt-store>
413413+{
414414+ "todos": [],
415415+ "filter": "all"
416416+}
417417+</script>
418418+419419+<!-- Add todo form -->
420420+<div data-volt data-volt-state='{"newTodo": ""}'>
421421+ <input data-volt-model="newTodo" data-volt-pin="todoInput" />
422422+ <button data-volt-on-click="$store.get('todos').push({ text: newTodo.get(), done: false }); newTodo.set(''); $pins.todoInput.focus()">
423423+ Add
424424+ </button>
425425+</div>
426426+427427+<!-- Filter buttons -->
428428+<div data-volt>
429429+ <button data-volt-on-click="$store.set('filter', 'all')">All</button>
430430+ <button data-volt-on-click="$store.set('filter', 'active')">Active</button>
431431+ <button data-volt-on-click="$store.set('filter', 'done')">Done</button>
432432+</div>
433433+434434+<!-- Todo list -->
435435+<div data-volt>
436436+ <div data-volt-for="todo in $store.get('todos')">
437437+ <div data-volt-if="$store.get('filter') === 'all' || ($store.get('filter') === 'done' && todo.done) || ($store.get('filter') === 'active' && !todo.done)">
438438+ <input type="checkbox" data-volt-bind:checked="todo.done" />
439439+ <span data-volt-text="todo.text"></span>
440440+ </div>
441441+ </div>
442442+</div>
443443+```
444444+445445+### Multi-Step Form
446446+447447+```html
448448+<script type="application/json" data-volt-store>
449449+{
450450+ "step": 1,
451451+ "formData": { "name": "", "email": "", "phone": "" }
452452+}
453453+</script>
454454+455455+<div data-volt>
456456+ <!-- Step indicator -->
457457+ <p data-volt-text="'Step ' + $store.get('step') + ' of 3'"></p>
458458+459459+ <!-- Step 1: Name -->
460460+ <div data-volt-if="$store.get('step') === 1">
461461+ <input data-volt-model="$store.get('formData').name" placeholder="Name" />
462462+ <button data-volt-on-click="$store.set('step', 2)">Next</button>
463463+ </div>
464464+465465+ <!-- Step 2: Email -->
466466+ <div data-volt-if="$store.get('step') === 2">
467467+ <input data-volt-model="$store.get('formData').email" placeholder="Email" />
468468+ <button data-volt-on-click="$store.set('step', 1)">Back</button>
469469+ <button data-volt-on-click="$store.set('step', 3)">Next</button>
470470+ </div>
471471+472472+ <!-- Step 3: Phone -->
473473+ <div data-volt-if="$store.get('step') === 3">
474474+ <input data-volt-model="$store.get('formData').phone" placeholder="Phone" />
475475+ <button data-volt-on-click="$store.set('step', 2)">Back</button>
476476+ <button data-volt-on-click="console.log('Submit:', $store.get('formData'))">Submit</button>
477477+ </div>
478478+</div>
479479+```
480480+481481+## API Reference
482482+483483+### `registerStore(state)`
484484+485485+Register global store state programmatically.
486486+487487+```typescript
488488+import { registerStore } from 'voltx.js';
489489+import { signal } from 'voltx.js';
490490+491491+registerStore({
492492+ theme: signal('dark'), // Existing signal
493493+ count: 0 // Auto-wrapped
494494+});
495495+```
496496+497497+### `getStore()`
498498+499499+Get the global store instance.
500500+501501+```typescript
502502+import { getStore } from 'voltx.js';
503503+504504+const store = getStore();
505505+store.set('theme', 'light');
506506+console.log(store.get('theme')); // 'light'
507507+console.log(store.has('theme')); // true
508508+```
509509+510510+### `getScopeMetadata(scope)`
511511+512512+Get metadata for a scope (advanced use).
513513+514514+```typescript
515515+import { getScopeMetadata } from 'voltx.js';
516516+517517+const metadata = getScopeMetadata(scope);
518518+console.log(metadata.origin); // Root element
519519+console.log(metadata.pins); // Pin registry
520520+console.log(metadata.uidCounter); // Current UID counter
521521+```
522522+523523+## See Also
524524+525525+- [Reactivity](/reactivity) - Understanding signals and computed values
526526+- [Plugins](/plugins) - Extending Volt with plugins
527527+- [HTTP Actions](/http) - Server communication patterns
+132-35
lib/src/core/binder.ts
···22 * Binder system for mounting and managing Volt.js bindings
33 */
4455-import type { Optional } from "$types/helpers";
55+import type { Nullable, Optional } from "$types/helpers";
66import type {
77 BindingContext,
88 CleanupFunction,
···1515} from "$types/volt";
1616import { BOOLEAN_ATTRS } from "./constants";
1717import { getVoltAttrs, parseClassBinding, setHTML, setText, toggleClass, walkDOM } from "./dom";
1818-import { evaluate, extractDeps } from "./evaluator";
1818+import { evaluate } from "./evaluator";
1919import { bindDelete, bindGet, bindPatch, bindPost, bindPut } from "./http";
2020import { execGlobalHooks, notifyBindingCreated, notifyElementMounted, notifyElementUnmounted } from "./lifecycle";
2121import { debounce, getModifierValue, hasModifier, parseModifiers, throttle } from "./modifiers";
2222import { getPlugin } from "./plugin";
2323-import { findScopedSignal, isNil } from "./shared";
2323+import { createScopeMetadata, getPin, registerPin } from "./scope-metadata";
2424+import { createArc, createProbe, createPulse, createUid } from "./scope-vars";
2525+import { findScopedSignal, isNil, updateAndRegister } from "./shared";
2626+import { getStore } from "./store";
24272528/**
2629 * Mount Volt.js on a root element and its descendants and binds all data-volt-* attributes to the provided scope.
···3033 * @returns Cleanup function to unmount and dispose all bindings.
3134 */
3235export function mount(root: Element, scope: Scope): CleanupFunction {
3636+ injectSpecialVars(scope, root);
3337 execGlobalHooks("beforeMount", root, scope);
34383539 const allElements = walkDOM(root);
36403741 const elements = allElements.filter((element) => {
3838- let current: Element | null = element;
4242+ let current: Nullable<Element> = element;
3943 while (current) {
4044 if (Object.hasOwn((current as HTMLElement).dataset, "voltSkip")) {
4145 return false;
···168172 bindModel(ctx, value, modifiers);
169173 break;
170174 }
175175+ case "pin": {
176176+ bindPin(ctx, value);
177177+ break;
178178+ }
179179+ case "init": {
180180+ bindInit(ctx, value);
181181+ break;
182182+ }
171183 case "for": {
172184 bindFor(ctx, value);
173185 break;
···222234 setHTML(ctx.element, String(value ?? ""));
223235 }
224236 };
225225- update();
226226-227227- const deps = extractDeps(expr, ctx.scope);
228228- for (const dep of deps) {
229229- const unsubscribe = dep.subscribe(update);
230230- ctx.cleanups.push(unsubscribe);
231231- }
237237+ updateAndRegister(ctx, update, expr);
232238 };
233233-}
234234-235235-/**
236236- * Helper function to execute an update function and subscribe to all signal dependencies.
237237- * Used by bindings that need reactive updates (class, show, style, for, if).
238238- */
239239-function updateAndUnsub(ctx: BindingContext, update: () => void, expr: string) {
240240- update();
241241- const deps = extractDeps(expr, ctx.scope);
242242- for (const dep of deps) {
243243- const unsubscribe = dep.subscribe(update);
244244- ctx.cleanups.push(unsubscribe);
245245- }
246239}
247240248241/**
···269262 prevClasses = classes;
270263 };
271264272272- updateAndUnsub(ctx, update, expr);
265265+ updateAndRegister(ctx, update, expr);
273266}
274267275268/**
···291284 }
292285 };
293286294294- updateAndUnsub(ctx, update, expr);
287287+ updateAndRegister(ctx, update, expr);
295288}
296289297290/**
···325318 }
326319 };
327320328328- updateAndUnsub(ctx, update, expr);
321321+ updateAndRegister(ctx, update, expr);
322322+}
323323+324324+function extractStatements(expr: string) {
325325+ const statements: string[] = [];
326326+ let current = "";
327327+ let depth = 0;
328328+ let inString: string | null = null;
329329+330330+ for (const [i, char] of [...expr].entries()) {
331331+ const prev = i > 0 ? expr[i - 1] : "";
332332+333333+ if ((char === "\"" || char === "'") && prev !== "\\") {
334334+ if (inString === char) {
335335+ inString = null;
336336+ } else if (inString === null) {
337337+ inString = char;
338338+ }
339339+ }
340340+341341+ if (inString === null) {
342342+ if (char === "(" || char === "{" || char === "[") {
343343+ depth++;
344344+ } else if (char === ")" || char === "}" || char === "]") {
345345+ depth--;
346346+ }
347347+ }
348348+349349+ if (char === ";" && depth === 0 && inString === null) {
350350+ if (current.trim()) {
351351+ statements.push(current.trim());
352352+ }
353353+ current = "";
354354+ } else {
355355+ current += char;
356356+ }
357357+ }
358358+359359+ if (current.trim()) {
360360+ statements.push(current.trim());
361361+ }
362362+363363+ return statements;
329364}
330365331366/**
···348383 const eventScope: Scope = { ...ctx.scope, $el: ctx.element, $event: event };
349384350385 try {
351351- const result = evaluate(expr, eventScope);
386386+ const statements = extractStatements(expr);
387387+ let result: unknown;
388388+ for (const stmt of statements) {
389389+ result = evaluate(stmt, eventScope);
390390+ }
391391+352392 if (typeof result === "function") {
353393 result(event);
354394 }
···560600 }
561601 };
562602563563- update();
603603+ updateAndRegister(ctx, update, expr);
604604+}
564605565565- const deps = extractDeps(expr, ctx.scope);
566566- for (const dep of deps) {
567567- const unsubscribe = dep.subscribe(update);
568568- ctx.cleanups.push(unsubscribe);
606606+/**
607607+ * Bind data-volt-init to run initialization code once when the element is mounted.
608608+ */
609609+function bindInit(ctx: BindingContext, expr: string): void {
610610+ try {
611611+ const statements = extractStatements(expr);
612612+ for (const stmt of statements) {
613613+ evaluate(stmt, ctx.scope);
614614+ }
615615+ } catch (error) {
616616+ console.error("Error in data-volt-init:", error);
569617 }
570618}
571619572620/**
621621+ * Bind data-volt-pin to register an element reference in the scope's pin registry.
622622+ * Makes the element accessible via $pins.name ($pins[name]) in expressions and event handlers.
623623+ *
624624+ * @example
625625+ * ```html
626626+ * <input data-volt-pin="username" />
627627+ * <button data-volt-on-click="$pins.username.focus()">Focus Input</button>
628628+ * ```
629629+ */
630630+function bindPin(ctx: BindingContext, name: string): void {
631631+ registerPin(ctx.scope, name, ctx.element);
632632+}
633633+634634+/**
573635 * Bind data-volt-for to render a list of items.
574636 * Subscribes to array signal and re-renders when array changes.
575637 */
···629691 }
630692 };
631693632632- updateAndUnsub(ctx, render, expr);
694694+ updateAndRegister(ctx, render, expr);
633695634696 ctx.cleanups.push(() => {
635697 for (const cleanup of renderedCleanups) {
···707769 }
708770 };
709771710710- updateAndUnsub(ctx, render, expr);
772772+ updateAndRegister(ctx, render, expr);
711773712774 ctx.cleanups.push(() => {
713775 if (currentCleanup) {
···799861 lifecycle,
800862 };
801863}
864864+865865+/**
866866+ * Inject special variables ($store, $origin, $scope, $pins, $pulse, $uid, $arc, $probe)
867867+ * into the scope for this root element.
868868+ *
869869+ * Creates scope metadata and makes runtime utilities available in expressions.
870870+ * We create a Proxy for $pins that dynamically reads from metadata to ensure pins registered later are immediately accessible
871871+ */
872872+function injectSpecialVars(scope: Scope, root: Element): void {
873873+ createScopeMetadata(scope, root);
874874+875875+ scope.$store = getStore();
876876+ scope.$pulse = createPulse();
877877+ scope.$origin = root;
878878+ scope.$scope = scope;
879879+880880+ scope.$pins = new Proxy({}, {
881881+ get(_target, prop: string) {
882882+ if (typeof prop === "string") {
883883+ return getPin(scope, prop);
884884+ }
885885+ return void 0;
886886+ },
887887+ has(_target, prop: string) {
888888+ if (typeof prop === "string") {
889889+ return getPin(scope, prop) !== undefined;
890890+ }
891891+ return false;
892892+ },
893893+ });
894894+895895+ scope.$uid = createUid(scope);
896896+ scope.$arc = createArc(root);
897897+ scope.$probe = createProbe(scope);
898898+}
+42
lib/src/core/charge.ts
···99import { evaluate } from "./evaluator";
1010import { getComputedAttributes, isNil } from "./shared";
1111import { computed, signal } from "./signal";
1212+import { registerStore } from "./store";
12131314/**
1415 * Discover and mount all Volt roots in the document.
1516 * Parses data-volt-state for initial state and data-volt-computed for derived values.
1717+ * Also parses declarative global store from script[data-volt-store] elements.
1618 *
1719 * @param rootSelector - Selector for root elements (default: "[data-volt]")
1820 * @returns ChargeResult containing mounted roots and cleanup function
1921 *
2022 * @example
2123 * ```html
2424+ * <!-- Global store (declarative) -->
2525+ * <script type="application/json" data-volt-store>
2626+ * {
2727+ * "theme": "dark",
2828+ * "user": { "name": "Alice" }
2929+ * }
3030+ * </script>
3131+ *
2232 * <div data-volt data-volt-state='{"count": 0}' data-volt-computed:double="count * 2">
2333 * <p data-volt-text="count"></p>
2434 * <p data-volt-text="double"></p>
3535+ * <p data-volt-text="$store.theme"></p>
2536 * </div>
2637 * ```
2738 *
···3142 * ```
3243 */
3344export function charge(rootSelector = "[data-volt]"): ChargeResult {
4545+ parseDeclarativeStore();
4646+3447 const elements = document.querySelectorAll(rootSelector);
3548 const chargedRoots: ChargedRoot[] = [];
3649···9410795108 return scope;
96109}
110110+111111+/**
112112+ * Parse and register global store from declarative script tags.
113113+ *
114114+ * Looks for: <script type="application/json" data-volt-store>
115115+ * Expects JSON object with key-value pairs to register in global store.
116116+ */
117117+function parseDeclarativeStore(): void {
118118+ const scripts = document.querySelectorAll("script[data-volt-store][type=\"application/json\"]");
119119+120120+ for (const script of scripts) {
121121+ try {
122122+ const content = script.textContent?.trim();
123123+ if (!content) continue;
124124+125125+ const data = JSON.parse(content);
126126+127127+ if (typeof data !== "object" || isNil(data) || Array.isArray(data)) {
128128+ console.error("data-volt-store script must contain a JSON object, got:", typeof data);
129129+ continue;
130130+ }
131131+132132+ registerStore(data);
133133+ } catch (error) {
134134+ console.error("Failed to parse data-volt-store script:", error);
135135+ console.error("Script element:", script);
136136+ }
137137+ }
138138+}
+2-48
lib/src/core/evaluator.ts
···55 * Includes sandboxing to prevent prototype pollution and sandbox escape attacks.
66 */
7788-import type { Dep, Scope } from "$types/volt";
88+import type { Scope } from "$types/volt";
99import { DANGEROUS_GLOBALS, DANGEROUS_PROPERTIES, SAFE_GLOBALS } from "./constants";
1010-import { findScopedSignal, isNil, isSignal } from "./shared";
1010+import { isNil, isSignal } from "./shared";
11111212const dangerousProps = new Set(DANGEROUS_PROPERTIES);
1313const safeGlobals = new Set(SAFE_GLOBALS);
···871871 return undefined;
872872 }
873873}
874874-875875-/**
876876- * Extract all signal dependencies from an expression by finding identifiers that correspond to signals in the scope.
877877- *
878878- * This function handles both simple property paths (e.g., "todo.title") and complex expressions (e.g., "email.length > 0 && emailValid").
879879- *
880880- * @param expr - The expression to analyze
881881- * @param scope - The scope containing potential signal dependencies
882882- * @returns Array of signals found in the expression
883883- */
884884-export function extractDeps(expr: string, scope: Scope): Array<Dep> {
885885- const deps: Array<Dep> = [];
886886- const seen = new Set<string>();
887887-888888- const identifierRegex = /\b([a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)*)\b/g;
889889- const matches = expr.matchAll(identifierRegex);
890890-891891- for (const match of matches) {
892892- const path = match[1];
893893-894894- if (["true", "false", "null", "undefined"].includes(path)) {
895895- continue;
896896- }
897897-898898- if (seen.has(path)) {
899899- continue;
900900- }
901901-902902- seen.add(path);
903903-904904- const signal = findScopedSignal(scope, path);
905905- if (signal) {
906906- deps.push(signal);
907907- continue;
908908- }
909909-910910- const parts = path.split(".");
911911- const topLevel = parts[0];
912912- const value = scope[topLevel];
913913- if (isSignal(value) && !deps.includes(value)) {
914914- deps.push(value);
915915- }
916916- }
917917-918918- return deps;
919919-}
+131
lib/src/core/scope-metadata.ts
···11+/**
22+ * Scope metadata management system
33+ *
44+ * Stores metadata for each reactive scope using WeakMap to avoid polluting scope objects.
55+ * Metadata includes origin element, pin registry, UID counter, and optional parent reference.
66+ */
77+88+import type { Scope, ScopeMetadata } from "$types/volt";
99+1010+/**
1111+ * WeakMap storing metadata for each scope.
1212+ * WeakMap ensures metadata is garbage collected when scope is no longer referenced.
1313+ */
1414+const scopeMetadataMap = new WeakMap<Scope, ScopeMetadata>();
1515+1616+/**
1717+ * Create and store metadata for a scope.
1818+ *
1919+ * @param scope - The reactive scope object
2020+ * @param origin - The root element that owns this scope
2121+ * @param parent - Optional parent scope for debugging/inspection
2222+ * @returns The created metadata object
2323+ *
2424+ * @example
2525+ * ```ts
2626+ * const scope = { count: signal(0) };
2727+ * const metadata = createScopeMetadata(scope, rootElement);
2828+ * ```
2929+ */
3030+export function createScopeMetadata(scope: Scope, origin: Element, parent?: Scope): ScopeMetadata {
3131+ const metadata: ScopeMetadata = { origin, pins: new Map<string, Element>(), uidCounter: 0, parent };
3232+3333+ scopeMetadataMap.set(scope, metadata);
3434+ return metadata;
3535+}
3636+3737+/**
3838+ * Get metadata for a scope.
3939+ *
4040+ * @param scope - The scope to get metadata for
4141+ * @returns The metadata object, or undefined if not found
4242+ *
4343+ * @example
4444+ * ```ts
4545+ * const metadata = getScopeMetadata(scope);
4646+ * if (metadata) {
4747+ * console.log('Origin element:', metadata.origin);
4848+ * }
4949+ * ```
5050+ */
5151+export function getScopeMetadata(scope: Scope): ScopeMetadata | undefined {
5252+ return scopeMetadataMap.get(scope);
5353+}
5454+5555+/**
5656+ * Register a pinned element in the scope's pin registry.
5757+ *
5858+ * @param scope - The scope to register the pin in
5959+ * @param name - The pin name
6060+ * @param element - The element to pin
6161+ *
6262+ * @example
6363+ * ```ts
6464+ * registerPin(scope, 'submitButton', buttonElement);
6565+ * // Later accessible via $pins.submitButton
6666+ * ```
6767+ */
6868+export function registerPin(scope: Scope, name: string, element: Element): void {
6969+ const metadata = scopeMetadataMap.get(scope);
7070+ if (metadata) {
7171+ metadata.pins.set(name, element);
7272+ }
7373+}
7474+7575+/**
7676+ * Get a pinned element by name from the scope.
7777+ *
7878+ * @param scope - The scope to search in
7979+ * @param name - The pin name to retrieve
8080+ * @returns The pinned element, or undefined if not found
8181+ *
8282+ * @example
8383+ * ```ts
8484+ * const button = getPin(scope, 'submitButton');
8585+ * if (button) {
8686+ * button.focus();
8787+ * }
8888+ * ```
8989+ */
9090+export function getPin(scope: Scope, name: string): Element | undefined {
9191+ const metadata = scopeMetadataMap.get(scope);
9292+ return metadata?.pins.get(name);
9393+}
9494+9595+/**
9696+ * Get all pins for a scope as a record object.
9797+ * This is what gets injected as $pins in the scope.
9898+ *
9999+ * @param scope - The scope to get pins for
100100+ * @returns Record mapping pin names to elements
101101+ *
102102+ * @example
103103+ * ```ts
104104+ * const pins = getPins(scope);
105105+ * // Access as: pins.submitButton, pins.inputField, etc.
106106+ * ```
107107+ */
108108+export function getPins(scope: Scope): Record<string, Element> {
109109+ const metadata = scopeMetadataMap.get(scope);
110110+ if (!metadata) return {};
111111+112112+ const pins: Record<string, Element> = {};
113113+ for (const [name, element] of metadata.pins) {
114114+ pins[name] = element;
115115+ }
116116+ return pins;
117117+}
118118+119119+/**
120120+ * Increment and return the UID counter for a scope.
121121+ * Used internally by $uid() to generate deterministic IDs.
122122+ *
123123+ * @param scope - The scope to increment counter for
124124+ * @returns The next UID number
125125+ */
126126+export function incrementUidCounter(scope: Scope): number {
127127+ const metadata = scopeMetadataMap.get(scope);
128128+ if (!metadata) return 0;
129129+130130+ return ++metadata.uidCounter;
131131+}
+142
lib/src/core/scope-vars.ts
···11+/**
22+ * Special scope variables ($pulse, $uid, $arc, $probe)
33+ *
44+ * Factory functions for creating the special `$` variables that are injected into scopes.
55+ * These provide runtime utilities accessible in expressions and event handlers.
66+ */
77+88+import type { ArcFunction, CleanupFunction, ProbeFunction, PulseFunction, Scope, UidFunction } from "$types/volt";
99+import { evaluate } from "./evaluator";
1010+import { incrementUidCounter } from "./scope-metadata";
1111+import { extractDeps } from "./shared";
1212+1313+/**
1414+ * Create the $pulse function for deferring execution to next microtask.
1515+ *
1616+ * $pulse() schedules a callback to run after the current execution context completes,
1717+ * ensuring that DOM updates have been applied before the callback runs.
1818+ *
1919+ * @returns The $pulse function
2020+ *
2121+ * @example
2222+ * ```html
2323+ * <button data-volt-on-click="count++; $pulse(() => console.log('DOM updated'))">
2424+ * Increment
2525+ * </button>
2626+ * ```
2727+ */
2828+export function createPulse(): PulseFunction {
2929+ return (cb: () => void) => {
3030+ queueMicrotask(cb);
3131+ };
3232+}
3333+3434+/**
3535+ * Create the $uid function for generating unique, deterministic IDs within a scope.
3636+ *
3737+ * Each call increments a counter specific to the scope, ensuring IDs are unique
3838+ * within that scope and deterministic across renders.
3939+ *
4040+ * @param scope - The reactive scope
4141+ * @returns The $uid function
4242+ *
4343+ * @example
4444+ * ```html
4545+ * <input data-volt-bind:id="$uid('field')" />
4646+ * <!-- Generates: "volt-field-1", "volt-field-2", etc. -->
4747+ * ```
4848+ */
4949+export function createUid(scope: Scope): UidFunction {
5050+ return (prefix?: string) => {
5151+ const counter = incrementUidCounter(scope);
5252+ return prefix ? `volt-${prefix}-${counter}` : `volt-${counter}`;
5353+ };
5454+}
5555+5656+/**
5757+ * Create the $arc function for dispatching CustomEvents from an element.
5858+ *
5959+ * $arc() provides a simple way to emit custom events with optional detail data.
6060+ * The event bubbles by default and can be caught by parent elements.
6161+ *
6262+ * @param element - The element to dispatch events from
6363+ * @returns The $arc function
6464+ *
6565+ * @example
6666+ * ```html
6767+ * <button data-volt-on-click="$arc('user:save', { id: userId })">
6868+ * Save
6969+ * </button>
7070+ *
7171+ * <div data-volt-on-user:save="handleSave($event.detail)">
7272+ * <!-- Catches the custom event -->
7373+ * </div>
7474+ * ```
7575+ */
7676+export function createArc(element: Element): ArcFunction {
7777+ return (eventName: string, detail?: unknown) => {
7878+ const event = new CustomEvent(eventName, { detail, bubbles: true, composed: true, cancelable: true });
7979+ element.dispatchEvent(event);
8080+ };
8181+}
8282+8383+/**
8484+ * Create the $probe function for imperatively observing reactive expressions.
8585+ *
8686+ * $probe() creates an effect that watches an expression and calls a callback
8787+ * whenever the expression's dependencies change. The callback is invoked immediately
8888+ * with the initial value, then whenever any dependency changes.
8989+ *
9090+ * @param scope - The reactive scope
9191+ * @returns The $probe function
9292+ *
9393+ * @example
9494+ * ```html
9595+ * <div data-volt-init="$probe('count', v => console.log('Count changed:', v))">
9696+ * <!-- Logs whenever count changes -->
9797+ * </div>
9898+ * ```
9999+ */
100100+export function createProbe(scope: Scope): ProbeFunction {
101101+ return (expr: string, cb: (value: unknown) => void): CleanupFunction => {
102102+ const unsubs: Array<() => void> = [];
103103+ let isDisposed = false;
104104+105105+ const runProbe = () => {
106106+ if (isDisposed) {
107107+ return;
108108+ }
109109+110110+ try {
111111+ const value = evaluate(expr, scope);
112112+ cb(value);
113113+ } catch (error) {
114114+ console.error("Error in $probe expression:", error);
115115+ }
116116+ };
117117+118118+ try {
119119+ const deps = extractDeps(expr, scope);
120120+121121+ for (const dep of deps) {
122122+ const unsubscribe = dep.subscribe(runProbe);
123123+ unsubs.push(unsubscribe);
124124+ }
125125+ } catch (error) {
126126+ console.error("Error extracting dependencies for $probe:", error);
127127+ }
128128+129129+ runProbe();
130130+ return () => {
131131+ isDisposed = true;
132132+133133+ for (const unsub of unsubs) {
134134+ try {
135135+ unsub();
136136+ } catch (error) {
137137+ console.error("Error unsubscribing $probe:", error);
138138+ }
139139+ }
140140+ };
141141+ };
142142+}
+86-1
lib/src/core/shared.ts
···11+/**
22+ * @packageDocumentation Shared module
33+ *
44+ * functions exported from this module should only depend on types and other helpers
55+ */
16import type { None, Optional } from "$types/helpers";
22-import type { Dep, Scope, Signal } from "$types/volt";
77+import type { BindingContext, Dep, Scope, Signal } from "$types/volt";
3849export function kebabToCamel(str: string): string {
510 return str.replaceAll(/-([a-z])/g, (_, letter) => letter.toUpperCase());
···6974export function sleep(ms: number): Promise<void> {
7075 return new Promise((resolve) => setTimeout(resolve, ms));
7176}
7777+7878+/**
7979+ * Extract all signal dependencies from an expression by finding identifiers that correspond to signals in the scope.
8080+ *
8181+ * This function handles both simple property paths (e.g., "todo.title") and complex expressions (e.g., "email.length > 0 && emailValid").
8282+ * It also handles special $store.get() and $store.set() calls by extracting the key and finding the underlying signal.
8383+ *
8484+ * @param expr - The expression to analyze
8585+ * @param scope - The scope containing potential signal dependencies
8686+ * @returns Array of signals found in the expression
8787+ */
8888+export function extractDeps(expr: string, scope: Scope): Array<Dep> {
8989+ const deps: Array<Dep> = [];
9090+ const seen = new Set<string>();
9191+ const storeCalls = expr.matchAll(/\$store\.(get|set|has)\s*\(\s*['"]([^'"]+)['"]\s*(?:,|\))/g);
9292+9393+ for (const match of storeCalls) {
9494+ const key = match[2];
9595+ const storeKey = `$store.${key}`;
9696+9797+ if (seen.has(storeKey)) {
9898+ continue;
9999+ }
100100+101101+ seen.add(storeKey);
102102+103103+ const store = scope.$store;
104104+ if (store && typeof store === "object" && "_signals" in store) {
105105+ const storeSignals = store._signals as Map<string, Signal<unknown>>;
106106+ const signal = storeSignals.get(key);
107107+ if (signal && !deps.includes(signal)) {
108108+ deps.push(signal);
109109+ }
110110+ }
111111+ }
112112+113113+ const matches = expr.matchAll(/\b([a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)*)\b/g);
114114+115115+ for (const match of matches) {
116116+ const path = match[1];
117117+118118+ if (["true", "false", "null", "undefined"].includes(path)) {
119119+ continue;
120120+ }
121121+122122+ if (seen.has(path)) {
123123+ continue;
124124+ }
125125+126126+ seen.add(path);
127127+128128+ const signal = findScopedSignal(scope, path);
129129+ if (signal) {
130130+ deps.push(signal);
131131+ continue;
132132+ }
133133+134134+ const parts = path.split(".");
135135+ const topLevel = parts[0];
136136+ const value = scope[topLevel];
137137+ if (isSignal(value) && !deps.includes(value)) {
138138+ deps.push(value);
139139+ }
140140+ }
141141+142142+ return deps;
143143+}
144144+145145+/**
146146+ * Helper function to execute an update function and subscribe to all signal dependencies.
147147+ * Used by bindings that need reactive updates (class, show, style, for, if) to register cleanup functions.
148148+ */
149149+export function updateAndRegister(ctx: BindingContext, update: () => void, expr: string) {
150150+ update();
151151+ const deps = extractDeps(expr, ctx.scope);
152152+ for (const dep of deps) {
153153+ const unsubscribe = dep.subscribe(update);
154154+ ctx.cleanups.push(unsubscribe);
155155+ }
156156+}
+105
lib/src/core/store.ts
···11+/**
22+ * Global reactive store for cross-scope state management
33+ *
44+ * The store holds signals that are accessible from any scope via $store.
55+ * Supports both programmatic registration and declarative initialization.
66+ */
77+88+import type { Optional } from "$types/helpers";
99+import type { GlobalStore, Signal } from "$types/volt";
1010+import { signal } from "./signal";
1111+1212+/**
1313+ * Internal signal registry for the global store
1414+ */
1515+const storeSignals = new Map<string, Signal<unknown>>();
1616+1717+/**
1818+ * Global store singleton with helper methods and direct signal access
1919+ */
2020+const store: GlobalStore = {
2121+ _signals: storeSignals,
2222+2323+ get<T = unknown>(key: string): Optional<T> {
2424+ const sig = storeSignals.get(key);
2525+ return sig ? (sig.get() as T) : undefined;
2626+ },
2727+2828+ set<T = unknown>(key: string, value: T): void {
2929+ const sig = storeSignals.get(key);
3030+ if (sig) {
3131+ sig.set(value);
3232+ } else {
3333+ storeSignals.set(key, signal(value));
3434+ }
3535+ },
3636+3737+ has(key: string): boolean {
3838+ return storeSignals.has(key);
3939+ },
4040+};
4141+4242+/**
4343+ * Register state in the global store.
4444+ *
4545+ * Accepts either:
4646+ * - An object of signals: { theme: signal('dark') }
4747+ * - An object of values: { count: 0 } (auto-wrapped in signals)
4848+ *
4949+ * @param state - Object containing signals or raw values to register globally
5050+ *
5151+ * @example
5252+ * ```ts
5353+ * // With signals
5454+ * registerStore({
5555+ * theme: signal('dark'),
5656+ * user: signal({ name: 'Alice' })
5757+ * });
5858+ *
5959+ * // With raw values (auto-wrapped)
6060+ * registerStore({
6161+ * count: 0,
6262+ * isLoggedIn: false
6363+ * });
6464+ * ```
6565+ */
6666+export function registerStore(state: Record<string, unknown>): void {
6767+ for (const [key, value] of Object.entries(state)) {
6868+ if (value && typeof value === "object" && "get" in value && "set" in value && "subscribe" in value) {
6969+ storeSignals.set(key, value as Signal<unknown>);
7070+ Object.defineProperty(store, key, { get: () => value, enumerable: true, configurable: true });
7171+ } else {
7272+ const sig = signal(value);
7373+ storeSignals.set(key, sig);
7474+ Object.defineProperty(store, key, { get: () => sig, enumerable: true, configurable: true });
7575+ }
7676+ }
7777+}
7878+7979+/**
8080+ * Get the global store instance.
8181+ *
8282+ * The store provides:
8383+ * - Direct signal access: `$store.theme` returns the signal
8484+ * - Helper methods: `$store.get('theme')` returns the unwrapped value
8585+ * - Signal creation: `$store.set('newKey', value)` creates a new signal
8686+ *
8787+ * @returns The global store singleton
8888+ *
8989+ * @example
9090+ * ```ts
9191+ * const store = getStore();
9292+ *
9393+ * // Access signal directly
9494+ * const themeSignal = store.theme;
9595+ *
9696+ * // Get unwrapped value
9797+ * const currentTheme = store.get('theme');
9898+ *
9999+ * // Set value (creates signal if doesn't exist)
100100+ * store.set('theme', 'light');
101101+ * ```
102102+ */
103103+export function getStore(): GlobalStore {
104104+ return store;
105105+}
+9
lib/src/index.ts
···1919} from "$core/lifecycle";
2020export { clearPlugins, getRegisteredPlugins, hasPlugin, registerPlugin, unregisterPlugin } from "$core/plugin";
2121export { isReactive, reactive, toRaw } from "$core/reactive";
2222+export { getScopeMetadata } from "$core/scope-metadata";
2223export { computed, effect, signal } from "$core/signal";
2324export { deserializeScope, hydrate, isHydrated, isServerRendered, serializeScope } from "$core/ssr";
2525+export { getStore, registerStore } from "$core/store";
2426export { persistPlugin, registerStorageAdapter } from "$plugins/persist";
2527export { scrollPlugin } from "$plugins/scroll";
2628export { urlPlugin } from "$plugins/url";
2729export type {
3030+ ArcFunction,
2831 AsyncEffectFunction,
2932 AsyncEffectOptions,
3033 ChargedRoot,
3134 ChargeResult,
3235 ComputedSignal,
3336 GlobalHookName,
3737+ GlobalStore,
3438 HydrateOptions,
3539 HydrateResult,
3640 IsReactive,
3741 ParsedHttpConfig,
4242+ PinRegistry,
3843 PluginContext,
3944 PluginHandler,
4545+ ProbeFunction,
4646+ PulseFunction,
4047 ReactiveArray,
4148 RetryConfig,
4949+ ScopeMetadata,
4250 SerializedScope,
4351 Signal,
5252+ UidFunction,
4453 UnwrapReactive,
4554} from "$types/volt";
···11import { mount } from "$core/binder";
22-import { extractDeps } from "$core/evaluator";
22+import { extractDeps } from "$core/shared";
33import { signal } from "$core/signal";
44import { describe, expect, it } from "vitest";
55