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
HTML
<!-- 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) ;
color: white ;
border-color: var(--site-primary) ;
}
#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 ;
}
#tutorial-resizer {
display: none;
}
#code-column,
#concepts-column {
min-width: 100% ;
}
}
</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 ${signal.get('reviewCount').value} reviews in ${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>