···26262727**Goal:** Establish project structure, tooling, and base reactivity primitives.
2828**Outcome:** A bootable TypeScript project with working reactivity primitives and test coverage.
2929-**Deliverables:**
3030- - ✓ Project scaffolding
3131- - ✓ `signal()` implementation with subscribe/set/get
3232- - ✓ Initial tests (signals, reactivity basics)
2929+**Summary:** A TypeScript project scaffold with implemented signal() (subscribe/set/get) and foundational reactivity tests establishes the base system.
33303431### Reactivity & Bindings
35323633**Goal:** Connect signals to DOM via declarative `data-x-*` bindings.
3734**Outcome:** Reactive text/attribute binding with signals → DOM synchronization.
3838-**Deliverables:**
3939- - ✓ `data-x-text`, `data-x-html`, `data-x-class` binding parser
4040- - ✓ Expression evaluator (safe, minimal subset)
4141- - ✓ DOM mutation batching & cleanup
4242- - ✓ Internal test harness for bindings
4343- - ✓ DOM Testing Library integration tests
3535+**Summary:** Reactive DOM bindings (`data-x-text`, `data-x-html`, `data-x-class`) with a safe expression evaluator, mutation batching, and DOM testing ensure synchronized updates between signals and UI.
44364537### Actions & Effects
46384739**Goal:** Add event-driven behavior and derived reactivity.
4840**Outcome:** Fully functional reactive UI layer with event bindings and computed updates.
4949-**Deliverables:**
5050- - ✓ Event binding system (`data-volt-on-*`)
5151- - ✓ `$el` and `$event` scoped references
5252- - ✓ Derived signals (`computed`, `effect`)
5353- - ✓ Async effects (e.g., fetch triggers)
4141+**Summary:** An event binding system with `$el`, `$event`, and derived signals (computed, effect, async effects) delivers a complete reactive event-driven UI layer.
54425543### Plugins Framework
56445745**Goal:** Build a modular plugin architecture with dynamic registration.
5846**Outcome:** Stable plugin API enabling community-driven extensions.
5959-**Deliverables:**
6060- - ✓ `registerPlugin(name, fn)` API
6161- - ✓ Context and lifecycle hooks
6262- - ✓ Built-ins:
6363- - ✓ `data-volt-persist`
6464- - ✓ `data-volt-scroll`
6565- - ✓ `data-volt-url`
6666- - ✓ Registry
4747+**Summary:** A modular plugin API with lifecycle hooks and built-in extensions (persist, scroll, url) enables dynamic feature registration and community contributions.
67486849### Backend Integration & HTTP Actions
69507051**Goal:** Provide backend integration with declarative HTTP requests and responses.
7152**Outcome:** Volt.js can make backend requests and update the DOM
7272-**Deliverables:**
7373- - ✓ HTTP action system (`data-volt-get`, `data-volt-post`, `data-volt-put`, `data-volt-patch`, `data-volt-delete`)
7474- - ✓ Request configuration (`data-volt-trigger`, `data-volt-target`, `data-volt-swap`)
7575- - ✓ Swap strategies (innerHTML, outerHTML, beforebegin, afterbegin, beforeend, afterend, delete, none)
7676- - ✓ Loading states and indicators (`data-volt-indicator`)
7777- - ✓ Error handling and retry logic
7878- - ✓ Form serialization and submission
7979- - ✓ Request/response headers customization
5353+**Summary:** Declarative HTTP directives (data-volt-get|post|put|patch|delete) with swap strategies, loading indicators, error handling, and form serialization integrate Volt.js seamlessly with backend APIs.
80548155### Markup Based Reactivity
82568357**Goal:** Allow Volt apps to declare state, bindings, and behavior entirely in HTML markup
8458**Outcome:** Authors can ship examples without companion JavaScript bundles
8585-**Deliverables:**
8686- - ✓ Auto-bootstrapping loader (`volt.min.js`) that detects `data-volt` roots and hydrates one scope per root.
8787- - ✓ Declarative state primitives (`data-volt-state`, `data-volt-computed:*`) aligned with `docs/reactivity-spec.md`.
8888- - ✓ Binding directives for text, attributes, classes, styles, and two-way form controls (`data-volt-[bind|text|model|class:*]`).
8989- - ✓ Control-flow directives (`data-volt-for`, `data-volt-if`, `data-volt-else`) with lifecycle-safe teardown.
9090- - ✓ Declarative event system (`data-volt-on:*`) with helper surface for list mutations and plugin hooks.
9191- - ✓ SSR compatibility helpers
9292- - ✓ Sandboxed expression evaluator
5959+**Summary:** Declarative HTML state, binding, control-flow, and event directives with SSR support and a sandboxed evaluator enable Volt apps to run without separate JavaScript bundles.
93609461### Proxy-Based Reactivity Enhancements
95629663**Goal:** Use JavaScript Proxies to improve reactivity ergonomics and automatic dependency tracking.
9764**Outcome:** More intuitive API with automatic dependency tracking and optional deep reactivity for objects/arrays.
9898-**Deliverables:**
9999- - ✓ Automatic dependency tracking for `computed()`
100100- - ✓ Eliminate manual dependency arrays via proxy-based tracking
101101- - ✓ Auto-detect signal access during computation
102102- - ✓ Track nested property access for fine-grained updates
103103- - ✓ `reactive()` primitive for deep object reactivity (optional, alongside `signal()`)
104104- - ✓ Nested property changes trigger updates automatically
105105- - ✓ Proxy-wrapped objects with transparent reactivity
106106- - ✓ Array reactivity improvements
107107- - ✓ Reactive array methods (push, pop, shift, unshift, splice, etc.)
108108- - ✓ Automatic updates on array mutations
109109- - ✓ Efficient tracking of index-based changes
110110- - ✓ Lazy signal initialization
111111- - ✓ Create signals on-demand when properties are accessed
112112- - ✓ Expose debugging utilities
6565+**Summary:** Proxy-driven automatic dependency tracking, deep reactive() objects, reactive arrays, lazy signal creation, and debugging utilities improve reactivity ergonomics and performance.
11366**Notes:**
11467 - Separate reactive() function for objects/arrays to gives users choice
11568 - Keep .get()/.set() - explicitness is valuable for understanding reactivity (include in docs)
···12174**Goal:** Extend Volt.js with expressive attribute patterns and event options for fine-grained control.
12275**Outcome:** Volt.js supports rich declarative behaviors and event semantics built entirely on standard DOM APIs.
12376**Deliverables:**
124124- - `data-volt-show` - toggles element visibility via CSS rather than DOM removal (complements `data-volt-if`)
125125- - `data-volt-style` - binds inline styles to reactive expressions
126126- - `data-volt-skip` - marks elements or subtrees to exclude from Volt’s reactive parsing
127127- - `data-volt-cloak` - hides content until the Volt runtime initializes
7777+ - ✓ `data-volt-show` - toggles element visibility via CSS rather than DOM removal (complements `data-volt-if`)
7878+ - ✓ `data-volt-style` - binds inline styles to reactive expressions
7979+ - ✓ `data-volt-skip` - marks elements or subtrees to exclude from Volt’s reactive parsing
8080+ - ✓ `data-volt-cloak` - hides content until the Volt runtime initializes
12881 - Event options for `data-volt-on-*` attributes:
12982 - `.prevent` - calls `preventDefault()` on the event
13083 - `.stop` - stops propagation
···14699**Goal:** Implement store/context pattern
147100**Outcome:** Volt.js provides intuitive global state management
148101**Deliverables:**
149149- - `$refs` - Scoped element references via data-volt-ref="name". Provides an object mapping ref names to DOM nodes.
150150- - Example: `data-volt-on-click="$refs.username.focus()"`
151151- - `$next()` - Defers execution to the next microtask tick after DOM updates.
152152- - Example: `data-volt-on-click="$count++; $next(() => console.log('updated'))"`
153153- - `$watch(expr, fn)` - Imperatively observes a reactive signal or expression within the current scope.
154154- - Example: `data-volt-init="$watch('count', v => console.log(v))"`
155155- - `$emit(event, detail?)` - Dispatches a native CustomEvent from the current element.
156156- - Example: `data-volt-on-click="$emit('user:save', { id })"`
102102+ - `$origin` - Reference to the root element of the active reactive scope.
103103+ - `$scope` - Reference to the current reactive scope object (signals + context).
104104+ - `$pulse()` - Defers execution to the next microtask tick after DOM updates.
105105+ - Example: `data-volt-on-click="$count++; $pulse(() => console.log('updated'))"`
157106 - `$store` - Accesses global reactive state registered with Volt’s global store.
158107 - Example: `data-volt-text="$store.theme"`
159108 - `$uid(name?)` - Generates a unique, deterministic ID string within the current scope.
160109 - Example: `data-volt-id="$uid('field')"`
161161- - `$root` - Reference to the root element of the active reactive scope.
162162- - `$scope` - Reference to the current reactive scope object (signals + context).
110110+ - `$probe(expr, fn)` - Imperatively observes a reactive signal or expression within the current scope.
111111+ - Example: `data-volt-init="$probe('count', v => console.log(v))"`
112112+ - `$pins` - Scoped element references via `data-volt-pin="name"`. Provides an object mapping ref/pin names to DOM nodes.
113113+ - Example: `data-volt-on-click="$pins.username.focus()"`
114114+ - `$arc(event, detail?)` - Dispatches a native CustomEvent from the current element.
115115+ - Example: `data-volt-on-click="$arc('user:save', { id })"`
163116164117### Animation & Transitions
165118166119**Goal:** Add animation primitives for smooth UI transitions with Alpine/Datastar parity.
167120**Outcome:** Volt.js enables declarative animations and view transitions alongside reactivity.
168121**Deliverables:**
169169- - `data-volt-transition` directive with enter/leave transitions
122122+ - `data-volt-surge` directive with enter/leave transitions
170123 - Transition modifiers (duration, delay, opacity, scale, etc.)
171124 - View Transitions API integration (when available)
172125 - CSS-based transition helpers
173173- - `data-volt-animate` plugin for keyframe animations
126126+ - `data-volt-shift` plugin for keyframe animations
174127 - Timing utilities and easing functions
175128 - Integration with `data-volt-if` and `data-volt-show` for automatic transitions
176129···180133**Outcome:** Volt.js can receive and apply live updates from the server
181134**Deliverables:**
182135 - Server-Sent Events (SSE) integration
183183- - `data-volt-stream` attribute for SSE endpoints
136136+ - `data-volt-flow` attribute for SSE endpoints
184137 - Signal patching from backend (`data-signals-*` merge system)
185185- - Backend action system with `$$action()` syntax (TBD on final syntax decision)
138138+ - Backend action system with `$$spark()` syntax
186139 - JSON Patch parser and DOM morphing engine
187140 - WebSocket as alternative to SSE
188141 - `data-volt-ignore-morph` for selective patch exclusion
+102-11
lib/src/core/binder.ts
···99import { bindDelete, bindGet, bindPatch, bindPost, bindPut } from "./http";
1010import { execGlobalHooks, notifyBindingCreated, notifyElementMounted, notifyElementUnmounted } from "./lifecycle";
1111import { getPlugin } from "./plugin";
1212-import { findScopedSignal } from "./shared";
1212+import { findScopedSignal, isNil } from "./shared";
13131414/**
1515 * Mount Volt.js on a root element and its descendants and binds all data-volt-* attributes to the provided scope.
···2121export function mount(root: Element, scope: Scope): CleanupFunction {
2222 execGlobalHooks("beforeMount", root, scope);
23232424- const elements = walkDOM(root);
2424+ const allElements = walkDOM(root);
2525+2626+ const elements = allElements.filter((element) => {
2727+ let current: Element | null = element;
2828+ while (current) {
2929+ if (Object.hasOwn((current as HTMLElement).dataset, "voltSkip")) {
3030+ return false;
3131+ }
3232+ if (current === root) break;
3333+ current = current.parentElement;
3434+ }
3535+ return true;
3636+ });
3737+2538 const allCleanups: CleanupFunction[] = [];
2639 const mountedElements: Element[] = [];
27402828- for (const element of elements) {
2929- const attributes = getVoltAttrs(element);
3030- const context: BindingContext = { element, scope, cleanups: [] };
4141+ for (const el of elements) {
4242+ if (Object.hasOwn((el as HTMLElement).dataset, "voltCloak")) {
4343+ delete (el as HTMLElement).dataset.voltCloak;
4444+ }
4545+4646+ const attributes = getVoltAttrs(el);
4747+ const context: BindingContext = { element: el, scope, cleanups: [] };
31483249 if (attributes.has("for")) {
3350 const forExpression = attributes.get("for")!;
3451 bindFor(context, forExpression);
3535- notifyBindingCreated(element, "for");
5252+ notifyBindingCreated(el, "for");
3653 } else if (attributes.has("if")) {
3754 const ifExpression = attributes.get("if")!;
3855 bindIf(context, ifExpression);
3939- notifyBindingCreated(element, "if");
5656+ notifyBindingCreated(el, "if");
4057 } else {
4158 for (const [name, value] of attributes) {
4259 bindAttribute(context, name, value);
4343- notifyBindingCreated(element, name);
6060+ notifyBindingCreated(el, name);
4461 }
4562 }
46634747- notifyElementMounted(element);
4848- mountedElements.push(element);
6464+ notifyElementMounted(el);
6565+ mountedElements.push(el);
4966 allCleanups.push(...context.cleanups);
5067 }
5168···98115 }
99116 case "class": {
100117 bindClass(ctx, value);
118118+ break;
119119+ }
120120+ case "show": {
121121+ bindShow(ctx, value);
122122+ break;
123123+ }
124124+ case "style": {
125125+ bindStyle(ctx, value);
101126 break;
102127 }
103128 case "model": {
···216241}
217242218243/**
244244+ * Bind data-volt-show to toggle element visibility via CSS display property.
245245+ * Unlike data-volt-if, this keeps the element in the DOM and toggles display: none.
246246+ */
247247+function bindShow(ctx: BindingContext, expr: string): void {
248248+ const el = ctx.element as HTMLElement;
249249+ const originalInlineDisplay = el.style.display;
250250+251251+ const update = () => {
252252+ const value = evaluate(expr, ctx.scope);
253253+ const shouldShow = Boolean(value);
254254+255255+ if (shouldShow) {
256256+ el.style.display = originalInlineDisplay;
257257+ } else {
258258+ el.style.display = "none";
259259+ }
260260+ };
261261+262262+ update();
263263+264264+ const deps = extractDeps(expr, ctx.scope);
265265+ for (const dep of deps) {
266266+ const unsubscribe = dep.subscribe(update);
267267+ ctx.cleanups.push(unsubscribe);
268268+ }
269269+}
270270+271271+/**
272272+ * Bind data-volt-style to reactively apply inline styles.
273273+ * Supports object notation {color: 'red', fontSize: '16px'} or string notation 'color: red; font-size: 16px'.
274274+ */
275275+function bindStyle(ctx: BindingContext, expr: string): void {
276276+ const element = ctx.element as HTMLElement;
277277+278278+ const update = () => {
279279+ const value = evaluate(expr, ctx.scope);
280280+281281+ if (typeof value === "object" && value !== null) {
282282+ for (const [key, val] of Object.entries(value)) {
283283+ const cssKey = key.replaceAll(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
284284+285285+ if (isNil(val)) {
286286+ element.style.removeProperty(cssKey);
287287+ } else {
288288+ try {
289289+ element.style.setProperty(cssKey, String(val));
290290+ } catch (error) {
291291+ console.warn(`[Volt] Failed to set style property "${cssKey}":`, error);
292292+ }
293293+ }
294294+ }
295295+ } else if (typeof value === "string") {
296296+ element.style.cssText = value;
297297+ }
298298+ };
299299+300300+ update();
301301+302302+ const deps = extractDeps(expr, ctx.scope);
303303+ for (const dep of deps) {
304304+ const unsubscribe = dep.subscribe(update);
305305+ ctx.cleanups.push(unsubscribe);
306306+ }
307307+}
308308+309309+/**
219310 * Bind data-volt-on-* to attach event listeners.
220311 * Provides $el and $event in the scope for the event handler.
221312 */
···357448 ctx.element.removeAttribute(attrName);
358449 }
359450 } else {
360360- if (value === null || value === undefined || value === false) {
451451+ if (isNil(value) || value === false) {
361452 ctx.element.removeAttribute(attrName);
362453 } else {
363454 ctx.element.setAttribute(attrName, String(value));
+2-2
lib/src/core/charge.ts
···77import type { ChargedRoot, ChargeResult, Scope } from "$types/volt";
88import { mount } from "./binder";
99import { evaluate } from "./evaluator";
1010-import { getComputedAttributes } from "./shared";
1010+import { getComputedAttributes, isNil } from "./shared";
1111import { computed, signal } from "./signal";
12121313/**
···7070 try {
7171 const stateData = JSON.parse(stateAttr);
72727373- if (typeof stateData !== "object" || stateData === null || Array.isArray(stateData)) {
7373+ if (typeof stateData !== "object" || isNil(stateData) || Array.isArray(stateData)) {
7474 console.error(`data-volt-state must be a JSON object, got ${typeof stateData}:`, el);
7575 } else {
7676 for (const [key, value] of Object.entries(stateData)) {
+3-3
lib/src/core/evaluator.ts
···66 */
7788import type { Dep, Scope } from "$types/volt";
99-import { findScopedSignal, isSignal } from "./shared";
99+import { findScopedSignal, isNil, isSignal } from "./shared";
10101111const DANGEROUS_PROPERTIES = new Set(["__proto__", "prototype", "constructor"]);
1212···547547 }
548548549549 private callMethod(object: unknown, methodName: string, args: unknown[]): unknown {
550550- if (object === null || object === undefined) {
550550+ if (isNil(object)) {
551551 throw new Error(`Cannot call method '${methodName}' on ${object}`);
552552 }
553553···794794 }
795795796796 private getMember(object: unknown, key: unknown): unknown {
797797- if (object === null || object === undefined) {
797797+ if (isNil(object)) {
798798 return undefined;
799799 }
800800
+10-3
lib/src/core/shared.ts
···11-import type { Optional } from "$types/helpers";
11+import type { None, Optional } from "$types/helpers";
22import type { Dep, Scope, Signal } from "$types/volt";
3344export function kebabToCamel(str: string): string {
55 return str.replaceAll(/-([a-z])/g, (_, letter) => letter.toUpperCase());
66+}
77+88+/**
99+ * Check if a value is null or undefined ({@link None}).
1010+ */
1111+export function isNil(value: unknown): value is None {
1212+ return value === null || value === undefined;
613}
714815export function isSignal(value: unknown): value is Dep {
···2027 let current: unknown = scope;
21282229 for (const part of parts) {
2323- if (current === null || current === undefined) {
3030+ if (isNil(current)) {
2431 return undefined;
2532 }
2633···5764}
58655966/**
6060- * Sleep for a specified duration
6767+ * Sleep for a specified duration in ms
6168 */
6269export function sleep(ms: number): Promise<void> {
6370 return new Promise((resolve) => setTimeout(resolve, ms));
+2-2
lib/src/core/ssr.ts
···1313import { computed, signal } from "./signal";
14141515/**
1616- * Serialize a scope object to JSON for embedding in server-rendered HTML
1616+ * Serialize a {@link Scope} object to JSON for embedding in server-rendered HTML
1717 *
1818 * Converts all signals in the scope to their plain values.
1919 * Computed signals are serialized as their current values.
···161161 try {
162162 const stateData = JSON.parse(stateAttr);
163163164164- if (typeof stateData !== "object" || stateData === null || Array.isArray(stateData)) {
164164+ if (typeof stateData !== "object" || isSignal(stateData) || Array.isArray(stateData)) {
165165 console.error(`data-volt-state must be a JSON object, got ${typeof stateData}:`, el);
166166 } else {
167167 for (const [key, value] of Object.entries(stateData)) {