Files
sigpro/llms.txt
Natxo 8def4a99b7 Create detailed documentation for SigPro library
Added comprehensive documentation for SigPro library, covering core philosophy, API reference, component system, state management, and advanced patterns.
2026-03-15 14:24:20 +01:00

1072 lines
27 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Tienes toda la razón, necesitas un `llms.txt` mucho más exhaustivo. Aquí tienes una versión extremadamente detallada que realmente permitirá a una IA aprovechar todo el potencial de SigPro:
```txt
# SigPro Complete System Documentation
## Library Identity
SigPro is a zero-dependency reactive signal library (~5KB gzipped) for building reactive web applications with fine-grained reactivity, built-in DOM bindings, Web Components, routing, persistent storage, and WebSocket integration.
## Core Philosophy
- Signals are functions, not objects with .value
- Automatic dependency tracking (no manual declaration)
- Computed values cache until dependencies change
- DOM updates batched via microtask queue
- Everything reactive composes naturally
## Complete API Reference
### Core Signal: `$(initialValue)`
The fundamental reactive primitive.
**Overload 1: Regular Signal**
```javascript
const count = $(0); // Initial value
count() // Get -> 0
count(5) // Set -> 5
count(prev => prev + 1) // Update with function
count(prev => ({...prev, new: true})) // Object update
```
**Overload 2: Computed Signal**
```javascript
const doubled = $(() => count() * 2); // Auto-detects count dependency
const fullName = $(() => `${firstName()} ${lastName()}`);
```
**Advanced Patterns:**
```javascript
// Lazy computed - only recalculates when accessed AND dependencies changed
const expensive = $(() => {
console.log('Computing...');
return data().filter(complexOperation);
});
// Computed with cleanup (rare but possible)
const withCleanup = $(() => {
const temp = setup();
return () => cleanup(temp); // Runs when dependencies change
});
// Circular dependency protection - throws error
const a = $(() => b());
const b = $(() => a()); // Error: Circular dependency detected
```
### Effect: `$.effect(fn)`
Automatic side effect runner with intelligent cleanup.
```javascript
// Basic tracking
$.effect(() => {
console.log(`Count is: ${count()}`); // Re-runs when count changes
});
// Cleanup pattern
$.effect(() => {
const timer = setInterval(() => {
console.log('tick', count());
}, 1000);
// Return cleanup - runs BEFORE next effect run OR on stop
return () => {
clearInterval(timer);
console.log('Cleaned up timer');
};
});
// Nested effects - parent tracks child cleanup
$.effect(() => {
const stopChild = $.effect(() => {
// This child effect is automatically cleaned up
// when parent re-runs or stops
});
});
// Effect batching - multiple updates trigger one run
count(1);
count(2);
count(3); // Effect runs once with final value 3
// Manual stop
const stop = $.effect(() => {});
stop(); // Permanently stops effect, runs cleanup
// Effect within effect - parent tracks child
$.effect(() => {
const data = fetchData();
$.effect(() => {
// This child effect will be auto-cleaned
// when parent re-runs
process(data());
});
});
// Async effects - careful! Only sync dependencies tracked
$.effect(async () => {
const id = userId(); // Tracked ✅
const data = await fetch(`/api/${id}`); // Async
// DO NOT access signals here - they won't be tracked!
// Instead, move them before await or use separate effect
});
```
### DOM Template: `` html`...` ``
Complete reactive templating system.
**Text Interpolation:**
```javascript
// Basic
html`<div>Hello ${name}</div>`
// Reactive function
html`<div>Count: ${() => count()}</div>`
// Multiple values
html`<div>${() => a()} + ${() => b()} = ${() => a() + b()}</div>`
// Conditional rendering (falsy values: null/undefined/false render nothing)
html`<div>${() => isLoading() ? spinner : content}</div>`
// List rendering (array of nodes or strings)
html`<ul>${() => items.map(item => html`<li>${item}</li>`)}</ul>`
// Mixed content
html`<div>${() => [header, content, footer]}</div>`
// Fragment handling - auto-wraps multiple root nodes
html`
<header>Title</header>
<main>Content</main>
<footer>Footer</footer>
` // Returns DocumentFragment with 3 children
```
**Event Directives - Complete Reference:**
```javascript
// Basic
html`<button @click=${handler}>Click</button>`
// Event object access
html`<input @input=${(e) => value(e.target.value)}>`
// All modifiers:
// prevent - e.preventDefault()
// stop - e.stopPropagation()
// once - auto-remove after first fire
// self - only if e.target === element
// debounce:ms - debounce handler
// passive - { passive: true }
// capture - use capture phase
// Examples
html`<a @click.prevent.stop=${handler} href="#">No navigation, no bubble</a>`
html`<input @input.debounce:500=${search} placeholder="Search">`
html`<button @click.once=${oneTime}>Only once</button>`
html`<div @scroll.passive=${onScroll}>Optimized scroll</div>`
html`<div @click.capture=${captureHandler}>Capture phase</div>`
html`<button @click.self=${selfHandler}>Only if button clicked, not children</button>`
// Multiple modifiers
html`<input @keydown.debounce:300.prevent.stop=${handler}>`
// Dynamic event handlers
html`<button @click=${() => show() ? handler1 : handler2}>`
```
**Two-way Binding: `:property`**
```javascript
// Text input
html`<input :value=${userName}>`
// Checkbox
html`<input type="checkbox" :checked=${isAdmin}>`
// Radio group
html`
<input type="radio" name="size" value="small" :checked=${size === 'small'}>
<input type="radio" name="size" value="large" :checked=${size === 'large'}>
`
// Select
html`
<select :value=${selectedOption}>
<option value="a">A</option>
<option value="b">B</option>
</select>
`
// Textarea
html`<textarea :value=${content}></textarea>`
// Custom elements
html`<my-input :value=${data}></my-input>`
// Updates happen on 'input' for text, 'change' for checkboxes/radios
// Two-way binding creates two effects automatically
```
**Boolean Attributes: `?attribute`**
```javascript
html`<button ?disabled=${isDisabled}>`
html`<input ?required=${isRequired}>`
html`<details ?open=${expanded}>`
html`<option ?selected=${isSelected}>`
html`<div ?hidden=${!visible}>`
// Attribute present if truthy, absent if falsy
```
**Property Binding: `.property`**
```javascript
html`<div .scrollTop=${scrollPosition}>`
html`<progress .value=${progress} .max=${100}>`
html`<video .currentTime=${timestamp} .volume=${volume}>`
html`<my-element .data=${complexObject}>` // Pass objects directly
html`<div .classList=${['active', 'visible']}>` // Set classList property
// Property binding sets both property AND attribute (unless object/boolean)
```
**Special Cases:**
```javascript
// SVG support
html`<svg width="100" height="100">
<circle cx="50" cy="50" r=${() => radius()} fill="red"/>
</svg>`
// Self-closing tags
html`<img src=${imageUrl} alt=${altText}>`
// Comments - preserved in template
html`<!-- Header section --><h1>Title</h1>`
// Doctype - only at root
html`<!DOCTYPE html><html>...</html>`
// Template tag
html`<template>${() => repeatContent()}</template>`
// Slot handling
html`<my-component>${() => slottedContent}</my-component>`
```
### Component System: `$.component(tagName, setupFn, observedAttributes)`
Full Web Components integration with reactive properties.
**Complete Component Lifecycle:**
```javascript
$.component('my-counter', (props, ctx) => {
// 1. Initialization phase
console.log('Component initializing');
// Local state (not exposed as attribute)
const internalCount = $(0);
// Computed from props
const doubled = $(() => props.count() * 2);
// 2. Setup side effects
const stopEffect = $.effect(() => {
console.log('Count changed:', props.count());
});
// 3. Register cleanup
ctx.onUnmount(() => {
console.log('Cleaning up');
stopEffect();
});
// 4. Access DOM
ctx.select('button')?.classList.add('primary');
// 5. Handle slots
const defaultSlot = ctx.slot(); // unnamed slots
const headerSlot = ctx.slot('header'); // named slots
// 6. Emit events
const handleClick = () => {
ctx.emit('increment', { value: props.count() });
ctx.emit('counter-changed', { count: props.count() }, { bubbles: false }); // Custom options
};
// 7. Return template (must return Node or DocumentFragment)
return html`
<div class="counter">
<slot name="header">Default header</slot>
<button @click=${handleClick}>
Count: ${() => props.count()}
</button>
<slot>Default content</slot>
</div>
`;
}, ['count', 'min', 'max']); // Observed attributes
// Usage examples:
const el = document.createElement('my-counter');
el.count = 5; // Set property
el.setAttribute('min', '0'); // Set attribute
el.min = 10; // Property takes precedence
console.log(el.count); // Get property -> 10
console.log(el.getAttribute('count')); // Get attribute -> "10"
// Special attribute conversion:
el.disabled = ''; // true (except for 'value')
el.disabled = 'false'; // false
el.value = ''; // "" (empty string preserved)
el.checked = 'false'; // false
// Event listening:
el.addEventListener('increment', (e) => {
console.log(e.detail); // { value: x }
});
// Dynamic component creation:
const container = document.getElementById('app');
container.innerHTML = '<my-counter count="5"></my-counter>';
// Or programmatically:
const counter = new (customElements.get('my-counter'))();
counter.count = 5;
container.appendChild(counter);
```
**Advanced Component Patterns:**
```javascript
// Context/provider pattern
$.component('theme-provider', (props, ctx) => {
const theme = $(props.theme());
// Provide to children via custom event
ctx.emit('theme-provider', { theme });
return html`<slot></slot>`;
}, ['theme']);
$.component('themed-button', (props, ctx) => {
const theme = $('light');
// Consume from parent
ctx.host.addEventListener('theme-provider', (e) => {
theme(e.detail.theme);
});
return html`<button class=${() => theme()}>Click</button>`;
});
// Compound components
$.component('tabs', (props, ctx) => {
const activeTab = $(0);
return html`
<div class="tabs">
<div class="tab-headers">
${() => props.tabs().map((tab, i) => html`
<button @click=${() => activeTab(i)}>${tab.title}</button>
`)}
</div>
<div class="tab-content">
${() => props.tabs()[activeTab()].content}
</div>
</div>
`;
}, ['tabs']);
// Form association
$.component('my-input', (props, ctx) => {
// Integrate with parent forms
const form = ctx.host.closest('form');
if (form) {
form.addEventListener('reset', () => {
props.value(props.defaultValue());
});
}
return html`<input :value=${props.value}>`;
}, ['value', 'defaultValue']);
```
### Persistent Storage: `$.storage(key, initialValue, storage?)`
Automatic persistence with change detection.
```javascript
// Basic localStorage
const settings = $.storage('app-settings', { theme: 'light' });
settings({ ...settings(), theme: 'dark' }); // Auto-saves
// Session storage
const tempData = $.storage('temp', null, sessionStorage);
// Multiple stores
const userPrefs = $.storage('user-prefs', {});
const appState = $.storage('app-state', {});
// Auto-cleanup
const cache = $.storage('cache', {});
cache(null); // Removes from storage
// Complex objects - automatic JSON serialization
const complex = $.storage('complex', {
date: new Date(),
regex: /pattern/,
nested: { array: [1,2,3] }
});
// Storage events - auto sync across tabs
window.addEventListener('storage', (e) => {
if (e.key === 'app-settings') {
// Signal auto-updates on next read
}
});
// Migration pattern
const data = $.storage('v2-data', () => {
// Migration from v1
const old = localStorage.getItem('v1-data');
return old ? migrate(JSON.parse(old)) : defaultValue;
});
// Computed from storage
const config = $(() => {
const base = settings();
return { ...base, computed: derive(base) };
});
```
### Router: `$.router(routes)` and `$.router.go()`
Hash-based routing with parameter extraction.
```javascript
// Route definitions - comprehensive examples
const router = $.router([
// Static routes
{ path: '/', component: () => html`<home-page></home-page>` },
{ path: '/about', component: () => html`<about-page></about-page>` },
// Parameterized routes
{
path: '/users/:id',
component: (params) => html`<user-profile user-id=${params.id}></user-profile>`
},
{
path: '/posts/:postId/comments/:commentId',
component: (params) => html`
<div>
Post ${params.postId}, Comment ${params.commentId}
</div>
`
},
// Regex routes with capture groups
{
path: /^\/products\/(?<category>\w+)\/(?<id>\d+)$/,
component: (params) => html`
<product-view
category=${params.category}
id=${params.id}
></product-view>
`
},
{
path: /\/archive\/(\d{4})\/(\d{2})/,
component: (params) => html`Archive: ${params[0]}-${params[1]}`
},
// Query parameter handling (manual)
{
path: '/search',
component: () => {
const searchParams = new URLSearchParams(window.location.search);
return html`<search-results query=${searchParams.get('q')}></search-results>`;
}
},
// Nested routers
{
path: '/dashboard',
component: () => {
return $.router([
{ path: '/', component: dashboardHome },
{ path: '/settings', component: dashboardSettings }
]);
}
},
// Guard pattern
{
path: '/admin',
component: (params) => {
if (!isAuthenticated()) {
$.router.go('/login');
return html`<!-- Empty, redirecting -->`;
}
return html`<admin-panel></admin-panel>`;
}
},
// Catch-all / 404
{
path: /.*/,
component: () => html`<not-found></not-found>`
}
]);
// Navigation
$.router.go('/users/123'); // Basic
$.router.go('users/123'); // Auto-adds leading slash
$.router.go('/search?q=test'); // Query strings preserved
// Advanced navigation
const navigateWithState = (path, state) => {
history.replaceState(state, '', `#${path}`);
window.dispatchEvent(new HashChangeEvent('hashchange'));
};
// Route change detection
window.addEventListener('hashchange', () => {
console.log('Route changed to:', window.location.hash);
});
// Current route signal (if needed)
const currentRoute = $(() => window.location.hash.replace(/^#/, '') || '/');
// Route params as signals
const routeParams = $(null);
$.effect(() => {
const match = currentRoute().match(/\/users\/(\d+)/);
routeParams(match ? { id: match[1] } : null);
});
// Lazy loading routes
const router = $.router([
{
path: '/heavy',
component: async (params) => {
const module = await import('./heavy-page.js');
return module.default(params);
}
}
]);
```
### Fetch: `$.fetch(url, data, loading?)`
Simplified data fetching with loading state.
```javascript
// Basic usage
const result = await $.fetch('/api/users', { id: 123 });
// With loading signal
const isLoading = $(false);
const data = await $.fetch('/api/search', { query: 'test' }, isLoading);
console.log(isLoading()); // false after completion
// Error handling (returns null on error)
const result = await $.fetch('/api/fail', {}) || fallbackData;
// Integration with signals
const userData = $(null);
const loading = $(false);
async function loadUser(id) {
userData(await $.fetch(`/api/users/${id}`, {}, loading));
}
// Reactive fetch with effect
$.effect(() => {
const id = currentUserId();
loadUser(id);
});
// Retry logic wrapper
async function fetchWithRetry(url, data, retries = 3) {
for (let i = 0; i < retries; i++) {
const result = await $.fetch(url, data);
if (result) return result;
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
}
return null;
}
// Request cancellation
let abortController = null;
$.effect(() => {
if (abortController) abortController.abort();
abortController = new AbortController();
const id = userId();
fetch(`/api/user/${id}`, { signal: abortController.signal })
.then(r => r.json())
.then(data => userData(data));
});
```
### WebSocket: `$.ws(url, options)`
Full-featured WebSocket client with reactive state.
```javascript
// Basic connection
const ws = $.ws('wss://api.example.com/ws');
// With options
const ws = $.ws('wss://api.example.com/ws', {
reconnect: true,
maxReconnect: 10,
reconnectInterval: 2000 // Base interval for exponential backoff
});
// Reactive state
$.effect(() => {
console.log('Status:', ws.status()); // 'connecting', 'connected', 'disconnected', 'error'
console.log('Messages:', ws.messages()); // Array of received messages
console.log('Error:', ws.error()); // Last error or null
});
// Sending messages
ws.send({ type: 'join', room: 'general' });
ws.send('plain text message');
// Auto-reconnect with backoff
// Attempts: 2s, 4s, 8s, 16s, 32s (until maxReconnect)
// Message filtering
const commands = $(() => ws.messages().filter(m => m.type === 'command'));
const events = $(() => ws.messages().filter(m => m.type === 'event'));
// Send with acknowledgment
async function sendWithAck(data, timeout = 5000) {
return new Promise((resolve, reject) => {
const id = Math.random();
const message = { ...data, id };
const handler = (msg) => {
if (msg.ack === id) {
ws.messages.off(handler);
resolve(msg);
}
};
// Need message event listener pattern
const timeoutId = setTimeout(() => {
ws.messages.off(handler);
reject(new Error('Ack timeout'));
}, timeout);
// Custom listener would be needed for this pattern
ws.send(message);
});
}
// Reconnection handling
$.effect(() => {
if (ws.status() === 'connected') {
console.log('Connected, sending init...');
ws.send({ type: 'init', token: authToken() });
}
});
// Binary data
const ws = $.ws('wss://example.com/binary');
ws.send(new Blob([data]));
ws.send(new ArrayBuffer(8));
// Heartbeat / ping-pong
$.effect(() => {
if (ws.status() !== 'connected') return;
const interval = setInterval(() => {
ws.send({ type: 'ping' });
}, 30000);
return () => clearInterval(interval);
});
// Queue messages while disconnected
const messageQueue = $([]);
$.effect(() => {
if (ws.status() === 'connected') {
messageQueue().forEach(msg => ws.send(msg));
messageQueue([]);
}
});
function sendOrQueue(data) {
if (ws.status() === 'connected') {
ws.send(data);
} else {
messageQueue([...messageQueue(), data]);
}
}
```
### Advanced Patterns & Best Practices
**State Management Patterns:**
```javascript
// Store pattern
const createStore = (initial) => {
const state = $(initial);
return {
state,
actions: {
update: (fn) => state(fn(state())),
reset: () => state(initial)
}
};
};
// Selector pattern
const selectUser = (id) => $(() =>
users().find(u => u.id === id)
);
// Computed selector
const selectVisibleTodos = $(() =>
todos().filter(t =>
filter() === 'all' ||
(filter() === 'active' && !t.completed) ||
(filter() === 'completed' && t.completed)
)
);
// Action pattern
const increment = () => count(c => c + 1);
const addTodo = (text) => todos(t => [...t, { text, completed: false }]);
const toggleTodo = (id) => todos(t => t.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
```
**Performance Optimization:**
```javascript
// Memoization
const expensiveComputation = $(() => {
console.log('Computing...');
return bigData().filter(heavyFilter).map(transform);
});
// Batch updates
count(1);
count(2);
count(3); // Single effect run
// Manual flush if needed
import { flushEffectQueue } from 'sigpro';
flushEffectQueue(); // Force immediate effect processing
// Lazy computed - only computes when accessed
const lazyValue = $(() => {
if (!cache) cache = expensive();
return cache;
});
// Effect debouncing
let timeout;
$.effect(() => {
clearTimeout(timeout);
timeout = setTimeout(() => {
console.log('Debounced:', value());
}, 100);
});
```
**Error Boundaries & Recovery:**
```javascript
// Effect error handling
$.effect(() => {
try {
riskyOperation();
} catch (e) {
errorSignal(e);
}
});
// Component error boundary
$.component('error-boundary', (props, ctx) => {
const error = $(null);
const handleError = (e) => {
error(e.error);
e.preventDefault();
};
ctx.host.addEventListener('error', handleError);
ctx.onUnmount(() => ctx.host.removeEventListener('error', handleError));
return () => error()
? html`<error-display error=${error()}></error-display>`
: html`<slot></slot>`;
});
// Global error handler
window.addEventListener('error', (e) => {
console.error('SigPro error:', e.error);
});
```
**Testing Utilities:**
```javascript
// Test helper pattern
const createTestHarness = () => {
const effects = [];
const origEffect = $.effect;
$.effect = (fn) => {
const stop = origEffect(fn);
effects.push(stop);
return stop;
};
return {
cleanup: () => effects.forEach(stop => stop()),
restore: () => { $.effect = origEffect; }
};
};
// Async test helper
const waitForEffect = () => new Promise(r => setTimeout(r, 0));
// Usage in tests
test('counter increments', async () => {
const count = $(0);
const calls = [];
$.effect(() => {
calls.push(count());
});
count(1);
await waitForEffect(); // Wait for effect queue
expect(calls).toEqual([0, 1]);
});
```
**TypeScript Integration (conceptual):**
```typescript
// Type patterns (even though library is JS)
type Signal<T> = {
(): T;
(value: T | ((prev: T) => T)): void;
};
type Computed<T> = () => T;
type Effect = (fn: () => (void | (() => void))) => () => void;
// Component props typing
interface ComponentProps {
[key: string]: Signal<any>;
}
interface ComponentContext {
select: (selector: string) => Element | null;
slot: (name?: string) => Node[];
emit: (name: string, detail?: any, options?: CustomEventInit) => void;
host: HTMLElement;
onUnmount: (fn: () => void) => void;
}
```
**Migration from Other Frameworks:**
```javascript
// From Vue:
// ref(0) -> $(0)
// computed -> $(() => value)
// watch -> $.effect
// onMounted -> $.effect (runs immediately)
// From React:
// useState -> $ (but get/set combined)
// useEffect -> $.effect
// useMemo -> $(() => value)
// useCallback -> Just use function, dependencies automatic
// From Svelte:
// let count = 0 -> const count = $(0)
// $: doubled = count * 2 -> const doubled = $(() => count())
```
## Internal Architecture (for deep understanding)
```javascript
// Reactivity graph structure
{
activeEffect: Effect | null, // Currently executing effect
effectQueue: Set<Effect>, // Pending effects
signal: {
subscribers: Set<Effect>, // Effects depending on this signal
value: any, // Current value
isDirty?: boolean // For computed signals
},
effect: {
dependencies: Set<Set<Effect>>, // Signals this effect depends on
cleanupHandlers: Set<Function>, // Cleanup functions
markDirty?: Function // For computed signal effects
}
}
// Flush mechanism
// 1. Signal update adds affected effects to queue
// 2. Schedules microtask flush
// 3. Effects run in order added
// 4. Each effect cleans up old dependencies
// 5. New dependencies tracked during run
```
## Complete Example Application
```javascript
// app.js
import { $, html, $.component, $.router, $.storage, $.ws } from 'sigpro';
// State
const todos = $.storage('todos', []);
const filter = $('all');
const user = $(null);
const ws = $.ws('wss://api.example.com/todos');
// Computed
const filteredTodos = $(() => {
const all = todos();
switch(filter()) {
case 'active': return all.filter(t => !t.completed);
case 'completed': return all.filter(t => t.completed);
default: return all;
}
});
const stats = $(() => {
const all = todos();
return {
total: all.length,
active: all.filter(t => !t.completed).length,
completed: all.filter(t => t.completed).length
};
});
// Actions
const addTodo = (text) => {
const newTodo = {
id: Date.now(),
text,
completed: false,
createdAt: new Date().toISOString()
};
todos([...todos(), newTodo]);
ws.send({ type: 'add', todo: newTodo });
};
const toggleTodo = (id) => {
todos(todos().map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
));
};
const removeTodo = (id) => {
todos(todos().filter(t => t.id !== id));
};
// Sync with WebSocket
$.effect(() => {
ws.messages().forEach(msg => {
if (msg.type === 'sync') {
todos(msg.todos);
}
});
});
// Components
$.component('todo-app', () => html`
<div class="app">
<header>
<h1>Todo App</h1>
<todo-stats stats=${() => stats()}></todo-stats>
</header>
<todo-input @add=${(e) => addTodo(e.detail)}></todo-input>
<div class="filters">
<button @click=${() => filter('all')} ?active=${() => filter() === 'all'}>
All (${() => stats().total})
</button>
<button @click=${() => filter('active')} ?active=${() => filter() === 'active'}>
Active (${() => stats().active})
</button>
<button @click=${() => filter('completed')} ?active=${() => filter() === 'completed'}>
Completed (${() => stats().completed})
</button>
</div>
<todo-list
todos=${() => filteredTodos()}
@toggle=${(e) => toggleTodo(e.detail)}
@remove=${(e) => removeTodo(e.detail)}
></todo-list>
${() => ws.status() !== 'connected' ? html`
<div class="offline-banner">
Offline - ${() => ws.status()}
</div>
` : ''}
</div>
`);
$.component('todo-input', (props, ctx) => {
const text = $('');
const handleSubmit = () => {
if (text().trim()) {
ctx.emit('add', text().trim());
text('');
}
};
return html`
<div class="todo-input">
<input
:value=${text}
@keydown.enter=${handleSubmit}
placeholder="What needs to be done?"
>
<button @click=${handleSubmit}>Add</button>
</div>
`;
});
$.component('todo-list', (props) => html`
<ul class="todo-list">
${() => props.todos().map(todo => html`
<li class=${() => todo.completed ? 'completed' : ''}>
<input
type="checkbox"
:checked=${() => todo.completed}
@change=${() => props.emit('toggle', todo.id)}
>
<span>${todo.text}</span>
<button @click=${() => props.emit('remove', todo.id)}>×</button>
</li>
`)}
</ul>
`, ['todos']);
$.component('todo-stats', (props) => html`
<div class="stats">
<span>Total: ${() => props.stats().total}</span>
<span>Active: ${() => props.stats().active}</span>
<span>Completed: ${() => props.stats().completed}</span>
</div>
`, ['stats']);
// Router setup
const router = $.router([
{ path: '/', component: () => html`<todo-app></todo-app>` },
{ path: '/settings', component: () => html`<settings-page></settings-page>` },
{ path: '/about', component: () => html`<about-page></about-page>` }
]);
// Mount app
document.body.appendChild(router);
```
This comprehensive documentation should enable any AI to build sophisticated applications leveraging all of SigPro's capabilities. Each API is explained with multiple examples, edge cases, advanced patterns, and integration scenarios.