UNPKG

lightview

Version:

A reactive UI library with features of Bau, Juris, and HTMX plus safe LLM UI generation

879 lines (772 loc) 43.8 kB
<!-- SEO-friendly SPA Shim --> <script src="/lightview-router.js?base=/index.html"></script> <div class="section"> <div class="section-content" style="max-width: 1200px;"> <h1>Getting Started</h1> <!-- Step Navigation --> <div class="tutorial-steps" style="display: flex; gap: 0.5rem; margin-bottom: 2rem; flex-wrap: wrap;"> <button class="btn btn-primary step-btn active" onclick="switchStep(1)">1. Basics</button> <button class="btn btn-secondary step-btn" onclick="switchStep(2)">2. Signals</button> <button class="btn btn-secondary step-btn" onclick="switchStep(3)">3. State</button> <button class="btn btn-secondary step-btn" onclick="switchStep(4)">4. Hypermedia</button> <button class="btn btn-secondary step-btn" onclick="switchStep(5)">5. Components</button> <button class="btn btn-secondary step-btn" onclick="switchStep(6)">6. Gating</button> <button class="btn btn-secondary step-btn" onclick="switchStep(7)">7. cDOM</button> </div> <!-- Tutorial Layout: Preview on top, Code & Concepts below --> <div class="tutorial-wrapper" style="border: 1px solid var(--site-border); border-radius: var(--site-radius-lg); overflow: hidden; background: var(--site-surface);"> <!-- Preview Area --> <div id="tutorial-preview" style="min-height: 350px; background: var(--site-bg); border-bottom: 1px solid var(--site-border);"> <!-- Iframes injected here --> </div> <!-- Split: Code | Resizer | Concepts --> <div id="tutorial-split" style="display: flex; min-height: 400px;"> <!-- Code Column --> <div id="code-column" class="tutorial-column" style="flex: 1; min-width: 200px; display: flex; flex-direction: column; border-right: none;"> <div style="padding: 0.5rem 1rem; background: var(--site-bg-alt); border-bottom: 1px solid var(--site-border);"> <span style="font-weight: 600; color: var(--site-text-secondary); font-size: 0.875rem;">CODE</span> </div> <!-- Code blocks injected here --> </div> <!-- Resizer Handle --> <div id="tutorial-resizer" style="width: 6px; background: var(--site-border); cursor: col-resize; flex-shrink: 0; transition: background 0.2s;"> </div> <!-- Concepts Column --> <div id="concepts-column" class="tutorial-column" style="flex: 1; min-width: 200px; display: flex; flex-direction: column;"> <div style="padding: 0.5rem 1rem; background: var(--site-bg-alt); border-bottom: 1px solid var(--site-border);"> <span style="font-weight: 600; color: var(--site-text-secondary); font-size: 0.875rem;">CONCEPTS</span> </div> <div id="tutorial-concepts" style="padding: 1.5rem; overflow-y: auto; flex: 1;"> <!-- Concept blocks injected here --> </div> </div> </div> </div> </div> </div> <style> .step-btn.active { background: var(--site-primary) !important; color: white !important; border-color: var(--site-primary) !important; } #tutorial-resizer:hover { background: var(--site-primary); } #tutorial-resizer.dragging { background: var(--site-primary); } /* Step content visibility */ .step-content { display: none; height: 100%; } .step-content.active { display: block; } /* Code column specific layout for active step */ #code-column .step-content.active { display: flex; flex-direction: column; flex: 1; overflow: auto; } /* Ensure pre tags fill space */ #code-column pre { flex: 1; margin: 0; outline: none; } @media (max-width: 900px) { #tutorial-split { flex-direction: column !important; } #tutorial-resizer { display: none; } #code-column, #concepts-column { min-width: 100% !important; } } </style> <script> (function () { const previewContainer = document.getElementById('tutorial-preview'); const codeContainer = document.getElementById('code-column'); const conceptsContainer = document.getElementById('tutorial-concepts'); const stepBtns = document.querySelectorAll('.step-btn'); // Tutorial data for each step const tutorialData = { 1: { options: { autoRun: true, }, code: `// STEP 1: BASICS - Pure UI with Tagged Functions const { tags, $ } = Lightview; const { div, h1, p, style } = tags; // 1. Build UI using Tagged Functions // These functions return Lightview elements (access raw DOM via .domEl) const App = div({ class: 'hero' }, h1('Welcome to Lightview'), p('Lightview is a library for humans and LLMs to build modern web interfaces.') ); // 2. The $ Function for Selections & Content // Inject the App into the #app container $('#app').content(App); // 3. Injecting Styles // Use $ with a location to inject styles at the end of the app $('#app').content(style({}, \` .hero { padding: 2rem; background: #f8fafc; border-radius: 12px; border: 1px solid #e2e8f0; font-family: system-ui, sans-serif; } h1 { color: #1e293b; margin: 0 0 1rem; font-size: 2rem; } p { color: #64748b; font-size: 1.1rem; line-height: 1.5; } \`), 'beforeend');`, concepts: ` <h3 style="margin-top: 0; color: var(--site-primary);">Step 1: Basic Content Creation</h3> <p>Welcome! Let's start with the basics of building UI without reactivity or components.</p> <h4>Key Concepts:</h4> <ul style="padding-left: 1.25rem; color: var(--site-text-secondary);"> <li style="margin-bottom: 0.75rem;"><code>tags</code> — Every HTML tag is available as a function. <code>div(...)</code> returns a virtual proxy; the real DOM element is accessible via <code>.domEl</code>.</li> <li style="margin-bottom: 0.75rem;"><code>$(selector)</code> — A powerful utility for selecting elements and manipulating them.</li> <li style="margin-bottom: 0.75rem;"><code>.content(node, location)</code> — Replaces or appends content. It automatically unboxes <code>.domEl</code> for you.</li> </ul> <p>Lightview elements are thin proxies over standard DOM elements, providing reactivity with minimal overhead.</p> ` }, 2: { options: { autoRun: true, }, code: `// STEP 2: SIGNALS - The foundation of reactivity const { signal, tags, $ } = Lightview; const { div, h3, p, button, img, style } = tags; // Create a reactive signal for the button state const added = signal(false); // Build the UI using Lightview's tag functions const App = div({ class: 'product-card' }, img({ src: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=500&auto=format&fit=crop', class: 'product-image' }), h3({ class: 'product-title' }, 'Red Nike Sneakers'), // Reactive button - text updates when signal changes div({ class: 'product-footer' }, button({ class: 'add-btn', onclick: () => added.value = !added.value // Toggle state }, () => added.value ? '✓ Added' : 'Add to Cart') ) ); $('#app').content(App); // --------------------------------------------------------------- // STYLES // --------------------------------------------------------------- $('#app').content(style({}, \`.product-card { width: 280px; font-family: system-ui, sans-serif; } .product-image { width: 100%; border-radius: 8px; margin-bottom: 1rem; } .product-title { margin: 0 0 0.25rem; font-size: 1.1rem; } .product-footer { display: flex; justify-content: flex-end; } .add-btn { background: #3b82f6; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: 500; } .add-btn:hover { background: #2563eb; }\`), 'beforeend');`, concepts: ` <h3 style="margin-top: 0; color: var(--site-primary);">Step 2: Reactive Signals</h3> <p>In this step, we use <strong>Signals</strong> to handle data that changes over time.</p> <h4>Key Concepts:</h4> <ul style="padding-left: 1.25rem; color: var(--site-text-secondary);"> <li style="margin-bottom: 0.75rem;"><code>signal(value)</code> — Creates a reactive container. Changing <code>added.value</code> automatically updates the UI.</li> <li style="margin-bottom: 0.75rem;"><code>tags</code> — Build HTML elements with pure JavaScript functions like <code>div()</code>, <code>button()</code>.</li> <li style="margin-bottom: 0.75rem;"><code>Inside Tags</code> — Pass a function as a child (e.g., <code>() => added.value ? ...</code>) to create a reactive text node.</li> </ul> <p><strong>Try it:</strong> Click the "Add to Cart" button to see it toggle!</p> ` }, 3: { options: { allowSameOrigin: true, autoRun: true, }, code: `// STEP 3: STATE + VDOM SYNTAX const { tags, $ } = Lightview; const { div, style } = tags; const { state } = LightviewX; // 1. Deep Reactivity with Optional Persistence const session = state({ cart: [ { id: 3, name: 'Green Reeboks', price: 60 } ], items: [ { id: 1, name: 'Red Nike Sneakers', price: 99 }, { id: 2, name: 'Blue Adidas', price: 85 }, { id: 3, name: 'Green Reeboks', price: 60 } ] }, { name: 'shopping-session', storage: sessionStorage }); const addToCart = (item) => session.cart.push(item); const clearCart = () => { session.cart.length = 0; // Clear reactive array sessionStorage.removeItem('shopping-session'); // Delete session variable }; // 2. vDOM Syntax: { tag, attributes, children } const App = div({ class: 'shop-container' }, [ // Cart Badge { tag: 'div', attributes: { class: 'cart-badge' }, children: [ { tag: 'span', attributes: {}, children: [ () => '🛒 Cart: ' + session.cart.length + ' items' ]}, { tag: 'button', attributes: { class: 'clear-btn', onclick: clearCart }, children: ['Clear Cart'] } ]}, // Product List { tag: 'div', attributes: { class: 'product-list' }, children: session.items.map(item => ({ tag: 'div', attributes: { class: 'product-row' }, children: [ { tag: 'span', children: [item.name + ' ($' + item.price + ')'] }, { tag: 'button', attributes: { class: 'add-btn', onclick: () => addToCart(item) }, children: ['Add'] } ] })) }, // Cart Contents { tag: 'h3', attributes: { class: 'cart-title' }, children: ['Cart:'] }, { tag: 'ul', attributes: { class: 'cart-list' }, children: [ () => session.cart.length === 0 ? { tag: 'li', attributes: { class: 'empty' }, children: ['Empty'] } : session.cart.map((item, i) => ({ tag: 'li', attributes: {}, children: [ item.name + ' ', { tag: 'button', attributes: { class: 'remove-btn', onclick: () => session.cart.splice(i, 1) }, children: ['×'] } ] })) ]}, // Styles { tag: 'style', attributes: {}, children: [ \`.shop-container { width: 320px; font-family: system-ui, sans-serif; } .cart-badge { padding: 0.75rem; background: #f3f4f6; border-radius: 8px; margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center; } .clear-btn { font-size: 0.7rem; padding: 4px 8px; background: #94a3b8; color: white; border: none; border-radius: 4px; cursor: pointer; } .product-list { display: flex; flex-direction: column; gap: 0.5rem; } .product-row { display: flex; justify-content: space-between; padding: 0.75rem; border: 1px solid #e5e7eb; border-radius: 6px; } .add-btn { background: #3b82f6; color: white; border: none; padding: 0.25rem 0.75rem; border-radius: 4px; cursor: pointer; } .cart-title { font-size: 1rem; margin-top: 1.5rem; } .cart-list { padding-left: 1.25rem; margin: 0.5rem 0; } .cart-list li { margin-bottom: 0.25rem; } .cart-list .empty { color: #9ca3af; } .remove-btn { font-size: 0.875rem; color: #ef4444; border: none; background: none; cursor: pointer; margin-left: 0.5rem; }\` ]} ]); $('#app').content(App);`, concepts: ` <h3 style="margin-top: 0; color: var(--site-primary);">Step 3: State & vDOM Syntax</h3> <p>This step introduces <strong>State</strong> and the <strong>vDOM</strong> object syntax.</p> <h4>Key Concepts:</h4> <ul style="padding-left: 1.25rem; color: var(--site-text-secondary);"> <li style="margin-bottom: 0.75rem;"><strong>vDOM Syntax</strong> — Elements are plain objects: <code>{ tag: 'div', attributes: {}, children: [] }</code>. This structure is excellent for programmatic generation, serialization (JSON), or building custom components.</li> <li style="margin-bottom: 0.75rem;"><strong>Deep Reactivity</strong> — Use <code>LightviewX.state()</code> for complex objects and arrays. Arrays methods like <code>.push()</code> and <code>.splice()</code> trigger updates automatically.</li> <li style="margin-bottom: 0.75rem;"><strong>State Persistence</strong> — Pass <code>{ name, storage }</code> to <code>state()</code> to automatically persist data to <code>sessionStorage</code> or <code>localStorage</code>.</li> <li style="margin-bottom: 0.75rem;"><strong>Interoperability</strong> — You can pass valid vDOM objects as children to standard <code>tags</code> functions directly.</li> </ul> <p><strong>Try it:</strong> Add items to the cart and re-run the code to see persistence!</p> ` }, 4: { options: { allowSameOrigin: true, autoRun: true }, code: `// STEP 4: HYPERMEDIA + OBJECT DOM SYNTAX (requires lightview-x) // Choose a format to load: HTML, VDOM, or Object DOM // Object DOM syntax integration is handled by lightview-x const { signal, element, $ } = Lightview; // Track which format was loaded and the review count const currentFormat = signal('none', 'currentFormat'); const reviewCount = signal(0, 'reviewCount'); // Helper to get source code signal const sourceCode = signal('Select a format...', 'sourceCode'); const loadFormat = async (format, file) => { currentFormat.value = format; reviewCount.value = 3; // Fetch source code for display try { const text = await fetch(file).then(r => r.text()); sourceCode.value = new String(text); } catch (e) { sourceCode.value = 'Error loading source'; } }; // Build UI using Object DOM syntax - compact JSON format const App = element('div', { class: 'reviews-container' }, [ // Header { div: { class: 'reviews-header', children: [ { h3: { class: 'reviews-title', children: ['Customer Reviews'] } }, { p: { class: 'reviews-subtitle', children: [ 'Choose a format to load reviews from external files:' ] } }, // Format selector buttons { div: { class: 'format-buttons', children: [ { button: { class: 'format-btn html', href: '/docs/getting-started/reviews.html', target: '#reviews-box', onclick: () => loadFormat('HTML', './reviews.html'), children: ['📄 HTML'] } }, { button: { class: 'format-btn vdom', href: '/docs/getting-started/reviews.vdom', target: '#reviews-box', onclick: () => loadFormat('VDOM', './reviews.vdom'), children: ['🔷 VDOM'] } }, { button: { class: 'format-btn odom', href: '/docs/getting-started/reviews.odom', target: '#reviews-box', onclick: () => loadFormat('ODOM', './reviews.odom'), children: ['🔶 Object DOM'] } } ] } } ] } }, // Split layout: Rendered UI | Source Code { div: { class: 'split-view', children: [ // Left: Rendered Content { div: { class: 'render-pane', children: [ { p: { class: 'pane-label', children: ['Rendered Output:'] } }, { div: { id: 'reviews-box', src: '' } }, { p: { class: 'status', children: [ () => currentFormat.value === 'none' ? 'Click a button above to load...' : 'Loaded ' + reviewCount.value + ' reviews from ' + currentFormat.value ] } } ] } }, // Right: Source Code { div: { class: 'source-pane', children: [ { p: { class: 'pane-label', children: ['Source Code:'] } }, { pre: { class: 'source-code', children: [ () => sourceCode.value ] } } ] } } ] } }, // Styles { style: { children: [ \`.reviews-container { width: 100%; box-sizing: border-box; font-family: system-ui, sans-serif; } .reviews-header { margin-bottom: 1rem; } .reviews-title { margin: 0 0 0.5rem; } .reviews-subtitle { color: #6b7280; margin: 0 0 0.75rem; font-size: 0.9em; } .format-buttons { display: flex; gap: 0.5rem; margin-bottom: 1rem; } .format-btn { flex: 1; padding: 0.5rem; cursor: pointer; border: none; border-radius: 6px; font-weight: 500; font-size: 0.9em; } .format-btn.html { background: #dbeafe; color: #1e40af; } .format-btn.vdom { background: #ede9fe; color: #6d28d9; } .format-btn.odom { background: #ffedd5; color: #c2410c; } .split-view { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; width: 100%; box-sizing: border-box; } .render-pane, .source-pane { border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem; background: #fff; min-width: 0; } .pane-label { font-size: 0.75rem; font-weight: bold; color: #9ca3af; text-transform: uppercase; margin: 0 0 0.5rem; } .source-code { margin: 0; font-family: monospace; font-size: 0.75rem; background: #f8f9fa; padding: 0.5rem; border-radius: 4px; overflow-x: auto; white-space: pre-wrap; color: #374151; height: 90%; overflow-y: auto; } .status { color: #6366f1; font-weight: 500; margin: 1rem 0 0; font-size: 0.9em; }\` ]}} ]); $('#app').content(App);`, concepts: ` <h3 style="margin-top: 0; color: var(--site-primary);">Step 4: Hypermedia + Multiple Formats</h3> <p>Lightview's <a href="/docs/hypermedia/">hypermedia</a> feature allows your application to load partial content dynamically from various file formats.</p> <h4>Key Concepts:</h4> <ul style="padding-left: 1.25rem; color: var(--site-text-secondary);"> <li style="margin-bottom: 0.75rem;"><code>href</code> on any element — Clicking it will fetch content and place it into the target.</li> <li style="margin-bottom: 0.75rem;"><code>src</code> — Defines the source URL to fetch content from.</li> <li style="margin-bottom: 0.75rem;"><strong>Template Literals</strong> — Loaded files (HTML, VDOM, ODOM) can contain <code>\${...}</code> syntax. These are evaluated reactively and can access named <code>signals</code> or <code>state</code> using <code>signal.get('name').value</code>. Note the code <code>Displaying &dollar;{signal.get('reviewCount').value} reviews in &dollar;{signal.get('currentFormat').value} format.</code> in the examples.</li> </ul> <hr style="margin: 1.5rem 0; border: none; border-top: 1px solid var(--site-border);"> <h4>Format Comparison:</h4> <div style="font-size: 0.875rem;"> <div style="margin-bottom: 1rem;"> <strong style="color: #1e40af;">📄 HTML</strong> <ul style="margin: 0.25rem 0 0; padding-left: 1.25rem; color: var(--site-text-secondary);"> <li><strong>Pros:</strong> Standard, easy to read, works with existing backend templates.</li> <li><strong>Cons:</strong> Verbose, no type safety, harder to manipulate programmatically.</li> </ul> </div> <div style="margin-bottom: 1rem;"> <strong style="color: #6d28d9;">🔷 VDOM JSON</strong> <ul style="margin: 0.25rem 0 0; padding-left: 1.25rem; color: var(--site-text-secondary);"> <li><strong>Pros:</strong> Explicit structure, easy to generate from servers, lightweight.</li> <li><strong>Cons:</strong> Verbose JSON structure (repeated "tag", "attributes" keys).</li> </ul> </div> <div> <strong style="color: #c2410c;">🔶 Object DOM JSON</strong> <ul style="margin: 0.25rem 0 0; padding-left: 1.25rem; color: var(--site-text-secondary);"> <li><strong>Pros:</strong> Extremely compact, human-readable JSON, expressive.</li> <li><strong>Cons:</strong> Non-standard structure, requires understanding the syntax mapping. Must load lightview-x.js</li> </ul> </div> </div> ` }, 5: { options: { autoRun: true }, code: `// STEP 5: COMPONENTS, COMPOSITION & COMPUTED // Import the real components from the component library import Button from '../../components/actions/button.js'; import Badge from '../../components/data-display/badge.js'; import '../../components/data-display/card.js'; const { tags, signal, computed, $ } = Lightview; const { div, h3, span, Card, style, img } = tags; // Logic for the product card const price = 99; const quantity = signal(1); // Reactive signal const total = computed(() => price * quantity.value); // Computed derived value const styles = \` .product-card { width: 500px; font-family: system-ui, sans-serif; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); background: white; border-radius: 1rem; overflow: hidden; } .product-body { padding: 1.25rem; } .product-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1rem; } .card-title { margin: 0; font-size: 1.1rem; font-weight: bold; } .product-controls { display: flex; justify-content: space-between; align-items: center; padding-top: 1rem; border-top: 1px solid #f3f4f6; } .total-price { font-size: 1.25rem; font-weight: 800; color: #111; } .quantity-controls { display: flex; align-items: center; gap: 0.5rem; } .quantity-value { width: 1.5rem; text-align: center; font-weight: 500; } \`; // 5. Styles for Shadow DOM // Registering a named stylesheet allows it to be used inside isolated component styles LightviewX.registerStyleSheet('product-styles', styles); const App = Card({ class: 'product-card', styleSheets: ['product-styles'] // Passes the registered sheet into the shadow root }, img({ src: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=500&auto=format&fit=crop', style: 'object-fit: cover;', alt: 'Red Nike Sneakers' }), Card.Body({ class: 'product-body' }, div({ class: 'product-header' }, h3({ class: 'card-title' }, 'Red Nike Sneakers'), Badge({ color: 'success', variant: 'outline' }, 'In Stock') ), // Quantity & Price Controls div({ class: 'product-controls' }, span({ class: 'total-price' }, () => '$' + total.value), div({ class: 'quantity-controls' }, Button({ size: 'sm', class: 'btn-circle', onclick: () => quantity.value = Math.max(1, quantity.value - 1) }, '-'), span({ class: 'quantity-value' }, () => quantity.value), Button({ size: 'sm', class: 'btn-circle', onclick: () => quantity.value++ }, '+') ) ) ) ); $('#app').content(App);`, concepts: `<h3 style="margin-top: 0; color: var(--site-primary);">Step 5: Components & Isolated Styling</h3> <p>Use the <strong>Lightview Component Library</strong> for polished UI elements that use Shadow DOM for isolation.</p> <h4>Key Concepts:</h4> <ul style="padding-left: 1.25rem; color: var(--site-text-secondary);"> <li style="margin-bottom: 0.75rem;"><strong>Components</strong> — Reusable blocks like <code>Card</code>, <code>Button</code>, and <code>Badge</code>.</li> <li style="margin-bottom: 0.75rem;"><strong>Auto-Registration</strong> — Components auto-register with <code>tags</code> for easy destructuring.</li> <li style="margin-bottom: 0.75rem;"><strong>Shadow DOM Isolation</strong> — Components isolate their styles (including DaisyUI/Tailwind) to avoid global CSS pollution.</li> <li style="margin-bottom: 0.75rem;"><strong>Named StyleSheets</strong> — Use <code>LightviewX.registerStyleSheet(name, css)</code> to define styles that can be safely passed into a component's <code>styleSheets</code> property.</li> <li style="margin-bottom: 0.75rem;"><strong>Computed</strong><code>computed(() => ...)</code> creates derived values that reactively update based on other signals.</li> </ul> <p><strong>Next:</strong> Explore the <a href="/docs/components">Components</a> library!</p>` }, 6: { options: { allowSameOrigin: true, autoRun: true }, code: `// STEP 6: GATING EVENTS (lv-before) const { tags, signal, $ } = Lightview; const { div, h3, button, input, style } = tags; // 1. Logic for our example const message = signal('Type something or click the button...'); const callCount = signal(0); // Global confirmation function used by lv-before window.confirmAction = function(msg,event) { if(confirm(msg)) { console.log('Allowed:',this,event); return true; } console.log('Blocked:',this,event); return false; } const App = div({ class: 'gating-demo' }, h3('Event Gating & Rate Limiting'), // Throttle: Only allows 1 click per 2 seconds div({ class: 'demo-section' }, div('1. Throttled Button (2s delay):'), button({ class: 'btn-throttle', // Intercept click and apply throttle gate 'lv-before': 'click throttle(2000)', onclick: () => callCount.value++ }, 'Spam Me!'), div({ class: 'counter' }, () => 'Actual clicks allowed: ' + callCount.value) ), // Debounce: Waits for 500ms of silence div({ class: 'demo-section' }, div('2. Debounced Search (500ms delay):'), input({ class: 'search-input', placeholder: 'Type fast...', // Wait for silence before letting oninput fire 'lv-before': 'input debounce(500)', oninput: (e) => message.value = 'Search triggered: "' + e.target.value + '"' }), div({ class: 'message' }, () => message.value) ), // Conditional Gate: Chaining a confirmation div({ class: 'demo-section' }, div('3. Chained Gates (Throttle + Confirm):'), button({ class: 'btn-confirm', 'lv-before': 'click throttle(3000) confirmAction("Are you sure you want to delete?",event)', onclick: () => message.value = 'Item deleted (not really)!' }, 'Delete Item') ), style({ children: [ \`.gating-demo { font-family: system-ui, sans-serif; padding: 1rem; } .demo-section { margin-bottom: 1.5rem; padding: 1rem; background: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0; } .demo-section div:first-child { font-weight: bold; margin-bottom: 0.5rem; color: #475569; } button { padding: 0.5rem 1rem; border: none; border-radius: 6px; cursor: pointer; color: white; font-weight: 500; } .btn-throttle { background: #3b82f6; } .btn-confirm { background: #ef4444; } .search-input { width: 100%; padding: 0.5rem; border: 1px solid #cbd5e1; border-radius: 6px; } .counter, .message { margin-top: 0.5rem; font-size: 0.9rem; font-family: monospace; color: #6366f1; }\` ]}) ); $('#app').content(App);`, concepts: `<h3 style="margin-top: 0; color: var(--site-primary);">Step 6: Declarative Event Gating</h3> <p>Use <code>lv-before</code> to control how and when your event handlers are executed.</p> <h4>Key Concepts:</h4> <ul style="padding-left: 1.25rem; color: var(--site-text-secondary);"> <li style="margin-bottom: 0.75rem;"><strong>Event Selection</strong> — You must specify which events to gate by name (e.g., <code>click</code>) or use <code>*</code> for all common UI and form events.</li> <li style="margin-bottom: 0.75rem;"><strong>Declarative Middleware</strong><code>lv-before</code> acts as a gatekeeper. It runs before your <code>onclick</code> or <code>oninput</code>.</li> <li style="margin-bottom: 0.75rem;"><strong>Built-in Modifiers</strong> — Use <code>throttle(ms)</code> and <code>debounce(ms)</code> for rate limiting without writing complex wrapper functions.</li> <li style="margin-bottom: 0.75rem;"><strong>Chaining</strong> — You can chain multiple gates (e.g., <code>click throttle(2000) confirmAction(...)</code>). If any gate returns false (or aborts), the final handler never fires. Spaces are used as separators, but are ignored inside quotes and parentheses.</li> <li style="margin-bottom: 0.75rem;"><strong>Context</strong> — Gates are standard functions. They have access to <code>this</code> (the element) and can accept arguments like <code>event</code>.</li> <li style="margin-bottom: 0.75rem;"><strong>Write your own</strong> — Write your own functions and add the to <code>globalThis</code> to make them available to all templates. They have access to <code>this</code> (the element) and can accept arguments like <code>event</code>.</li> </ul> <p><strong>Try it:</strong> Click the Spam Me button rapidly, or type quickly into the search box to see the limiters in action!</p>` }, 7: { options: { autoRun: true }, code: `// STEP 7: cDOM - Compressed DOM & JPRX await import('/lightview-cdom.js'); const { parseJPRX, hydrate } = globalThis.LightviewCDOM; const { $ } = Lightview; const cdomString = \`{ div: { onmount: =signal(0,"count"), children: [ { h3: ["Standard JPRX Counter"] }, { p: { children: ["Count: ", =/count] }}, { div: { children: [ { button: { onclick: =decrement(/count), children: ["-"] } }, { button: { onclick: =/count++, children: ["+"] } } ]}} ] } }\`; const hydrated = hydrate(parseJPRX(cdomString)); $('#app').content(hydrated);`, concepts: ` <h3 style="margin-top: 0; color: var(--site-primary);">Step 7: cDOM & JPRX</h3> <p><strong><a href="/docs/cdom.html">cDOM</a></strong> (compressed DOM) combined with <strong><a href="/docs/cdom.html#JPRX">JPRX</a></strong> (JSON Path Reactive eXpressions) allows you to define entire reactive UIs as pure JSON-compatible strings.</p> <h4>Key Concepts:</h4> <ul style="padding-left: 1.25rem; color: var(--site-text-secondary);"> <li style="margin-bottom: 0.75rem;"><strong>Portability</strong> — Since the UI is a string, it can be easily stored in databases, sent over the wire, or generated by an LLM without security risks of <code>eval()</code>.</li> <li style="margin-bottom: 0.75rem;"><strong>JPRX Expressions</strong> — Use <code>=</code> to denote reactive expressions within the string. For example, <code>=/count</code> binds to a signal.</li> <li style="margin-bottom: 0.75rem;"><strong>Flexible Syntax</strong> — Operators and helpers can be called as functions (<code>=decrement(/count)</code>) or using prefix (<code>=--/count</code>), infix (<code>=/count - 1</code>), or postfix (<code>=/count--</code>) notation.</li> <li style="margin-bottom: 0.75rem;"><strong>Hydration</strong><code>hydrate()</code> turns the parsed JPRX into a live, reactive DOM tree.</li> </ul> <p><strong>Try it:</strong> Edit the <code>cdomString</code> to change the labels or add new elements!</p> ` } }; // Initialize Steps Object.keys(tutorialData).forEach(step => { const data = tutorialData[step]; // 1. Create Preview Container const previewEl = document.createElement('div'); previewEl.id = `preview-${step}`; previewEl.className = 'step-content'; previewContainer.appendChild(previewEl); // 2. Create Code Container const codeWrapper = document.createElement('div'); codeWrapper.id = `code-container-${step}`; codeWrapper.className = 'step-content'; const preEl = document.createElement('pre'); preEl.style.flex = '1'; preEl.style.margin = '0'; preEl.style.overflow = 'auto'; const codeEl = document.createElement('code'); codeEl.id = `code-${step}`; codeEl.contentEditable = "true"; codeEl.style.display = 'block'; codeEl.style.padding = '1rem'; codeEl.style.minHeight = '100%'; codeEl.style.outline = 'none'; codeEl.textContent = data.code; preEl.appendChild(codeEl); codeWrapper.appendChild(preEl); codeContainer.appendChild(codeWrapper); // 3. Create Concepts Container const conceptsEl = document.createElement('div'); conceptsEl.id = `concepts-${step}`; conceptsEl.className = 'step-content'; conceptsEl.innerHTML = data.concepts; conceptsContainer.appendChild(conceptsEl); // 4. Initialize Examplify // Simplified: Use same origin for localhost to avoid connectivity issues with 127.0.0.1 const useOrigin = (data.options?.allowSameOrigin && (globalThis.location.hostname === 'localhost' || globalThis.location.hostname === '127.0.0.1')) ? null : (data.options?.allowSameOrigin ? 'https://sandbox.lightview.dev' : null); // Check if step uses modules (looking for import statement) const isModule = data.code.includes('import '); const exResult = examplify(codeEl, { scripts: ['../../../lightview.js', '../../../lightview-x.js'], html: '<div id="app" style="padding: 1rem;"></div>', location: 'afterEnd', type: isModule ? 'module' : 'text/javascript', useOrigin, ...data.options || {} }); // Move iframe to preview container if (data.previewSrc) { // Use external file for preview (e.g., SSR demo) const iframe = document.createElement('iframe'); iframe.src = data.previewSrc; iframe.style.width = '100%'; iframe.style.border = 'none'; iframe.style.height = '100%'; previewEl.appendChild(iframe); } else if (exResult && exResult.iframe) { previewEl.appendChild(exResult.iframe); exResult.iframe.style.width = '100%'; exResult.iframe.style.border = 'none'; exResult.iframe.style.height = '100%'; } }); // Step switching logic globalThis.switchStep = function (step) { // Update buttons stepBtns.forEach((btn, i) => { const btnStep = i + 1; if (btnStep === step) { btn.classList.add('active', 'btn-primary'); btn.classList.remove('btn-secondary'); } else { btn.classList.remove('active', 'btn-primary'); btn.classList.add('btn-secondary'); } }); // Toggle Content ['preview', 'code-container', 'concepts'].forEach(prefix => { document.querySelectorAll(`[id ^= "${prefix}-"]`).forEach(el => { if (el.id === `${prefix}-${step}`) { el.classList.add('active'); } else { el.classList.remove('active'); } }); }); // Trigger onLoad for the step if it exists (e.g. fetching files) const data = tutorialData[step]; if (data && data.onLoad) data.onLoad(); }; // ---- Resizable columns ---- const resizer = document.getElementById('tutorial-resizer'); const splitContainer = document.getElementById('tutorial-split'); let isResizing = false; resizer.addEventListener('mousedown', (e) => { isResizing = true; resizer.classList.add('dragging'); document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isResizing) return; const containerRect = splitContainer.getBoundingClientRect(); // const containerWidth = containerRect.width - 6; const offsetX = e.clientX - containerRect.left; // Calculate percentages with min/max constraints let codePercent = (offsetX / containerRect.width) * 100; codePercent = Math.max(20, Math.min(80, codePercent)); codeContainer.style.flex = 'none'; codeContainer.style.width = codePercent + '%'; conceptsContainer.style.flex = 'none'; conceptsContainer.style.width = (100 - codePercent - 1) + '%'; }); document.addEventListener('mouseup', () => { if (isResizing) { isResizing = false; resizer.classList.remove('dragging'); document.body.style.cursor = ''; document.body.style.userSelect = ''; } }); // Initial load switchStep(1); })(); </script>