diff --git a/lib/index.html b/lib/index.html index 16c679b..24ca538 100644 --- a/lib/index.html +++ b/lib/index.html @@ -4,62 +4,11 @@ - Volt.js Demo + Volt.js Demo - Reactive Framework & Volt CSS Showcase - -
-
-

Loading...

-

A reactive framework demo powered by Volt.js

-
- -
-
-

Event Bindings & Computed Values

-

- Count: 0
- Doubled: 0 -

- - - - -
- -
-

Form Input

- -

You typed: nothing yet

-
- -
-

Class Bindings

-

This text has dynamic classes applied.

-

- Active: false -

- -
- -
-

HTML Binding

-
Fallback content
-
-
-
+
diff --git a/lib/src/demo/index.ts b/lib/src/demo/index.ts new file mode 100644 index 0000000..90f2912 --- /dev/null +++ b/lib/src/demo/index.ts @@ -0,0 +1,251 @@ +/** + * Demo module for showcasing Volt.js features and volt.css styling + * + * This module creates the entire demo structure programmatically using DOM APIs, + * demonstrating how to build complex UIs with Volt.js. + */ + +import { persistPlugin } from "$plugins/persist"; +import { scrollPlugin } from "$plugins/scroll"; +import { urlPlugin } from "$plugins/url"; +import { computed, effect, mount, registerPlugin, signal } from "$volt"; +import { createFormsSection } from "./sections/forms"; +import { createInteractivitySection } from "./sections/interactivity"; +import { createPluginsSection } from "./sections/plugins"; +import { createReactivitySection } from "./sections/reactivity"; +import { createTypographySection } from "./sections/typography"; +import * as dom from "./utils"; + +registerPlugin("persist", persistPlugin); +registerPlugin("scroll", scrollPlugin); +registerPlugin("url", urlPlugin); + +const message = signal("Welcome to Volt.js Demo"); +const count = signal(0); +const doubled = computed(() => count.get() * 2); + +const formData = signal({ name: "", email: "", bio: "", country: "us", newsletter: false, plan: "free" }); + +const todos = signal([{ id: 1, text: "Learn Volt.js", done: false }, { id: 2, text: "Build an app", done: false }, { + id: 3, + text: "Ship to production", + done: false, +}]); + +const newTodoText = signal(""); +let todoIdCounter = 4; + +const showAdvanced = signal(false); + +const isActive = signal(true); +const isHighlighted = signal(false); + +const dialogMessage = signal(""); +const dialogInput = signal(""); + +const persistedCount = signal(0); +const scrollPosition = signal(0); +const urlParam = signal(""); + +const activeTodos = computed(() => todos.get().filter((todo) => !todo.done)); +const completedTodos = computed(() => todos.get().filter((todo) => todo.done)); + +effect(() => { + console.log("Count changed:", count.get()); +}); + +const increment = () => { + count.set(count.get() + 1); +}; + +const decrement = () => { + count.set(count.get() - 1); +}; + +const reset = () => { + count.set(0); +}; + +const updateMessage = () => { + message.set(`Count is now ${count.get()}`); +}; + +const openDialog = () => { + const dialog = document.querySelector("#demo-dialog") as HTMLDialogElement; + if (dialog) { + dialogMessage.set(""); + dialogInput.set(""); + dialog.showModal(); + } +}; + +const closeDialog = () => { + const dialog = document.querySelector("#demo-dialog") as HTMLDialogElement; + if (dialog) { + dialog.close(); + } +}; + +const submitDialog = (event: Event) => { + event.preventDefault(); + dialogMessage.set(`You entered: ${dialogInput.get()}`); + setTimeout(closeDialog, 2000); +}; + +const addTodo = () => { + const text = newTodoText.get().trim(); + if (text) { + todos.set([...todos.get(), { id: todoIdCounter++, text, done: false }]); + newTodoText.set(""); + } +}; + +const toggleTodo = (id: number) => { + todos.set(todos.get().map((todo) => todo.id === id ? { ...todo, done: !todo.done } : todo)); +}; + +const removeTodo = (id: number) => { + todos.set(todos.get().filter((todo) => todo.id !== id)); +}; + +const handleFormSubmit = (event: Event) => { + event.preventDefault(); + console.log("Form submitted:", formData.get()); + alert(`Form submitted! Check console for data.`); +}; + +const toggleAdvanced = () => { + showAdvanced.set(!showAdvanced.get()); +}; + +const toggleActive = () => { + isActive.set(!isActive.get()); +}; + +const toggleHighlight = () => { + isHighlighted.set(!isHighlighted.get()); +}; + +const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: "smooth" }); +}; + +const scrollToSection = (id: string) => { + const element = document.querySelector(`#${id}`); + if (element) { + element.scrollIntoView({ behavior: "smooth" }); + } +}; + +export const demoScope = { + message, + count, + doubled, + formData, + todos, + newTodoText, + activeTodos, + completedTodos, + showAdvanced, + isActive, + isHighlighted, + dialogMessage, + dialogInput, + persistedCount, + scrollPosition, + urlParam, + increment, + decrement, + reset, + updateMessage, + openDialog, + closeDialog, + submitDialog, + addTodo, + toggleTodo, + removeTodo, + handleFormSubmit, + toggleAdvanced, + toggleActive, + toggleHighlight, + scrollToTop, + scrollToSection, +}; + +/** + * Build the complete demo structure programmatically + */ +function buildDemoStructure(): HTMLElement { + const container = dom.div( + null, + dom.header( + null, + dom.h1({ "data-volt-text": "message" }, "Loading..."), + dom.p( + null, + "A comprehensive demo showcasing Volt.js reactive framework and Volt CSS classless styling.", + dom.small( + null, + "This demo demonstrates both the framework's reactive capabilities and the elegant, semantic styling of Volt CSS. No CSS classes needed!", + ), + ), + dom.nav( + null, + dom.a({ href: "#typography" }, "Typography"), + " | ", + dom.a({ href: "#interactivity" }, "Interactivity"), + " | ", + dom.a({ href: "#forms" }, "Forms"), + " | ", + dom.a({ href: "#reactivity" }, "Reactivity"), + " | ", + dom.a({ href: "#plugins" }, "Plugins"), + ), + ), + dom.el( + "main", + null, + createTypographySection(), + createInteractivitySection(), + createFormsSection(), + createReactivitySection(), + createPluginsSection(), + ), + dom.footer( + null, + dom.hr(null), + dom.p( + null, + "Built with ", + dom.a({ href: "https://github.com/stormlightlabs/volt" }, "Volt.js"), + " - A lightweight reactive framework", + ), + dom.p( + null, + dom.small( + null, + "This demo showcases both Volt.js reactive features and Volt CSS classless styling. View source to see how everything works!", + ), + ), + ), + ); + + return container; +} + +export function setupDemo() { + const app = document.querySelector("#app"); + if (!app) { + console.error("App container not found"); + return; + } + + const demoStructure = buildDemoStructure(); + app.append(demoStructure); + + mount(app, demoScope); + + window.addEventListener("scroll", () => { + scrollPosition.set(window.scrollY); + }); +} diff --git a/lib/src/demo/sections/forms.ts b/lib/src/demo/sections/forms.ts new file mode 100644 index 0000000..3459dc2 --- /dev/null +++ b/lib/src/demo/sections/forms.ts @@ -0,0 +1,83 @@ +/** + * Forms Section + * Demonstrates form elements and two-way data binding + */ + +import * as dom from "../utils"; + +export function createFormsSection(): HTMLElement { + return dom.article( + { id: "forms" }, + dom.h2(null, "Forms & Two-Way Binding"), + dom.section( + null, + dom.h3(null, "Complete Form Example"), + dom.p( + null, + "The ", + dom.code(null, "data-volt-model"), + " attribute provides two-way data binding.", + dom.small( + null, + "Changes in the input automatically update the signal, and changes to the signal automatically update the input. No manual event handlers needed!", + ), + ), + dom.form( + { "data-volt-on-submit": "handleFormSubmit" }, + dom.fieldset( + null, + dom.legend(null, "User Information"), + ...dom.labelFor("Name", { + type: "text", + id: "name", + "data-volt-model": "formData.name", + placeholder: "John Doe", + required: true, + }), + ...dom.labelFor("Email", { + type: "email", + id: "email", + "data-volt-model": "formData.email", + placeholder: "john@example.com", + required: true, + }), + dom.label({ for: "bio" }, "Bio"), + dom.textarea({ + id: "bio", + "data-volt-model": "formData.bio", + placeholder: "Tell us about yourself...", + rows: "4", + }), + dom.label({ for: "country" }, "Country"), + dom.select( + { id: "country", "data-volt-model": "formData.country" }, + ...dom.options([["us", "United States"], ["uk", "United Kingdom"], ["ca", "Canada"], ["au", "Australia"], [ + "other", + "Other", + ]]), + ), + dom.labelWith("Subscribe to newsletter", { type: "checkbox", "data-volt-model": "formData.newsletter" }), + dom.fieldset( + null, + dom.legend(null, "Plan"), + dom.labelWith("Free", { type: "radio", name: "plan", value: "free", "data-volt-model": "formData.plan" }), + dom.labelWith("Pro", { type: "radio", name: "plan", value: "pro", "data-volt-model": "formData.plan" }), + dom.labelWith("Enterprise", { + type: "radio", + name: "plan", + value: "enterprise", + "data-volt-model": "formData.plan", + }), + ), + dom.button({ type: "submit" }, "Submit Form"), + dom.button({ type: "reset" }, "Clear"), + ), + ), + dom.details( + null, + dom.summary(null, "Current Form Data (Live)"), + dom.pre(null, dom.code({ "data-volt-text": "JSON.stringify(formData.get(), null, 2)" }, "Loading...")), + ), + ), + ); +} diff --git a/lib/src/demo/sections/interactivity.ts b/lib/src/demo/sections/interactivity.ts new file mode 100644 index 0000000..4ec97b3 --- /dev/null +++ b/lib/src/demo/sections/interactivity.ts @@ -0,0 +1,78 @@ +/** + * Interactivity Section + * Demonstrates dialogs and event-based interactions + */ + +import * as dom from "../utils"; + +export function createInteractivitySection(): HTMLElement { + return dom.article( + { id: "interactivity" }, + dom.h2(null, "Dialogs & Interactivity"), + dom.section( + null, + dom.h3(null, "Native Dialog Element"), + dom.p( + null, + "The HTML ", + dom.code(null, ""), + " element provides semantic modal functionality.", + dom.small( + null, + "Modern browsers support the dialog element natively, providing built-in accessibility features and keyboard handling (ESC to close, focus trapping, etc.).", + ), + " Volt CSS styles it elegantly, and Volt.js handles the interaction.", + ), + dom.button({ "data-volt-on-click": "openDialog" }, "Open Dialog"), + dom.p({ "data-volt-if": "dialogMessage.get()", "data-volt-text": "dialogMessage" }), + dom.dialog( + { id: "demo-dialog" }, + dom.article( + null, + dom.header( + null, + dom.h3(null, "Dialog Demo"), + dom.button({ + "data-volt-on-click": "closeDialog", + "aria-label": "Close", + style: "float: right; background: none; border: none; font-size: 1.5rem; cursor: pointer;", + }, "×"), + ), + dom.form( + { "data-volt-on-submit": "submitDialog" }, + ...dom.labelFor("Enter something:", { + type: "text", + id: "dialog-input", + "data-volt-model": "dialogInput", + placeholder: "Type here...", + required: true, + }), + dom.footer( + { style: "display: flex; gap: 1rem; justify-content: flex-end;" }, + dom.button({ type: "button", "data-volt-on-click": "closeDialog" }, "Cancel"), + dom.button({ type: "submit" }, "Submit"), + ), + ), + ), + ), + ), + dom.section( + null, + dom.h3(null, "Button Interactions"), + dom.p( + null, + "Count: ", + dom.strong({ "data-volt-text": "count" }, "0"), + " | Doubled: ", + dom.strong({ "data-volt-text": "doubled" }, "0"), + ), + dom.div( + { style: "display: flex; gap: 0.5rem; flex-wrap: wrap;" }, + ...dom.buttons([["Increment", "increment"], ["Decrement", "decrement"], ["Reset", "reset"], [ + "Update Header", + "updateMessage", + ]]), + ), + ), + ); +} diff --git a/lib/src/demo/sections/plugins.ts b/lib/src/demo/sections/plugins.ts new file mode 100644 index 0000000..4b59752 --- /dev/null +++ b/lib/src/demo/sections/plugins.ts @@ -0,0 +1,80 @@ +/** + * Plugins Section + * Demonstrates persist, scroll, and URL plugins + */ + +import * as dom from "../utils"; + +export function createPluginsSection(): HTMLElement { + return dom.article( + { id: "plugins" }, + dom.h2(null, "Plugin Demos"), + dom.section( + null, + dom.h3(null, "Persist Plugin"), + dom.p( + null, + "The persist plugin syncs signals with localStorage, sessionStorage, or IndexedDB.", + dom.small( + null, + "Try incrementing the counter, then refresh the page. The value persists! This uses localStorage by default.", + ), + ), + dom.p(null, "Persisted Count: ", dom.strong({ "data-volt-text": "persistedCount" }, "0")), + dom.div( + { "data-volt-persist:persistedCount": "localStorage" }, + dom.button({ "data-volt-on-click": "persistedCount.set(persistedCount.get() + 1)" }, "Increment Persisted"), + " ", + dom.button({ "data-volt-on-click": "persistedCount.set(0)" }, "Reset Persisted"), + ), + ), + dom.section( + null, + dom.h3(null, "Scroll Plugin"), + dom.p( + null, + "The scroll plugin provides scroll tracking and smooth scrolling utilities.", + dom.small( + null, + "Current scroll position is tracked in real-time. The scroll position updates as you scroll the page, and you can jump to sections smoothly.", + ), + ), + dom.p( + null, + "Current Scroll Position: ", + dom.strong({ "data-volt-text": "Math.round(scrollPosition.get())" }, "0"), + "px", + ), + dom.div( + { style: "display: flex; gap: 0.5rem; flex-wrap: wrap;" }, + dom.button({ "data-volt-on-click": "scrollToTop" }, "Scroll to Top"), + dom.button({ "data-volt-on-click": "scrollToSection('typography')" }, "Go to Typography"), + dom.button({ "data-volt-on-click": "scrollToSection('forms')" }, "Go to Forms"), + dom.button({ "data-volt-on-click": "scrollToSection('plugins')" }, "Go to Plugins"), + ), + ), + dom.section( + null, + dom.h3(null, "URL Plugin"), + dom.p( + null, + "The URL plugin syncs signals with URL query parameters or hash.", + dom.small( + null, + "Try typing in the input below. Notice how the URL updates automatically! Bookmark the URL and return later - the state is preserved.", + ), + ), + dom.div( + { "data-volt-url:urlParam": "query" }, + dom.label({ for: "url-input" }, "URL Parameter (synced with ?urlParam=...):"), + dom.input({ + type: "text", + id: "url-input", + "data-volt-model": "urlParam", + placeholder: "Type to update URL...", + }), + dom.p(null, "Current value: ", dom.strong({ "data-volt-text": "urlParam.get() || '(empty)'" }, "Loading...")), + ), + ), + ); +} diff --git a/lib/src/demo/sections/reactivity.ts b/lib/src/demo/sections/reactivity.ts new file mode 100644 index 0000000..2e26c20 --- /dev/null +++ b/lib/src/demo/sections/reactivity.ts @@ -0,0 +1,126 @@ +/** + * Reactivity Section + * Demonstrates conditional rendering, list rendering, and class bindings + */ + +import * as dom from "../utils"; + +export function createReactivitySection(): HTMLElement { + const style = dom.el( + "style", + null, + `.active { + color: var(--color-accent); + font-weight: bold; +} +.highlight { + background: var(--color-mark); + padding: 0.25rem 0.5rem; + border-radius: var(--radius-sm); +}`, + ); + + return dom.article( + { id: "reactivity" }, + dom.h2(null, "Reactivity Features"), + dom.section( + null, + dom.h3(null, "Conditional Rendering"), + dom.p( + null, + "Use ", + dom.code(null, "data-volt-if"), + " and ", + dom.code(null, "data-volt-else"), + " for conditional display:", + ), + dom.button( + { "data-volt-on-click": "toggleAdvanced" }, + dom.span({ "data-volt-text": "showAdvanced.get() ? 'Hide' : 'Show'" }, "Show"), + " Advanced Options", + ), + dom.div( + { "data-volt-if": "showAdvanced.get()" }, + dom.h4(null, "Advanced Configuration"), + dom.p(null, "These options are only visible when advanced mode is enabled."), + dom.labelWith("Enable debug mode", { type: "checkbox" }), + dom.labelWith("Use experimental features", { type: "checkbox" }), + ), + dom.div( + { "data-volt-else": true }, + dom.p(null, dom.em(null, "Advanced options are hidden. Click the button above to reveal them.")), + ), + ), + dom.section( + null, + dom.h3(null, "List Rendering"), + dom.p( + null, + "Use ", + dom.code(null, "data-volt-for"), + " to render dynamic lists.", + dom.small( + null, + `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.`, + ), + ), + dom.div( + null, + dom.input({ + type: "text", + "data-volt-model": "newTodoText", + placeholder: "New todo...", + "data-volt-on-keydown": "$event.key === 'Enter' ? addTodo() : null", + }), + dom.button({ "data-volt-on-click": "addTodo" }, "Add Todo"), + ), + dom.h4(null, "Active Todos (", dom.span({ "data-volt-text": "activeTodos.get().length" }, "0"), ")"), + dom.ul( + null, + dom.li( + { "data-volt-for": "todo in todos.get().filter(t => !t.done)" }, + dom.input({ type: "checkbox", "data-volt-on-change": "toggleTodo(todo.id)" }), + " ", + dom.span({ "data-volt-text": "todo.text" }, "Todo item"), + " ", + dom.button({ "data-volt-on-click": "removeTodo(todo.id)" }, "Remove"), + ), + ), + dom.h4(null, "Completed Todos (", dom.span({ "data-volt-text": "completedTodos.get().length" }, "0"), ")"), + dom.ul( + null, + dom.li( + { "data-volt-for": "todo in todos.get().filter(t => t.done)" }, + dom.input({ type: "checkbox", checked: true, "data-volt-on-change": "toggleTodo(todo.id)" }), + " ", + dom.del({ "data-volt-text": "todo.text" }, "Todo item"), + " ", + dom.button({ "data-volt-on-click": "removeTodo(todo.id)" }, "Remove"), + ), + ), + ), + dom.section( + null, + dom.h3(null, "Class Bindings"), + dom.p(null, "Toggle CSS classes reactively using ", dom.code(null, "data-volt-class"), ":"), + dom.p( + { "data-volt-class": "{ active: isActive.get(), highlight: isHighlighted.get() }" }, + "This paragraph has dynamic classes. Try the buttons below!", + ), + dom.button( + { "data-volt-on-click": "toggleActive" }, + "Toggle Active (currently: ", + dom.span({ "data-volt-text": "isActive.get() ? 'ON' : 'OFF'" }, "OFF"), + ")", + ), + " ", + dom.button( + { "data-volt-on-click": "toggleHighlight" }, + "Toggle Highlight (currently: ", + dom.span({ "data-volt-text": "isHighlighted.get() ? 'ON' : 'OFF'" }, "OFF"), + ")", + ), + style, + ), + ); +} diff --git a/lib/src/demo/sections/typography.ts b/lib/src/demo/sections/typography.ts new file mode 100644 index 0000000..d3deb1a --- /dev/null +++ b/lib/src/demo/sections/typography.ts @@ -0,0 +1,163 @@ +/** + * Typography & Layout Section + * Demonstrates Volt CSS typography features including Tufte-style sidenotes + */ + +import * as dom from "../utils"; + +export function createTypographySection(): HTMLElement { + return dom.article( + { id: "typography" }, + dom.h2(null, "Typography & Layout"), + dom.section( + null, + dom.h3(null, "Headings & Hierarchy"), + dom.p( + null, + "Volt CSS provides a harmonious type scale based on a 1.25 ratio (major third).", + dom.small( + null, + "The modular scale creates visual hierarchy without requiring any CSS classes. Font sizes range from 0.889rem to 2.566rem.", + ), + " All headings automatically receive appropriate sizing, spacing, and weight.", + ), + dom.h4(null, "Level 4 Heading"), + dom.p(null, "Demonstrates the fourth level of hierarchy."), + dom.h5(null, "Level 5 Heading"), + dom.p(null, "Even smaller, but still distinct and readable."), + dom.h6(null, "Level 6 Heading"), + dom.p(null, "The smallest heading level in the hierarchy."), + ), + dom.section( + null, + dom.h3(null, "Tufte-Style Sidenotes"), + dom.p( + null, + "One of the signature features of Volt CSS is Tufte-style sidenotes.", + dom.small( + null, + "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.", + ), + " These appear in the margin on desktop and inline on mobile devices.", + ), + dom.p( + null, + "Sidenotes are created using the semantic ", + dom.code(null, ""), + " element.", + dom.small( + null, + "The element represents side comments and fine print, making it semantically appropriate for sidenotes. No custom attributes needed!", + ), + " This keeps markup clean and portable while maintaining semantic meaning.", + ), + dom.p( + null, + "The responsive behavior ensures readability across all devices.", + dom.small( + null, + "On narrow screens, sidenotes appear inline with subtle styling. On wider screens (≥1200px), they float into the right margin.", + ), + " Try resizing your browser to see the effect.", + ), + ), + dom.section( + null, + dom.h3(null, "Lists"), + dom.p(null, "Both ordered and unordered lists are styled with appropriate spacing:"), + dom.h4(null, "Unordered List"), + dom.ul( + null, + ...dom.repeat(dom.li, [ + "Reactive signals for state management", + "Computed values derived from signals", + "Effect system for side effects", + "Declarative data binding via attributes", + ]), + ), + dom.h4(null, "Ordered List"), + dom.ol( + null, + ...dom.repeat(dom.li, [ + "Define your signals and computed values", + "Write semantic HTML markup", + "Add data-volt-* attributes for reactivity", + "Mount the scope and watch the magic happen", + ]), + ), + dom.h4(null, "Description List"), + dom.dl( + null, + ...dom.kv([["Signal", "A reactive primitive that holds a value and notifies subscribers of changes."], [ + "Computed", + "A derived value that automatically updates when its dependencies change.", + ], ["Effect", "A side effect that runs when its reactive dependencies change."]]), + ), + ), + dom.section( + null, + dom.h3(null, "Blockquotes & Citations"), + dom.blockquote( + null, + dom.p( + null, + "Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.", + ), + dom.cite(null, "Antoine de Saint-Exupéry"), + ), + dom.blockquote( + null, + dom.p( + null, + "The best programs are written so that computing machines can perform them quickly and so that human beings can understand them clearly.", + ), + dom.cite(null, "Donald Knuth"), + ), + ), + dom.section( + null, + dom.h3(null, "Code & Preformatted Text"), + dom.p(null, "Inline code uses ", dom.code(null, "monospace font"), " for clarity."), + dom.p(null, "Code blocks preserve formatting and provide syntax-appropriate styling:"), + dom.pre( + null, + dom.code( + null, + `import { signal, computed, mount } from 'volt'; + +const count = signal(0); +const doubled = computed(() => count.get() * 2); + +mount(document.querySelector('#app'), { + count, + doubled, + increment: () => count.set(count.get() + 1) +});`, + ), + ), + ), + dom.section( + null, + dom.h3(null, "Tables"), + dom.p( + null, + "Tables receive zebra striping and responsive styling automatically.", + dom.small( + null, + "Tables use alternating row colors for improved scannability. On mobile, they remain scrollable horizontally if needed.", + ), + ), + dom.table( + null, + dom.thead(null, dom.tr(null, ...dom.repeat(dom.th, ["Feature", "Volt.js", "Framework X", "Framework Y"]))), + dom.tbody( + null, + dom.tr(null, ...dom.repeat(dom.td, ["Bundle Size", "< 15KB gzipped", "~40KB", "~30KB"])), + dom.tr(null, dom.td(null, "Virtual DOM"), dom.td(null, "No"), dom.td(null, "Yes"), dom.td(null, "Yes")), + dom.tr(null, ...dom.repeat(dom.td, ["Reactive System", "Signals", "Proxy-based", "Observable"])), + dom.tr(null, ...dom.repeat(dom.td, ["Learning Curve", "Gentle", "Moderate", "Steep"])), + ), + ), + ), + ); +} diff --git a/lib/src/demo/utils.ts b/lib/src/demo/utils.ts new file mode 100644 index 0000000..d07635e --- /dev/null +++ b/lib/src/demo/utils.ts @@ -0,0 +1,223 @@ +/** + * DOM creation utilities for building demo sections programmatically + */ + +import { isNil } from "$core/shared"; +import type { None, Nullable } from "$types/helpers"; + +type Attributes = Record; + +type Attrs = Nullable; + +type CreateFn = ( + attrs?: Attrs, + ...children: (Node | string)[] +) => HTMLElementTagNameMap[K]; + +type ElementFactory = ( + tag: K, + attrs?: Attrs, + ...children: (Node | string)[] +) => HTMLElementTagNameMap[K]; + +type ListFactory = ( + createFn: CreateFn, + items: string[], + attrs?: Attrs, +) => HTMLElementTagNameMap[K][]; + +export const el: ElementFactory = (tag, attrs?, ...children) => { + const element = document.createElement(tag); + + if (typeof attrs === "string") { + element.className = attrs; + } else if (attrs) { + for (const [key, value] of Object.entries(attrs)) { + if (isNil(value) || value === false) continue; + if (value === true) { + element.setAttribute(key, ""); + } else { + element.setAttribute(key, String(value)); + } + } + } + + for (const child of children) { + if (typeof child === "string") { + element.append(document.createTextNode(child)); + } else { + element.append(child); + } + } + + return element; +}; + +export function text(content: string): Text { + return document.createTextNode(content); +} + +export function fragment(...children: (Node | string)[]): DocumentFragment { + const frag = document.createDocumentFragment(); + for (const child of children) { + if (typeof child === "string") { + frag.append(document.createTextNode(child)); + } else { + frag.append(child); + } + } + return frag; +} + +export const repeat: ListFactory = (createFn, items, attrs) => { + return items.map((item) => createFn(attrs, item)); +}; + +/** + * Create key-value pairs for description lists (dt/dd) + * + * @example + * dl(null, ...kv([ + * ["Term", "Definition"], + * ["Signal", "A reactive primitive"] + * ])) + */ +export function kv(pairs: Array<[string, string]>, dtAttrs?: Attrs, ddAttrs?: Attrs): HTMLElement[] { + const elements = []; + for (const [term, definition] of pairs) { + elements.push(dt(dtAttrs, term), dd(ddAttrs, definition)); + } + return elements; +} + +/** + * Create option elements for select dropdowns + * + * @example + * select({ id: "country" }, ...options([ + * ["us", "United States"], + * ["uk", "United Kingdom"] + * ])) + */ +export function options(items: Array<[string, string]>, attrs?: Attrs): HTMLOptionElement[] { + return items.map(([value, label]) => { + const optionAttrs = typeof attrs === "string" ? { class: attrs, value } : { ...attrs, value }; + return option(optionAttrs, label); + }); +} + +/** + * Create a label and input as adjacent siblings + * The label's `for` attribute will match the input's `id` + * + * @example + * ...labelFor("Name", { id: "name", type: "text", required: true }) + */ +export function labelFor( + labelText: string, + inputAttrs: Attrs & { id: string }, + labelAttrs?: Attrs, +): [HTMLLabelElement, HTMLInputElement] { + const labelElement = label( + typeof labelAttrs === "string" ? labelAttrs : { ...labelAttrs, for: inputAttrs.id }, + labelText, + ); + const inputElement = input(inputAttrs); + return [labelElement, inputElement]; +} + +/** + * Create a label wrapping an input element + * No `for` or `id` needed since the input is wrapped + * + * @example + * labelWith("Subscribe to newsletter", { type: "checkbox", "data-volt-model": "newsletter" }) + */ +export function labelWith( + labelText: string | (Node | string)[], + inputAttrs: Attrs, + labelAttrs?: Attrs, +): HTMLLabelElement { + const inputElement = input(inputAttrs); + if (typeof labelText === "string") { + return label(labelAttrs, inputElement, " ", labelText); + } + return label(labelAttrs, inputElement, " ", ...labelText); +} + +/** + * Create multiple buttons with different click handlers + * + * @example + * ...buttons([ + * ["Increment", "increment"], + * ["Decrement", "decrement"], + * { label: "Reset", onClick: "reset", type: "reset" } + * ]) + */ +export function buttons( + items: Array<[string, string] | { label: string; onClick: string } & Attributes>, + sharedAttrs?: Attrs, +): HTMLButtonElement[] { + return items.map((item) => { + if (Array.isArray(item)) { + const [label, onClick] = item; + const baseAttrs = typeof sharedAttrs === "object" && sharedAttrs !== null ? sharedAttrs : {}; + const attrs = { ...baseAttrs, "data-volt-on-click": onClick }; + return button(attrs, label); + } + const { label: buttonLabel, onClick, ...restAttrs } = item; + const baseAttrs = typeof sharedAttrs === "object" && sharedAttrs !== null ? sharedAttrs : {}; + const attrs = { ...baseAttrs, ...restAttrs, "data-volt-on-click": onClick }; + return button(attrs, buttonLabel); + }); +} + +export const h1: CreateFn<"h1"> = (attrs?, ...children) => el("h1", attrs, ...children); +export const h2: CreateFn<"h2"> = (attrs?, ...children) => el("h2", attrs, ...children); +export const h3: CreateFn<"h3"> = (attrs?, ...children) => el("h3", attrs, ...children); +export const h4: CreateFn<"h4"> = (attrs?, ...children) => el("h4", attrs, ...children); +export const h5: CreateFn<"h5"> = (attrs?, ...children) => el("h5", attrs, ...children); +export const h6: CreateFn<"h6"> = (attrs?, ...children) => el("h6", attrs, ...children); +export const p: CreateFn<"p"> = (attrs?, ...children) => el("p", attrs, ...children); +export const div: CreateFn<"div"> = (attrs?, ...children) => el("div", attrs, ...children); +export const span: CreateFn<"span"> = (attrs?, ...children) => el("span", attrs, ...children); +export const small: CreateFn<"small"> = (attrs?, ...children) => el("small", attrs, ...children); +export const article: CreateFn<"article"> = (attrs?, ...children) => el("article", attrs, ...children); +export const section: CreateFn<"section"> = (attrs?, ...children) => el("section", attrs, ...children); +export const header: CreateFn<"header"> = (attrs?, ...children) => el("header", attrs, ...children); +export const footer: CreateFn<"footer"> = (attrs?, ...children) => el("footer", attrs, ...children); +export const nav: CreateFn<"nav"> = (attrs?, ...children) => el("nav", attrs, ...children); +export const ul: CreateFn<"ul"> = (attrs?, ...children) => el("ul", attrs, ...children); +export const ol: CreateFn<"ol"> = (attrs?, ...children) => el("ol", attrs, ...children); +export const li: CreateFn<"li"> = (attrs?, ...children) => el("li", attrs, ...children); +export const dl: CreateFn<"dl"> = (attrs?, ...children) => el("dl", attrs, ...children); +export const dt: CreateFn<"dt"> = (attrs?, ...children) => el("dt", attrs, ...children); +export const dd: CreateFn<"dd"> = (attrs?, ...children) => el("dd", attrs, ...children); +export const a: CreateFn<"a"> = (attrs?, ...children) => el("a", attrs, ...children); +export const button: CreateFn<"button"> = (attrs?, ...children) => el("button", attrs, ...children); +export const input: CreateFn<"input"> = (attrs?: Attributes | string | null) => el("input", attrs); +export const textarea: CreateFn<"textarea"> = (attrs?, ...children) => el("textarea", attrs, ...children); +export const select: CreateFn<"select"> = (attrs?, ...children) => el("select", attrs, ...children); +export const option: CreateFn<"option"> = (attrs?, ...children) => el("option", attrs, ...children); +export const label: CreateFn<"label"> = (attrs?, ...children) => el("label", attrs, ...children); +export const form: CreateFn<"form"> = (attrs?, ...children) => el("form", attrs, ...children); +export const fieldset: CreateFn<"fieldset"> = (attrs?, ...children) => el("fieldset", attrs, ...children); +export const legend: CreateFn<"legend"> = (attrs?, ...children) => el("legend", attrs, ...children); +export const table: CreateFn<"table"> = (attrs?, ...children) => el("table", attrs, ...children); +export const thead: CreateFn<"thead"> = (attrs?, ...children) => el("thead", attrs, ...children); +export const tbody: CreateFn<"tbody"> = (attrs?, ...children) => el("tbody", attrs, ...children); +export const tr: CreateFn<"tr"> = (attrs?, ...children) => el("tr", attrs, ...children); +export const th: CreateFn<"th"> = (attrs?, ...children) => el("th", attrs, ...children); +export const td: CreateFn<"td"> = (attrs?, ...children) => el("td", attrs, ...children); +export const blockquote: CreateFn<"blockquote"> = (attrs?, ...children) => el("blockquote", attrs, ...children); +export const cite: CreateFn<"cite"> = (attrs?, ...children) => el("cite", attrs, ...children); +export const code: CreateFn<"code"> = (attrs?, ...children) => el("code", attrs, ...children); +export const pre: CreateFn<"pre"> = (attrs?, ...children) => el("pre", attrs, ...children); +export const dialog: CreateFn<"dialog"> = (attrs?, ...children) => el("dialog", attrs, ...children); +export const details: CreateFn<"details"> = (attrs?, ...children) => el("details", attrs, ...children); +export const summary: CreateFn<"summary"> = (attrs?, ...children) => el("summary", attrs, ...children); +export const strong: CreateFn<"strong"> = (attrs?, ...children) => el("strong", attrs, ...children); +export const em: CreateFn<"em"> = (attrs?, ...children) => el("em", attrs, ...children); +export const del: CreateFn<"del"> = (attrs?, ...children) => el("del", attrs, ...children); +export const hr: CreateFn<"hr"> = (attrs?) => el("hr", attrs); diff --git a/lib/src/main.ts b/lib/src/main.ts index 7bd5389..6bdcf22 100644 --- a/lib/src/main.ts +++ b/lib/src/main.ts @@ -1,58 +1,12 @@ -import { persistPlugin } from "$plugins/persist"; -import { scrollPlugin } from "$plugins/scroll"; -import { urlPlugin } from "$plugins/url"; -import { computed, effect, mount, registerPlugin, signal } from "$volt"; - -registerPlugin("persist", persistPlugin); -registerPlugin("scroll", scrollPlugin); -registerPlugin("url", urlPlugin); - -const count = signal(0); -const message = signal("Welcome to Volt.js!"); -const isActive = signal(true); -const inputValue = signal(""); -const scrollPos = signal(0); -const section1Visible = signal(false); -const section2Visible = signal(false); - -const doubled = computed(() => count.get() * 2); - -effect(() => { - console.log("Count changed:", count.get()); -}); - -const scope = { - count, - doubled, - message, - isActive, - inputValue, - scrollPos, - section1Visible, - section2Visible, - classes: signal({ active: true, highlight: false }), - increment: () => { - count.set(count.get() + 1); - }, - decrement: () => { - count.set(count.get() - 1); - }, - reset: () => { - count.set(0); - }, - toggleActive: () => { - isActive.set(!isActive.get()); - }, - updateMessage: () => { - message.set(`Count is now ${count.get()}`); - }, - handleInput: (event: Event) => { - const target = event.target as HTMLInputElement; - inputValue.set(target.value); - }, -}; - -const app = document.querySelector("#app"); -if (app) { - mount(app, scope); -} +/** + * Entry point for Volt.js development demo + * + * This file initializes the comprehensive demo showcasing: + * - Volt.js reactive features (signals, computed, effects, bindings) + * - Volt CSS classless styling (typography, layout, Tufte sidenotes) + * - Plugin system (persist, scroll, url) + */ + +import { setupDemo } from "./demo"; + +setupDemo(); diff --git a/lib/src/styles/base.css b/lib/src/styles/base.css index 0d3cfc7..5c692ba 100644 --- a/lib/src/styles/base.css +++ b/lib/src/styles/base.css @@ -1,3 +1,14 @@ +/* + 8b d8 88 + `8b d8' 88 ,d + `8b d8' 88 88 + `8b d8' ,adPPYba, 88 MM88MMM ,adPPYba, ,adPPYba, ,adPPYba, + `8b d8' a8" "8a 88 88 a8" "" I8[ "" I8[ "" + `8b d8' 8b d8 88 88 8b `"Y8ba, `"Y8ba, + `888' "8a, ,a8" 88 88, 888 "8a, ,aa aa ]8I aa ]8I + `8' `"YbbdP"' 88 "Y888 888 `"Ybbd8"' `"YbbdP"' `"YbbdP"' +*/ + /** * Volt CSS - Classless stylesheet for elegant, readable web documents *