a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: generated docs build: updated aliases

+709 -144
+9 -9
README.md
··· 2 2 3 3 ## Philosophy/Goals 4 4 5 - - Behavior is declared via `data-x-*` attributes. 5 + - Behavior is declared via `data-volt-*` attributes. 6 6 - HTML drives the UI, not components. 7 7 - Core under **20 KB gzipped**, zero dependencies. 8 8 - Signals update the DOM directly without a virtual DOM. ··· 20 20 21 21 ## Concepts 22 22 23 - | Concept | Description | 24 - | -------- | ---------------------------------------------------------------------------------------- | 25 - | Signals | Reactive primitives that automatically update DOM bindings when changed. | 26 - | Bindings | `data-x-text`, `data-x-html`, `data-x-class` connect attributes or text to expressions. | 27 - | Actions | `data-x-on-click`, `data-x-on-input`, etc. attach event handlers declaratively. | 28 - | Streams | `data-x-stream="/events"` listens for SSE or WebSocket updates and applies JSON patches. | 29 - | Plugins | Modular extensions (`data-x-persist`, `data-x-animate`, etc.) that enhance the core. | 23 + | Concept | Description | 24 + | -------- | ------------------------------------------------------------------------------------------------- | 25 + | Signals | Reactive primitives that automatically update DOM bindings when changed. | 26 + | Bindings | `data-volt-text`, `data-volt-html`, `data-volt-class` connect attributes or text to expressions. | 27 + | Actions | `data-volt-on-click`, `data-volt-on-input`, etc. attach event handlers declaratively. | 28 + | Streams | `data-volt-stream="/events"` listens for SSE or WebSocket updates and applies JSON patches. | 29 + | Plugins | Modular extensions (`data-volt-persist`, `data-volt-animate`, etc.) that enhance the core. | 30 30 31 31 ## Project Structure 32 32 ··· 44 44 │ │ ├── patch.ts # JSON patch engine 45 45 │ │ ├── stream.ts # SSE / WebSocket layer 46 46 │ │ ├── plugin.ts # plugin registration API 47 - │ │ └── binder.ts # mounts and binds data-x-* attributes 47 + │ │ └── binder.ts # mounts and binds data-volt-* attributes 48 48 │ └── plugins/ 49 49 │ ├── persist.ts 50 50 │ ├── scroll.ts
+6 -6
cli/tests/docs.test.ts
··· 1 + import { getLibSrcPath } from "$utils/paths.js"; 1 2 import { readFile } from "node:fs/promises"; 2 - import { join } from "node:path"; 3 + import path from "node:path"; 3 4 import { describe, expect, it } from "vitest"; 4 - import { getLibSrcPath } from "../src/utils/paths.js"; 5 5 6 6 describe("docs generation", () => { 7 7 it("should extract function documentation", async () => { 8 8 const srcPath = await getLibSrcPath(); 9 - const testFile = join(srcPath, "core", "signal.ts"); 10 - const content = await readFile(testFile, "utf-8"); 9 + const testFile = path.join(srcPath, "core", "signal.ts"); 10 + const content = await readFile(testFile, "utf8"); 11 11 12 12 expect(content).toContain("Creates a new signal"); 13 13 expect(content).toContain("@param initialValue"); ··· 17 17 18 18 it("should extract interface documentation", async () => { 19 19 const srcPath = await getLibSrcPath(); 20 - const typesFile = join(srcPath, "types", "volt.d.ts"); 21 - const content = await readFile(typesFile, "utf-8"); 20 + const typesFile = path.join(srcPath, "types", "volt.d.ts"); 21 + const content = await readFile(typesFile, "utf8"); 22 22 23 23 expect(content).toContain("interface Signal"); 24 24 expect(content).toContain("interface ComputedSignal");
+7 -7
cli/tests/paths.test.ts
··· 1 - import { existsSync } from "node:fs"; 2 - import { join } from "node:path"; 3 - import { describe, expect, it } from "vitest"; 4 1 import { 5 2 findMonorepoRoot, 6 3 getDocsPath, ··· 8 5 getLibPath, 9 6 getLibSrcPath, 10 7 getLibTestPath, 11 - } from "../src/utils/paths.js"; 8 + } from "$utils/paths.js"; 9 + import { existsSync } from "node:fs"; 10 + import path from "node:path"; 11 + import { describe, expect, it } from "vitest"; 12 12 13 13 describe("path utilities", () => { 14 14 it("should find monorepo root from cli directory", async () => { 15 15 const root = await findMonorepoRoot(process.cwd()); 16 16 17 17 expect(root).toBeTruthy(); 18 - expect(existsSync(join(root, "pnpm-workspace.yaml"))).toBe(true); 18 + expect(existsSync(path.join(root, "pnpm-workspace.yaml"))).toBe(true); 19 19 }); 20 20 21 21 it("should find lib package path", async () => { ··· 23 23 24 24 expect(libPath).toBeTruthy(); 25 25 expect(libPath).toContain("lib"); 26 - expect(existsSync(join(libPath, "package.json"))).toBe(true); 26 + expect(existsSync(path.join(libPath, "package.json"))).toBe(true); 27 27 }); 28 28 29 29 it("should find lib src path", async () => { ··· 49 49 50 50 expect(docsPath).toBeTruthy(); 51 51 expect(docsPath).toContain("docs"); 52 - expect(existsSync(join(docsPath, "package.json"))).toBe(true); 52 + expect(existsSync(path.join(docsPath, "package.json"))).toBe(true); 53 53 }); 54 54 55 55 it("should find examples directory path", async () => {
+5 -5
cli/tests/stats.test.ts
··· 1 + import { getLibSrcPath } from "$utils/paths.js"; 1 2 import { readFile } from "node:fs/promises"; 2 - import { join } from "node:path"; 3 + import path from "node:path"; 3 4 import { describe, expect, it } from "vitest"; 4 - import { getLibSrcPath } from "../src/utils/paths.js"; 5 5 6 6 describe("stats command", () => { 7 7 it("should count lines excluding doc comments", async () => { 8 8 const srcPath = await getLibSrcPath(); 9 - const testDir = join(srcPath, "core"); 10 - const signalFile = join(testDir, "signal.ts"); 11 - const content = await readFile(signalFile, "utf-8"); 9 + const testDir = path.join(srcPath, "core"); 10 + const signalFile = path.join(testDir, "signal.ts"); 11 + const content = await readFile(signalFile, "utf8"); 12 12 13 13 const lines = content.split("\n"); 14 14 let codeLines = 0;
+1 -1
cli/tsconfig.json
··· 23 23 "$console/*": ["./src/console/*"] 24 24 } 25 25 }, 26 - "include": ["src"] 26 + "include": ["src", "tests"] 27 27 }
+49
docs/api/asyncEffect.md
··· 1 + --- 2 + version: 1.0 3 + updated: 2025-10-18 4 + --- 5 + 6 + # asyncEffect 7 + 8 + Async effect system with abort, race protection, debounce, throttle, and error handling 9 + 10 + ## asyncEffect 11 + 12 + Creates an async side effect that runs when dependencies change. 13 + Supports abort signals, race protection, debouncing, throttling, and error handling. 14 + 15 + ```typescript 16 + export function asyncEffect( effectFunction: AsyncEffectFunction, dependencies: Array<Signal<unknown> | ComputedSignal<unknown>>, options: AsyncEffectOptions = {}, ): () => void 17 + ``` 18 + 19 + **Example:** 20 + 21 + ```typescript 22 + // Fetch with abort on cleanup 23 + const query = signal(''); 24 + const cleanup = asyncEffect(async (signal) => { 25 + const response = await fetch(`/api/search?q=${query.get()}`, { signal }); 26 + const data = await response.json(); 27 + results.set(data); 28 + }, [query], { abortable: true }); 29 + 30 + // Debounced search 31 + asyncEffect(async () => { 32 + const response = await fetch(`/api/search?q=${searchQuery.get()}`); 33 + results.set(await response.json()); 34 + }, [searchQuery], { debounce: 300 }); 35 + 36 + // Error handling with retries 37 + asyncEffect(async () => { 38 + const response = await fetch('/api/data'); 39 + if (!response.ok) throw new Error('Failed to fetch'); 40 + data.set(await response.json()); 41 + }, [refreshTrigger], { 42 + retries: 3, 43 + retryDelay: 1000, 44 + onError: (error, retry) => { 45 + console.error('Fetch failed:', error); 46 + // Optionally call retry() to retry immediately 47 + } 48 + }); 49 + ```
+1 -2
docs/api/binder.md
··· 9 9 10 10 ## mount 11 11 12 - Mount Volt.js on a root element and its descendants. 13 - Binds all data-x-* attributes to the provided scope. 12 + Mount Volt.js on a root element and its descendants and binds all data-volt-* attributes to the provided scope. 14 13 Returns a cleanup function to unmount and dispose all bindings. 15 14 16 15 ```typescript
+35
docs/api/charge.md
··· 1 + --- 2 + version: 1.0 3 + updated: 2025-10-18 4 + --- 5 + 6 + # charge 7 + 8 + Charge system (bootstrap) for auto-discovery and initialization of Volt roots 9 + 10 + Handles declarative state initialization via data-volt-state and data-volt-computed 11 + 12 + ## charge 13 + 14 + Discover and mount all Volt roots in the document. 15 + Parses data-volt-state for initial state and data-volt-computed for derived values. 16 + 17 + ```typescript 18 + export function charge(rootSelector = "[data-volt]"): ChargeResult 19 + ``` 20 + 21 + **Example:** 22 + 23 + ```typescript 24 + ```html 25 + <div data-volt data-volt-state='{"count": 0}' data-volt-computed:double="count * 2"> 26 + <p data-volt-text="count"></p> 27 + <p data-volt-text="double"></p> 28 + </div> 29 + ``` 30 + 31 + ```ts 32 + const { cleanup } = charge(); 33 + // Later: cleanup() to unmount all 34 + ``` 35 + ```
+8 -5
docs/api/dom.md
··· 9 9 10 10 ## walkDOM 11 11 12 - Walk the DOM tree and collect all elements with data-x-* attributes. 13 - Returns elements in document order (parent before children). 12 + Walk the DOM tree and collect all elements with data-volt-* attributes in document order (parent before children). 13 + 14 + Skips children of elements with data-volt-for or data-volt-if since those will be processed when the parent element is cloned and mounted. 14 15 15 16 ```typescript 16 17 export function walkDOM(root: Element): Element[] ··· 18 19 19 20 ## hasVoltAttribute 20 21 21 - Check if an element has any data-x-* attributes. 22 + Check if an element has any data-volt-* attributes. 22 23 23 24 ```typescript 24 25 export function hasVoltAttribute(element: Element): boolean ··· 26 27 27 28 ## getVoltAttributes 28 29 29 - Get all data-x-* attributes from an element. 30 + Get all data-volt-\* attributes from an element. 31 + Excludes charge metadata attributes (state, computed:*) that are processed separately. 30 32 31 33 ```typescript 32 34 export function getVoltAttributes(element: Element): Map<string, string> ··· 60 62 ## parseClassBinding 61 63 62 64 Parse a class binding expression. 63 - Supports both string values ("active") and object notation ({active: true}). 65 + Supports string values ("active"), object notation ({active: true}), 66 + and other primitives (true, false, numbers) which are converted to strings. 64 67 65 68 ```typescript 66 69 export function parseClassBinding(value: unknown): Map<string, boolean>
+17 -11
docs/api/evaluator.md
··· 5 5 6 6 # evaluator 7 7 8 - Safe expression evaluation of simple expressions without using eval() for bindings 8 + Safe expression evaluation with operators support 9 9 10 - ## Scope 10 + Implements a recursive descent parser for expressions without using eval() 11 11 12 - Safe expression evaluation of simple expressions without using eval() for bindings 12 + ## isSignal 13 13 14 14 ```typescript 15 - Record<string, unknown> 15 + export function isSignal(value: unknown): value is Dep 16 16 ``` 17 17 18 18 ## evaluate 19 19 20 - Evaluate a simple expression against a scope object. 21 - Supports: 22 - - Property access: "count", "user.name", "items.length" 23 - - Simple literals: "true", "false", "null", "undefined" 24 - - Numbers: "42", "3.14" 25 - - Strings: "'hello'", '"world"' 20 + Evaluate an expression against a scope object. 21 + 22 + Supports literals, property access, operators, and member access. 26 23 27 24 ```typescript 28 - export function evaluate(expression: string, scope: Scope): unknown 25 + export function evaluate(expr: string, scope: Scope): unknown 26 + ``` 27 + 28 + ## extractDependencies 29 + 30 + Extract all signal dependencies from an expression by finding identifiers 31 + that correspond to signals in the scope. 32 + 33 + ```typescript 34 + export function extractDependencies(expr: string, scope: Scope): Array<Dep> 29 35 ```
+135
docs/api/lifecycle.md
··· 1 + --- 2 + version: 1.0 3 + updated: 2025-10-18 4 + --- 5 + 6 + # lifecycle 7 + 8 + Global lifecycle hook system for Volt.js 9 + Provides beforeMount, afterMount, beforeUnmount, and afterUnmount hooks 10 + 11 + ## registerGlobalHook 12 + 13 + Register a global lifecycle hook. 14 + Global hooks run for every mount/unmount operation in the application. 15 + 16 + ```typescript 17 + export function registerGlobalHook(name: GlobalHookName, cb: MountHookCallback | UnmountHookCallback): () => void 18 + ``` 19 + 20 + **Example:** 21 + 22 + ```typescript 23 + // Log every mount operation 24 + registerGlobalHook('beforeMount', (root, scope) => { 25 + console.log('Mounting', root, 'with scope', scope); 26 + }); 27 + 28 + // Track mounted elements 29 + const mountedElements = new Set<Element>(); 30 + registerGlobalHook('afterMount', (root) => { 31 + mountedElements.add(root); 32 + }); 33 + registerGlobalHook('beforeUnmount', (root) => { 34 + mountedElements.delete(root); 35 + }); 36 + ``` 37 + 38 + ## unregisterGlobalHook 39 + 40 + Unregister a global lifecycle hook. 41 + 42 + ```typescript 43 + export function unregisterGlobalHook(name: GlobalHookName, cb: MountHookCallback | UnmountHookCallback): boolean 44 + ``` 45 + 46 + ## clearGlobalHooks 47 + 48 + Clear all global hooks for a specific lifecycle event. 49 + 50 + ```typescript 51 + export function clearGlobalHooks(name: GlobalHookName): void 52 + ``` 53 + 54 + ## clearAllGlobalHooks 55 + 56 + ```typescript 57 + export function clearAllGlobalHooks(): void 58 + ``` 59 + 60 + ## getGlobalHooks 61 + 62 + Get all registered hooks for a specific lifecycle event. 63 + Used internally by the binder system. 64 + 65 + ```typescript 66 + export function getGlobalHooks(name: GlobalHookName): Array<MountHookCallback | UnmountHookCallback> 67 + ``` 68 + 69 + ## executeGlobalHooks 70 + 71 + Execute all registered hooks for a lifecycle event. 72 + Used internally by the binder system. 73 + 74 + ```typescript 75 + export function executeGlobalHooks(hookName: GlobalHookName, root: Element, scope?: Scope): void 76 + ``` 77 + 78 + ## registerElementHook 79 + 80 + Register a per-element lifecycle hook. 81 + These hooks are specific to individual elements. 82 + 83 + ```typescript 84 + export function registerElementHook(element: Element, hookType: "mount" | "unmount", cb: () => void): void 85 + ``` 86 + 87 + ## notifyElementMounted 88 + 89 + Notify that an element has been mounted. 90 + Executes all registered onMount callbacks for the element. 91 + 92 + ```typescript 93 + export function notifyElementMounted(element: Element): void 94 + ``` 95 + 96 + ## notifyElementUnmounted 97 + 98 + Notify that an element is being unmounted. 99 + Executes all registered onUnmount callbacks for the element. 100 + 101 + ```typescript 102 + export function notifyElementUnmounted(element: Element): void 103 + ``` 104 + 105 + ## notifyBindingCreated 106 + 107 + Notify that a binding has been created on an element. 108 + 109 + ```typescript 110 + export function notifyBindingCreated(element: Element, name: string): void 111 + ``` 112 + 113 + ## notifyBindingDestroyed 114 + 115 + Notify that a binding has been destroyed on an element. 116 + 117 + ```typescript 118 + export function notifyBindingDestroyed(element: Element, name: string): void 119 + ``` 120 + 121 + ## isElementMounted 122 + 123 + Check if an element is currently mounted. 124 + 125 + ```typescript 126 + export function isElementMounted(element: Element): boolean 127 + ``` 128 + 129 + ## getElementBindings 130 + 131 + Get all bindings on an element. 132 + 133 + ```typescript 134 + export function getElementBindings(element: Element): string[] 135 + ```
+33
docs/api/persist.md
··· 1 + --- 2 + version: 1.0 3 + updated: 2025-10-18 4 + --- 5 + 6 + # persist 7 + 8 + Persistence plugin for synchronizing signals with storage 9 + Supports localStorage, sessionStorage, IndexedDB, and custom adapters 10 + 11 + ## registerStorageAdapter 12 + 13 + Register a custom storage adapter. 14 + 15 + ```typescript 16 + export function registerStorageAdapter(name: string, adapter: StorageAdapter): void 17 + ``` 18 + 19 + ## persistPlugin 20 + 21 + Persist plugin handler. 22 + Synchronizes signal values with persistent storage. 23 + 24 + Syntax: data-volt-persist="signalPath:storageType" 25 + Examples: 26 + - data-volt-persist="count:local" 27 + - data-volt-persist="formData:session" 28 + - data-volt-persist="userData:indexeddb" 29 + - data-volt-persist="settings:customAdapter" 30 + 31 + ```typescript 32 + export function persistPlugin(context: PluginContext, value: string): void 33 + ```
+74
docs/api/plugin.md
··· 1 + --- 2 + version: 1.0 3 + updated: 2025-10-18 4 + --- 5 + 6 + # plugin 7 + 8 + Plugin system for extending Volt.js with custom bindings 9 + 10 + ## registerPlugin 11 + 12 + Register a custom plugin with a given name. 13 + Plugins extend Volt.js with custom data-volt-* attribute bindings. 14 + 15 + ```typescript 16 + export function registerPlugin(name: string, handler: PluginHandler): void 17 + ``` 18 + 19 + **Example:** 20 + 21 + ```typescript 22 + registerPlugin('tooltip', (context, value) => { 23 + const tooltip = document.createElement('div'); 24 + tooltip.className = 'tooltip'; 25 + tooltip.textContent = value; 26 + context.element.addEventListener('mouseenter', () => { 27 + document.body.appendChild(tooltip); 28 + }); 29 + context.element.addEventListener('mouseleave', () => { 30 + tooltip.remove(); 31 + }); 32 + context.addCleanup(() => tooltip.remove()); 33 + }); 34 + ``` 35 + 36 + ## getPlugin 37 + 38 + Get a plugin handler by name. 39 + 40 + ```typescript 41 + export function getPlugin(name: string): PluginHandler | undefined 42 + ``` 43 + 44 + ## hasPlugin 45 + 46 + Check if a plugin is registered. 47 + 48 + ```typescript 49 + export function hasPlugin(name: string): boolean 50 + ``` 51 + 52 + ## unregisterPlugin 53 + 54 + Unregister a plugin by name. 55 + 56 + ```typescript 57 + export function unregisterPlugin(name: string): boolean 58 + ``` 59 + 60 + ## getRegisteredPlugins 61 + 62 + Get all registered plugin names. 63 + 64 + ```typescript 65 + export function getRegisteredPlugins(): string[] 66 + ``` 67 + 68 + ## clearPlugins 69 + 70 + Clear all registered plugins. 71 + 72 + ```typescript 73 + export function clearPlugins(): void 74 + ```
+25
docs/api/scroll.md
··· 1 + --- 2 + version: 1.0 3 + updated: 2025-10-18 4 + --- 5 + 6 + # scroll 7 + 8 + Scroll plugin for managing scroll behavior 9 + Supports position restoration, scroll-to, scroll spy, and smooth scrolling 10 + 11 + ## scrollPlugin 12 + 13 + Scroll plugin handler. 14 + Manages various scroll-related behaviors. 15 + 16 + Syntax: data-volt-scroll="mode:signalPath" 17 + Modes: 18 + - restore:signalPath - Save/restore scroll position 19 + - scrollTo:signalPath - Scroll to element when signal changes 20 + - spy:signalPath - Update signal when element is visible 21 + - smooth:signalPath - Enable smooth scrolling behavior 22 + 23 + ```typescript 24 + export function scrollPlugin(context: PluginContext, value: string): void 25 + ```
+7 -9
docs/api/signal.md
··· 5 5 6 6 # signal 7 7 8 - A reactive primitive that notifies subscribers when its value changes. 8 + Creates a new signal with the given initial value. 9 9 10 - ## Signal 10 + @param initialValue - The initial value of the signal 11 + @returns A Signal object with get, set, and subscribe methods 11 12 12 - A reactive primitive that notifies subscribers when its value changes. 13 - 14 - ## ComputedSignal 15 - 16 - A computed signal that derives its value from other signals. 13 + @example 14 + const count = signal(0); 15 + count.subscribe(value => console.log('Count:', value)); 16 + count.set(1); // Logs: Count: 1 17 17 18 18 ## signal 19 19 20 20 Creates a new signal with the given initial value. 21 - Signals are reactive primitives that automatically notify subscribers when changed. 22 21 23 22 ```typescript 24 23 export function signal<T>(initialValue: T): Signal<T> ··· 54 53 ## effect 55 54 56 55 Creates a side effect that runs when dependencies change. 57 - Effects run immediately on creation and whenever dependencies update. 58 56 59 57 ```typescript 60 58 export function effect( effectFunction: () => void | (() => void), dependencies: Array<Signal<unknown> | ComputedSignal<unknown>>, ): () => void
+24
docs/api/url.md
··· 1 + --- 2 + version: 1.0 3 + updated: 2025-10-18 4 + --- 5 + 6 + # url 7 + 8 + URL plugin for synchronizing signals with URL parameters and hash routing 9 + Supports one-way read, bidirectional sync, and hash-based routing 10 + 11 + ## urlPlugin 12 + 13 + URL plugin handler. 14 + Synchronizes signal values with URL parameters and hash. 15 + 16 + Syntax: data-volt-url="mode:signalPath" 17 + Modes: 18 + - read:signalPath - Read URL param into signal on mount (one-way) 19 + - sync:signalPath - Bidirectional sync between signal and URL param 20 + - hash:signalPath - Sync with hash portion for routing 21 + 22 + ```typescript 23 + export function urlPlugin(context: PluginContext, value: string): void 24 + ```
+188
docs/api/volt.d.md
··· 1 + --- 2 + version: 1.0 3 + updated: 2025-10-18 4 + --- 5 + 6 + # volt.d 7 + 8 + Context object available to all bindings 9 + 10 + ## CleanupFunction 11 + 12 + ```typescript 13 + () => void 14 + ``` 15 + 16 + ## Scope 17 + 18 + ```typescript 19 + Record<string, unknown> 20 + ``` 21 + 22 + ## BindingContext 23 + 24 + Context object available to all bindings 25 + 26 + ```typescript 27 + { element: Element; scope: Scope; cleanups: CleanupFunction[] } 28 + ``` 29 + 30 + ## PluginContext 31 + 32 + Context object provided to plugin handlers. 33 + Contains utilities and references for implementing custom bindings. 34 + 35 + ### Members 36 + 37 + - **element**: `Element` 38 + The DOM element the plugin is bound to 39 + - **scope**: `Scope` 40 + The scope object containing signals and data 41 + - **lifecycle**: `PluginLifecycle` 42 + Lifecycle hooks for plugin-specific mount/unmount behavior 43 + 44 + ## PluginHandler 45 + 46 + Plugin handler function signature. 47 + Receives context and the attribute value, performs binding setup. 48 + 49 + ```typescript 50 + (context: PluginContext, value: string) => void 51 + ``` 52 + 53 + ## Signal 54 + 55 + A reactive primitive that notifies subscribers when its value changes. 56 + 57 + ## ComputedSignal 58 + 59 + A computed signal that derives its value from other signals. 60 + 61 + ## StorageAdapter 62 + 63 + Storage adapter interface for custom persistence backends 64 + 65 + ## ChargedRoot 66 + 67 + Information about a mounted Volt root after charging 68 + 69 + element: The root element that was mounted 70 + scope: The reactive scope created for this root 71 + cleanup: Cleanup function to unmount this root 72 + 73 + ```typescript 74 + { element: Element; scope: Scope; cleanup: CleanupFunction } 75 + ``` 76 + 77 + ## ChargeResult 78 + 79 + Result of charging Volt roots 80 + 81 + roots: Array of all charged roots 82 + cleanup: Cleanup function to unmount all roots 83 + 84 + ```typescript 85 + { roots: ChargedRoot[]; cleanup: CleanupFunction } 86 + ``` 87 + 88 + ## Dep 89 + 90 + ```typescript 91 + { get: () => unknown; subscribe: (callback: (value: unknown) => void) => () => void } 92 + ``` 93 + 94 + ## AsyncEffectOptions 95 + 96 + Options for configuring async effects 97 + 98 + ### Members 99 + 100 + - **abortable**: `boolean` 101 + Enable automatic AbortController integration. 102 + When true, provides an AbortSignal to the effect function for canceling async operations. 103 + - **debounce**: `number` 104 + Debounce delay in milliseconds. 105 + Effect execution is delayed until this duration has passed without dependencies changing. 106 + - **throttle**: `number` 107 + Throttle delay in milliseconds. 108 + Effect execution is rate-limited to at most once per this duration. 109 + - **onError**: `(error: Error, retry: () => void) => void` 110 + Error handler for async effect failures. 111 + Receives the error and a retry function. 112 + - **retries**: `number` 113 + Number of automatic retry attempts on error. 114 + Defaults to 0 (no retries). 115 + - **retryDelay**: `number` 116 + Delay in milliseconds between retry attempts. 117 + Defaults to 0 (immediate retry). 118 + 119 + ## AsyncEffectFunction 120 + 121 + Async effect function signature. 122 + Receives an optional AbortSignal when abortable option is enabled. 123 + Can return a cleanup function or a Promise that resolves to a cleanup function. 124 + 125 + ```typescript 126 + (signal?: AbortSignal) => Promise<void | (() => void)> 127 + ``` 128 + 129 + ## LifecycleHookCallback 130 + 131 + Lifecycle hook callback types 132 + 133 + ```typescript 134 + () => void 135 + ``` 136 + 137 + ## MountHookCallback 138 + 139 + ```typescript 140 + (root: Element, scope: Scope) => void 141 + ``` 142 + 143 + ## UnmountHookCallback 144 + 145 + ```typescript 146 + (root: Element) => void 147 + ``` 148 + 149 + ## ElementMountHookCallback 150 + 151 + ```typescript 152 + (element: Element, scope: Scope) => void 153 + ``` 154 + 155 + ## ElementUnmountHookCallback 156 + 157 + ```typescript 158 + (element: Element) => void 159 + ``` 160 + 161 + ## BindingHookCallback 162 + 163 + ```typescript 164 + (element: Element, bindingName: string) => void 165 + ``` 166 + 167 + ## GlobalHookName 168 + 169 + Lifecycle hook names 170 + 171 + ```typescript 172 + "beforeMount" | "afterMount" | "beforeUnmount" | "afterUnmount" 173 + ``` 174 + 175 + ## PluginLifecycle 176 + 177 + Extended plugin context with lifecycle hooks 178 + 179 + ### Members 180 + 181 + - **onMount**: `(callback: LifecycleHookCallback) => void` 182 + Register a callback to run when the plugin is initialized for an element 183 + - **onUnmount**: `(callback: LifecycleHookCallback) => void` 184 + Register a callback to run when the element is being unmounted 185 + - **beforeBinding**: `(callback: LifecycleHookCallback) => void` 186 + Register a callback to run before the binding is created 187 + - **afterBinding**: `(callback: LifecycleHookCallback) => void` 188 + Register a callback to run after the binding is created
+11 -23
docs/css/semantics.md
··· 21 21 - `--font-size-3xl`: `1.802rem` 22 22 - `--font-size-4xl`: `2.027rem` 23 23 - `--font-size-5xl`: `2.566rem` 24 - - `--font-sans`: `"Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif` 25 - - `--font-serif`: `"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif` 26 - - `--font-mono`: `"SF Mono", "Cascadia Code", "Fira Code", "Roboto Mono", Consolas, monospace` 24 + - `--font-sans`: `"Inter", sans-serif` 25 + - `--font-serif`: `"Libre Baskerville", serif` 26 + - `--font-mono`: `"Google Sans Code", monospace` 27 27 - `--line-height-tight`: `1.25` 28 28 - `--line-height-base`: `1.6` 29 29 - `--line-height-relaxed`: `1.8` ··· 124 124 125 125 ### `*, *::before, *::after` 126 126 127 - Modern CSS reset with sensible defaults 127 + CSS reset 128 128 129 129 ### `html` 130 130 ··· 148 148 149 149 ### `h1 + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p` 150 150 151 - First paragraph after headings - No top margin Common convention in academic typography 151 + First paragraph after headings - No top margin Inspired by tufte.css 152 152 153 153 ### `a` 154 154 155 - Links - Accessible and distinctive Uses accent color with underline for clarity 155 + Links Uses accent color with underline for clarity 156 156 157 157 ### `em` 158 158 ··· 172 172 173 173 ### `ul, ol` 174 174 175 - List spacing and indentation Nested lists inherit proper spacing 175 + List spacing and indentation Nested lists inherit spacing 176 176 177 177 ### `li` 178 178 ··· 208 208 209 209 ### `code` 210 210 211 - Inline code Monospace font with subtle background for distinction 211 + Inline code Monospace font with subtle background 212 212 213 213 ### `kbd` 214 214 ··· 228 228 229 229 ### `hr` 230 230 231 - Section dividers Centered decorative element with breathing room 231 + Section dividers Centered decorative element 232 232 233 233 ### `table` 234 234 ··· 236 236 237 237 ### `thead` 238 238 239 - Table header styling Bold text with bottom border for separation 239 + Table header styling Bold text with bottom border 240 240 241 241 ### `td` 242 242 ··· 296 296 297 297 ### `video, audio` 298 298 299 - Video and audio Responsive and accessible 300 - 301 - ### `canvas, svg` 302 - 303 - Canvas and SVG 304 - 305 - ### `iframe` 306 - 307 - iframe - Responsive wrapper 299 + Video and audio 308 300 309 301 ### `article, section` 310 302 ··· 317 309 ### `header` 318 310 319 311 Header and Footer 320 - 321 - ### `nav` 322 - 323 - Nav Navigation menus 324 312 325 313 ### `details` 326 314
+12 -12
docs/overview.md
··· 6 6 7 7 Volt.js is a lightweight, hypermedia based reactive framework for building declarative UIs. 8 8 9 - It combines HTML-driven behavior via `data-x-*` attributes with signal-based reactivity. 9 + It combines HTML-driven behavior via `data-volt-*` attributes with signal-based reactivity. 10 10 11 11 ## Architecture 12 12 ··· 33 33 34 34 ### Binding System 35 35 36 - Bindings connect signals to DOM via `data-x-*` attributes: 36 + Bindings connect signals to DOM via `data-volt-*` attributes: 37 37 38 38 ```html 39 39 <div id="app"> 40 - <p data-x-text="count">0</p> 41 - <button data-x-on-click="increment">+</button> 42 - <div data-x-if="isPositive">Positive</div> 40 + <p data-volt-text="count">0</p> 41 + <button data-volt-on-click="increment">+</button> 42 + <div data-volt-if="isPositive">Positive</div> 43 43 </div> 44 44 ``` 45 45 ··· 53 53 54 54 Core bindings: 55 55 56 - - `data-x-text` - Update text content 57 - - `data-x-html` - Update HTML content 58 - - `data-x-class` - Toggle CSS classes 59 - - `data-x-on-*` - Attach event handlers 60 - - `data-x-if` - Conditional rendering 61 - - `data-x-for` - List rendering 56 + - `data-volt-text` - Update text content 57 + - `data-volt-html` - Update HTML content 58 + - `data-volt-class` - Toggle CSS classes 59 + - `data-volt-on-*` - Attach event handlers 60 + - `data-volt-if` - Conditional rendering 61 + - `data-volt-for` - List rendering 62 62 63 63 ### Plugin System 64 64 65 - Extend functionality via custom `data-x-*` bindings: 65 + Extend functionality via custom `data-volt-*` bindings: 66 66 67 67 ```js 68 68 import { registerPlugin } from 'volt';
+28 -28
docs/plugin-spec.md
··· 2 2 3 3 ## Overview 4 4 5 - The plugin system enables extending the framework with custom `data-x-*` attribute bindings. 5 + The plugin system enables extending the framework with custom `data-volt-*` attribute bindings. 6 6 7 7 Plugins follow the same binding patterns as core bindings (text, html, class, events) but can implement specialized behaviors like persistence, scrolling, and URL synchronization. 8 8 ··· 34 34 registerPlugin(name: string, handler: PluginHandler): void 35 35 ``` 36 36 37 - The plugin name becomes the `data-x-*` attribute suffix. For example, registering a plugin named `"tooltip"` enables `data-x-tooltip` attributes. 37 + The plugin name becomes the `data-volt-*` attribute suffix. For example, registering a plugin named `"tooltip"` enables `data-volt-tooltip` attributes. 38 38 39 39 ### Plugin Handler 40 40 ··· 100 100 101 101 Volt.js ships with three built-in plugins that must be explicitly registered. 102 102 103 - ### data-x-persist 103 + ### data-volt-persist 104 104 105 105 Synchronizes signal values with persistent storage (`localStorage`, `sessionStorage`, `IndexedDB`). 106 106 107 107 **Syntax:** 108 108 109 109 ```html 110 - <input data-x-persist="signalName:storageType" /> 110 + <input data-volt-persist="signalName:storageType" /> 111 111 ``` 112 112 113 113 **Storage Types:** ··· 127 127 128 128 ```html 129 129 <!-- Persist counter to localStorage --> 130 - <div data-x-text="count" data-x-persist="count:local"></div> 130 + <div data-volt-text="count" data-volt-persist="count:local"></div> 131 131 132 132 <!-- Persist form state to sessionStorage --> 133 - <input data-x-on-input="updateForm" data-x-persist="formData:session" /> 133 + <input data-volt-on-input="updateForm" data-volt-persist="formData:session" /> 134 134 135 135 <!-- Persist large dataset to IndexedDB --> 136 - <div data-x-persist="userData:indexeddb"></div> 136 + <div data-volt-persist="userData:indexeddb"></div> 137 137 ``` 138 138 139 139 **Custom Storage Adapters:** ··· 152 152 }); 153 153 ``` 154 154 155 - ### data-x-scroll 155 + ### data-volt-scroll 156 156 157 157 Manages scroll behavior including position restoration, programmatic scrolling, scroll spy, and smooth scrolling. 158 158 ··· 160 160 161 161 ```html 162 162 <!-- Scroll position restoration --> 163 - <div data-x-scroll="restore:position"></div> 163 + <div data-volt-scroll="restore:position"></div> 164 164 165 165 <!-- Scroll to element when signal changes --> 166 - <div data-x-scroll="scrollTo:targetId"></div> 166 + <div data-volt-scroll="scrollTo:targetId"></div> 167 167 168 168 <!-- Scroll spy (updates signal when in viewport) --> 169 - <div data-x-scroll="spy:isVisible"></div> 169 + <div data-volt-scroll="spy:isVisible"></div> 170 170 171 171 <!-- Smooth scroll behavior --> 172 - <div data-x-scroll="smooth:true"></div> 172 + <div data-volt-scroll="smooth:true"></div> 173 173 ``` 174 174 175 175 **Behaviors:** ··· 177 177 **Position Restoration:** 178 178 179 179 ```html 180 - <div id="content" data-x-scroll="restore:scrollPos"> 180 + <div id="content" data-volt-scroll="restore:scrollPos"> 181 181 <!-- scroll position saved on scroll, restored on mount --> 182 182 </div> 183 183 ``` ··· 187 187 **Scroll-To:** 188 188 189 189 ```html 190 - <button data-x-on-click="scrollToSection.set('section2')">Go to Section 2</button> 191 - <div id="section2" data-x-scroll="scrollTo:scrollToSection"></div> 190 + <button data-volt-on-click="scrollToSection.set('section2')">Go to Section 2</button> 191 + <div id="section2" data-volt-scroll="scrollTo:scrollToSection"></div> 192 192 ``` 193 193 194 194 Scrolls to element when the specified signal changes to match element's ID or selector. ··· 197 197 198 198 ```html 199 199 <nav> 200 - <a data-x-class="{ active: section1Visible }">Section 1</a> 201 - <a data-x-class="{ active: section2Visible }">Section 2</a> 200 + <a data-volt-class="{ active: section1Visible }">Section 1</a> 201 + <a data-volt-class="{ active: section2Visible }">Section 2</a> 202 202 </nav> 203 - <div data-x-scroll="spy:section1Visible"></div> 204 - <div data-x-scroll="spy:section2Visible"></div> 203 + <div data-volt-scroll="spy:section1Visible"></div> 204 + <div data-volt-scroll="spy:section2Visible"></div> 205 205 ``` 206 206 207 207 Updates signal with boolean visibility state using Intersection Observer. ··· 209 209 **Smooth Scrolling:** 210 210 211 211 ```html 212 - <div data-x-scroll="smooth:behavior"></div> 212 + <div data-volt-scroll="smooth:behavior"></div> 213 213 ``` 214 214 215 215 Enables smooth scrolling with configurable behavior from signal. 216 216 217 - ### data-x-url 217 + ### data-volt-url 218 218 219 219 Synchronizes signal values with URL parameters and hash-based routing. 220 220 ··· 222 222 223 223 ```html 224 224 <!-- One-way: Read URL param into signal on mount --> 225 - <input data-x-url="read:searchQuery" /> 225 + <input data-volt-url="read:searchQuery" /> 226 226 227 227 <!-- Bidirectional: Keep URL and signal in sync --> 228 - <input data-x-url="sync:filter" /> 228 + <input data-volt-url="sync:filter" /> 229 229 230 230 <!-- Hash-based routing --> 231 - <div data-x-url="hash:currentRoute"></div> 231 + <div data-volt-url="hash:currentRoute"></div> 232 232 ``` 233 233 234 234 **Behaviors:** ··· 237 237 238 238 ```html 239 239 <!-- Initialize signal from ?tab=profile --> 240 - <div data-x-url="read:tab"></div> 240 + <div data-volt-url="read:tab"></div> 241 241 ``` 242 242 243 243 Reads URL parameter on mount and sets signal value. Signal changes do not update URL. ··· 246 246 247 247 ```html 248 248 <!-- Keep ?search=query in sync with searchQuery signal --> 249 - <input data-x-on-input="handleSearch" data-x-url="sync:searchQuery" /> 249 + <input data-volt-on-input="handleSearch" data-volt-url="sync:searchQuery" /> 250 250 ``` 251 251 252 252 Changes to signal update URL parameter, changes to URL update signal. Uses History API for clean URLs. ··· 255 255 256 256 ```html 257 257 <!-- Sync with #/page/about --> 258 - <div data-x-url="hash:route"></div> 259 - <div data-x-text="route === '/page/about' ? 'About Page' : 'Home'"></div> 258 + <div data-volt-url="hash:route"></div> 259 + <div data-volt-text="route === '/page/about' ? 'About Page' : 'Home'"></div> 260 260 ``` 261 261 262 262 Keeps hash portion of URL in sync with signal. Useful for client-side routing.
+13 -13
lib/index.html
··· 22 22 <body> 23 23 <div id="app"> 24 24 <header> 25 - <h1 data-x-text="message">Loading...</h1> 25 + <h1 data-volt-text="message">Loading...</h1> 26 26 <p>A reactive framework demo powered by Volt.js</p> 27 27 </header> 28 28 ··· 30 30 <section> 31 31 <h2>Event Bindings & Computed Values</h2> 32 32 <p> 33 - Count: <strong data-x-text="count">0</strong><br /> 34 - Doubled: <strong data-x-text="doubled">0</strong> 33 + Count: <strong data-volt-text="count">0</strong><br /> 34 + Doubled: <strong data-volt-text="doubled">0</strong> 35 35 </p> 36 - <button data-x-on-click="increment">Increment</button> 37 - <button data-x-on-click="decrement">Decrement</button> 38 - <button data-x-on-click="reset">Reset</button> 39 - <button data-x-on-click="updateMessage">Update Message</button> 36 + <button data-volt-on-click="increment">Increment</button> 37 + <button data-volt-on-click="decrement">Decrement</button> 38 + <button data-volt-on-click="reset">Reset</button> 39 + <button data-volt-on-click="updateMessage">Update Message</button> 40 40 </section> 41 41 42 42 <section> 43 43 <h2>Form Input</h2> 44 - <input type="text" data-x-on-input="handleInput" placeholder="Type something..." /> 45 - <p>You typed: <strong data-x-text="inputValue">nothing yet</strong></p> 44 + <input type="text" data-volt-on-input="handleInput" placeholder="Type something..." /> 45 + <p>You typed: <strong data-volt-text="inputValue">nothing yet</strong></p> 46 46 </section> 47 47 48 48 <section> 49 49 <h2>Class Bindings</h2> 50 - <p data-x-class="classes">This text has dynamic classes applied.</p> 50 + <p data-volt-class="classes">This text has dynamic classes applied.</p> 51 51 <p> 52 - Active: <span data-x-text="isActive">false</span> 52 + Active: <span data-volt-text="isActive">false</span> 53 53 </p> 54 - <button data-x-on-click="toggleActive">Toggle Active</button> 54 + <button data-volt-on-click="toggleActive">Toggle Active</button> 55 55 </section> 56 56 57 57 <section> 58 58 <h2>HTML Binding</h2> 59 - <div data-x-html="'<em>This is rendered as HTML</em>'">Fallback content</div> 59 + <div data-volt-html="'<em>This is rendered as HTML</em>'">Fallback content</div> 60 60 </section> 61 61 </article> 62 62 </div>
+1 -1
lib/src/core/asyncEffect.ts
··· 2 2 * Async effect system with abort, race protection, debounce, throttle, and error handling 3 3 */ 4 4 5 - import type { AsyncEffectFunction, AsyncEffectOptions, ComputedSignal, Signal } from "../types/volt"; 5 + import type { AsyncEffectFunction, AsyncEffectOptions, ComputedSignal, Signal } from "$types/volt"; 6 6 7 7 /** 8 8 * Creates an async side effect that runs when dependencies change.
+1 -1
lib/src/core/binder.ts
··· 2 2 * Binder system for mounting and managing Volt.js bindings 3 3 */ 4 4 5 - import type { BindingContext, CleanupFunction, PluginContext, Scope, Signal } from "../types/volt"; 5 + import type { BindingContext, CleanupFunction, PluginContext, Scope, Signal } from "$types/volt"; 6 6 import { getVoltAttributes, parseClassBinding, setHTML, setText, toggleClass, walkDOM } from "./dom"; 7 7 import { evaluate, extractDependencies, isSignal } from "./evaluator"; 8 8 import { executeGlobalHooks, notifyBindingCreated, notifyElementMounted, notifyElementUnmounted } from "./lifecycle";
+1 -1
lib/src/core/charge.ts
··· 4 4 * Handles declarative state initialization via data-volt-state and data-volt-computed 5 5 */ 6 6 7 - import type { ChargedRoot, ChargeResult, Scope } from "../types/volt"; 7 + import type { ChargedRoot, ChargeResult, Scope } from "$types/volt"; 8 8 import { mount } from "./binder"; 9 9 import { evaluate, extractDependencies } from "./evaluator"; 10 10 import { computed, signal } from "./signal";
+3 -3
lib/src/core/plugin.ts
··· 2 2 * Plugin system for extending Volt.js with custom bindings 3 3 */ 4 4 5 - import type { PluginHandler } from "../types/volt"; 5 + import type { PluginHandler } from "$types/volt"; 6 6 7 7 const pluginRegistry = new Map<string, PluginHandler>(); 8 8 9 9 /** 10 10 * Register a custom plugin with a given name. 11 - * Plugins extend Volt.js with custom data-x-* attribute bindings. 11 + * Plugins extend Volt.js with custom data-volt-* attribute bindings. 12 12 * 13 - * @param name - Plugin name (will be used as data-x-{name}) 13 + * @param name - Plugin name (will be used as data-volt-{name}) 14 14 * @param handler - Plugin handler function 15 15 * 16 16 * @example
+1 -1
lib/src/core/signal.ts
··· 1 - import type { ComputedSignal, Signal } from "../types/volt"; 1 + import type { ComputedSignal, Signal } from "$types/volt"; 2 2 3 3 /** 4 4 * Creates a new signal with the given initial value.
+1 -1
lib/src/index.ts
··· 29 29 } from "@volt/core/lifecycle"; 30 30 export { clearPlugins, getRegisteredPlugins, hasPlugin, registerPlugin, unregisterPlugin } from "@volt/core/plugin"; 31 31 export { computed, effect, signal } from "@volt/core/signal"; 32 - export { persistPlugin, registerStorageAdapter, scrollPlugin, urlPlugin } from "@volt/plugins/index"; 32 + export { persistPlugin, registerStorageAdapter, scrollPlugin, urlPlugin } from "@volt/plugins";
+2 -2
lib/src/main.ts
··· 1 - import { persistPlugin, scrollPlugin, urlPlugin } from "@volt/plugins/index"; 2 - import { computed, effect, mount, registerPlugin, signal } from "./index"; 1 + import { computed, effect, mount, registerPlugin, signal } from "@volt"; 2 + import { persistPlugin, scrollPlugin, urlPlugin } from "@volt/plugins"; 3 3 4 4 registerPlugin("persist", persistPlugin); 5 5 registerPlugin("scroll", scrollPlugin);
+1 -1
lib/test/integration/list-rendering.test.ts
··· 1 + import { computed, mount, signal } from "@volt"; 1 2 import { describe, expect, it } from "vitest"; 2 - import { computed, mount, signal } from "../../src/index"; 3 3 4 4 describe("integration: list rendering", () => { 5 5 it("creates a reactive todo list", () => {
+1 -1
lib/test/integration/mount.test.ts
··· 1 + import { mount, signal } from "@volt"; 1 2 import { describe, expect, it } from "vitest"; 2 - import { mount, signal } from "../../src/index"; 3 3 4 4 describe("integration: mount", () => { 5 5 it("creates a reactive counter", () => {
+8 -1
lib/tsconfig.json
··· 18 18 "noFallthroughCasesInSwitch": true, 19 19 "noUncheckedSideEffectImports": true, 20 20 "baseUrl": ".", 21 - "paths": { "$types/*": ["./src/types/*"], "@volt/core/*": ["./src/core/*"], "@volt/plugins/*": ["./src/plugins/*"] } 21 + "paths": { 22 + "$types/*": ["./src/types/*"], 23 + "@volt/core": ["./src/core/index.ts"], 24 + "@volt/core/*": ["./src/core/*"], 25 + "@volt/plugins": ["./src/plugins/index.ts"], 26 + "@volt/plugins/*": ["./src/plugins/*"], 27 + "@volt": ["./src/index.ts"] 28 + } 22 29 }, 23 30 "include": ["src", "test"] 24 31 }
+1
lib/vite.config.ts
··· 20 20 resolve: { 21 21 alias: { 22 22 "$types": path.resolve(__dirname, "./src/types"), 23 + "@volt": path.resolve(__dirname, "./src/index.ts"), 23 24 "@volt/core": path.resolve(__dirname, "./src/core"), 24 25 "@volt/plugins": path.resolve(__dirname, "./src/plugins"), 25 26 },