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

feat(wip): demo with rendering utilities

+1027 -109
+2 -53
lib/index.html
··· 4 <meta charset="UTF-8" /> 5 <link rel="icon" type="image/svg+xml" href="/vite.svg" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 - <title>Volt.js Demo</title> 8 <link rel="stylesheet" href="/src/styles/base.css" /> 9 - <style> 10 - /* Demo-specific utility classes */ 11 - .active { 12 - color: var(--color-accent); 13 - font-weight: bold; 14 - } 15 - .highlight { 16 - background: var(--color-mark); 17 - padding: 0.25rem 0.5rem; 18 - border-radius: var(--radius-sm); 19 - } 20 - </style> 21 </head> 22 <body> 23 - <div id="app"> 24 - <header> 25 - <h1 data-volt-text="message">Loading...</h1> 26 - <p>A reactive framework demo powered by Volt.js</p> 27 - </header> 28 - 29 - <article> 30 - <section> 31 - <h2>Event Bindings & Computed Values</h2> 32 - <p> 33 - Count: <strong data-volt-text="count">0</strong><br /> 34 - Doubled: <strong data-volt-text="doubled">0</strong> 35 - </p> 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 - </section> 41 - 42 - <section> 43 - <h2>Form Input</h2> 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 - </section> 47 - 48 - <section> 49 - <h2>Class Bindings</h2> 50 - <p data-volt-class="classes">This text has dynamic classes applied.</p> 51 - <p> 52 - Active: <span data-volt-text="isActive">false</span> 53 - </p> 54 - <button data-volt-on-click="toggleActive">Toggle Active</button> 55 - </section> 56 - 57 - <section> 58 - <h2>HTML Binding</h2> 59 - <div data-volt-html="'<em>This is rendered as HTML</em>'">Fallback content</div> 60 - </section> 61 - </article> 62 - </div> 63 <script type="module" src="/src/main.ts"></script> 64 </body> 65 </html>
··· 4 <meta charset="UTF-8" /> 5 <link rel="icon" type="image/svg+xml" href="/vite.svg" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 + <title>Volt.js Demo - Reactive Framework & Volt CSS Showcase</title> 8 <link rel="stylesheet" href="/src/styles/base.css" /> 9 </head> 10 <body> 11 + <div id="app"></div> 12 <script type="module" src="/src/main.ts"></script> 13 </body> 14 </html>
+251
lib/src/demo/index.ts
···
··· 1 + /** 2 + * Demo module for showcasing Volt.js features and volt.css styling 3 + * 4 + * This module creates the entire demo structure programmatically using DOM APIs, 5 + * demonstrating how to build complex UIs with Volt.js. 6 + */ 7 + 8 + import { persistPlugin } from "$plugins/persist"; 9 + import { scrollPlugin } from "$plugins/scroll"; 10 + import { urlPlugin } from "$plugins/url"; 11 + import { computed, effect, mount, registerPlugin, signal } from "$volt"; 12 + import { createFormsSection } from "./sections/forms"; 13 + import { createInteractivitySection } from "./sections/interactivity"; 14 + import { createPluginsSection } from "./sections/plugins"; 15 + import { createReactivitySection } from "./sections/reactivity"; 16 + import { createTypographySection } from "./sections/typography"; 17 + import * as dom from "./utils"; 18 + 19 + registerPlugin("persist", persistPlugin); 20 + registerPlugin("scroll", scrollPlugin); 21 + registerPlugin("url", urlPlugin); 22 + 23 + const message = signal("Welcome to Volt.js Demo"); 24 + const count = signal(0); 25 + const doubled = computed(() => count.get() * 2); 26 + 27 + const formData = signal({ name: "", email: "", bio: "", country: "us", newsletter: false, plan: "free" }); 28 + 29 + const todos = signal([{ id: 1, text: "Learn Volt.js", done: false }, { id: 2, text: "Build an app", done: false }, { 30 + id: 3, 31 + text: "Ship to production", 32 + done: false, 33 + }]); 34 + 35 + const newTodoText = signal(""); 36 + let todoIdCounter = 4; 37 + 38 + const showAdvanced = signal(false); 39 + 40 + const isActive = signal(true); 41 + const isHighlighted = signal(false); 42 + 43 + const dialogMessage = signal(""); 44 + const dialogInput = signal(""); 45 + 46 + const persistedCount = signal(0); 47 + const scrollPosition = signal(0); 48 + const urlParam = signal(""); 49 + 50 + const activeTodos = computed(() => todos.get().filter((todo) => !todo.done)); 51 + const completedTodos = computed(() => todos.get().filter((todo) => todo.done)); 52 + 53 + effect(() => { 54 + console.log("Count changed:", count.get()); 55 + }); 56 + 57 + const increment = () => { 58 + count.set(count.get() + 1); 59 + }; 60 + 61 + const decrement = () => { 62 + count.set(count.get() - 1); 63 + }; 64 + 65 + const reset = () => { 66 + count.set(0); 67 + }; 68 + 69 + const updateMessage = () => { 70 + message.set(`Count is now ${count.get()}`); 71 + }; 72 + 73 + const openDialog = () => { 74 + const dialog = document.querySelector("#demo-dialog") as HTMLDialogElement; 75 + if (dialog) { 76 + dialogMessage.set(""); 77 + dialogInput.set(""); 78 + dialog.showModal(); 79 + } 80 + }; 81 + 82 + const closeDialog = () => { 83 + const dialog = document.querySelector("#demo-dialog") as HTMLDialogElement; 84 + if (dialog) { 85 + dialog.close(); 86 + } 87 + }; 88 + 89 + const submitDialog = (event: Event) => { 90 + event.preventDefault(); 91 + dialogMessage.set(`You entered: ${dialogInput.get()}`); 92 + setTimeout(closeDialog, 2000); 93 + }; 94 + 95 + const addTodo = () => { 96 + const text = newTodoText.get().trim(); 97 + if (text) { 98 + todos.set([...todos.get(), { id: todoIdCounter++, text, done: false }]); 99 + newTodoText.set(""); 100 + } 101 + }; 102 + 103 + const toggleTodo = (id: number) => { 104 + todos.set(todos.get().map((todo) => todo.id === id ? { ...todo, done: !todo.done } : todo)); 105 + }; 106 + 107 + const removeTodo = (id: number) => { 108 + todos.set(todos.get().filter((todo) => todo.id !== id)); 109 + }; 110 + 111 + const handleFormSubmit = (event: Event) => { 112 + event.preventDefault(); 113 + console.log("Form submitted:", formData.get()); 114 + alert(`Form submitted! Check console for data.`); 115 + }; 116 + 117 + const toggleAdvanced = () => { 118 + showAdvanced.set(!showAdvanced.get()); 119 + }; 120 + 121 + const toggleActive = () => { 122 + isActive.set(!isActive.get()); 123 + }; 124 + 125 + const toggleHighlight = () => { 126 + isHighlighted.set(!isHighlighted.get()); 127 + }; 128 + 129 + const scrollToTop = () => { 130 + window.scrollTo({ top: 0, behavior: "smooth" }); 131 + }; 132 + 133 + const scrollToSection = (id: string) => { 134 + const element = document.querySelector(`#${id}`); 135 + if (element) { 136 + element.scrollIntoView({ behavior: "smooth" }); 137 + } 138 + }; 139 + 140 + export const demoScope = { 141 + message, 142 + count, 143 + doubled, 144 + formData, 145 + todos, 146 + newTodoText, 147 + activeTodos, 148 + completedTodos, 149 + showAdvanced, 150 + isActive, 151 + isHighlighted, 152 + dialogMessage, 153 + dialogInput, 154 + persistedCount, 155 + scrollPosition, 156 + urlParam, 157 + increment, 158 + decrement, 159 + reset, 160 + updateMessage, 161 + openDialog, 162 + closeDialog, 163 + submitDialog, 164 + addTodo, 165 + toggleTodo, 166 + removeTodo, 167 + handleFormSubmit, 168 + toggleAdvanced, 169 + toggleActive, 170 + toggleHighlight, 171 + scrollToTop, 172 + scrollToSection, 173 + }; 174 + 175 + /** 176 + * Build the complete demo structure programmatically 177 + */ 178 + function buildDemoStructure(): HTMLElement { 179 + const container = dom.div( 180 + null, 181 + dom.header( 182 + null, 183 + dom.h1({ "data-volt-text": "message" }, "Loading..."), 184 + dom.p( 185 + null, 186 + "A comprehensive demo showcasing Volt.js reactive framework and Volt CSS classless styling.", 187 + dom.small( 188 + null, 189 + "This demo demonstrates both the framework's reactive capabilities and the elegant, semantic styling of Volt CSS. No CSS classes needed!", 190 + ), 191 + ), 192 + dom.nav( 193 + null, 194 + dom.a({ href: "#typography" }, "Typography"), 195 + " | ", 196 + dom.a({ href: "#interactivity" }, "Interactivity"), 197 + " | ", 198 + dom.a({ href: "#forms" }, "Forms"), 199 + " | ", 200 + dom.a({ href: "#reactivity" }, "Reactivity"), 201 + " | ", 202 + dom.a({ href: "#plugins" }, "Plugins"), 203 + ), 204 + ), 205 + dom.el( 206 + "main", 207 + null, 208 + createTypographySection(), 209 + createInteractivitySection(), 210 + createFormsSection(), 211 + createReactivitySection(), 212 + createPluginsSection(), 213 + ), 214 + dom.footer( 215 + null, 216 + dom.hr(null), 217 + dom.p( 218 + null, 219 + "Built with ", 220 + dom.a({ href: "https://github.com/stormlightlabs/volt" }, "Volt.js"), 221 + " - A lightweight reactive framework", 222 + ), 223 + dom.p( 224 + null, 225 + dom.small( 226 + null, 227 + "This demo showcases both Volt.js reactive features and Volt CSS classless styling. View source to see how everything works!", 228 + ), 229 + ), 230 + ), 231 + ); 232 + 233 + return container; 234 + } 235 + 236 + export function setupDemo() { 237 + const app = document.querySelector("#app"); 238 + if (!app) { 239 + console.error("App container not found"); 240 + return; 241 + } 242 + 243 + const demoStructure = buildDemoStructure(); 244 + app.append(demoStructure); 245 + 246 + mount(app, demoScope); 247 + 248 + window.addEventListener("scroll", () => { 249 + scrollPosition.set(window.scrollY); 250 + }); 251 + }
+83
lib/src/demo/sections/forms.ts
···
··· 1 + /** 2 + * Forms Section 3 + * Demonstrates form elements and two-way data binding 4 + */ 5 + 6 + import * as dom from "../utils"; 7 + 8 + export function createFormsSection(): HTMLElement { 9 + return dom.article( 10 + { id: "forms" }, 11 + dom.h2(null, "Forms & Two-Way Binding"), 12 + dom.section( 13 + null, 14 + dom.h3(null, "Complete Form Example"), 15 + dom.p( 16 + null, 17 + "The ", 18 + dom.code(null, "data-volt-model"), 19 + " attribute provides two-way data binding.", 20 + dom.small( 21 + null, 22 + "Changes in the input automatically update the signal, and changes to the signal automatically update the input. No manual event handlers needed!", 23 + ), 24 + ), 25 + dom.form( 26 + { "data-volt-on-submit": "handleFormSubmit" }, 27 + dom.fieldset( 28 + null, 29 + dom.legend(null, "User Information"), 30 + ...dom.labelFor("Name", { 31 + type: "text", 32 + id: "name", 33 + "data-volt-model": "formData.name", 34 + placeholder: "John Doe", 35 + required: true, 36 + }), 37 + ...dom.labelFor("Email", { 38 + type: "email", 39 + id: "email", 40 + "data-volt-model": "formData.email", 41 + placeholder: "john@example.com", 42 + required: true, 43 + }), 44 + dom.label({ for: "bio" }, "Bio"), 45 + dom.textarea({ 46 + id: "bio", 47 + "data-volt-model": "formData.bio", 48 + placeholder: "Tell us about yourself...", 49 + rows: "4", 50 + }), 51 + dom.label({ for: "country" }, "Country"), 52 + dom.select( 53 + { id: "country", "data-volt-model": "formData.country" }, 54 + ...dom.options([["us", "United States"], ["uk", "United Kingdom"], ["ca", "Canada"], ["au", "Australia"], [ 55 + "other", 56 + "Other", 57 + ]]), 58 + ), 59 + dom.labelWith("Subscribe to newsletter", { type: "checkbox", "data-volt-model": "formData.newsletter" }), 60 + dom.fieldset( 61 + null, 62 + dom.legend(null, "Plan"), 63 + dom.labelWith("Free", { type: "radio", name: "plan", value: "free", "data-volt-model": "formData.plan" }), 64 + dom.labelWith("Pro", { type: "radio", name: "plan", value: "pro", "data-volt-model": "formData.plan" }), 65 + dom.labelWith("Enterprise", { 66 + type: "radio", 67 + name: "plan", 68 + value: "enterprise", 69 + "data-volt-model": "formData.plan", 70 + }), 71 + ), 72 + dom.button({ type: "submit" }, "Submit Form"), 73 + dom.button({ type: "reset" }, "Clear"), 74 + ), 75 + ), 76 + dom.details( 77 + null, 78 + dom.summary(null, "Current Form Data (Live)"), 79 + dom.pre(null, dom.code({ "data-volt-text": "JSON.stringify(formData.get(), null, 2)" }, "Loading...")), 80 + ), 81 + ), 82 + ); 83 + }
+78
lib/src/demo/sections/interactivity.ts
···
··· 1 + /** 2 + * Interactivity Section 3 + * Demonstrates dialogs and event-based interactions 4 + */ 5 + 6 + import * as dom from "../utils"; 7 + 8 + export function createInteractivitySection(): HTMLElement { 9 + return dom.article( 10 + { id: "interactivity" }, 11 + dom.h2(null, "Dialogs & Interactivity"), 12 + dom.section( 13 + null, 14 + dom.h3(null, "Native Dialog Element"), 15 + dom.p( 16 + null, 17 + "The HTML ", 18 + dom.code(null, "<dialog>"), 19 + " element provides semantic modal functionality.", 20 + dom.small( 21 + null, 22 + "Modern browsers support the dialog element natively, providing built-in accessibility features and keyboard handling (ESC to close, focus trapping, etc.).", 23 + ), 24 + " Volt CSS styles it elegantly, and Volt.js handles the interaction.", 25 + ), 26 + dom.button({ "data-volt-on-click": "openDialog" }, "Open Dialog"), 27 + dom.p({ "data-volt-if": "dialogMessage.get()", "data-volt-text": "dialogMessage" }), 28 + dom.dialog( 29 + { id: "demo-dialog" }, 30 + dom.article( 31 + null, 32 + dom.header( 33 + null, 34 + dom.h3(null, "Dialog Demo"), 35 + dom.button({ 36 + "data-volt-on-click": "closeDialog", 37 + "aria-label": "Close", 38 + style: "float: right; background: none; border: none; font-size: 1.5rem; cursor: pointer;", 39 + }, "×"), 40 + ), 41 + dom.form( 42 + { "data-volt-on-submit": "submitDialog" }, 43 + ...dom.labelFor("Enter something:", { 44 + type: "text", 45 + id: "dialog-input", 46 + "data-volt-model": "dialogInput", 47 + placeholder: "Type here...", 48 + required: true, 49 + }), 50 + dom.footer( 51 + { style: "display: flex; gap: 1rem; justify-content: flex-end;" }, 52 + dom.button({ type: "button", "data-volt-on-click": "closeDialog" }, "Cancel"), 53 + dom.button({ type: "submit" }, "Submit"), 54 + ), 55 + ), 56 + ), 57 + ), 58 + ), 59 + dom.section( 60 + null, 61 + dom.h3(null, "Button Interactions"), 62 + dom.p( 63 + null, 64 + "Count: ", 65 + dom.strong({ "data-volt-text": "count" }, "0"), 66 + " | Doubled: ", 67 + dom.strong({ "data-volt-text": "doubled" }, "0"), 68 + ), 69 + dom.div( 70 + { style: "display: flex; gap: 0.5rem; flex-wrap: wrap;" }, 71 + ...dom.buttons([["Increment", "increment"], ["Decrement", "decrement"], ["Reset", "reset"], [ 72 + "Update Header", 73 + "updateMessage", 74 + ]]), 75 + ), 76 + ), 77 + ); 78 + }
+80
lib/src/demo/sections/plugins.ts
···
··· 1 + /** 2 + * Plugins Section 3 + * Demonstrates persist, scroll, and URL plugins 4 + */ 5 + 6 + import * as dom from "../utils"; 7 + 8 + export function createPluginsSection(): HTMLElement { 9 + return dom.article( 10 + { id: "plugins" }, 11 + dom.h2(null, "Plugin Demos"), 12 + dom.section( 13 + null, 14 + dom.h3(null, "Persist Plugin"), 15 + dom.p( 16 + null, 17 + "The persist plugin syncs signals with localStorage, sessionStorage, or IndexedDB.", 18 + dom.small( 19 + null, 20 + "Try incrementing the counter, then refresh the page. The value persists! This uses localStorage by default.", 21 + ), 22 + ), 23 + dom.p(null, "Persisted Count: ", dom.strong({ "data-volt-text": "persistedCount" }, "0")), 24 + dom.div( 25 + { "data-volt-persist:persistedCount": "localStorage" }, 26 + dom.button({ "data-volt-on-click": "persistedCount.set(persistedCount.get() + 1)" }, "Increment Persisted"), 27 + " ", 28 + dom.button({ "data-volt-on-click": "persistedCount.set(0)" }, "Reset Persisted"), 29 + ), 30 + ), 31 + dom.section( 32 + null, 33 + dom.h3(null, "Scroll Plugin"), 34 + dom.p( 35 + null, 36 + "The scroll plugin provides scroll tracking and smooth scrolling utilities.", 37 + dom.small( 38 + null, 39 + "Current scroll position is tracked in real-time. The scroll position updates as you scroll the page, and you can jump to sections smoothly.", 40 + ), 41 + ), 42 + dom.p( 43 + null, 44 + "Current Scroll Position: ", 45 + dom.strong({ "data-volt-text": "Math.round(scrollPosition.get())" }, "0"), 46 + "px", 47 + ), 48 + dom.div( 49 + { style: "display: flex; gap: 0.5rem; flex-wrap: wrap;" }, 50 + dom.button({ "data-volt-on-click": "scrollToTop" }, "Scroll to Top"), 51 + dom.button({ "data-volt-on-click": "scrollToSection('typography')" }, "Go to Typography"), 52 + dom.button({ "data-volt-on-click": "scrollToSection('forms')" }, "Go to Forms"), 53 + dom.button({ "data-volt-on-click": "scrollToSection('plugins')" }, "Go to Plugins"), 54 + ), 55 + ), 56 + dom.section( 57 + null, 58 + dom.h3(null, "URL Plugin"), 59 + dom.p( 60 + null, 61 + "The URL plugin syncs signals with URL query parameters or hash.", 62 + dom.small( 63 + null, 64 + "Try typing in the input below. Notice how the URL updates automatically! Bookmark the URL and return later - the state is preserved.", 65 + ), 66 + ), 67 + dom.div( 68 + { "data-volt-url:urlParam": "query" }, 69 + dom.label({ for: "url-input" }, "URL Parameter (synced with ?urlParam=...):"), 70 + dom.input({ 71 + type: "text", 72 + id: "url-input", 73 + "data-volt-model": "urlParam", 74 + placeholder: "Type to update URL...", 75 + }), 76 + dom.p(null, "Current value: ", dom.strong({ "data-volt-text": "urlParam.get() || '(empty)'" }, "Loading...")), 77 + ), 78 + ), 79 + ); 80 + }
+126
lib/src/demo/sections/reactivity.ts
···
··· 1 + /** 2 + * Reactivity Section 3 + * Demonstrates conditional rendering, list rendering, and class bindings 4 + */ 5 + 6 + import * as dom from "../utils"; 7 + 8 + export function createReactivitySection(): HTMLElement { 9 + const style = dom.el( 10 + "style", 11 + null, 12 + `.active { 13 + color: var(--color-accent); 14 + font-weight: bold; 15 + } 16 + .highlight { 17 + background: var(--color-mark); 18 + padding: 0.25rem 0.5rem; 19 + border-radius: var(--radius-sm); 20 + }`, 21 + ); 22 + 23 + return dom.article( 24 + { id: "reactivity" }, 25 + dom.h2(null, "Reactivity Features"), 26 + dom.section( 27 + null, 28 + dom.h3(null, "Conditional Rendering"), 29 + dom.p( 30 + null, 31 + "Use ", 32 + dom.code(null, "data-volt-if"), 33 + " and ", 34 + dom.code(null, "data-volt-else"), 35 + " for conditional display:", 36 + ), 37 + dom.button( 38 + { "data-volt-on-click": "toggleAdvanced" }, 39 + dom.span({ "data-volt-text": "showAdvanced.get() ? 'Hide' : 'Show'" }, "Show"), 40 + " Advanced Options", 41 + ), 42 + dom.div( 43 + { "data-volt-if": "showAdvanced.get()" }, 44 + dom.h4(null, "Advanced Configuration"), 45 + dom.p(null, "These options are only visible when advanced mode is enabled."), 46 + dom.labelWith("Enable debug mode", { type: "checkbox" }), 47 + dom.labelWith("Use experimental features", { type: "checkbox" }), 48 + ), 49 + dom.div( 50 + { "data-volt-else": true }, 51 + dom.p(null, dom.em(null, "Advanced options are hidden. Click the button above to reveal them.")), 52 + ), 53 + ), 54 + dom.section( 55 + null, 56 + dom.h3(null, "List Rendering"), 57 + dom.p( 58 + null, 59 + "Use ", 60 + dom.code(null, "data-volt-for"), 61 + " to render dynamic lists.", 62 + dom.small( 63 + null, 64 + `The syntax is "item in items" where items is a signal or expression. Each item gets its own scope with access to the item data and parent scope.`, 65 + ), 66 + ), 67 + dom.div( 68 + null, 69 + dom.input({ 70 + type: "text", 71 + "data-volt-model": "newTodoText", 72 + placeholder: "New todo...", 73 + "data-volt-on-keydown": "$event.key === 'Enter' ? addTodo() : null", 74 + }), 75 + dom.button({ "data-volt-on-click": "addTodo" }, "Add Todo"), 76 + ), 77 + dom.h4(null, "Active Todos (", dom.span({ "data-volt-text": "activeTodos.get().length" }, "0"), ")"), 78 + dom.ul( 79 + null, 80 + dom.li( 81 + { "data-volt-for": "todo in todos.get().filter(t => !t.done)" }, 82 + dom.input({ type: "checkbox", "data-volt-on-change": "toggleTodo(todo.id)" }), 83 + " ", 84 + dom.span({ "data-volt-text": "todo.text" }, "Todo item"), 85 + " ", 86 + dom.button({ "data-volt-on-click": "removeTodo(todo.id)" }, "Remove"), 87 + ), 88 + ), 89 + dom.h4(null, "Completed Todos (", dom.span({ "data-volt-text": "completedTodos.get().length" }, "0"), ")"), 90 + dom.ul( 91 + null, 92 + dom.li( 93 + { "data-volt-for": "todo in todos.get().filter(t => t.done)" }, 94 + dom.input({ type: "checkbox", checked: true, "data-volt-on-change": "toggleTodo(todo.id)" }), 95 + " ", 96 + dom.del({ "data-volt-text": "todo.text" }, "Todo item"), 97 + " ", 98 + dom.button({ "data-volt-on-click": "removeTodo(todo.id)" }, "Remove"), 99 + ), 100 + ), 101 + ), 102 + dom.section( 103 + null, 104 + dom.h3(null, "Class Bindings"), 105 + dom.p(null, "Toggle CSS classes reactively using ", dom.code(null, "data-volt-class"), ":"), 106 + dom.p( 107 + { "data-volt-class": "{ active: isActive.get(), highlight: isHighlighted.get() }" }, 108 + "This paragraph has dynamic classes. Try the buttons below!", 109 + ), 110 + dom.button( 111 + { "data-volt-on-click": "toggleActive" }, 112 + "Toggle Active (currently: ", 113 + dom.span({ "data-volt-text": "isActive.get() ? 'ON' : 'OFF'" }, "OFF"), 114 + ")", 115 + ), 116 + " ", 117 + dom.button( 118 + { "data-volt-on-click": "toggleHighlight" }, 119 + "Toggle Highlight (currently: ", 120 + dom.span({ "data-volt-text": "isHighlighted.get() ? 'ON' : 'OFF'" }, "OFF"), 121 + ")", 122 + ), 123 + style, 124 + ), 125 + ); 126 + }
+163
lib/src/demo/sections/typography.ts
···
··· 1 + /** 2 + * Typography & Layout Section 3 + * Demonstrates Volt CSS typography features including Tufte-style sidenotes 4 + */ 5 + 6 + import * as dom from "../utils"; 7 + 8 + export function createTypographySection(): HTMLElement { 9 + return dom.article( 10 + { id: "typography" }, 11 + dom.h2(null, "Typography & Layout"), 12 + dom.section( 13 + null, 14 + dom.h3(null, "Headings & Hierarchy"), 15 + dom.p( 16 + null, 17 + "Volt CSS provides a harmonious type scale based on a 1.25 ratio (major third).", 18 + dom.small( 19 + null, 20 + "The modular scale creates visual hierarchy without requiring any CSS classes. Font sizes range from 0.889rem to 2.566rem.", 21 + ), 22 + " All headings automatically receive appropriate sizing, spacing, and weight.", 23 + ), 24 + dom.h4(null, "Level 4 Heading"), 25 + dom.p(null, "Demonstrates the fourth level of hierarchy."), 26 + dom.h5(null, "Level 5 Heading"), 27 + dom.p(null, "Even smaller, but still distinct and readable."), 28 + dom.h6(null, "Level 6 Heading"), 29 + dom.p(null, "The smallest heading level in the hierarchy."), 30 + ), 31 + dom.section( 32 + null, 33 + dom.h3(null, "Tufte-Style Sidenotes"), 34 + dom.p( 35 + null, 36 + "One of the signature features of Volt CSS is Tufte-style sidenotes.", 37 + dom.small( 38 + null, 39 + "Edward Tufte is renowned for his work on information design and data visualization. His books feature extensive use of margin notes that provide context without interrupting the main narrative flow.", 40 + ), 41 + " These appear in the margin on desktop and inline on mobile devices.", 42 + ), 43 + dom.p( 44 + null, 45 + "Sidenotes are created using the semantic ", 46 + dom.code(null, "<small>"), 47 + " element.", 48 + dom.small( 49 + null, 50 + "The <small> element represents side comments and fine print, making it semantically appropriate for sidenotes. No custom attributes needed!", 51 + ), 52 + " This keeps markup clean and portable while maintaining semantic meaning.", 53 + ), 54 + dom.p( 55 + null, 56 + "The responsive behavior ensures readability across all devices.", 57 + dom.small( 58 + null, 59 + "On narrow screens, sidenotes appear inline with subtle styling. On wider screens (≥1200px), they float into the right margin.", 60 + ), 61 + " Try resizing your browser to see the effect.", 62 + ), 63 + ), 64 + dom.section( 65 + null, 66 + dom.h3(null, "Lists"), 67 + dom.p(null, "Both ordered and unordered lists are styled with appropriate spacing:"), 68 + dom.h4(null, "Unordered List"), 69 + dom.ul( 70 + null, 71 + ...dom.repeat(dom.li, [ 72 + "Reactive signals for state management", 73 + "Computed values derived from signals", 74 + "Effect system for side effects", 75 + "Declarative data binding via attributes", 76 + ]), 77 + ), 78 + dom.h4(null, "Ordered List"), 79 + dom.ol( 80 + null, 81 + ...dom.repeat(dom.li, [ 82 + "Define your signals and computed values", 83 + "Write semantic HTML markup", 84 + "Add data-volt-* attributes for reactivity", 85 + "Mount the scope and watch the magic happen", 86 + ]), 87 + ), 88 + dom.h4(null, "Description List"), 89 + dom.dl( 90 + null, 91 + ...dom.kv([["Signal", "A reactive primitive that holds a value and notifies subscribers of changes."], [ 92 + "Computed", 93 + "A derived value that automatically updates when its dependencies change.", 94 + ], ["Effect", "A side effect that runs when its reactive dependencies change."]]), 95 + ), 96 + ), 97 + dom.section( 98 + null, 99 + dom.h3(null, "Blockquotes & Citations"), 100 + dom.blockquote( 101 + null, 102 + dom.p( 103 + null, 104 + "Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.", 105 + ), 106 + dom.cite(null, "Antoine de Saint-Exupéry"), 107 + ), 108 + dom.blockquote( 109 + null, 110 + dom.p( 111 + null, 112 + "The best programs are written so that computing machines can perform them quickly and so that human beings can understand them clearly.", 113 + ), 114 + dom.cite(null, "Donald Knuth"), 115 + ), 116 + ), 117 + dom.section( 118 + null, 119 + dom.h3(null, "Code & Preformatted Text"), 120 + dom.p(null, "Inline code uses ", dom.code(null, "monospace font"), " for clarity."), 121 + dom.p(null, "Code blocks preserve formatting and provide syntax-appropriate styling:"), 122 + dom.pre( 123 + null, 124 + dom.code( 125 + null, 126 + `import { signal, computed, mount } from 'volt'; 127 + 128 + const count = signal(0); 129 + const doubled = computed(() => count.get() * 2); 130 + 131 + mount(document.querySelector('#app'), { 132 + count, 133 + doubled, 134 + increment: () => count.set(count.get() + 1) 135 + });`, 136 + ), 137 + ), 138 + ), 139 + dom.section( 140 + null, 141 + dom.h3(null, "Tables"), 142 + dom.p( 143 + null, 144 + "Tables receive zebra striping and responsive styling automatically.", 145 + dom.small( 146 + null, 147 + "Tables use alternating row colors for improved scannability. On mobile, they remain scrollable horizontally if needed.", 148 + ), 149 + ), 150 + dom.table( 151 + null, 152 + dom.thead(null, dom.tr(null, ...dom.repeat(dom.th, ["Feature", "Volt.js", "Framework X", "Framework Y"]))), 153 + dom.tbody( 154 + null, 155 + dom.tr(null, ...dom.repeat(dom.td, ["Bundle Size", "< 15KB gzipped", "~40KB", "~30KB"])), 156 + dom.tr(null, dom.td(null, "Virtual DOM"), dom.td(null, "No"), dom.td(null, "Yes"), dom.td(null, "Yes")), 157 + dom.tr(null, ...dom.repeat(dom.td, ["Reactive System", "Signals", "Proxy-based", "Observable"])), 158 + dom.tr(null, ...dom.repeat(dom.td, ["Learning Curve", "Gentle", "Moderate", "Steep"])), 159 + ), 160 + ), 161 + ), 162 + ); 163 + }
+223
lib/src/demo/utils.ts
···
··· 1 + /** 2 + * DOM creation utilities for building demo sections programmatically 3 + */ 4 + 5 + import { isNil } from "$core/shared"; 6 + import type { None, Nullable } from "$types/helpers"; 7 + 8 + type Attributes = Record<string, string | boolean | None>; 9 + 10 + type Attrs = Nullable<Attributes | string>; 11 + 12 + type CreateFn<K extends keyof HTMLElementTagNameMap> = ( 13 + attrs?: Attrs, 14 + ...children: (Node | string)[] 15 + ) => HTMLElementTagNameMap[K]; 16 + 17 + type ElementFactory = <K extends keyof HTMLElementTagNameMap>( 18 + tag: K, 19 + attrs?: Attrs, 20 + ...children: (Node | string)[] 21 + ) => HTMLElementTagNameMap[K]; 22 + 23 + type ListFactory = <K extends keyof HTMLElementTagNameMap>( 24 + createFn: CreateFn<K>, 25 + items: string[], 26 + attrs?: Attrs, 27 + ) => HTMLElementTagNameMap[K][]; 28 + 29 + export const el: ElementFactory = (tag, attrs?, ...children) => { 30 + const element = document.createElement(tag); 31 + 32 + if (typeof attrs === "string") { 33 + element.className = attrs; 34 + } else if (attrs) { 35 + for (const [key, value] of Object.entries(attrs)) { 36 + if (isNil(value) || value === false) continue; 37 + if (value === true) { 38 + element.setAttribute(key, ""); 39 + } else { 40 + element.setAttribute(key, String(value)); 41 + } 42 + } 43 + } 44 + 45 + for (const child of children) { 46 + if (typeof child === "string") { 47 + element.append(document.createTextNode(child)); 48 + } else { 49 + element.append(child); 50 + } 51 + } 52 + 53 + return element; 54 + }; 55 + 56 + export function text(content: string): Text { 57 + return document.createTextNode(content); 58 + } 59 + 60 + export function fragment(...children: (Node | string)[]): DocumentFragment { 61 + const frag = document.createDocumentFragment(); 62 + for (const child of children) { 63 + if (typeof child === "string") { 64 + frag.append(document.createTextNode(child)); 65 + } else { 66 + frag.append(child); 67 + } 68 + } 69 + return frag; 70 + } 71 + 72 + export const repeat: ListFactory = (createFn, items, attrs) => { 73 + return items.map((item) => createFn(attrs, item)); 74 + }; 75 + 76 + /** 77 + * Create key-value pairs for description lists (dt/dd) 78 + * 79 + * @example 80 + * dl(null, ...kv([ 81 + * ["Term", "Definition"], 82 + * ["Signal", "A reactive primitive"] 83 + * ])) 84 + */ 85 + export function kv(pairs: Array<[string, string]>, dtAttrs?: Attrs, ddAttrs?: Attrs): HTMLElement[] { 86 + const elements = []; 87 + for (const [term, definition] of pairs) { 88 + elements.push(dt(dtAttrs, term), dd(ddAttrs, definition)); 89 + } 90 + return elements; 91 + } 92 + 93 + /** 94 + * Create option elements for select dropdowns 95 + * 96 + * @example 97 + * select({ id: "country" }, ...options([ 98 + * ["us", "United States"], 99 + * ["uk", "United Kingdom"] 100 + * ])) 101 + */ 102 + export function options(items: Array<[string, string]>, attrs?: Attrs): HTMLOptionElement[] { 103 + return items.map(([value, label]) => { 104 + const optionAttrs = typeof attrs === "string" ? { class: attrs, value } : { ...attrs, value }; 105 + return option(optionAttrs, label); 106 + }); 107 + } 108 + 109 + /** 110 + * Create a label and input as adjacent siblings 111 + * The label's `for` attribute will match the input's `id` 112 + * 113 + * @example 114 + * ...labelFor("Name", { id: "name", type: "text", required: true }) 115 + */ 116 + export function labelFor( 117 + labelText: string, 118 + inputAttrs: Attrs & { id: string }, 119 + labelAttrs?: Attrs, 120 + ): [HTMLLabelElement, HTMLInputElement] { 121 + const labelElement = label( 122 + typeof labelAttrs === "string" ? labelAttrs : { ...labelAttrs, for: inputAttrs.id }, 123 + labelText, 124 + ); 125 + const inputElement = input(inputAttrs); 126 + return [labelElement, inputElement]; 127 + } 128 + 129 + /** 130 + * Create a label wrapping an input element 131 + * No `for` or `id` needed since the input is wrapped 132 + * 133 + * @example 134 + * labelWith("Subscribe to newsletter", { type: "checkbox", "data-volt-model": "newsletter" }) 135 + */ 136 + export function labelWith( 137 + labelText: string | (Node | string)[], 138 + inputAttrs: Attrs, 139 + labelAttrs?: Attrs, 140 + ): HTMLLabelElement { 141 + const inputElement = input(inputAttrs); 142 + if (typeof labelText === "string") { 143 + return label(labelAttrs, inputElement, " ", labelText); 144 + } 145 + return label(labelAttrs, inputElement, " ", ...labelText); 146 + } 147 + 148 + /** 149 + * Create multiple buttons with different click handlers 150 + * 151 + * @example 152 + * ...buttons([ 153 + * ["Increment", "increment"], 154 + * ["Decrement", "decrement"], 155 + * { label: "Reset", onClick: "reset", type: "reset" } 156 + * ]) 157 + */ 158 + export function buttons( 159 + items: Array<[string, string] | { label: string; onClick: string } & Attributes>, 160 + sharedAttrs?: Attrs, 161 + ): HTMLButtonElement[] { 162 + return items.map((item) => { 163 + if (Array.isArray(item)) { 164 + const [label, onClick] = item; 165 + const baseAttrs = typeof sharedAttrs === "object" && sharedAttrs !== null ? sharedAttrs : {}; 166 + const attrs = { ...baseAttrs, "data-volt-on-click": onClick }; 167 + return button(attrs, label); 168 + } 169 + const { label: buttonLabel, onClick, ...restAttrs } = item; 170 + const baseAttrs = typeof sharedAttrs === "object" && sharedAttrs !== null ? sharedAttrs : {}; 171 + const attrs = { ...baseAttrs, ...restAttrs, "data-volt-on-click": onClick }; 172 + return button(attrs, buttonLabel); 173 + }); 174 + } 175 + 176 + export const h1: CreateFn<"h1"> = (attrs?, ...children) => el("h1", attrs, ...children); 177 + export const h2: CreateFn<"h2"> = (attrs?, ...children) => el("h2", attrs, ...children); 178 + export const h3: CreateFn<"h3"> = (attrs?, ...children) => el("h3", attrs, ...children); 179 + export const h4: CreateFn<"h4"> = (attrs?, ...children) => el("h4", attrs, ...children); 180 + export const h5: CreateFn<"h5"> = (attrs?, ...children) => el("h5", attrs, ...children); 181 + export const h6: CreateFn<"h6"> = (attrs?, ...children) => el("h6", attrs, ...children); 182 + export const p: CreateFn<"p"> = (attrs?, ...children) => el("p", attrs, ...children); 183 + export const div: CreateFn<"div"> = (attrs?, ...children) => el("div", attrs, ...children); 184 + export const span: CreateFn<"span"> = (attrs?, ...children) => el("span", attrs, ...children); 185 + export const small: CreateFn<"small"> = (attrs?, ...children) => el("small", attrs, ...children); 186 + export const article: CreateFn<"article"> = (attrs?, ...children) => el("article", attrs, ...children); 187 + export const section: CreateFn<"section"> = (attrs?, ...children) => el("section", attrs, ...children); 188 + export const header: CreateFn<"header"> = (attrs?, ...children) => el("header", attrs, ...children); 189 + export const footer: CreateFn<"footer"> = (attrs?, ...children) => el("footer", attrs, ...children); 190 + export const nav: CreateFn<"nav"> = (attrs?, ...children) => el("nav", attrs, ...children); 191 + export const ul: CreateFn<"ul"> = (attrs?, ...children) => el("ul", attrs, ...children); 192 + export const ol: CreateFn<"ol"> = (attrs?, ...children) => el("ol", attrs, ...children); 193 + export const li: CreateFn<"li"> = (attrs?, ...children) => el("li", attrs, ...children); 194 + export const dl: CreateFn<"dl"> = (attrs?, ...children) => el("dl", attrs, ...children); 195 + export const dt: CreateFn<"dt"> = (attrs?, ...children) => el("dt", attrs, ...children); 196 + export const dd: CreateFn<"dd"> = (attrs?, ...children) => el("dd", attrs, ...children); 197 + export const a: CreateFn<"a"> = (attrs?, ...children) => el("a", attrs, ...children); 198 + export const button: CreateFn<"button"> = (attrs?, ...children) => el("button", attrs, ...children); 199 + export const input: CreateFn<"input"> = (attrs?: Attributes | string | null) => el("input", attrs); 200 + export const textarea: CreateFn<"textarea"> = (attrs?, ...children) => el("textarea", attrs, ...children); 201 + export const select: CreateFn<"select"> = (attrs?, ...children) => el("select", attrs, ...children); 202 + export const option: CreateFn<"option"> = (attrs?, ...children) => el("option", attrs, ...children); 203 + export const label: CreateFn<"label"> = (attrs?, ...children) => el("label", attrs, ...children); 204 + export const form: CreateFn<"form"> = (attrs?, ...children) => el("form", attrs, ...children); 205 + export const fieldset: CreateFn<"fieldset"> = (attrs?, ...children) => el("fieldset", attrs, ...children); 206 + export const legend: CreateFn<"legend"> = (attrs?, ...children) => el("legend", attrs, ...children); 207 + export const table: CreateFn<"table"> = (attrs?, ...children) => el("table", attrs, ...children); 208 + export const thead: CreateFn<"thead"> = (attrs?, ...children) => el("thead", attrs, ...children); 209 + export const tbody: CreateFn<"tbody"> = (attrs?, ...children) => el("tbody", attrs, ...children); 210 + export const tr: CreateFn<"tr"> = (attrs?, ...children) => el("tr", attrs, ...children); 211 + export const th: CreateFn<"th"> = (attrs?, ...children) => el("th", attrs, ...children); 212 + export const td: CreateFn<"td"> = (attrs?, ...children) => el("td", attrs, ...children); 213 + export const blockquote: CreateFn<"blockquote"> = (attrs?, ...children) => el("blockquote", attrs, ...children); 214 + export const cite: CreateFn<"cite"> = (attrs?, ...children) => el("cite", attrs, ...children); 215 + export const code: CreateFn<"code"> = (attrs?, ...children) => el("code", attrs, ...children); 216 + export const pre: CreateFn<"pre"> = (attrs?, ...children) => el("pre", attrs, ...children); 217 + export const dialog: CreateFn<"dialog"> = (attrs?, ...children) => el("dialog", attrs, ...children); 218 + export const details: CreateFn<"details"> = (attrs?, ...children) => el("details", attrs, ...children); 219 + export const summary: CreateFn<"summary"> = (attrs?, ...children) => el("summary", attrs, ...children); 220 + export const strong: CreateFn<"strong"> = (attrs?, ...children) => el("strong", attrs, ...children); 221 + export const em: CreateFn<"em"> = (attrs?, ...children) => el("em", attrs, ...children); 222 + export const del: CreateFn<"del"> = (attrs?, ...children) => el("del", attrs, ...children); 223 + export const hr: CreateFn<"hr"> = (attrs?) => el("hr", attrs);
+10 -56
lib/src/main.ts
··· 1 - import { persistPlugin } from "$plugins/persist"; 2 - import { scrollPlugin } from "$plugins/scroll"; 3 - import { urlPlugin } from "$plugins/url"; 4 - import { computed, effect, mount, registerPlugin, signal } from "$volt"; 5 6 - registerPlugin("persist", persistPlugin); 7 - registerPlugin("scroll", scrollPlugin); 8 - registerPlugin("url", urlPlugin); 9 - 10 - const count = signal(0); 11 - const message = signal("Welcome to Volt.js!"); 12 - const isActive = signal(true); 13 - const inputValue = signal(""); 14 - const scrollPos = signal(0); 15 - const section1Visible = signal(false); 16 - const section2Visible = signal(false); 17 - 18 - const doubled = computed(() => count.get() * 2); 19 - 20 - effect(() => { 21 - console.log("Count changed:", count.get()); 22 - }); 23 - 24 - const scope = { 25 - count, 26 - doubled, 27 - message, 28 - isActive, 29 - inputValue, 30 - scrollPos, 31 - section1Visible, 32 - section2Visible, 33 - classes: signal({ active: true, highlight: false }), 34 - increment: () => { 35 - count.set(count.get() + 1); 36 - }, 37 - decrement: () => { 38 - count.set(count.get() - 1); 39 - }, 40 - reset: () => { 41 - count.set(0); 42 - }, 43 - toggleActive: () => { 44 - isActive.set(!isActive.get()); 45 - }, 46 - updateMessage: () => { 47 - message.set(`Count is now ${count.get()}`); 48 - }, 49 - handleInput: (event: Event) => { 50 - const target = event.target as HTMLInputElement; 51 - inputValue.set(target.value); 52 - }, 53 - }; 54 55 - const app = document.querySelector("#app"); 56 - if (app) { 57 - mount(app, scope); 58 - }
··· 1 + /** 2 + * Entry point for Volt.js development demo 3 + * 4 + * This file initializes the comprehensive demo showcasing: 5 + * - Volt.js reactive features (signals, computed, effects, bindings) 6 + * - Volt CSS classless styling (typography, layout, Tufte sidenotes) 7 + * - Plugin system (persist, scroll, url) 8 + */ 9 10 + import { setupDemo } from "./demo"; 11 12 + setupDemo();
+11
lib/src/styles/base.css
··· 1 /** 2 * Volt CSS - Classless stylesheet for elegant, readable web documents 3 *
··· 1 + /* 2 + 8b d8 88 3 + `8b d8' 88 ,d 4 + `8b d8' 88 88 5 + `8b d8' ,adPPYba, 88 MM88MMM ,adPPYba, ,adPPYba, ,adPPYba, 6 + `8b d8' a8" "8a 88 88 a8" "" I8[ "" I8[ "" 7 + `8b d8' 8b d8 88 88 8b `"Y8ba, `"Y8ba, 8 + `888' "8a, ,a8" 88 88, 888 "8a, ,aa aa ]8I aa ]8I 9 + `8' `"YbbdP"' 88 "Y888 888 `"Ybbd8"' `"YbbdP"' `"YbbdP"' 10 + */ 11 + 12 /** 13 * Volt CSS - Classless stylesheet for elegant, readable web documents 14 *