` // Set classList property
-
-// Property binding sets both property AND attribute (unless object/boolean)
-```
-
-**Special Cases:**
-```javascript
-// SVG support
-html`
`
-
-// Self-closing tags
-html`

`
-
-// Comments - preserved in template
-html`
Title
`
-
-// Doctype - only at root
-html`...`
-
-// Template tag
-html`
${() => repeatContent()}`
-
-// Slot handling
-html`
${() => slottedContent}`
-```
-
-### 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`
-
- Default header
-
- Default content
-
- `;
-}, ['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 = '
';
-
-// 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`
`;
-}, ['theme']);
-
-$.component('themed-button', (props, ctx) => {
- const theme = $('light');
-
- // Consume from parent
- ctx.host.addEventListener('theme-provider', (e) => {
- theme(e.detail.theme);
- });
-
- return html`
`;
-});
-
-// Compound components
-$.component('tabs', (props, ctx) => {
- const activeTab = $(0);
-
- return html`
-
-
-
- ${() => props.tabs()[activeTab()].content}
-
-
- `;
-}, ['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`
`;
-}, ['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`
` },
- { path: '/about', component: () => html`
` },
-
- // Parameterized routes
- {
- path: '/users/:id',
- component: (params) => html`
`
- },
- {
- path: '/posts/:postId/comments/:commentId',
- component: (params) => html`
-
- Post ${params.postId}, Comment ${params.commentId}
-
- `
- },
-
- // Regex routes with capture groups
- {
- path: /^\/products\/(?
\w+)\/(?\d+)$/,
- component: (params) => html`
-
- `
- },
- {
- 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``;
- }
- },
-
- // 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``;
- }
- return html``;
- }
- },
-
- // Catch-all / 404
- {
- path: /.*/,
- component: () => html``
- }
-]);
-
-// 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``
- : html``;
-});
-
-// 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;
- (value: T | ((prev: T) => T)): void;
-};
-
-type Computed = () => T;
-
-type Effect = (fn: () => (void | (() => void))) => () => void;
-
-// Component props typing
-interface ComponentProps {
- [key: string]: Signal;
-}
-
-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, // Pending effects
- signal: {
- subscribers: Set, // Effects depending on this signal
- value: any, // Current value
- isDirty?: boolean // For computed signals
- },
- effect: {
- dependencies: Set>, // Signals this effect depends on
- cleanupHandlers: Set, // 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`
-
-
-
-
addTodo(e.detail)}>
-
-
-
-
-
-
-
-
filteredTodos()}
- @toggle=${(e) => toggleTodo(e.detail)}
- @remove=${(e) => removeTodo(e.detail)}
- >
-
- ${() => ws.status() !== 'connected' ? html`
-
- Offline - ${() => ws.status()}
-
- ` : ''}
-
-`);
-
-$.component('todo-input', (props, ctx) => {
- const text = $('');
-
- const handleSubmit = () => {
- if (text().trim()) {
- ctx.emit('add', text().trim());
- text('');
- }
- };
-
- return html`
-
-
-
-
- `;
-});
-
-$.component('todo-list', (props) => html`
-
-`, ['todos']);
-
-$.component('todo-stats', (props) => html`
-
- Total: ${() => props.stats().total}
- Active: ${() => props.stats().active}
- Completed: ${() => props.stats().completed}
-
-`, ['stats']);
-
-// Router setup
-const router = $.router([
- { path: '/', component: () => html`` },
- { path: '/settings', component: () => html`` },
- { path: '/about', component: () => html`` }
-]);
-
-// Mount app
-document.body.appendChild(router);
-```