# SigPro 🚀 A minimalist reactive library for building web interfaces with signals, effects, and native web components. No compilation, no virtual DOM, just pure JavaScript and intelligent reactivity. [![npm version](https://img.shields.io/npm/v/sigpro.svg)](https://www.npmjs.com/package/sigpro) [![bundle size](https://img.shields.io/bundlephobia/minzip/sigpro)](https://bundlephobia.com/package/sigpro) [![license](https://img.shields.io/npm/l/sigpro)](https://github.com/yourusername/sigpro/blob/main/LICENSE) ## 📦 Installation ```bash npm install sigpro ``` Or directly in the browser: ```html ``` ## 🎯 Philosophy SigPro (Signal Professional) embraces the web platform. Built on top of Custom Elements and reactive proxies, it offers a development experience similar to modern frameworks but with a minimal footprint and zero dependencies. **Core Principles:** - 📡 **True Reactivity** - Automatic dependency tracking, no manual subscriptions - ⚡ **Surgical Updates** - Only the exact nodes that depend on changed values are updated - 🧩 **Web Standards** - Built on Custom Elements, no custom rendering engine - 🎨 **Intuitive API** - Learn once, use everywhere - 🔬 **Predictable** - No magic, just signals and effects ## 📚 API Reference --- ### `$(initialValue)` - Signals Creates a reactive value that notifies dependents when changed. #### Basic Signal (Getter/Setter) ```typescript import { $ } from 'sigpro'; // Create a signal const count = $(0); // Read value (outside reactive context) console.log(count()); // 0 // Write value count(5); count(prev => prev + 1); // Use function for previous value // Read with dependency tracking (inside effect) $$(() => { console.log(count()); // Will be registered as dependency }); ``` #### Computed Signal ```typescript import { $, $$ } from 'sigpro'; const firstName = $('John'); const lastName = $('Doe'); // Computed signal - automatically updates when dependencies change const fullName = $(() => `${firstName()} ${lastName()}`); console.log(fullName()); // "John Doe" firstName('Jane'); console.log(fullName()); // "Jane Doe" // Computed signals cache until dependencies change const expensiveComputation = $(() => { console.log('Computing...'); return firstName().length + lastName().length; }); console.log(expensiveComputation()); // "Computing..." 7 console.log(expensiveComputation()); // 7 (cached, no log) ``` #### Signal with Custom Equality ```typescript import { $ } from 'sigpro'; const user = $({ id: 1, name: 'John' }); // Signals use Object.is comparison user({ id: 1, name: 'John' }); // Won't trigger updates (same values, new object) user({ id: 1, name: 'Jane' }); // Will trigger updates ``` **Parameters:** - `initialValue`: Initial value or getter function for computed signal **Returns:** Function that acts as getter/setter with the following signature: ```typescript type Signal = { (): T; // Getter (value: T | ((prev: T) => T)): void; // Setter } ``` --- ### `$$(effect)` - Effects Executes a function and automatically re-runs it when its dependencies change. #### Basic Effect ```typescript import { $, $$ } from 'sigpro'; const count = $(0); const name = $('World'); // Effect runs immediately and on dependency changes $$(() => { console.log(`Count is: ${count()}`); // Only depends on count }); // Log: "Count is: 0" count(1); // Log: "Count is: 1" name('Universe'); // No log (name is not a dependency) ``` #### Effect with Cleanup ```typescript import { $, $$ } from 'sigpro'; const userId = $(1); $$(() => { const id = userId(); let isSubscribed = true; // Simulate API subscription const subscription = api.subscribe(id, (data) => { if (isSubscribed) { console.log('New data:', data); } }); // Return cleanup function return () => { isSubscribed = false; subscription.unsubscribe(); }; }); userId(2); // Previous subscription cleaned up, new one created ``` #### Nested Effects ```typescript import { $, $$ } from 'sigpro'; const show = $(true); const count = $(0); $$(() => { if (!show()) return; // This effect is nested inside the conditional // It will only be active when show() is true $$(() => { console.log('Count changed:', count()); }); }); show(false); // Inner effect is automatically cleaned up count(1); // No log (inner effect not active) show(true); // Inner effect recreated, logs "Count changed: 1" ``` #### Manual Effect Control ```typescript import { $, $$ } from 'sigpro'; const count = $(0); // Stop effect manually const stop = $$(() => { console.log('Effect running:', count()); }); count(1); // Log: "Effect running: 1" stop(); count(2); // No log ``` **Parameters:** - `effect`: Function to execute. Can return a cleanup function **Returns:** Function to stop the effect --- ### `html` - Template Literal Tag Creates reactive DOM fragments using template literals with intelligent binding. #### Basic Usage ```typescript import { $, html } from 'sigpro'; const count = $(0); const name = $('World'); const fragment = html`

Hello ${name}

Count: ${count}

`; document.body.appendChild(fragment); ``` #### Directive Reference ##### `@event` - Event Listeners ```typescript import { html } from 'sigpro'; const handleClick = (event) => console.log('Clicked!', event); const handleInput = (value) => console.log('Input:', value); html` console.log(e.target.value)} /> ` ``` ##### `:property` - Two-way Binding Automatically syncs between signal and DOM element. ```typescript import { $, html } from 'sigpro'; const text = $(''); const checked = $(false); const selected = $('option1'); html`

You typed: ${text}

Checkbox is: ${() => checked() ? 'checked' : 'unchecked'}

` ``` ##### `?attribute` - Boolean Attributes ```typescript import { $, html } from 'sigpro'; const isDisabled = $(true); const isChecked = $(false); const hasError = $(false); html`
!hasError()} class="error"> An error occurred
` ``` ##### `.property` - Property Binding Directly binds to DOM properties, not attributes. ```typescript import { $, html } from 'sigpro'; const scrollTop = $(0); const user = $({ name: 'John', age: 30 }); const items = $([1, 2, 3]); html`
Content...
` ``` ##### Regular Attributes ```typescript import { $, html } from 'sigpro'; const className = $('big red'); const href = $('#section'); const style = $('color: blue'); // Static attributes html`
` // Dynamic attributes (non-directive) html`
` // Mix of static and dynamic html`Link` // Reactive attributes update when signal changes $$(() => { // The attribute updates automatically console.log('Class changed:', className()); }); ``` #### Conditional Rendering ```typescript import { $, html } from 'sigpro'; const show = $(true); const user = $({ name: 'John', role: 'admin' }); // Using ternary html` ${() => show() ? html`
Content is visible
` : html`
Content is hidden
`} ` // Using logical AND html` ${() => user().role === 'admin' && html` `} ` // Complex conditions html` ${() => { if (!show()) return null; if (user().role === 'admin') { return html`
Admin view
`; } return html`
User view
`; }} ` ``` #### List Rendering ```typescript import { $, html } from 'sigpro'; const items = $([1, 2, 3, 4, 5]); const todos = $([ { text: 'Learn SigPro', done: true }, { text: 'Build an app', done: false } ]); // Basic list html` ` // List with keys (for efficient updates) html` ` // Nested lists const matrix = $([[1, 2], [3, 4], [5, 6]]); html` ${() => matrix().map(row => html` ${() => row.map(cell => html` `)} `)}
${cell}
` ``` #### Dynamic Tag Names ```typescript import { $, html } from 'sigpro'; const tagName = $('h1'); const level = $(1); html`
This will be wrapped in ${tagName} tags
${() => { const Tag = `h${level()}`; return html` <${Tag}>Level ${level()} Heading `; }} ` ``` #### Template Composition ```typescript import { $, html } from 'sigpro'; const Header = () => html`
Header
`; const Footer = () => html``; const Layout = ({ children }) => html` ${Header()}
${children}
${Footer()} ` const Page = () => html` ${Layout({ children: html`

Page Content

Some content here

` })} ` ``` --- ### `$component(tag, setup, observedAttributes)` - Web Components Creates Custom Elements with automatic reactive properties. #### Basic Component ```typescript import { $, $component, html } from 'sigpro'; $component('my-counter', (props, context) => { // props contains signals for each observed attribute // context provides component utilities const increment = () => { props.value(v => v + 1); }; return html`

Count: ${props.value}

`; }, ['value']); // Observed attributes ``` Usage: ```html Additional content ``` #### Component with Complex Props ```typescript import { $, $component, html } from 'sigpro'; $component('user-profile', (props, context) => { // Transform string attributes to appropriate types const user = $(() => ({ id: parseInt(props.id()), name: props.name(), age: parseInt(props.age()), active: props.active() === 'true' })); return html`

${user().name}

ID: ${user().id}

Age: ${user().age}

Status: ${() => user().active ? 'Active' : 'Inactive'}

`; }, ['id', 'name', 'age', 'active']); ``` #### Component Lifecycle & Context ```typescript import { $, $component, html } from 'sigpro'; $component('lifecycle-demo', (props, { select, // Query selector scoped to component selectAll, // Query selector all scoped to component slot, // Access slots emit, // Dispatch custom events host, // Reference to the host element onMount, // Register mount callback onUnmount, // Register unmount callback onAttribute, // Listen to attribute changes getAttribute, // Get raw attribute value setAttribute, // Set raw attribute value }) => { // Access slots const defaultSlot = slot(); // Unnamed slot const headerSlot = slot('header'); // Named slot // Query internal elements const button = select('button'); const allSpans = selectAll('span'); // Lifecycle hooks onMount(() => { console.log('Component mounted'); // Access DOM after mount button?.classList.add('mounted'); }); onUnmount(() => { console.log('Component unmounting'); // Cleanup resources }); // Listen to specific attribute changes onAttribute('value', (newValue, oldValue) => { console.log(`Value changed from ${oldValue} to ${newValue}`); }); // Emit custom events const handleClick = () => { emit('button-click', { timestamp: Date.now() }); emit('value-change', props.value(), { bubbles: true }); }; // Access host directly host.style.display = 'block'; return html`
${headerSlot} ${defaultSlot}
`; }, ['value']); ``` #### Component with Methods ```typescript import { $, $component, html } from 'sigpro'; $component('timer-widget', (props, { host }) => { const seconds = $(0); let intervalId; // Expose methods to the host element Object.assign(host, { start() { if (intervalId) return; intervalId = setInterval(() => { seconds(s => s + 1); }, 1000); }, stop() { clearInterval(intervalId); intervalId = null; }, reset() { seconds(0); }, get currentTime() { return seconds(); } }); return html`

${seconds} seconds

`; }, []); ``` Usage: ```html ``` #### Component Inheritance ```typescript import { $, $component, html } from 'sigpro'; // Base component $component('base-button', (props, { slot }) => { return html` `; }, ['disabled']); // Extended component $component('primary-button', (props, context) => { // Reuse base component return html` ${context.slot()} `; }, ['disabled']); ``` --- ### `$router(routes)` - Router Hash-based router for SPAs with reactive integration. #### Basic Routing ```typescript import { $router, html } from 'sigpro'; const router = $router([ { path: '/', component: () => html`

Home Page

About ` }, { path: '/about', component: () => html`

About Page

Home ` } ]); document.body.appendChild(router); ``` #### Route Parameters ```typescript import { $router, html } from 'sigpro'; const router = $router([ { path: '/user/:id', component: (params) => html`

User Profile

User ID: ${params.id}

Edit ` }, { path: '/user/:id/posts/:postId', component: (params) => html`

Post ${params.postId} by User ${params.id}

` }, { path: /^\/product\/(?\w+)\/(?\d+)$/, component: (params) => html`

Product ${params.id} in ${params.category}

` } ]); ``` #### Nested Routes ```typescript import { $router, html, $ } from 'sigpro'; const router = $router([ { path: '/', component: () => html`

Home

` }, { path: '/dashboard', component: () => { // Nested router const subRouter = $router([ { path: '/', component: () => html`

Dashboard Home

` }, { path: '/settings', component: () => html`

Dashboard Settings

` }, { path: '/profile/:id', component: (params) => html`

Profile ${params.id}

` } ]); return html`

Dashboard

${subRouter}
`; } } ]); ``` #### Route Guards ```typescript import { $router, html, $ } from 'sigpro'; const isAuthenticated = $(false); const requireAuth = (component) => (params) => { if (!isAuthenticated()) { $router.go('/login'); return null; } return component(params); }; const router = $router([ { path: '/', component: () => html`

Public Home

` }, { path: '/dashboard', component: requireAuth((params) => html`

Protected Dashboard

`) }, { path: '/login', component: () => html`

Login

` } ]); ``` #### Navigation ```typescript import { $router } from 'sigpro'; // Navigate to path $router.go('/user/42'); // Navigate with replace $router.go('/dashboard', { replace: true }); // Go back $router.back(); // Go forward $router.forward(); // Get current path const currentPath = $router.getCurrentPath(); // Listen to navigation $router.listen((path, oldPath) => { console.log(`Navigated from ${oldPath} to ${path}`); }); ``` #### Route Transitions ```typescript import { $router, html, $$ } from 'sigpro'; const router = $router([ { path: '/', component: () => html`
Home
` }, { path: '/about', component: () => html`
About
` } ]); // Add transitions $$(() => { const currentPath = router.getCurrentPath(); const pages = document.querySelectorAll('.page'); pages.forEach(page => { page.style.opacity = '0'; page.style.transition = 'opacity 0.3s'; setTimeout(() => { page.style.opacity = '1'; }, 50); }); }); ``` --- ## 🎮 Complete Examples ### Real-time Todo Application ```typescript import { $, $$, html, $component } from 'sigpro'; // Styles const styles = html` `; $component('todo-app', () => { // State const todos = $(() => { const saved = localStorage.getItem('todos'); return saved ? JSON.parse(saved) : []; }); const newTodo = $(''); const filter = $('all'); // 'all', 'active', 'completed' const editingId = $(null); const editText = $(''); // Save to localStorage on changes $$(() => { localStorage.setItem('todos', JSON.stringify(todos())); }); // Filtered todos const filteredTodos = $(() => { const currentFilter = filter(); const allTodos = todos(); switch (currentFilter) { case 'active': return allTodos.filter(t => !t.completed); case 'completed': return allTodos.filter(t => t.completed); default: return allTodos; } }); // Stats const stats = $(() => { const all = todos(); return { total: all.length, completed: all.filter(t => t.completed).length, active: all.filter(t => !t.completed).length }; }); // Actions const addTodo = () => { const text = newTodo().trim(); if (!text) return; todos([ ...todos(), { id: Date.now(), text, completed: false, createdAt: new Date().toISOString() } ]); newTodo(''); }; const toggleTodo = (id) => { todos(todos().map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo )); }; const deleteTodo = (id) => { todos(todos().filter(todo => todo.id !== id)); if (editingId() === id) { editingId(null); } }; const startEdit = (todo) => { editingId(todo.id); editText(todo.text); }; const saveEdit = (id) => { const text = editText().trim(); if (!text) { deleteTodo(id); } else { todos(todos().map(todo => todo.id === id ? { ...todo, text } : todo )); } editingId(null); }; const clearCompleted = () => { todos(todos().filter(t => !t.completed)); }; return html` ${styles}

📝 Todo App

e.key === 'Enter' && addTodo()} />
${() => filteredTodos().map(todo => html`
${editingId() === todo.id ? html` { if (e.key === 'Enter') saveEdit(todo.id); if (e.key === 'Escape') editingId(null); }} @blur=${() => saveEdit(todo.id)} autofocus /> ` : html` toggleTodo(todo.id)} /> startEdit(todo)}> ${todo.text} `}
`)}
${() => { const s = stats(); return html` Total: ${s.total} | Active: ${s.active} | Completed: ${s.completed} ${s.completed > 0 ? html` ` : ''} `; }}
`; }, []); ``` ### Data Dashboard with Real-time Updates ```typescript import { $, $$, html, $component } from 'sigpro'; // Simulated WebSocket connection class DataStream { constructor() { this.listeners = new Set(); this.interval = setInterval(() => { const data = { timestamp: Date.now(), value: Math.random() * 100, category: ['A', 'B', 'C'][Math.floor(Math.random() * 3)] }; this.listeners.forEach(fn => fn(data)); }, 1000); } subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); } destroy() { clearInterval(this.interval); } } $component('data-dashboard', () => { const stream = new DataStream(); const dataPoints = $([]); const selectedCategory = $('all'); const timeWindow = $(60); // seconds // Subscribe to data stream $$(() => { const unsubscribe = stream.subscribe((newData) => { dataPoints(prev => { const updated = [...prev, newData]; const maxAge = timeWindow() * 1000; const cutoff = Date.now() - maxAge; return updated.filter(d => d.timestamp > cutoff); }); }); return unsubscribe; }); // Filtered data const filteredData = $(() => { const data = dataPoints(); const category = selectedCategory(); if (category === 'all') return data; return data.filter(d => d.category === category); }); // Statistics const statistics = $(() => { const data = filteredData(); if (data.length === 0) return null; const values = data.map(d => d.value); return { count: data.length, avg: values.reduce((a, b) => a + b, 0) / values.length, min: Math.min(...values), max: Math.max(...values), last: values[values.length - 1] }; }); // Cleanup on unmount onUnmount(() => { stream.destroy(); }); return html`

📊 Real-time Dashboard

Time window: ${timeWindow}s
${() => { const stats = statistics(); if (!stats) return html`

Waiting for data...

`; return html`
Points: ${stats.count}
Average: ${stats.avg.toFixed(2)}
Min: ${stats.min.toFixed(2)}
Max: ${stats.max.toFixed(2)}
Last: ${stats.last.toFixed(2)}
`; }}
${() => filteredData().map(point => html`
`)}
`; }, []); ``` ## 🔧 Advanced Patterns ### Custom Hooks ```typescript import { $, $$ } from 'sigpro'; // useLocalStorage hook function useLocalStorage(key, initialValue) { const stored = localStorage.getItem(key); const signal = $(stored ? JSON.parse(stored) : initialValue); $$(() => { localStorage.setItem(key, JSON.stringify(signal())); }); return signal; } // useDebounce hook function useDebounce(signal, delay) { const debounced = $(signal()); let timeout; $$(() => { const value = signal(); clearTimeout(timeout); timeout = setTimeout(() => { debounced(value); }, delay); }); return debounced; } // useFetch hook function useFetch(url) { const data = $(null); const error = $(null); const loading = $(true); const fetchData = async () => { loading(true); error(null); try { const response = await fetch(url()); const json = await response.json(); data(json); } catch (e) { error(e); } finally {