diff --git a/.gitignore b/.gitignore index 3b717cd..e02ef7a 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,4 @@ Thumbs.db # Tests and Tools .nyc_output .eslintcache -.stylelintcache +.stylelintcache \ No newline at end of file diff --git a/Readme.md b/Readme.md index 85d888e..704dc25 100644 --- a/Readme.md +++ b/Readme.md @@ -1,1008 +1,142 @@ -# 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. - -**~3KB** gzipped ⚡ - -[![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/natxocc/sigpro/blob/main/LICENSE) - -## ❓ Why? - -After years of building applications with React, Vue, and Svelte—investing countless hours mastering their unique mental models, build tools, and update cycles—I kept circling back to the same realization: no matter how sophisticated the framework, it all eventually compiles down to HTML, CSS, and vanilla JavaScript. The web platform has evolved tremendously, yet many libraries continue to reinvent the wheel, creating parallel universes with their own rules, their own syntaxes, and their own steep learning curves. - -**SigPro is my answer to a simple question:** Why fight the platform when we can embrace it? - -Modern browsers now offer powerful primitives—Custom Elements, Shadow DOM, CSS custom properties, and microtask queues—that make true reactivity possible without virtual DOM diffing, without compilers, and without lock-in. SigPro strips away the complexity, delivering a reactive programming model that feels familiar but stays remarkably close to vanilla JS. No JSX transformations, no template compilers, no proprietary syntax to learn—just functions, signals, and template literals that work exactly as you'd expect. - -What emerged is a library that proves we've reached a turning point: the web is finally mature enough that we don't need to abstract it anymore. We can build reactive, component-based applications using virtually pure JavaScript, leveraging the platform's latest advances instead of working against them. SigPro isn't just another framework—it's a return to fundamentals, showing that the dream of simple, powerful reactivity is now achievable with the tools browsers give us out of the box. - -## 📊 Comparison Table - -| Metric | SigPro | Solid | Svelte | Vue | React | -|--------|--------|-------|--------|-----|-------| -| **Bundle Size** (gzip) | 🥇 **5.2KB** | 🥈 15KB | 🥉 16.6KB | 20.4KB | 43.9KB | -| **Time to Interactive** | 🥇 **0.8s** | 🥈 1.3s | 🥉 1.4s | 1.6s | 2.3s | -| **Initial Render** (ms) | 🥇 **124ms** | 🥈 198ms | 🥉 287ms | 298ms | 452ms | -| **Update Performance** (ms) | 🥇 **4ms** | 🥈 5ms | 🥈 5ms | 🥉 7ms | 18ms | -| **Code Splitting** | 🥇 **Zero overhead** | 🥈 Minimal | 🥉 Moderate | 🥉 Moderate | High | -| **Learning Curve** (hours) | 🥇 **2h** | 🥈 20h | 🥉 30h | 40h | 60h | -| **Dependencies** | 🥇 0 | 🥇 0 | 🥇 0 | 🥈 2 | 🥉 5 | -| **Compilation Required** | 🥇 No | 🥇 No | 🥈 Yes | 🥇 No | 🥇 No | -| **Browser Native** | 🥇 **Yes** | 🥈 Partial | 🥉 Partial | 🥉 Partial | No | -| **Framework Lock-in** | 🥇 **None** | 🥈 Medium | 🥉 High | 🥈 Medium | 🥉 High | -| **Longevity** (standards-based) | 🥇 **10+ years** | 🥈 5 years | 🥉 3 years | 🥈 5 years | 🥈 5 years | - -**The Verdict:** While other frameworks build parallel universes with proprietary syntax and compilation steps, SigPro embraces the web platform. The result isn't just smaller bundles or faster rendering—it's code that will still run 10 years from now, in any browser, without maintenance. - -*"Stop fighting the platform. Start building with it."* - -## 📦 Installation - -```bash -npm install sigpro -``` -or -```bash -bun add sigpro -``` -or more simple: - -copy `sigpro.js` file where you want to use it. - -## 🎯 Philosophy - -SigPro (Signal Professional) embraces the web platform. Built on top of Custom Elements and reactive signals, 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 - -## 💡 Hint for VS Code - -For the best development experience with SigPro, install these VS Code extensions: - -- **Prettier** – Automatically formats your template literals for better readability -- **lit-html** – Adds syntax highlighting and inline HTML color previews inside `html` tagged templates - -This combination gives you framework-level developer experience without the framework complexity—syntax highlighting, color previews, and automatic formatting for your reactive templates, all while writing pure JavaScript. - -```javascript -// With lit-html extension, this gets full syntax highlighting and color previews! -html` -
-

Beautiful highlighted template

-
-` -``` - -# SigPro API - Quick Reference - -| Function | Description | Example | -|----------|-------------|---------| -| **`$`** | Reactive signal (getter/setter) | `const count = $(0); count(5); count()` | -| **`$.effect`** | Runs effect when dependencies change | `$.effect(() => console.log(count()))` | -| **`$.page`** | Creates a page with automatic cleanup | `export default $.page(() => { ... })` | -| **`$.component`** | Creates reactive Web Component | `$.component('my-menu', setup, ['items'])` | -| **`$.fetch`** | Fetch wrapper with loading signal | `const data = await $.fetch('/api', data, loading)` | -| **`$.router`** | Hash-based router with params | `$.router([{path:'/', component:Home}])` | -| **`$.storage`** | Persistent signal (localStorage) | `const theme = $.storage('theme', 'light')` | -| **`html`** | Template literal for reactive HTML | `` html`
${count}
` `` | - -```javascript -import { $, html } from "sigpro"; -``` - ---- - -## 📚 API Reference - ---- - -### `$(initialValue)` - Signals - -Creates a reactive value that notifies dependents when changed. - -#### Basic Signal (Getter/Setter) - -```javascript -import { $ } from 'sigpro'; - -// Create a signal -const count = $(0); - -// Read value -console.log(count()); // 0 - -// Write value -count(5); -count(prev => prev + 1); // Use function for previous value - -// Read with dependency tracking (inside effect) -$.effect(() => { - console.log(count()); // Will be registered as dependency -}); -``` - -#### Computed Signal - -```javascript -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" -``` - -**Returns:** Function that acts as getter/setter - ---- - -### `$.effect(effectFn)` - Effects - -Executes a function and automatically re-runs it when its dependencies change. - -#### Basic Effect - -```javascript -import { $ } from 'sigpro'; - -const count = $(0); - -$.effect(() => { - console.log(`Count is: ${count()}`); -}); -// Log: "Count is: 0" - -count(1); -// Log: "Count is: 1" -``` - -#### Effect with Cleanup - -```javascript -import { $ } from 'sigpro'; - -const userId = $(1); - -$.effect(() => { - const id = userId(); - - // Simulate subscription - const timer = setInterval(() => { - console.log('Polling user', id); - }, 1000); - - // Return cleanup function - return () => clearInterval(timer); -}); - -userId(2); // Previous timer cleared, new one created -``` - -**Parameters:** -- `effectFn`: Function to execute. Can return a cleanup function - -**Returns:** Function to stop the effect - ---- - -### `$.page(setupFunction)` - Pages - -Creates a page with automatic cleanup of all signals and effects when navigated away. - -```javascript -// pages/about.js -import { html, $ } from "sigpro"; - -export default $.page(() => { - const count = $(0); - const loading = $(false); - - $.effect(() => { - if (loading()) { - // Fetch data... - } - }); - - return html` -
-

About Page

-

Count: ${count}

- -
- `; -}); -``` - -**With parameters:** -```javascript -// pages/user.js -export default $.page(({ params }) => { - const userId = params.id; - const userData = $(null); - - $.effect(() => { - fetch(`/api/users/${userId}`) - .then(r => r.json()) - .then(userData); - }); - - return html`
User: ${userData}
`; -}); -``` - -**Parameters:** -- `setupFunction`: Function that returns the page content. Receives `{ params, onUnmount }` - -**Returns:** A function that creates page instances with props - ---- - -### `$.component(tagName, setupFunction, observedAttributes, useShadowDOM)` - Web Components - -Creates Custom Elements with reactive properties. Choose between **Light DOM** (default) or **Shadow DOM** for style encapsulation. - -### Parameters - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `tagName` | `string` | (required) | Custom element tag name (must include a hyphen, e.g., `my-button`) | -| `setupFunction` | `Function` | (required) | Function that renders the component | -| `observedAttributes` | `string[]` | `[]` | Observed attributes that react to changes | -| `useShadowDOM` | `boolean` | `false` | `true` = Shadow DOM (encapsulated), `false` = Light DOM (inherits styles) | - ---- - -#### 🏠 **Light DOM** (`useShadowDOM = false`) - Default - -The component **inherits global styles** from the application. Ideal for components that should visually integrate with the rest of the interface. - -##### Example: Button with Tailwind CSS - -```javascript -// button-tailwind.js -import { $, html } from 'sigpro'; - -$.component('tw-button', (props, { slot, emit }) => { - const variant = props.variant() || 'primary'; - - const variants = { - primary: 'bg-blue-500 hover:bg-blue-600 text-white', - secondary: 'bg-gray-500 hover:bg-gray-600 text-white', - outline: 'border border-blue-500 text-blue-500 hover:bg-blue-50' - }; - - return html` - - `; -}, ['variant']); // Observe the 'variant' attribute -``` - -**Usage in HTML:** -```html - - - Save changes - - - - Cancel - -``` - -##### Example: Form Input with Validation - -```javascript -// form-input.js -$.component('form-input', (props, { emit }) => { - const handleInput = (e) => { - const value = e.target.value; - props.value(value); - emit('update', value); - - // Simple validation - if (props.pattern()) { - const regex = new RegExp(props.pattern()); - const isValid = regex.test(value); - emit('validate', isValid); - } - }; - - return html` -
- - - ${props.error() ? html` -
${props.error()}
- ` : ''} -
- `; -}, ['label', 'type', 'value', 'error', 'placeholder', 'disabled', 'pattern']); -``` - -**Usage:** -```html - email(e.detail)} - @validate=${(e) => setEmailValid(e.detail)} -> - -``` - -##### Example: Card that uses global design system - -```javascript -// content-card.js -$.component('content-card', (props, { slot }) => { - return html` -
-
-

${props.title()}

-
-
- ${slot()} -
- ${props.footer() ? html` - - ` : ''} -
- `; -}, ['title', 'footer']); -``` - -**Usage:** -```html - -

Your dashboard updates will appear here.

-
-``` - ---- - -#### 🛡️ **Shadow DOM** (`useShadowDOM = true`) - Encapsulated - -The component **encapsulates its styles** completely. External styles don't affect it, and its styles don't leak out. Perfect for: -- UI libraries distributed across projects -- Third-party widgets -- Components with very specific styling needs - -##### Example: Calendar Component (Distributable UI) - -```javascript -// ui-calendar.js -$.component('ui-calendar', (props, { select }) => { - const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - const currentDate = props.date() ? new Date(props.date()) : new Date(); - - return html` - - -
-
- - ${currentDate.toLocaleString('default', { month: 'long', year: 'numeric' })} - -
- -
- ${days.map(day => html`${day}`)} -
- -
- ${generateDays(currentDate).map(day => html` -
selectDate(day.date)} - > - ${day.number} -
- `)} -
-
- `; -}, ['date'], true); // true = use Shadow DOM -``` - -**Usage - anywhere, anytime, looks identical:** -```html - - -``` - -##### Example: Third-party Chat Widget - -```javascript -// chat-widget.js -$.component('chat-widget', (props, { select }) => { - return html` - - -
-
-
💬
-
-
Support Chat
-
Online
-
-
- -
- ${props.messages().map(msg => html` -
${msg.text}
- `)} -
- - -
- `; -}, ['messages', 'currentMessage'], true); -``` - -**Usage - embed in ANY website:** -```html - -``` - ---- - -### 🎯 **Quick Decision Guide** - -| Use Light DOM (`false`) when... | Use Shadow DOM (`true`) when... | -|--------------------------------|-------------------------------| -| ✅ Component is part of your main app | ✅ Building a UI library for others | -| ✅ Using global CSS (Tailwind, Bootstrap) | ✅ Creating embeddable widgets | -| ✅ Need to inherit theme variables | ✅ Styles must be pixel-perfect everywhere | -| ✅ Working with existing design system | ✅ Component has complex, specific styles | -| ✅ Quick prototyping | ✅ Distributing to different projects | -| ✅ Form elements that should match site | ✅ Need style isolation/encapsulation | - -### 💡 **Pro Tips** - -1. **Light DOM components** are great for app-specific UI that should feel "native" to your site -2. **Shadow DOM components** are perfect for reusable "products" that must look identical everywhere -3. You can mix both in the same app - choose per component based on needs -4. Shadow DOM also provides DOM isolation - great for complex widgets - -```javascript -// Mix and match in the same app! -$.component('app-header', setup, ['title']); // Light DOM -$.component('user-menu', setup, ['items']); // Light DOM -$.component('chat-widget', setup, ['messages'], true); // Shadow DOM -$.component('data-grid', setup, ['columns', 'data'], true); // Shadow DOM -``` - ---- - -### `$.fetch(url, data, [loading])` - Fetch - -Simple fetch wrapper with automatic JSON handling and optional loading signal. - -```javascript -import { $ } from 'sigpro'; - -const loading = $(false); - -async function loadUser(id) { - const data = await $.fetch(`/api/users/${id}`, null, loading); - if (data) userData(data); -} - -// In your UI -html`
${() => loading() ? 'Loading...' : userData()?.name}
`; -``` - -**Parameters:** -- `url`: Endpoint URL -- `data`: Data to send (auto JSON.stringify'd) -- `loading`: Optional signal function to track loading state - -**Returns:** `Promise` - Parsed JSON response or null on error - ---- - -### `$.storage(key, initialValue, [storage])` - Persistent Signal - -Signal that automatically syncs with localStorage or sessionStorage. - -```javascript -import { $ } from 'sigpro'; - -// Automatically saves to localStorage -const theme = $.storage('theme', 'light'); -const user = $.storage('user', null); - -theme('dark'); // Saved to localStorage -// Page refresh... theme() returns 'dark' - -// Use sessionStorage instead -const tempData = $.storage('temp', {}, sessionStorage); -``` - -**Parameters:** -- `key`: Storage key name -- `initialValue`: Default value if none stored -- `storage`: Storage type (default: `localStorage`, options: `sessionStorage`) - -**Returns:** Signal function that persists to storage on changes - ---- - -### `$.router(routes)` - Hash-Based Router - -Creates a simple, powerful hash-based router for Single Page Applications (SPAs) with **automatic page cleanup** and **zero configuration**. Built on native browser APIs - no dependencies, no complex setup. - -### Why Hash-Based? - -Hash routing (`#/about`) works **everywhere** - no server configuration needed. It's perfect for: -- Static sites and SPAs -- GitHub Pages, Netlify, any static hosting -- Local development without a server -- Projects that need to work immediately - -### Basic Usage - -```javascript -import { $, html } from 'sigpro'; -import HomePage from './pages/HomePage.js'; -import AboutPage from './pages/AboutPage.js'; -import UserPage from './pages/UserPage.js'; -import NotFound from './pages/NotFound.js'; - -// Define your routes -const routes = [ - { path: '/', component: () => HomePage() }, - { path: '/about', component: () => AboutPage() }, - { path: '/users/:id', component: (params) => UserPage(params) }, - { path: /^\/posts\/(?\d+)$/, component: (params) => PostPage(params) }, -]; - -// Create and mount the router -const router = $.router(routes); -document.body.appendChild(router); -``` - ---- - -### 📋 Route Definition - -Each route is an object with two properties: - -| Property | Type | Description | -|----------|------|-------------| -| `path` | `string` or `RegExp` | Route pattern to match | -| `component` | `Function` | Function that returns page content (receives `params`) | - -#### String Paths (Simple Routes) - -```javascript -{ path: '/', component: () => HomePage() } -{ path: '/about', component: () => AboutPage() } -{ path: '/contact', component: () => ContactPage() } -{ path: '/users/:id', component: (params) => UserPage(params) } // With parameter -``` - -String paths support: -- **Static segments**: `/about`, `/contact`, `/products` -- **Named parameters**: `:id`, `:slug`, `:username` (captured in `params`) - -#### RegExp Paths (Advanced Routing) - -```javascript -// Match numeric IDs only -{ path: /^\/users\/(?\d+)$/, component: (params) => UserPage(params) } - -// Match product slugs (letters, numbers, hyphens) -{ path: /^\/products\/(?[a-z0-9-]+)$/, component: (params) => ProductPage(params) } - -// Match blog posts by year/month -{ path: /^\/blog\/(?\d{4})\/(?\d{2})$/, component: (params) => BlogArchive(params) } - -// Match optional language prefix -{ path: /^\/(?en|es|fr)?\/?about$/, component: (params) => AboutPage(params) } -``` - -RegExp gives you **full control** over route matching with named capture groups. - ---- - -## 🧭 `$.router(routes)` - Simple Router with Parameters - -Creates a hash-based router with support for `:param` parameters. Automatically cleans up pages when navigating away. - -### 📋 Route Parameters (Human-Friendly) - -```javascript -const routes = [ - { path: '/', component: HomePage }, - { path: '/about', component: AboutPage }, - { path: '/user/:id', component: UserPage }, // /user/42 → { id: '42' } - { path: '/user/:id/posts', component: UserPostsPage }, // /user/42/posts → { id: '42' } - { path: '/user/:id/posts/:pid', component: PostPage }, // /user/42/posts/123 → { id: '42', pid: '123' } - { path: '/search/:query/page/:num', component: SearchPage }, // /search/js/page/2 → { query: 'js', num: '2' } -]; -``` - -### 🎯 Accessing Parameters in Pages - -Parameters are automatically extracted and passed to your page component: - -```javascript -// pages/UserPage.js -import { $, html } from 'sigpro'; - -export default (params) => $.page(() => { - // /user/42 → params = { id: '42' } - const userId = params.id; - const userData = $(null); - - return html` -
-

User Profile: ${userId}

-

Loading user data...

-
- `; -}); - -// pages/PostPage.js -export default (params) => $.page(() => { - // /user/42/posts/123 → params = { id: '42', pid: '123' } - const { id, pid } = params; - - return html` -
-

Post ${pid} from user ${id}

-
- `; -}); -``` - -### 🧭 Navigation - -```javascript -// Programmatic navigation -$.router.go('/user/42'); -$.router.go('/search/javascript/page/2'); -$.router.go('about'); // Same as '/about' (auto-adds leading slash) - -// Link navigation (in templates) -html` - -`; -``` - -### 🔄 Automatic Page Cleanup - -```javascript -export default (params) => $.page(({ onUnmount }) => { - // Set up interval - const interval = setInterval(() => { - fetchData(params.id); - }, 5000); - - // Auto-cleaned when navigating away - onUnmount(() => clearInterval(interval)); - - return html`
Page content
`; -}); -``` - -### 📦 Usage in Templates - -```javascript -import { $, html } from 'sigpro'; -import HomePage from './pages/Home.js'; -import UserPage from './pages/User.js'; - -const routes = [ - { path: '/', component: HomePage }, - { path: '/user/:id', component: UserPage }, -]; - -// Mount router directly in your template -const App = () => html` -
-
My App
-
- ${$.router(routes)} -
-
-`; - -document.body.appendChild(App()); -``` - -### 🎯 API Reference - -#### `$.router(routes)` -- **routes**: `Array<{path: string, component: Function}>` - Route configurations with `:param` support -- **Returns**: `HTMLDivElement` - Container that renders the current page - -#### `$.router.go(path)` -- **path**: `string` - Route path (automatically adds leading slash) - -### 💡 Pro Tips - -1. **Order matters** - Define more specific routes first: -```javascript -[ - { path: '/user/:id/edit', component: EditUser }, // More specific first - { path: '/user/:id', component: ViewUser }, // Then generic -] -``` - -2. **Cleanup is automatic** - All effects, intervals, and event listeners in `$.page` are cleaned up - -3. **Zero config** - Just define routes and use them - ---- - -### `html` - Template Literal Tag - -Creates reactive DOM fragments using template literals. - -#### Basic Usage - -```javascript -import { $, html } from 'sigpro'; - -const count = $(0); - -const fragment = html` -
-

Count: ${count}

- -
-`; -``` - -#### Directive Reference - -| Directive | Example | Description | -|-----------|---------|-------------| -| `@event` | `@click=${handler}` | Event listener | -| `:property` | `:value=${signal}` | Two-way binding | -| `?attribute` | `?disabled=${signal}` | Boolean attribute | -| `.property` | `.scrollTop=${signal}` | Property binding | - -**Two-way binding example:** -```javascript -const text = $(''); - -html` - -

You typed: ${text}

-`; -``` - -## 📝 License - -MIT © natxocc +# SigPro ⚡ +**The Ultra-Lightweight, Reactive Plugin-Based Framework.** + +SigPro 2 is a modern, minimalist JavaScript framework designed for developers who want the power of reactivity without the overhead of heavy runtimes. It weighs less than **2KB**, uses a signal-based architecture, and is fully extensible through a modular plugin system. + + +[![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/natxocc/sigpro/blob/main/LICENSE) + +--- + +## 🚀 Key Features + +* **Nano-Sized:** Optimized for speed and minimal bundle impact. +* **Signal-Based Reactivity:** Surgical DOM updates—only what changes is re-rendered. +* **Plugin Ecosystem:** Modular by design. Only load what you need (Router, Fetch, Storage, etc.). +* **File-Based Routing:** Automated route generation via Vite. +* **Zero Boilerplate:** No complex build steps or proprietary syntax. Just JavaScript. + +--- + +## 📦 Installation + +Install the core engine and the essential plugins. + +::: code-group +```bash [NPM] +npm install sigpro +``` + +```bash [Bun] +bun add sigpro +``` +::: + +--- + +## 🛠️ Quick Start + +### 1. Configure Vite +Add the automatic router to your `vite.config.js`. + +```javascript +import { defineConfig } from 'vite'; +import { sigproRouter } from 'sigpro/vite'; + +export default defineConfig({ + plugins: [sigproRouter()] +}); +``` + +### 2. Initialize the App +Register plugins and mount your application shell. + +```javascript +// src/main.js +import { $ } from 'sigpro'; +import { Router, Fetch, UI } from 'sigpro/plugins'; + +$.plugin([Router, Fetch, UI]).then(() => { + import('./App.js').then(app => $.mount(app.default, '#app')); +}); +``` + +--- + +## 🧩 Official Plugins + +SigPro is powered by a "Pay-only-for-what-you-use" architecture. + +### 🚦 Router (`_router`) +Automated file-based routing. Just create a file in `src/pages/` and it becomes a route. +```javascript +import { routes } from 'virtual:sigpro-routes'; +const App = () => main(_router(routes)); +``` + +### 📡 Fetch (`_fetch`) +Reactive data fetching with built-in loading and error states. +```javascript +const { $data, $loading } = _fetch('https://api.example.com/data'); +``` + +### 💾 Storage (`_storage`) +Automatic synchronization between Signals and `localStorage`. +```javascript +const $theme = _storage($('light'), 'app_theme'); +``` + +### 🐞 Debug (`_debug`) +Beautifully formatted console logs for tracking state changes in real-time. +```javascript +_debug($mySignal, "User Profile"); +``` + +--- + +## 📂 Project Structure + +A typical SigPro project follows this clean convention: + +```text +my-app/ +├── src/ +│ ├── pages/ # Automatic Routes +│ │ ├── index.js # -> #/ +│ │ └── about.js # -> #/about +│ ├── plugins/ # Custom Plugins +│ ├── App.js # Main Layout +│ └── main.js # Entry Point +├── vite.config.js +└── package.json +``` + +--- + +## 🎨 Example Component + +SigPro uses standard JavaScript functions and a clean, declarative syntax. + +```javascript +export default () => { + const $count = $(0); // Create a Signal + + return div({ class: 'p-8 text-center' }, [ + h1("Hello SigPro!"), + p(`Current count is: ${$count()}`), + + _button({ + onclick: () => $count(c => c + 1), + class: 'btn-primary' + }, "Increment") + ]); +}; +``` + +--- + +## 📄 License + +MIT © 2026 SigPro Team. \ No newline at end of file diff --git a/bun.lock b/bun.lock index 5ee9a83..86a7a7f 100644 --- a/bun.lock +++ b/bun.lock @@ -3,13 +3,8 @@ "configVersion": 1, "workspaces": { "": { - "name": "sigpro", - "dependencies": { - "daisyui": "^5.5.19", - }, + "name": "sigpro2", "devDependencies": { - "@tailwindcss/vite": "^4.2.2", - "vite": "^8.0.0", "vitepress": "^1.6.4", }, }, @@ -65,12 +60,6 @@ "@docsearch/react": ["@docsearch/react@3.8.2", "", { "dependencies": { "@algolia/autocomplete-core": "1.17.7", "@algolia/autocomplete-preset-algolia": "1.17.7", "@docsearch/css": "3.8.2", "algoliasearch": "^5.14.2" }, "peerDependencies": { "@types/react": ">= 16.8.0 < 19.0.0", "react": ">= 16.8.0 < 19.0.0", "react-dom": ">= 16.8.0 < 19.0.0", "search-insights": ">= 1 < 3" }, "optionalPeers": ["@types/react", "react", "react-dom", "search-insights"] }, "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg=="], - "@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="], - - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], @@ -121,103 +110,57 @@ "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.1", "", { "os": "android", "cpu": "arm" }, "sha512-xB0b51TB7IfDEzAojXahmr+gfA00uYVInJGgNNkeQG6RPnCPGr7udsylFLTubuIUSRE6FkcI1NElyRt83PP5oQ=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.1", "", { "os": "android", "cpu": "arm64" }, "sha512-XOjPId0qwSDKHaIsdzHJtKCxX0+nH8MhBwvrNsT7tVyKmdTx1jJ4XzN5RZXCdTzMpufLb+B8llTC0D8uCrLhcw=="], - "@oxc-project/runtime": ["@oxc-project/runtime@0.115.0", "", {}, "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vQuRd28p0gQpPrS6kppd8IrWmFo42U8Pz1XLRjSZXq5zCqyMDYFABT7/sywL11mO1EL10Qhh7MVPEwkG8GiBeg=="], - "@oxc-project/types": ["@oxc-project/types@0.115.0", "", {}, "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-x6VG6U29+Ivlnajrg1IHdzXeAwSoEHBFVO+CtC9Brugx6de712CUJobRUxsIA0KYrQvCmzNrMPFTT1A4CCqNTg=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.9", "", { "os": "android", "cpu": "arm64" }, "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-Sgi0Uo6t1YCHJMNO3Y8+bm+SvOanUGkoZKn/VJPwYUe2kp31X5KnXmzKd/NjW8iA3gFcfNZ64zh14uOGrIllCQ=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-AM4xnwEZwukdhk7laMWfzWu9JGSVnJd+Fowt6Fd7QW1nrf3h0Hp7Qx5881M4aqrUlKBCybOxz0jofvIIfl7C5g=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.1", "", { "os": "linux", "cpu": "arm" }, "sha512-KUizqxpwaR2AZdAUsMWfL/C94pUu7TKpoPd88c8yFVixJ+l9hejkrwoK5Zj3wiNh65UeyryKnJyxL1b7yNqFQA=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MZoQ/am77ckJtZGFAtPucgUuJWiop3m2R3lw7tC0QCcbfl4DRhQUBUkHWCkcrT3pqy5Mzv5QQgY6Dmlba6iTWg=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm" }, "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Sez95TP6xGjkWB1608EfhCX1gdGrO5wzyN99VqzRtC17x/1bhw5VU1V0GfKUwbW/Xr1J8mSasoFoJa6Y7aGGSA=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Cs2Seq98LWNOJzR89EGTZoiP8EkZ9UbQhBlDgfAkM6asVna1xJ04W2CLYWDN/RpUgOjtQvcv8wQVi1t5oQazA=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.1", "", { "os": "linux", "cpu": "none" }, "sha512-n9yqttftgFy7IrNEnHy1bOp6B4OSe8mJDiPkT7EqlM9FnKOwUMnCK62ixW0Kd9Clw0/wgvh8+SqaDXMFvw3KqQ=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.1", "", { "os": "linux", "cpu": "none" }, "sha512-SfpNXDzVTqs/riak4xXcLpq5gIQWsqGWMhN1AGRQKB4qGSs4r0sEs3ervXPcE1O9RsQ5bm8Muz6zmQpQnPss1g=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-LjaChED0wQnjKZU+tsmGbN+9nN1XhaWUkAlSbTdhpEseCS4a15f/Q8xC2BN4GDKRzhhLZpYtJBZr2NZhR0jvNw=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "x64" }, "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ojW7iTJSIs4pwB2xV6QXGwNyDctvXOivYllttuPbXguuKDX5vwpqYJsHc6D2LZzjDGHML414Tuj3LvVPe1CT1A=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.9", "", { "os": "linux", "cpu": "x64" }, "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.1", "", { "os": "linux", "cpu": "none" }, "sha512-FP+Q6WTcxxvsr0wQczhSE+tOZvFPV8A/mUE6mhZYFW9/eea/y/XqAgRoLLMuE9Cz0hfX5bi7p116IWoB+P237A=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.9", "", { "os": "none", "cpu": "arm64" }, "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.1", "", { "os": "linux", "cpu": "none" }, "sha512-L1uD9b/Ig8Z+rn1KttCJjwhN1FgjRMBKsPaBsDKkfUl7GfFq71pU4vWCnpOsGljycFEbkHWARZLf4lMYg3WOLw=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.9", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-EZc9NGTk/oSUzzOD4nYY4gIjteo2M3CiozX6t1IXGCOdgxJTlVu/7EdPeiqeHPSIrxkLhavqpBAUCfvC6vBOug=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.1", "", { "os": "linux", "cpu": "x64" }, "sha512-NQ9KyU1Anuy59L8+HHOKM++CoUxrQWrZWXRik4BJFm+7i5NP6q/SW43xIBr80zzt+PDBJ7LeNmloQGfa0JGk0w=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.9", "", { "os": "win32", "cpu": "x64" }, "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GZkLk2t6naywsveSFBsEb0PLU+JC9ggVjbndsbG20VPhar6D1gkMfCx4NfP9owpovBXTN+eRdqGSkDGIxPHhmQ=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.9", "", {}, "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1hjG9Jpl2KDOetr64iQd8AZAEjkDUUK5RbDkYWsViYLC1op1oNzdjMJeFiofcGhqbNTaY2kfgqowE7DILifsrA=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ARoKfflk0SiiYm3r1fmF73K/yB+PThmOwfWCk1sr7x/k9dc3uGLWuEE9if+Pw21el8MSpp3TMnG5vLNsJ/MMGQ=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-oOST61G6VM45Mz2vdzWMr1s2slI7y9LqxEV5fCoWi2MDONmMvgsJVHSXxce/I2xOSZPTZ47nDPOl1tkwKWSHcw=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-x5WgLi5dWpRz7WclKBGEF15LcWTh0ewrHM6Cq4A+WUbkysUMZNeqt05bwPonOQ3ihPS/WMhAZV5zB1DfnI4Sxg=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.1", "", { "os": "win32", "cpu": "x64" }, "sha512-wS+zHAJRVP5zOL0e+a3V3E/NTEwM2HEvvNKoDy5Xcfs0o8lljxn+EAFPkUsxihBdmDq1JWzXmmB9cbssCPdxxw=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], - - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], - - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rhHyrMeLpErT/C7BxcEsU4COHQUzHyrPYW5tOZUeUhziNtRuYxmDWvqQqzpuUt8xpOgmbKa1btGXfnA/ANVO+g=="], "@shikijs/core": ["@shikijs/core@2.5.0", "", { "dependencies": { "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg=="], @@ -235,38 +178,6 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], - "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="], - - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="], - - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="], - - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="], - - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="], - - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="], - - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="], - - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="], - - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="], - - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="], - - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="], - - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="], - - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="], - - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="], - - "@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="], - - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], @@ -335,32 +246,22 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "daisyui": ["daisyui@5.5.19", "", {}, "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA=="], - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], "emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="], - "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="], - "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "focus-trap": ["focus-trap@7.8.0", "", { "dependencies": { "tabbable": "^6.4.0" } }, "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], @@ -371,32 +272,6 @@ "is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="], - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - - "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "mark.js": ["mark.js@8.11.1", "", {}, "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ=="], @@ -425,8 +300,6 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], "preact": ["preact@10.29.0", "", {}, "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg=="], @@ -441,9 +314,7 @@ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - "rolldown": ["rolldown@1.0.0-rc.9", "", { "dependencies": { "@oxc-project/types": "=0.115.0", "@rolldown/pluginutils": "1.0.0-rc.9" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-x64": "1.0.0-rc.9", "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q=="], - - "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + "rollup": ["rollup@4.59.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.1", "@rollup/rollup-android-arm64": "4.59.1", "@rollup/rollup-darwin-arm64": "4.59.1", "@rollup/rollup-darwin-x64": "4.59.1", "@rollup/rollup-freebsd-arm64": "4.59.1", "@rollup/rollup-freebsd-x64": "4.59.1", "@rollup/rollup-linux-arm-gnueabihf": "4.59.1", "@rollup/rollup-linux-arm-musleabihf": "4.59.1", "@rollup/rollup-linux-arm64-gnu": "4.59.1", "@rollup/rollup-linux-arm64-musl": "4.59.1", "@rollup/rollup-linux-loong64-gnu": "4.59.1", "@rollup/rollup-linux-loong64-musl": "4.59.1", "@rollup/rollup-linux-ppc64-gnu": "4.59.1", "@rollup/rollup-linux-ppc64-musl": "4.59.1", "@rollup/rollup-linux-riscv64-gnu": "4.59.1", "@rollup/rollup-linux-riscv64-musl": "4.59.1", "@rollup/rollup-linux-s390x-gnu": "4.59.1", "@rollup/rollup-linux-x64-gnu": "4.59.1", "@rollup/rollup-linux-x64-musl": "4.59.1", "@rollup/rollup-openbsd-x64": "4.59.1", "@rollup/rollup-openharmony-arm64": "4.59.1", "@rollup/rollup-win32-arm64-msvc": "4.59.1", "@rollup/rollup-win32-ia32-msvc": "4.59.1", "@rollup/rollup-win32-x64-gnu": "4.59.1", "@rollup/rollup-win32-x64-msvc": "4.59.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iZKH8BeoCwTCBTZBZWQQMreekd4mdomwdjIQ40GC1oZm6o+8PnNMIxFOiCsGMWeS8iDJ7KZcl7KwmKk/0HOQpA=="], "search-insights": ["search-insights@2.17.3", "", {}, "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ=="], @@ -461,16 +332,8 @@ "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], - "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="], - - "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], - - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], @@ -485,26 +348,12 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "vite": ["vite@8.0.0", "", { "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.9", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q=="], + "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], "vitepress": ["vitepress@1.6.4", "", { "dependencies": { "@docsearch/css": "3.8.2", "@docsearch/js": "3.8.2", "@iconify-json/simple-icons": "^1.2.21", "@shikijs/core": "^2.1.0", "@shikijs/transformers": "^2.1.0", "@shikijs/types": "^2.1.0", "@types/markdown-it": "^14.1.2", "@vitejs/plugin-vue": "^5.2.1", "@vue/devtools-api": "^7.7.0", "@vue/shared": "^3.5.13", "@vueuse/core": "^12.4.0", "@vueuse/integrations": "^12.4.0", "focus-trap": "^7.6.4", "mark.js": "8.11.1", "minisearch": "^7.1.1", "shiki": "^2.1.0", "vite": "^5.4.14", "vue": "^3.5.13" }, "peerDependencies": { "markdown-it-mathjax3": "^4", "postcss": "^8" }, "optionalPeers": ["markdown-it-mathjax3", "postcss"], "bin": { "vitepress": "bin/vitepress.js" } }, "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg=="], "vue": ["vue@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/compiler-sfc": "3.5.30", "@vue/runtime-dom": "3.5.30", "@vue/server-renderer": "3.5.30", "@vue/shared": "3.5.30" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], - - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], - - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - - "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "vitepress/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], } } diff --git a/docs/404.html b/docs/404.html index 86400bb..5d32f7d 100644 --- a/docs/404.html +++ b/docs/404.html @@ -6,7 +6,7 @@ 404 | SigPro - + @@ -16,7 +16,7 @@
- + \ No newline at end of file diff --git a/docs/api/$.html b/docs/api/$.html new file mode 100644 index 0000000..22f0a25 --- /dev/null +++ b/docs/api/$.html @@ -0,0 +1,51 @@ + + + + + + The Reactive Core: $( ) | SigPro + + + + + + + + + + + + + + +
Skip to content

The Reactive Core: $( )

The $ function is the heart of SigPro. It is a Unified Reactive Constructor that handles state, derivations, and side effects through a single, consistent interface.

1. The Constructor: $( input )

Depending on what you pass into $( ), SigPro creates a different type of reactive primitive:

Input TypeResultInternal Behavior
Value (String, Number, Object...)SignalCreates a piece of mutable state.
FunctionComputed / EffectCreates a derived value that tracks dependencies.

2. Signal (State)

A Signal is a "box" that holds a value. It provides a getter/setter function to interact with that value.

  • When to use: For data that changes over time (counters, user input, toggle states, API data).
  • Syntax: const $state = $(initialValue);

Example:

javascript
const $name = $("Alice");
+
+// Read the value (Getter)
+console.log($name()); // "Alice"
+
+// Update the value (Setter)
+$name("Bob"); 
+
+// Update based on previous value
+$name(current => current + " Smith");

3. Computed (Derived State)

When you pass a function to $( ) that returns a value, SigPro creates a Computed Signal. It automatically tracks which signals are used inside it and re-runs only when they change.

  • When to use: For values that depend on other signals (totals, filtered lists, formatted strings).
  • Syntax: const $derived = $(() => logic);

Example:

javascript
const $price = $(100);
+const $qty = $(2);
+
+// Automatically tracks $price and $qty
+const $total = $(() => $price() * $qty());
+
+console.log($total()); // 200
+
+$qty(3); // $total updates to 300 automatically

4. Effects (Side Effects)

An Effect is a function passed to $( ) that does not return a value (or returns undefined). SigPro treats this as a subscription that performs an action whenever its dependencies change.

  • When to use: For DOM manipulations, logging, or syncing with external APIs (LocalStorage, Fetch).
  • Syntax: $(() => { action });

Example:

javascript
const $theme = $("light");
+
+// This effect runs every time $theme changes
+$(() => {
+  document.body.className = $theme();
+  console.log("Theme updated to:", $theme());
+});
+
+$theme("dark"); // Logs: Theme updated to: dark

5. Summary Table: Usage Guide

PrimitiveLogic TypeReturns Value?Typical Use Case
SignalStaticYes (Mutable)const $user = $("Guest")
ComputedRead-onlyYes (Automatic)const $isLoggedIn = $(() => $user() !== "Guest")
EffectImperativeNo$(() => localStorage.setItem('user', $user()))

💡 Pro Tip: Naming Convention

In SigPro, we use the $ prefix (e.g., $count) for variables that hold a reactive function. This makes it easy to distinguish between a standard variable and a reactive one at a glance:

javascript
let count = 0;   // Static
+const $count = $(0); // Reactive (Function)
+ + + + \ No newline at end of file diff --git a/docs/api/components.html b/docs/api/components.html deleted file mode 100644 index fef27c7..0000000 --- a/docs/api/components.html +++ /dev/null @@ -1,595 +0,0 @@ - - - - - - Components API 🧩 | SigPro - - - - - - - - - - - - - - -
Skip to content

Components API 🧩

Components in SigPro are native Web Components built on the Custom Elements standard. They provide a way to create reusable, encapsulated pieces of UI with reactive properties and automatic cleanup.

$.component(tagName, setupFunction, observedAttributes, useShadowDOM)

Creates a custom element with reactive properties and automatic dependency tracking.

javascript
import { $, html } from 'sigpro';
-
-$.component('my-button', (props, { slot, emit }) => {
-  return html`
-    <button 
-      class="btn"
-      @click=${() => emit('click')}
-    >
-      ${slot()}
-    </button>
-  `;
-}, ['variant']); // Observe the 'variant' attribute

📋 API Reference

Parameters

ParameterTypeDefaultDescription
tagNamestringrequiredCustom element tag name (must include a hyphen, e.g., my-button)
setupFunctionFunctionrequiredFunction that returns the component's template
observedAttributesstring[][]Attributes to observe for changes (become reactive props)
useShadowDOMbooleanfalsetrue = Shadow DOM (encapsulated), false = Light DOM (inherits styles)

Setup Function Parameters

The setup function receives two arguments:

  1. props - Object containing reactive signals for each observed attribute
  2. context - Object with helper methods and properties

Context Object Properties

PropertyTypeDescription
slot(name)FunctionReturns array of child nodes for the specified slot
emit(name, detail)FunctionDispatches a custom event
select(selector)FunctionQuery selector within component's root
selectAll(selector)FunctionQuery selector all within component's root
hostHTMLElementReference to the custom element instance
rootNodeComponent's root (shadow root or element itself)
onUnmount(callback)FunctionRegister cleanup function

🏠 Light DOM vs Shadow DOM

Light DOM (useShadowDOM = false) - Default

The component inherits global styles from the application. Perfect for components that should integrate with your site's design system.

javascript
// Button that uses global Tailwind CSS
-$.component('tw-button', (props, { slot, emit }) => {
-  const variant = props.variant() || 'primary';
-  
-  const variants = {
-    primary: 'bg-blue-500 hover:bg-blue-600 text-white',
-    secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
-    outline: 'border border-blue-500 text-blue-500 hover:bg-blue-50'
-  };
-  
-  return html`
-    <button 
-      class="px-4 py-2 rounded font-semibold transition-colors ${variants[variant]}"
-      @click=${() => emit('click')}
-    >
-      ${slot()}
-    </button>
-  `;
-}, ['variant']);

Shadow DOM (useShadowDOM = true) - Encapsulated

The component encapsulates its styles completely. External styles don't affect it, and its styles don't leak out.

javascript
// Calendar with encapsulated styles
-$.component('ui-calendar', (props) => {
-  return html`
-    <style>
-      /* These styles won't affect the rest of the page */
-      .calendar {
-        font-family: system-ui, sans-serif;
-        background: white;
-        border-radius: 12px;
-        padding: 20px;
-        box-shadow: 0 4px 12px rgba(0,0,0,0.1);
-      }
-      .day {
-        aspect-ratio: 1;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        cursor: pointer;
-        border-radius: 50%;
-      }
-      .day.selected {
-        background: #2196f3;
-        color: white;
-      }
-    </style>
-    
-    <div class="calendar">
-      ${renderCalendar(props.date())}
-    </div>
-  `;
-}, ['date'], true); // true = use Shadow DOM

🎯 Basic Examples

Simple Counter Component

javascript
// counter.js
-$.component('my-counter', (props) => {
-  const count = $(0);
-  
-  return html`
-    <div class="counter">
-      <p>Count: ${count}</p>
-      <button @click=${() => count(c => c + 1)}>+</button>
-      <button @click=${() => count(c => c - 1)}>-</button>
-      <button @click=${() => count(0)}>Reset</button>
-    </div>
-  `;
-});

Usage:

html
<my-counter></my-counter>

Component with Props

javascript
// greeting.js
-$.component('my-greeting', (props) => {
-  const name = props.name() || 'World';
-  const greeting = $(() => `Hello, ${name}!`);
-  
-  return html`
-    <div class="greeting">
-      <h1>${greeting}</h1>
-      <p>This is a greeting component.</p>
-    </div>
-  `;
-}, ['name']); // Observe the 'name' attribute

Usage:

html
<my-greeting name="John"></my-greeting>
-<my-greeting name="Jane"></my-greeting>

Component with Events

javascript
// toggle.js
-$.component('my-toggle', (props, { emit }) => {
-  const isOn = $(props.initial() === 'on');
-  
-  const toggle = () => {
-    isOn(!isOn());
-    emit('toggle', { isOn: isOn() });
-    emit(isOn() ? 'on' : 'off');
-  };
-  
-  return html`
-    <button 
-      class="toggle ${() => isOn() ? 'active' : ''}"
-      @click=${toggle}
-    >
-      ${() => isOn() ? 'ON' : 'OFF'}
-    </button>
-  `;
-}, ['initial']);

Usage:

html
<my-toggle 
-  initial="off"
-  @toggle=${(e) => console.log('Toggled:', e.detail)}
-  @on=${() => console.log('Turned on')}
-  @off=${() => console.log('Turned off')}
-></my-toggle>

🎨 Advanced Examples

Form Input Component

javascript
// form-input.js
-$.component('form-input', (props, { emit }) => {
-  const value = $(props.value() || '');
-  const error = $(null);
-  const touched = $(false);
-  
-  // Validation effect
-  $.effect(() => {
-    if (props.pattern() && touched()) {
-      const regex = new RegExp(props.pattern());
-      const isValid = regex.test(value());
-      error(isValid ? null : props.errorMessage() || 'Invalid input');
-      emit('validate', { isValid, value: value() });
-    }
-  });
-  
-  const handleInput = (e) => {
-    value(e.target.value);
-    emit('update', e.target.value);
-  };
-  
-  const handleBlur = () => {
-    touched(true);
-  };
-  
-  return html`
-    <div class="form-group">
-      ${props.label() ? html`
-        <label class="form-label">
-          ${props.label()}
-          ${props.required() ? html`<span class="required">*</span>` : ''}
-        </label>
-      ` : ''}
-      
-      <input
-        type="${props.type() || 'text'}"
-        class="form-control ${() => error() ? 'is-invalid' : ''}"
-        :value=${value}
-        @input=${handleInput}
-        @blur=${handleBlur}
-        placeholder="${props.placeholder() || ''}"
-        ?disabled=${props.disabled}
-        ?required=${props.required}
-      />
-      
-      ${() => error() ? html`
-        <div class="error-message">${error()}</div>
-      ` : ''}
-      
-      ${props.helpText() ? html`
-        <small class="help-text">${props.helpText()}</small>
-      ` : ''}
-    </div>
-  `;
-}, ['label', 'type', 'value', 'placeholder', 'disabled', 'required', 'pattern', 'errorMessage', 'helpText']);

Usage:

html
<form-input
-  label="Email"
-  type="email"
-  required
-  pattern="^[^\s@]+@[^\s@]+\.[^\s@]+$"
-  errorMessage="Please enter a valid email"
-  @update=${(e) => formData.email = e.detail}
-  @validate=${(e) => setEmailValid(e.detail.isValid)}
->
-</form-input>
javascript
// modal.js
-$.component('my-modal', (props, { slot, emit, onUnmount }) => {
-  const isOpen = $(false);
-  
-  // Handle escape key
-  const handleKeydown = (e) => {
-    if (e.key === 'Escape' && isOpen()) {
-      close();
-    }
-  };
-  
-  $.effect(() => {
-    if (isOpen()) {
-      document.addEventListener('keydown', handleKeydown);
-      document.body.style.overflow = 'hidden';
-    } else {
-      document.removeEventListener('keydown', handleKeydown);
-      document.body.style.overflow = '';
-    }
-  });
-  
-  // Cleanup on unmount
-  onUnmount(() => {
-    document.removeEventListener('keydown', handleKeydown);
-    document.body.style.overflow = '';
-  });
-  
-  const open = () => {
-    isOpen(true);
-    emit('open');
-  };
-  
-  const close = () => {
-    isOpen(false);
-    emit('close');
-  };
-  
-  // Expose methods to parent
-  props.open = open;
-  props.close = close;
-  
-  return html`
-    <div>
-      <!-- Trigger button -->
-      <button 
-        class="modal-trigger"
-        @click=${open}
-      >
-        ${slot('trigger') || 'Open Modal'}
-      </button>
-      
-      <!-- Modal overlay -->
-      ${() => isOpen() ? html`
-        <div class="modal-overlay" @click=${close}>
-          <div class="modal-content" @click.stop>
-            <div class="modal-header">
-              <h3>${props.title() || 'Modal'}</h3>
-              <button class="close-btn" @click=${close}>&times;</button>
-            </div>
-            <div class="modal-body">
-              ${slot('body')}
-            </div>
-            <div class="modal-footer">
-              ${slot('footer') || html`
-                <button @click=${close}>Close</button>
-              `}
-            </div>
-          </div>
-        </div>
-      ` : ''}
-    </div>
-  `;
-}, ['title'], false);

Usage:

html
<my-modal title="Confirm Delete">
-  <button slot="trigger">Delete Item</button>
-  
-  <div slot="body">
-    <p>Are you sure you want to delete this item?</p>
-    <p class="warning">This action cannot be undone.</p>
-  </div>
-  
-  <div slot="footer">
-    <button class="cancel" @click=${close}>Cancel</button>
-    <button class="delete" @click=${handleDelete}>Delete</button>
-  </div>
-</my-modal>

Data Table Component

javascript
// data-table.js
-$.component('data-table', (props, { emit }) => {
-  const data = $(props.data() || []);
-  const columns = $(props.columns() || []);
-  const sortColumn = $(null);
-  const sortDirection = $('asc');
-  const filterText = $('');
-  
-  // Computed: filtered and sorted data
-  const processedData = $(() => {
-    let result = [...data()];
-    
-    // Filter
-    if (filterText()) {
-      const search = filterText().toLowerCase();
-      result = result.filter(row => 
-        Object.values(row).some(val => 
-          String(val).toLowerCase().includes(search)
-        )
-      );
-    }
-    
-    // Sort
-    if (sortColumn()) {
-      const col = sortColumn();
-      const direction = sortDirection() === 'asc' ? 1 : -1;
-      
-      result.sort((a, b) => {
-        if (a[col] < b[col]) return -direction;
-        if (a[col] > b[col]) return direction;
-        return 0;
-      });
-    }
-    
-    return result;
-  });
-  
-  const handleSort = (col) => {
-    if (sortColumn() === col) {
-      sortDirection(sortDirection() === 'asc' ? 'desc' : 'asc');
-    } else {
-      sortColumn(col);
-      sortDirection('asc');
-    }
-    emit('sort', { column: col, direction: sortDirection() });
-  };
-  
-  return html`
-    <div class="data-table">
-      <!-- Search input -->
-      <div class="table-toolbar">
-        <input
-          type="search"
-          :value=${filterText}
-          placeholder="Search..."
-          class="search-input"
-        />
-        <span class="record-count">
-          ${() => `${processedData().length} of ${data().length} records`}
-        </span>
-      </div>
-      
-      <!-- Table -->
-      <table>
-        <thead>
-          <tr>
-            ${columns().map(col => html`
-              <th 
-                @click=${() => handleSort(col.field)}
-                class:sortable=${true}
-                class:sorted=${() => sortColumn() === col.field}
-              >
-                ${col.label}
-                ${() => sortColumn() === col.field ? html`
-                  <span class="sort-icon">
-                    ${sortDirection() === 'asc' ? '↑' : '↓'}
-                  </span>
-                ` : ''}
-              </th>
-            `)}
-          </tr>
-        </thead>
-        <tbody>
-          ${() => processedData().map(row => html`
-            <tr @click=${() => emit('row-click', row)}>
-              ${columns().map(col => html`
-                <td>${row[col.field]}</td>
-              `)}
-            </tr>
-          `)}
-        </tbody>
-      </table>
-      
-      <!-- Empty state -->
-      ${() => processedData().length === 0 ? html`
-        <div class="empty-state">
-          No data found
-        </div>
-      ` : ''}
-    </div>
-  `;
-}, ['data', 'columns']);

Usage:

javascript
const userColumns = [
-  { field: 'id', label: 'ID' },
-  { field: 'name', label: 'Name' },
-  { field: 'email', label: 'Email' },
-  { field: 'role', label: 'Role' }
-];
-
-const userData = [
-  { id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' },
-  { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' }
-];
html
<data-table 
-  .data=${userData}
-  .columns=${userColumns}
-  @row-click=${(e) => console.log('Row clicked:', e.detail)}
->
-</data-table>

Tabs Component

javascript
// tabs.js
-$.component('my-tabs', (props, { slot, emit }) => {
-  const activeTab = $(props.active() || 0);
-  
-  // Get all tab headers from slots
-  const tabs = $(() => {
-    const headers = slot('tab');
-    return headers.map((node, index) => ({
-      index,
-      title: node.textContent,
-      content: slot(`panel-${index}`)[0]
-    }));
-  });
-  
-  $.effect(() => {
-    emit('change', { index: activeTab(), tab: tabs()[activeTab()] });
-  });
-  
-  return html`
-    <div class="tabs">
-      <div class="tab-headers">
-        ${tabs().map(tab => html`
-          <button
-            class="tab-header ${() => activeTab() === tab.index ? 'active' : ''}"
-            @click=${() => activeTab(tab.index)}
-          >
-            ${tab.title}
-          </button>
-        `)}
-      </div>
-      
-      <div class="tab-panels">
-        ${tabs().map(tab => html`
-          <div 
-            class="tab-panel"
-            style="display: ${() => activeTab() === tab.index ? 'block' : 'none'}"
-          >
-            ${tab.content}
-          </div>
-        `)}
-      </div>
-    </div>
-  `;
-}, ['active']);

Usage:

html
<my-tabs @change=${(e) => console.log('Tab changed:', e.detail)}>
-  <div slot="tab">Profile</div>
-  <div slot="panel-0">
-    <h3>Profile Settings</h3>
-    <form>...</form>
-  </div>
-  
-  <div slot="tab">Security</div>
-  <div slot="panel-1">
-    <h3>Security Settings</h3>
-    <form>...</form>
-  </div>
-  
-  <div slot="tab">Notifications</div>
-  <div slot="panel-2">
-    <h3>Notification Preferences</h3>
-    <form>...</form>
-  </div>
-</my-tabs>

Component with External Data

javascript
// user-profile.js
-$.component('user-profile', (props, { emit, onUnmount }) => {
-  const user = $(null);
-  const loading = $(false);
-  const error = $(null);
-  
-  // Fetch user data when userId changes
-  $.effect(() => {
-    const userId = props.userId();
-    if (!userId) return;
-    
-    loading(true);
-    error(null);
-    
-    const controller = new AbortController();
-    
-    fetch(`/api/users/${userId}`, { signal: controller.signal })
-      .then(res => res.json())
-      .then(data => {
-        user(data);
-        emit('loaded', data);
-      })
-      .catch(err => {
-        if (err.name !== 'AbortError') {
-          error(err.message);
-          emit('error', err);
-        }
-      })
-      .finally(() => loading(false));
-    
-    // Cleanup: abort fetch if component unmounts or userId changes
-    onUnmount(() => controller.abort());
-  });
-  
-  return html`
-    <div class="user-profile">
-      ${() => loading() ? html`
-        <div class="spinner">Loading...</div>
-      ` : error() ? html`
-        <div class="error">Error: ${error()}</div>
-      ` : user() ? html`
-        <div class="user-info">
-          <img src="${user().avatar}" class="avatar" />
-          <h2>${user().name}</h2>
-          <p>${user().email}</p>
-          <p>Member since: ${new Date(user().joined).toLocaleDateString()}</p>
-        </div>
-      ` : html`
-        <div class="no-user">No user selected</div>
-      `}
-    </div>
-  `;
-}, ['user-id']);

📦 Component Libraries

Building a Reusable Component Library

javascript
// components/index.js
-import { $, html } from 'sigpro';
-
-// Button component
-export const Button = $.component('ui-button', (props, { slot, emit }) => {
-  const variant = props.variant() || 'primary';
-  const size = props.size() || 'md';
-  
-  const sizes = {
-    sm: 'px-2 py-1 text-sm',
-    md: 'px-4 py-2',
-    lg: 'px-6 py-3 text-lg'
-  };
-  
-  const variants = {
-    primary: 'bg-blue-500 hover:bg-blue-600 text-white',
-    secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
-    danger: 'bg-red-500 hover:bg-red-600 text-white'
-  };
-  
-  return html`
-    <button
-      class="rounded font-semibold transition-colors ${sizes[size]} ${variants[variant]}"
-      ?disabled=${props.disabled}
-      @click=${() => emit('click')}
-    >
-      ${slot()}
-    </button>
-  `;
-}, ['variant', 'size', 'disabled']);
-
-// Card component
-export const Card = $.component('ui-card', (props, { slot }) => {
-  return html`
-    <div class="card border rounded-lg shadow-sm overflow-hidden">
-      ${props.title() ? html`
-        <div class="card-header bg-gray-50 px-4 py-3 border-b">
-          <h3 class="font-semibold">${props.title()}</h3>
-        </div>
-      ` : ''}
-      
-      <div class="card-body p-4">
-        ${slot()}
-      </div>
-      
-      ${props.footer() ? html`
-        <div class="card-footer bg-gray-50 px-4 py-3 border-t">
-          ${slot('footer')}
-        </div>
-      ` : ''}
-    </div>
-  `;
-}, ['title']);
-
-// Badge component
-export const Badge = $.component('ui-badge', (props, { slot }) => {
-  const type = props.type() || 'default';
-  
-  const types = {
-    default: 'bg-gray-100 text-gray-800',
-    success: 'bg-green-100 text-green-800',
-    warning: 'bg-yellow-100 text-yellow-800',
-    error: 'bg-red-100 text-red-800',
-    info: 'bg-blue-100 text-blue-800'
-  };
-  
-  return html`
-    <span class="inline-block px-2 py-1 text-xs font-semibold rounded ${types[type]}">
-      ${slot()}
-    </span>
-  `;
-}, ['type']);
-
-export { $, html };

Usage:

javascript
import { Button, Card, Badge } from './components/index.js';
-
-// Use components anywhere
-const app = html`
-  <div>
-    <Card title="Welcome">
-      <p>This is a card component</p>
-      <div slot="footer">
-        <Button variant="primary" @click=${handleClick}>
-          Save Changes
-        </Button>
-        <Badge type="success">New</Badge>
-      </div>
-    </Card>
-  </div>
-`;

🎯 Decision Guide: Light DOM vs Shadow DOM

Use Light DOM (false) when...Use Shadow DOM (true) when...
Component is part of your main appBuilding a UI library for others
Using global CSS (Tailwind, Bootstrap)Creating embeddable widgets
Need to inherit theme variablesStyles must be pixel-perfect everywhere
Working with existing design systemComponent has complex, specific styles
Quick prototypingDistributing to different projects
Form elements that should match siteNeed style isolation/encapsulation

📊 Summary

FeatureDescription
Native Web ComponentsBuilt on Custom Elements standard
Reactive PropsObserved attributes become signals
Two Rendering ModesLight DOM (default) or Shadow DOM
Automatic CleanupEffects and listeners cleaned up on disconnect
Event SystemCustom events with emit()
Slot SupportFull slot API for content projection
Zero DependenciesPure vanilla JavaScript

Pro Tip: Start with Light DOM components for app-specific UI, and use Shadow DOM when building components that need to work identically across different projects or websites.

- - - - \ No newline at end of file diff --git a/docs/api/effects.html b/docs/api/effects.html deleted file mode 100644 index 50d7b2f..0000000 --- a/docs/api/effects.html +++ /dev/null @@ -1,787 +0,0 @@ - - - - - - Effects API 🔄 | SigPro - - - - - - - - - - - - - - -
Skip to content

Effects API 🔄

Effects are the bridge between reactive signals and side effects in your application. They automatically track signal dependencies and re-run whenever those signals change, enabling everything from DOM updates to data fetching and localStorage synchronization.

Core Concepts

What is an Effect?

An effect is a function that:

  • Runs immediately when created
  • Tracks all signals read during its execution
  • Re-runs automatically when any tracked signal changes
  • Can return a cleanup function that runs before the next execution or when the effect is stopped

How Effects Work

  1. When an effect runs, it sets itself as the activeEffect
  2. Any signal read during execution adds the effect to its subscribers
  3. When a signal changes, it queues all its subscribers
  4. Effects are batched and run in the next microtask
  5. If an effect returns a function, it's stored as a cleanup handler

$.effect(effectFn)

Creates a reactive effect that automatically tracks dependencies and re-runs when they change.

javascript
import { $ } from 'sigpro';
-
-const count = $(0);
-
-$.effect(() => {
-  console.log(`Count is: ${count()}`);
-});
-// Logs: "Count is: 0"
-
-count(1);
-// Logs: "Count is: 1"

📋 API Reference

PatternExampleDescription
Basic Effect$.effect(() => console.log(count()))Run on dependency changes
With Cleanup$.effect(() => { timer = setInterval(...); return () => clearInterval(timer) })Return cleanup function
Stop Effectconst stop = $.effect(...); stop()Manually stop an effect

Effect Object (Internal)

Property/MethodDescription
dependenciesSet of signal subscriber sets this effect belongs to
cleanupHandlersSet of cleanup functions to run before next execution
run()Executes the effect and tracks dependencies
stop()Stops the effect and runs all cleanup handlers

🎯 Basic Examples

Console Logging

javascript
import { $ } from 'sigpro';
-
-const name = $('World');
-const count = $(0);
-
-$.effect(() => {
-  console.log(`Hello ${name()}! Count is ${count()}`);
-});
-// Logs: "Hello World! Count is 0"
-
-name('John');
-// Logs: "Hello John! Count is 0"
-
-count(5);
-// Logs: "Hello John! Count is 5"

DOM Updates

javascript
import { $ } from 'sigpro';
-
-const count = $(0);
-const element = document.getElementById('counter');
-
-$.effect(() => {
-  element.textContent = `Count: ${count()}`;
-});
-
-// Updates DOM automatically when count changes
-count(10); // Element text becomes "Count: 10"

Document Title

javascript
import { $ } from 'sigpro';
-
-const page = $('home');
-const unreadCount = $(0);
-
-$.effect(() => {
-  const base = page() === 'home' ? 'Home' : 'Dashboard';
-  const unread = unreadCount() > 0 ? ` (${unreadCount()})` : '';
-  document.title = `${base}${unread} - My App`;
-});
-
-page('dashboard'); // Title: "Dashboard - My App"
-unreadCount(3);    // Title: "Dashboard (3) - My App"

🧹 Effects with Cleanup

Cleanup functions are essential for managing resources like intervals, event listeners, and subscriptions.

Basic Cleanup

javascript
import { $ } from 'sigpro';
-
-const userId = $(1);
-
-$.effect(() => {
-  const id = userId();
-  console.log(`Setting up timer for user ${id}`);
-  
-  const timer = setInterval(() => {
-    console.log(`Polling user ${id}...`);
-  }, 1000);
-  
-  // Cleanup runs before next effect execution
-  return () => {
-    console.log(`Cleaning up timer for user ${id}`);
-    clearInterval(timer);
-  };
-});
-// Sets up timer for user 1
-
-userId(2);
-// Cleans up timer for user 1
-// Sets up timer for user 2

Event Listener Cleanup

javascript
import { $ } from 'sigpro';
-
-const isListening = $(false);
-
-$.effect(() => {
-  if (!isListening()) return;
-  
-  const handleClick = (e) => {
-    console.log('Window clicked:', e.clientX, e.clientY);
-  };
-  
-  window.addEventListener('click', handleClick);
-  console.log('Click listener added');
-  
-  return () => {
-    window.removeEventListener('click', handleClick);
-    console.log('Click listener removed');
-  };
-});
-
-isListening(true);  // Adds listener
-isListening(false); // Removes listener
-isListening(true);  // Adds listener again

WebSocket Connection

javascript
import { $ } from 'sigpro';
-
-const room = $('general');
-const messages = $([]);
-
-$.effect(() => {
-  const currentRoom = room();
-  console.log(`Connecting to room: ${currentRoom}`);
-  
-  const ws = new WebSocket(`wss://chat.example.com/${currentRoom}`);
-  
-  ws.onmessage = (event) => {
-    messages([...messages(), JSON.parse(event.data)]);
-  };
-  
-  ws.onerror = (error) => {
-    console.error('WebSocket error:', error);
-  };
-  
-  // Cleanup: close connection when room changes
-  return () => {
-    console.log(`Disconnecting from room: ${currentRoom}`);
-    ws.close();
-  };
-});
-
-room('random'); // Closes 'general' connection, opens 'random'

⏱️ Effect Timing and Batching

Microtask Batching

Effects are batched using queueMicrotask for optimal performance:

javascript
import { $ } from 'sigpro';
-
-const a = $(1);
-const b = $(2);
-const c = $(3);
-
-$.effect(() => {
-  console.log('Effect ran with:', a(), b(), c());
-});
-// Logs immediately: "Effect ran with: 1 2 3"
-
-// Multiple updates in same tick - only one effect run!
-a(10);
-b(20);
-c(30);
-// Only logs once: "Effect ran with: 10 20 30"

Async Effects

Effects can be asynchronous, but be careful with dependency tracking:

javascript
import { $ } from 'sigpro';
-
-const userId = $(1);
-const userData = $(null);
-
-$.effect(() => {
-  const id = userId();
-  console.log(`Fetching user ${id}...`);
-  
-  // Only id() is tracked (synchronous part)
-  fetch(`/api/users/${id}`)
-    .then(res => res.json())
-    .then(data => {
-      // This runs later - no dependency tracking here!
-      userData(data);
-    });
-});
-
-userId(2); // Triggers effect again, cancels previous fetch

Effect with AbortController

For proper async cleanup with fetch:

javascript
import { $ } from 'sigpro';
-
-const userId = $(1);
-const userData = $(null);
-const loading = $(false);
-
-$.effect(() => {
-  const id = userId();
-  const controller = new AbortController();
-  
-  loading(true);
-  
-  fetch(`/api/users/${id}`, { signal: controller.signal })
-    .then(res => res.json())
-    .then(data => {
-      userData(data);
-      loading(false);
-    })
-    .catch(err => {
-      if (err.name !== 'AbortError') {
-        console.error('Fetch error:', err);
-        loading(false);
-      }
-    });
-  
-  // Cleanup: abort fetch if userId changes before completion
-  return () => {
-    controller.abort();
-  };
-});

🎨 Advanced Effect Patterns

Debounced Effects

javascript
import { $ } from 'sigpro';
-
-const searchTerm = $('');
-const results = $([]);
-let debounceTimeout;
-
-$.effect(() => {
-  const term = searchTerm();
-  
-  // Clear previous timeout
-  clearTimeout(debounceTimeout);
-  
-  // Don't search if term is too short
-  if (term.length < 3) {
-    results([]);
-    return;
-  }
-  
-  // Debounce search
-  debounceTimeout = setTimeout(async () => {
-    console.log('Searching for:', term);
-    const data = await fetch(`/api/search?q=${term}`).then(r => r.json());
-    results(data);
-  }, 300);
-  
-  // Cleanup on effect re-run
-  return () => clearTimeout(debounceTimeout);
-});

Throttled Effects

javascript
import { $ } from 'sigpro';
-
-const scrollPosition = $(0);
-let lastRun = 0;
-let rafId = null;
-
-$.effect(() => {
-  const pos = scrollPosition();
-  
-  // Throttle with requestAnimationFrame
-  if (rafId) cancelAnimationFrame(rafId);
-  
-  rafId = requestAnimationFrame(() => {
-    console.log('Scroll position:', pos);
-    updateScrollUI(pos);
-    lastRun = Date.now();
-    rafId = null;
-  });
-  
-  return () => {
-    if (rafId) {
-      cancelAnimationFrame(rafId);
-      rafId = null;
-    }
-  };
-});
-
-// Even with many updates, effect runs at most once per frame
-for (let i = 0; i < 100; i++) {
-  scrollPosition(i);
-}

Conditional Effects

javascript
import { $ } from 'sigpro';
-
-const isEnabled = $(false);
-const value = $(0);
-const threshold = $(10);
-
-$.effect(() => {
-  // Effect only runs when isEnabled is true
-  if (!isEnabled()) return;
-  
-  console.log(`Monitoring value: ${value()}, threshold: ${threshold()}`);
-  
-  if (value() > threshold()) {
-    alert(`Value ${value()} exceeded threshold ${threshold()}!`);
-  }
-});
-
-isEnabled(true);  // Effect starts monitoring
-value(15);        // Triggers alert
-isEnabled(false); // Effect stops (still runs, but condition prevents logic)

Effect with Multiple Cleanups

javascript
import { $ } from 'sigpro';
-
-const config = $({ theme: 'light', notifications: true });
-
-$.effect(() => {
-  const { theme, notifications } = config();
-  const cleanups = [];
-  
-  // Setup theme
-  document.body.className = `theme-${theme}`;
-  cleanups.push(() => {
-    document.body.classList.remove(`theme-${theme}`);
-  });
-  
-  // Setup notifications
-  if (notifications) {
-    const handler = (e) => console.log('Notification:', e.detail);
-    window.addEventListener('notification', handler);
-    cleanups.push(() => {
-      window.removeEventListener('notification', handler);
-    });
-  }
-  
-  // Return combined cleanup
-  return () => {
-    cleanups.forEach(cleanup => cleanup());
-  };
-});

🎯 Effects in Components

Component Lifecycle

javascript
import { $, html } from 'sigpro';
-
-$.component('timer-display', () => {
-  const seconds = $(0);
-  
-  // Effect for timer - automatically cleaned up when component unmounts
-  $.effect(() => {
-    const interval = setInterval(() => {
-      seconds(s => s + 1);
-    }, 1000);
-    
-    return () => clearInterval(interval);
-  });
-  
-  return html`
-    <div>
-      <h2>Timer: ${seconds}s</h2>
-    </div>
-  `;
-});

Effects with Props

javascript
import { $, html } from 'sigpro';
-
-$.component('data-viewer', (props) => {
-  const data = $(null);
-  const error = $(null);
-  
-  // Effect reacts to prop changes
-  $.effect(() => {
-    const url = props.url();
-    if (!url) return;
-    
-    const controller = new AbortController();
-    
-    fetch(url, { signal: controller.signal })
-      .then(res => res.json())
-      .then(data)
-      .catch(err => {
-        if (err.name !== 'AbortError') {
-          error(err.message);
-        }
-      });
-    
-    return () => controller.abort();
-  });
-  
-  return html`
-    <div>
-      ${() => {
-        if (error()) return html`<div class="error">${error()}</div>`;
-        if (!data()) return html`<div>Loading...</div>`;
-        return html`<pre>${JSON.stringify(data(), null, 2)}</pre>`;
-      }}
-    </div>
-  `;
-}, ['url']);

🔧 Effect Management

Stopping Effects

javascript
import { $ } from 'sigpro';
-
-const count = $(0);
-
-// Start effect
-const stopEffect = $.effect(() => {
-  console.log('Count:', count());
-});
-
-count(1); // Logs: "Count: 1"
-count(2); // Logs: "Count: 2"
-
-// Stop the effect
-stopEffect();
-
-count(3); // No logging - effect is stopped

Conditional Effect Stopping

javascript
import { $ } from 'sigpro';
-
-const isActive = $(true);
-const count = $(0);
-
-let currentEffect = null;
-
-$.effect(() => {
-  if (isActive()) {
-    // Start or restart the monitoring effect
-    if (currentEffect) currentEffect();
-    
-    currentEffect = $.effect(() => {
-      console.log('Monitoring count:', count());
-    });
-  } else {
-    // Stop monitoring
-    if (currentEffect) {
-      currentEffect();
-      currentEffect = null;
-    }
-  }
-});

Nested Effects

javascript
import { $ } from 'sigpro';
-
-const user = $({ id: 1, name: 'John' });
-const settings = $({ theme: 'dark' });
-
-$.effect(() => {
-  console.log('User changed:', user().name);
-  
-  // Nested effect - tracks settings independently
-  $.effect(() => {
-    console.log('Settings changed:', settings().theme);
-  });
-  
-  // When user changes, the nested effect is recreated
-});

🚀 Real-World Examples

Auto-saving Form

javascript
import { $ } from 'sigpro';
-
-const formData = $({
-  title: '',
-  content: '',
-  tags: []
-});
-
-const lastSaved = $(null);
-const saveStatus = $('idle'); // 'idle', 'saving', 'saved', 'error'
-let saveTimeout;
-
-$.effect(() => {
-  const data = formData();
-  
-  // Clear previous timeout
-  clearTimeout(saveTimeout);
-  
-  // Don't save empty form
-  if (!data.title && !data.content) {
-    saveStatus('idle');
-    return;
-  }
-  
-  saveStatus('saving');
-  
-  // Debounce save
-  saveTimeout = setTimeout(async () => {
-    try {
-      await fetch('/api/posts', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify(data)
-      });
-      saveStatus('saved');
-      lastSaved(new Date());
-    } catch (error) {
-      saveStatus('error');
-      console.error('Auto-save failed:', error);
-    }
-  }, 1000);
-  
-  return () => clearTimeout(saveTimeout);
-});
-
-// UI feedback
-const statusMessage = $(() => {
-  const status = saveStatus();
-  const saved = lastSaved();
-  
-  if (status === 'saving') return 'Saving...';
-  if (status === 'error') return 'Save failed';
-  if (status === 'saved' && saved) {
-    return `Last saved: ${saved().toLocaleTimeString()}`;
-  }
-  return '';
-});

Real-time Search with Debounce

javascript
import { $ } from 'sigpro';
-
-const searchInput = $('');
-const searchResults = $([]);
-const searchStatus = $('idle'); // 'idle', 'searching', 'results', 'no-results', 'error'
-let searchTimeout;
-let abortController = null;
-
-$.effect(() => {
-  const query = searchInput().trim();
-  
-  // Clear previous timeout
-  clearTimeout(searchTimeout);
-  
-  // Cancel previous request
-  if (abortController) {
-    abortController.abort();
-    abortController = null;
-  }
-  
-  // Don't search for short queries
-  if (query.length < 2) {
-    searchResults([]);
-    searchStatus('idle');
-    return;
-  }
-  
-  searchStatus('searching');
-  
-  // Debounce search
-  searchTimeout = setTimeout(async () => {
-    abortController = new AbortController();
-    
-    try {
-      const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
-        signal: abortController.signal
-      });
-      
-      const data = await response.json();
-      
-      if (!abortController.signal.aborted) {
-        searchResults(data);
-        searchStatus(data.length ? 'results' : 'no-results');
-        abortController = null;
-      }
-    } catch (error) {
-      if (error.name !== 'AbortError') {
-        console.error('Search failed:', error);
-        searchStatus('error');
-      }
-    }
-  }, 300);
-  
-  return () => {
-    clearTimeout(searchTimeout);
-    if (abortController) {
-      abortController.abort();
-      abortController = null;
-    }
-  };
-});

Analytics Tracking

javascript
import { $ } from 'sigpro';
-
-// Analytics configuration
-const analyticsEnabled = $(true);
-const currentPage = $('/');
-const userProperties = $({});
-
-// Track page views
-$.effect(() => {
-  if (!analyticsEnabled()) return;
-  
-  const page = currentPage();
-  const properties = userProperties();
-  
-  console.log('Track page view:', page, properties);
-  
-  // Send to analytics
-  gtag('config', 'GA-MEASUREMENT-ID', {
-    page_path: page,
-    ...properties
-  });
-});
-
-// Track user interactions
-const trackEvent = (eventName, properties = {}) => {
-  $.effect(() => {
-    if (!analyticsEnabled()) return;
-    
-    console.log('Track event:', eventName, properties);
-    gtag('event', eventName, properties);
-  });
-};
-
-// Usage
-currentPage('/dashboard');
-userProperties({ userId: 123, plan: 'premium' });
-trackEvent('button_click', { buttonId: 'signup' });

Keyboard Shortcuts

javascript
import { $ } from 'sigpro';
-
-const shortcuts = $({
-  'ctrl+s': { handler: null, description: 'Save' },
-  'ctrl+z': { handler: null, description: 'Undo' },
-  'ctrl+shift+z': { handler: null, description: 'Redo' },
-  'escape': { handler: null, description: 'Close modal' }
-});
-
-const pressedKeys = new Set();
-
-$.effect(() => {
-  const handleKeyDown = (e) => {
-    const key = e.key.toLowerCase();
-    const ctrl = e.ctrlKey ? 'ctrl+' : '';
-    const shift = e.shiftKey ? 'shift+' : '';
-    const alt = e.altKey ? 'alt+' : '';
-    const meta = e.metaKey ? 'meta+' : '';
-    
-    const combo = `${ctrl}${shift}${alt}${meta}${key}`.replace(/\+$/, '');
-    
-    const shortcut = shortcuts()[combo];
-    if (shortcut?.handler) {
-      e.preventDefault();
-      shortcut.handler();
-    }
-  };
-  
-  window.addEventListener('keydown', handleKeyDown);
-  
-  return () => window.removeEventListener('keydown', handleKeyDown);
-});
-
-// Register shortcuts
-shortcuts({
-  ...shortcuts(),
-  'ctrl+s': {
-    handler: () => saveDocument(),
-    description: 'Save document'
-  },
-  'ctrl+z': {
-    handler: () => undo(),
-    description: 'Undo'
-  }
-});

Infinite Scroll

javascript
import { $ } from 'sigpro';
-
-const posts = $([]);
-const page = $(1);
-const hasMore = $(true);
-const loading = $(false);
-let observer = null;
-
-// Load more posts
-const loadMore = async () => {
-  if (loading() || !hasMore()) return;
-  
-  loading(true);
-  try {
-    const response = await fetch(`/api/posts?page=${page()}`);
-    const newPosts = await response.json();
-    
-    if (newPosts.length === 0) {
-      hasMore(false);
-    } else {
-      posts([...posts(), ...newPosts]);
-      page(p => p + 1);
-    }
-  } finally {
-    loading(false);
-  }
-};
-
-// Setup intersection observer for infinite scroll
-$.effect(() => {
-  const sentinel = document.getElementById('sentinel');
-  if (!sentinel) return;
-  
-  observer = new IntersectionObserver(
-    (entries) => {
-      if (entries[0].isIntersecting && !loading() && hasMore()) {
-        loadMore();
-      }
-    },
-    { threshold: 0.1 }
-  );
-  
-  observer.observe(sentinel);
-  
-  return () => {
-    if (observer) {
-      observer.disconnect();
-      observer = null;
-    }
-  };
-});
-
-// Initial load
-loadMore();

📊 Performance Considerations

PatternPerformance ImpactBest Practice
Multiple signal readsO(n) per effectGroup related signals
Deep object accessMinimalUse computed signals
Large arraysO(n) for iterationMemoize with computed
Frequent updatesBatchedLet batching work
Heavy computationsBlockingUse Web Workers

🎯 Best Practices

1. Keep Effects Focused

javascript
// ❌ Avoid doing too much in one effect
-$.effect(() => {
-  updateUI(count());           // UI update
-  saveToStorage(count());      // Storage
-  sendAnalytics(count());      // Analytics
-  validate(count());           // Validation
-});
-
-// ✅ Split into focused effects
-$.effect(() => updateUI(count()));
-$.effect(() => saveToStorage(count()));
-$.effect(() => sendAnalytics(count()));
-$.effect(() => validate(count()));

2. Always Clean Up

javascript
// ❌ Missing cleanup
-$.effect(() => {
-  const timer = setInterval(() => {}, 1000);
-  // Memory leak!
-});
-
-// ✅ Proper cleanup
-$.effect(() => {
-  const timer = setInterval(() => {}, 1000);
-  return () => clearInterval(timer);
-});

3. Avoid Writing to Signals in Effects

javascript
import { $ } from 'sigpro';
-
-const a = $(1);
-const b = $(2);
-
-// ❌ Avoid - can cause loops
-$.effect(() => {
-  a(b()); // Writing to a while reading b
-});
-
-// ✅ Use computed signals instead
-const sum = $(() => a() + b());

4. Use Conditional Logic Carefully

javascript
// ❌ Condition affects dependency tracking
-$.effect(() => {
-  if (condition()) {
-    console.log(a()); // Only tracks a when condition is true
-  }
-});
-
-// ✅ Track all dependencies explicitly
-$.effect(() => {
-  const cond = condition(); // Track condition
-  if (cond) {
-    console.log(a()); // Track a
-  }
-});

5. Memoize Expensive Computations

javascript
import { $ } from 'sigpro';
-
-const items = $([]);
-
-// ❌ Expensive computation runs on every effect
-$.effect(() => {
-  const total = items().reduce((sum, i) => sum + i.price, 0);
-  updateTotal(total);
-});
-
-// ✅ Memoize with computed signal
-const total = $(() => items().reduce((sum, i) => sum + i.price, 0));
-$.effect(() => updateTotal(total()));

🔍 Debugging Effects

Logging Effect Runs

javascript
import { $ } from 'sigpro';
-
-const withLogging = (effectFn, name) => {
-  return $.effect(() => {
-    console.log(`[${name}] Running...`);
-    const start = performance.now();
-    
-    const result = effectFn();
-    
-    const duration = performance.now() - start;
-    console.log(`[${name}] Completed in ${duration.toFixed(2)}ms`);
-    
-    return result;
-  });
-};
-
-// Usage
-withLogging(() => {
-  console.log('Count:', count());
-}, 'count-effect');

Effect Inspector

javascript
import { $ } from 'sigpro';
-
-const createEffectInspector = () => {
-  const effects = new Map();
-  let id = 0;
-  
-  const trackedEffect = (fn, name = `effect-${++id}`) => {
-    const info = {
-      name,
-      runs: 0,
-      lastRun: null,
-      duration: 0,
-      dependencies: new Set()
-    };
-    
-    const wrapped = () => {
-      info.runs++;
-      info.lastRun = new Date();
-      const start = performance.now();
-      
-      const result = fn();
-      
-      info.duration = performance.now() - start;
-      return result;
-    };
-    
-    const stop = $.effect(wrapped);
-    effects.set(stop, info);
-    
-    return stop;
-  };
-  
-  const getReport = () => {
-    const report = {};
-    effects.forEach((info, stop) => {
-      report[info.name] = {
-        runs: info.runs,
-        lastRun: info.lastRun,
-        avgDuration: info.duration / info.runs
-      };
-    });
-    return report;
-  };
-  
-  return { trackedEffect, getReport };
-};
-
-// Usage
-const inspector = createEffectInspector();
-inspector.trackedEffect(() => {
-  console.log('Count:', count());
-}, 'counter-effect');

📊 Summary

FeatureDescription
Automatic TrackingDependencies tracked automatically
Cleanup FunctionsReturn function to clean up resources
Batch UpdatesMultiple changes batched in microtask
Manual StopCan stop effects with returned function
Nested EffectsEffects can contain other effects
Auto-cleanupEffects in pages/components auto-cleaned

Pro Tip: Effects are the perfect place for side effects like DOM updates, data fetching, and subscriptions. Keep them focused and always clean up resources!

- - - - \ No newline at end of file diff --git a/docs/api/fetch.html b/docs/api/fetch.html deleted file mode 100644 index 5436a6c..0000000 --- a/docs/api/fetch.html +++ /dev/null @@ -1,873 +0,0 @@ - - - - - - Fetch API 🌐 | SigPro - - - - - - - - - - - - - - -
Skip to content

Fetch API 🌐

SigPro provides a simple, lightweight wrapper around the native Fetch API that integrates seamlessly with signals for loading state management. It's designed for common use cases with sensible defaults.

Core Concepts

What is $.fetch?

A ultra-simple fetch wrapper that:

  • Automatically handles JSON serialization and parsing
  • Integrates with signals for loading state
  • Returns null on error (no try/catch needed for basic usage)
  • Works great with effects for reactive data fetching

$.fetch(url, data, [loading])

Makes a POST request with JSON data and optional loading signal.

javascript
import { $ } from 'sigpro';
-
-const loading = $(false);
-
-async function loadUser() {
-  const user = await $.fetch('/api/user', { id: 123 }, loading);
-  if (user) {
-    console.log('User loaded:', user);
-  }
-}

📋 API Reference

Parameters

ParameterTypeDescription
urlstringEndpoint URL
dataObjectData to send (automatically JSON.stringify'd)
loadingFunction (optional)Signal function to track loading state

Returns

ReturnDescription
Promise<Object|null>Parsed JSON response or null on error

🎯 Basic Examples

Simple Data Fetching

javascript
import { $ } from 'sigpro';
-
-const userData = $(null);
-
-async function fetchUser(id) {
-  const data = await $.fetch('/api/user', { id });
-  if (data) {
-    userData(data);
-  }
-}
-
-fetchUser(123);

With Loading State

javascript
import { $, html } from 'sigpro';
-
-const user = $(null);
-const loading = $(false);
-
-async function loadUser(id) {
-  const data = await $.fetch('/api/user', { id }, loading);
-  if (data) user(data);
-}
-
-// In your template
-html`
-  <div>
-    ${() => loading() ? html`
-      <div class="spinner">Loading...</div>
-    ` : user() ? html`
-      <div>
-        <h2>${user().name}</h2>
-        <p>Email: ${user().email}</p>
-      </div>
-    ` : html`
-      <p>No user found</p>
-    `}
-  </div>
-`;

In an Effect

javascript
import { $ } from 'sigpro';
-
-const userId = $(1);
-const user = $(null);
-const loading = $(false);
-
-$.effect(() => {
-  const id = userId();
-  if (id) {
-    $.fetch(`/api/users/${id}`, null, loading).then(data => {
-      if (data) user(data);
-    });
-  }
-});
-
-userId(2); // Automatically fetches new user

🚀 Advanced Examples

User Profile with Loading States

javascript
import { $, html } from 'sigpro';
-
-const Profile = () => {
-  const userId = $(1);
-  const user = $(null);
-  const loading = $(false);
-  const error = $(null);
-
-  const fetchUser = async (id) => {
-    error(null);
-    const data = await $.fetch('/api/user', { id }, loading);
-    if (data) {
-      user(data);
-    } else {
-      error('Failed to load user');
-    }
-  };
-
-  // Fetch when userId changes
-  $.effect(() => {
-    fetchUser(userId());
-  });
-
-  return html`
-    <div class="profile">
-      <div class="user-selector">
-        <button @click=${() => userId(1)}>User 1</button>
-        <button @click=${() => userId(2)}>User 2</button>
-        <button @click=${() => userId(3)}>User 3</button>
-      </div>
-
-      ${() => {
-        if (loading()) {
-          return html`<div class="spinner">Loading profile...</div>`;
-        }
-        
-        if (error()) {
-          return html`<div class="error">${error()}</div>`;
-        }
-        
-        if (user()) {
-          return html`
-            <div class="user-info">
-              <h2>${user().name}</h2>
-              <p>Email: ${user().email}</p>
-              <p>Role: ${user().role}</p>
-              <p>Joined: ${new Date(user().joined).toLocaleDateString()}</p>
-            </div>
-          `;
-        }
-        
-        return html`<p>Select a user</p>`;
-      }}
-    </div>
-  `;
-};

Todo List with API

javascript
import { $, html } from 'sigpro';
-
-const TodoApp = () => {
-  const todos = $([]);
-  const loading = $(false);
-  const newTodo = $('');
-  const filter = $('all'); // 'all', 'active', 'completed'
-
-  // Load todos
-  const loadTodos = async () => {
-    const data = await $.fetch('/api/todos', {}, loading);
-    if (data) todos(data);
-  };
-
-  // Add todo
-  const addTodo = async () => {
-    if (!newTodo().trim()) return;
-    
-    const todo = await $.fetch('/api/todos', {
-      text: newTodo(),
-      completed: false
-    });
-    
-    if (todo) {
-      todos([...todos(), todo]);
-      newTodo('');
-    }
-  };
-
-  // Toggle todo
-  const toggleTodo = async (id, completed) => {
-    const updated = await $.fetch(`/api/todos/${id}`, {
-      completed: !completed
-    });
-    
-    if (updated) {
-      todos(todos().map(t => 
-        t.id === id ? updated : t
-      ));
-    }
-  };
-
-  // Delete todo
-  const deleteTodo = async (id) => {
-    const result = await $.fetch(`/api/todos/${id}/delete`, {});
-    if (result) {
-      todos(todos().filter(t => t.id !== id));
-    }
-  };
-
-  // Filtered todos
-  const filteredTodos = $(() => {
-    const currentFilter = filter();
-    if (currentFilter === 'all') return todos();
-    if (currentFilter === 'active') {
-      return todos().filter(t => !t.completed);
-    }
-    return todos().filter(t => t.completed);
-  });
-
-  // Load on mount
-  loadTodos();
-
-  return html`
-    <div class="todo-app">
-      <h1>Todo List</h1>
-      
-      <div class="add-todo">
-        <input
-          type="text"
-          :value=${newTodo}
-          @keydown.enter=${addTodo}
-          placeholder="Add a new todo..."
-        />
-        <button @click=${addTodo}>Add</button>
-      </div>
-      
-      <div class="filters">
-        <button 
-          class:active=${() => filter() === 'all'}
-          @click=${() => filter('all')}
-        >
-          All
-        </button>
-        <button 
-          class:active=${() => filter() === 'active'}
-          @click=${() => filter('active')}
-        >
-          Active
-        </button>
-        <button 
-          class:active=${() => filter() === 'completed'}
-          @click=${() => filter('completed')}
-        >
-          Completed
-        </button>
-      </div>
-      
-      ${() => loading() ? html`
-        <div class="spinner">Loading todos...</div>
-      ) : html`
-        <ul class="todo-list">
-          ${filteredTodos().map(todo => html`
-            <li class="todo-item">
-              <input
-                type="checkbox"
-                :checked=${todo.completed}
-                @change=${() => toggleTodo(todo.id, todo.completed)}
-              />
-              <span class:completed=${todo.completed}>${todo.text}</span>
-              <button @click=${() => deleteTodo(todo.id)}>🗑️</button>
-            </li>
-          `)}
-        </ul>
-      `}
-    </div>
-  `;
-};

Infinite Scroll with Pagination

javascript
import { $, html } from 'sigpro';
-
-const InfiniteScroll = () => {
-  const posts = $([]);
-  const page = $(1);
-  const loading = $(false);
-  const hasMore = $(true);
-  const error = $(null);
-
-  const loadMore = async () => {
-    if (loading() || !hasMore()) return;
-    
-    const data = await $.fetch('/api/posts', { 
-      page: page(),
-      limit: 10 
-    }, loading);
-    
-    if (data) {
-      if (data.posts.length === 0) {
-        hasMore(false);
-      } else {
-        posts([...posts(), ...data.posts]);
-        page(p => p + 1);
-      }
-    } else {
-      error('Failed to load posts');
-    }
-  };
-
-  // Intersection Observer for infinite scroll
-  $.effect(() => {
-    const observer = new IntersectionObserver(
-      (entries) => {
-        if (entries[0].isIntersecting) {
-          loadMore();
-        }
-      },
-      { threshold: 0.1 }
-    );
-    
-    const sentinel = document.getElementById('sentinel');
-    if (sentinel) observer.observe(sentinel);
-    
-    return () => observer.disconnect();
-  });
-
-  // Initial load
-  loadMore();
-
-  return html`
-    <div class="infinite-scroll">
-      <h1>Posts</h1>
-      
-      <div class="posts">
-        ${posts().map(post => html`
-          <article class="post">
-            <h2>${post.title}</h2>
-            <p>${post.body}</p>
-            <small>By ${post.author}</small>
-          </article>
-        `)}
-      </div>
-      
-      <div id="sentinel" class="sentinel">
-        ${() => {
-          if (loading()) {
-            return html`<div class="spinner">Loading more...</div>`;
-          }
-          if (error()) {
-            return html`<div class="error">${error()}</div>`;
-          }
-          if (!hasMore()) {
-            return html`<div class="end">No more posts</div>`;
-          }
-          return '';
-        }}
-      </div>
-    </div>
-  `;
-};

Search with Debounce

javascript
import { $, html } from 'sigpro';
-
-const SearchComponent = () => {
-  const query = $('');
-  const results = $([]);
-  const loading = $(false);
-  const error = $(null);
-  let searchTimeout;
-
-  const performSearch = async (searchQuery) => {
-    if (!searchQuery.trim()) {
-      results([]);
-      return;
-    }
-    
-    const data = await $.fetch('/api/search', { 
-      q: searchQuery 
-    }, loading);
-    
-    if (data) {
-      results(data);
-    } else {
-      error('Search failed');
-    }
-  };
-
-  // Debounced search
-  $.effect(() => {
-    const searchQuery = query();
-    
-    clearTimeout(searchTimeout);
-    
-    if (searchQuery.length < 2) {
-      results([]);
-      return;
-    }
-    
-    searchTimeout = setTimeout(() => {
-      performSearch(searchQuery);
-    }, 300);
-    
-    return () => clearTimeout(searchTimeout);
-  });
-
-  return html`
-    <div class="search">
-      <div class="search-box">
-        <input
-          type="search"
-          :value=${query}
-          placeholder="Search..."
-          class="search-input"
-        />
-        ${() => loading() ? html`
-          <span class="spinner-small">⌛</span>
-        ) : ''}
-      </div>
-      
-      ${() => {
-        if (error()) {
-          return html`<div class="error">${error()}</div>`;
-        }
-        
-        if (results().length > 0) {
-          return html`
-            <ul class="results">
-              ${results().map(item => html`
-                <li class="result-item">
-                  <h3>${item.title}</h3>
-                  <p>${item.description}</p>
-                </li>
-              `)}
-            </ul>
-          `;
-        }
-        
-        if (query().length >= 2 && !loading()) {
-          return html`<p class="no-results">No results found</p>`;
-        }
-        
-        return '';
-      }}
-    </div>
-  `;
-};

Form Submission

javascript
import { $, html } from 'sigpro';
-
-const ContactForm = () => {
-  const formData = $({
-    name: '',
-    email: '',
-    message: ''
-  });
-  
-  const submitting = $(false);
-  const submitError = $(null);
-  const submitSuccess = $(false);
-
-  const handleSubmit = async (e) => {
-    e.preventDefault();
-    
-    submitError(null);
-    submitSuccess(false);
-    
-    const result = await $.fetch('/api/contact', formData(), submitting);
-    
-    if (result) {
-      submitSuccess(true);
-      formData({ name: '', email: '', message: '' });
-    } else {
-      submitError('Failed to send message. Please try again.');
-    }
-  };
-
-  const updateField = (field, value) => {
-    formData({
-      ...formData(),
-      [field]: value
-    });
-  };
-
-  return html`
-    <form class="contact-form" @submit=${handleSubmit}>
-      <h2>Contact Us</h2>
-      
-      <div class="form-group">
-        <label for="name">Name:</label>
-        <input
-          type="text"
-          id="name"
-          :value=${() => formData().name}
-          @input=${(e) => updateField('name', e.target.value)}
-          required
-          ?disabled=${submitting}
-        />
-      </div>
-      
-      <div class="form-group">
-        <label for="email">Email:</label>
-        <input
-          type="email"
-          id="email"
-          :value=${() => formData().email}
-          @input=${(e) => updateField('email', e.target.value)}
-          required
-          ?disabled=${submitting}
-        />
-      </div>
-      
-      <div class="form-group">
-        <label for="message">Message:</label>
-        <textarea
-          id="message"
-          :value=${() => formData().message}
-          @input=${(e) => updateField('message', e.target.value)}
-          required
-          rows="5"
-          ?disabled=${submitting}
-        ></textarea>
-      </div>
-      
-      ${() => {
-        if (submitting()) {
-          return html`<div class="submitting">Sending...</div>`;
-        }
-        
-        if (submitError()) {
-          return html`<div class="error">${submitError()}</div>`;
-        }
-        
-        if (submitSuccess()) {
-          return html`<div class="success">Message sent successfully!</div>`;
-        }
-        
-        return '';
-      }}
-      
-      <button 
-        type="submit" 
-        ?disabled=${submitting}
-      >
-        Send Message
-      </button>
-    </form>
-  `;
-};

Real-time Dashboard with Multiple Endpoints

javascript
import { $, html } from 'sigpro';
-
-const Dashboard = () => {
-  // Multiple data streams
-  const metrics = $({});
-  const alerts = $([]);
-  const logs = $([]);
-  
-  const loading = $({
-    metrics: false,
-    alerts: false,
-    logs: false
-  });
-
-  const refreshInterval = $(5000); // 5 seconds
-
-  const fetchMetrics = async () => {
-    const data = await $.fetch('/api/metrics', {}, loading().metrics);
-    if (data) metrics(data);
-  };
-
-  const fetchAlerts = async () => {
-    const data = await $.fetch('/api/alerts', {}, loading().alerts);
-    if (data) alerts(data);
-  };
-
-  const fetchLogs = async () => {
-    const data = await $.fetch('/api/logs', { 
-      limit: 50 
-    }, loading().logs);
-    if (data) logs(data);
-  };
-
-  // Auto-refresh all data
-  $.effect(() => {
-    fetchMetrics();
-    fetchAlerts();
-    fetchLogs();
-    
-    const interval = setInterval(() => {
-      fetchMetrics();
-      fetchAlerts();
-    }, refreshInterval());
-    
-    return () => clearInterval(interval);
-  });
-
-  return html`
-    <div class="dashboard">
-      <header>
-        <h1>System Dashboard</h1>
-        <div class="refresh-control">
-          <label>
-            Refresh interval:
-            <select :value=${refreshInterval} @change=${(e) => refreshInterval(parseInt(e.target.value))}>
-              <option value="2000">2 seconds</option>
-              <option value="5000">5 seconds</option>
-              <option value="10000">10 seconds</option>
-              <option value="30000">30 seconds</option>
-            </select>
-          </label>
-        </div>
-      </header>
-      
-      <div class="dashboard-grid">
-        <!-- Metrics Panel -->
-        <div class="panel metrics">
-          <h2>System Metrics</h2>
-          ${() => loading().metrics ? html`
-            <div class="spinner">Loading metrics...</div>
-          ) : html`
-            <div class="metrics-grid">
-              <div class="metric">
-                <label>CPU</label>
-                <span>${metrics().cpu || 0}%</span>
-              </div>
-              <div class="metric">
-                <label>Memory</label>
-                <span>${metrics().memory || 0}%</span>
-              </div>
-              <div class="metric">
-                <label>Requests</label>
-                <span>${metrics().requests || 0}/s</span>
-              </div>
-            </div>
-          `}
-        </div>
-        
-        <!-- Alerts Panel -->
-        <div class="panel alerts">
-          <h2>Active Alerts</h2>
-          ${() => loading().alerts ? html`
-            <div class="spinner">Loading alerts...</div>
-          ) : alerts().length > 0 ? html`
-            <ul>
-              ${alerts().map(alert => html`
-                <li class="alert ${alert.severity}">
-                  <strong>${alert.type}</strong>
-                  <p>${alert.message}</p>
-                  <small>${new Date(alert.timestamp).toLocaleTimeString()}</small>
-                </li>
-              `)}
-            </ul>
-          ) : html`
-            <p class="no-data">No active alerts</p>
-          `}
-        </div>
-        
-        <!-- Logs Panel -->
-        <div class="panel logs">
-          <h2>Recent Logs</h2>
-          ${() => loading().logs ? html`
-            <div class="spinner">Loading logs...</div>
-          ) : html`
-            <ul>
-              ${logs().map(log => html`
-                <li class="log ${log.level}">
-                  <span class="timestamp">${new Date(log.timestamp).toLocaleTimeString()}</span>
-                  <span class="message">${log.message}</span>
-                </li>
-              `)}
-            </ul>
-          `}
-        </div>
-      </div>
-    </div>
-  `;
-};

File Upload

javascript
import { $, html } from 'sigpro';
-
-const FileUploader = () => {
-  const files = $([]);
-  const uploading = $(false);
-  const uploadProgress = $({});
-  const uploadResults = $([]);
-
-  const handleFileSelect = (e) => {
-    files([...e.target.files]);
-  };
-
-  const uploadFiles = async () => {
-    if (files().length === 0) return;
-    
-    uploading(true);
-    uploadResults([]);
-    
-    for (const file of files()) {
-      const formData = new FormData();
-      formData.append('file', file);
-      
-      // Track progress for this file
-      uploadProgress({
-        ...uploadProgress(),
-        [file.name]: 0
-      });
-      
-      try {
-        // Custom fetch for FormData
-        const response = await fetch('/api/upload', {
-          method: 'POST',
-          body: formData
-        });
-        
-        const result = await response.json();
-        
-        uploadResults([
-          ...uploadResults(),
-          { file: file.name, success: true, result }
-        ]);
-      } catch (error) {
-        uploadResults([
-          ...uploadResults(),
-          { file: file.name, success: false, error: error.message }
-        ]);
-      }
-      
-      uploadProgress({
-        ...uploadProgress(),
-        [file.name]: 100
-      });
-    }
-    
-    uploading(false);
-  };
-
-  return html`
-    <div class="file-uploader">
-      <h2>Upload Files</h2>
-      
-      <input
-        type="file"
-        multiple
-        @change=${handleFileSelect}
-        ?disabled=${uploading}
-      />
-      
-      ${() => files().length > 0 ? html`
-        <div class="file-list">
-          <h3>Selected Files:</h3>
-          <ul>
-            ${files().map(file => html`
-              <li>
-                ${file.name} (${(file.size / 1024).toFixed(2)} KB)
-                ${() => uploadProgress()[file.name] ? html`
-                  <progress value="${uploadProgress()[file.name]}" max="100"></progress>
-                ) : ''}
-              </li>
-            `)}
-          </ul>
-          
-          <button 
-            @click=${uploadFiles}
-            ?disabled=${uploading}
-          >
-            ${() => uploading() ? 'Uploading...' : 'Upload Files'}
-          </button>
-        </div>
-      ` : ''}
-      
-      ${() => uploadResults().length > 0 ? html`
-        <div class="upload-results">
-          <h3>Upload Results:</h3>
-          <ul>
-            ${uploadResults().map(result => html`
-              <li class="${result.success ? 'success' : 'error'}">
-                ${result.file}: 
-                ${result.success ? 'Uploaded successfully' : `Failed: ${result.error}`}
-              </li>
-            `)}
-          </ul>
-        </div>
-      ` : ''}
-    </div>
-  `;
-};

Retry Logic

javascript
import { $ } from 'sigpro';
-
-// Enhanced fetch with retry
-const fetchWithRetry = async (url, data, loading, maxRetries = 3) => {
-  let lastError;
-  
-  for (let attempt = 1; attempt <= maxRetries; attempt++) {
-    try {
-      if (loading) loading(true);
-      
-      const result = await $.fetch(url, data);
-      if (result !== null) {
-        return result;
-      }
-      
-      // If we get null but no error, wait and retry
-      if (attempt < maxRetries) {
-        await new Promise(resolve => 
-          setTimeout(resolve, Math.pow(2, attempt) * 1000) // Exponential backoff
-        );
-      }
-    } catch (error) {
-      lastError = error;
-      console.warn(`Attempt ${attempt} failed:`, error);
-      
-      if (attempt < maxRetries) {
-        await new Promise(resolve => 
-          setTimeout(resolve, Math.pow(2, attempt) * 1000)
-        );
-      }
-    } finally {
-      if (attempt === maxRetries && loading) {
-        loading(false);
-      }
-    }
-  }
-  
-  console.error('All retry attempts failed:', lastError);
-  return null;
-};
-
-// Usage
-const loading = $(false);
-const data = await fetchWithRetry('/api/unreliable-endpoint', {}, loading, 5);

🎯 Best Practices

1. Always Handle Null Responses

javascript
// ❌ Don't assume success
-const data = await $.fetch('/api/data');
-console.log(data.property); // Might throw if data is null
-
-// ✅ Check for null
-const data = await $.fetch('/api/data');
-if (data) {
-  console.log(data.property);
-} else {
-  showError('Failed to load data');
-}

2. Use with Effects for Reactivity

javascript
// ❌ Manual fetching
-button.addEventListener('click', async () => {
-  const data = await $.fetch('/api/data');
-  updateUI(data);
-});
-
-// ✅ Reactive fetching
-const trigger = $(false);
-
-$.effect(() => {
-  if (trigger()) {
-    $.fetch('/api/data').then(data => {
-      if (data) updateUI(data);
-    });
-  }
-});
-
-trigger(true); // Triggers fetch

3. Combine with Loading Signals

javascript
// ✅ Always show loading state
-const loading = $(false);
-const data = $(null);
-
-async function load() {
-  const result = await $.fetch('/api/data', {}, loading);
-  if (result) data(result);
-}
-
-// In template
-html`
-  <div>
-    ${() => loading() ? '<Spinner />' : 
-      data() ? '<Data />' : 
-      '<Empty />'}
-  </div>
-`;

4. Cancel In-flight Requests

javascript
// ✅ Use AbortController with effects
-let controller;
-
-$.effect(() => {
-  if (controller) {
-    controller.abort();
-  }
-  
-  controller = new AbortController();
-  
-  fetch(url, { signal: controller.signal })
-    .then(res => res.json())
-    .then(data => {
-      if (!controller.signal.aborted) {
-        updateData(data);
-      }
-    });
-  
-  return () => controller.abort();
-});

📊 Error Handling

Basic Error Handling

javascript
const data = await $.fetch('/api/data');
-if (!data) {
-  // Handle error (show message, retry, etc.)
-}

With Error Signal

javascript
const data = $(null);
-const error = $(null);
-const loading = $(false);
-
-async function loadData() {
-  error(null);
-  const result = await $.fetch('/api/data', {}, loading);
-  
-  if (result) {
-    data(result);
-  } else {
-    error('Failed to load data');
-  }
-}

Pro Tip: Combine $.fetch with $.effect and loading signals for a complete reactive data fetching solution. The loading signal integration makes it trivial to show loading states in your UI.

- - - - \ No newline at end of file diff --git a/docs/api/html.html b/docs/api/html.html new file mode 100644 index 0000000..7a61d9a --- /dev/null +++ b/docs/api/html.html @@ -0,0 +1,46 @@ + + + + + + Rendering Engine: $.html | SigPro + + + + + + + + + + + + + + +
Skip to content

Rendering Engine: $.html

The $.html function is the architect of your UI. It creates standard HTML elements and wires them directly to your signals without the need for a Virtual DOM.

1. Syntax: $.html(tag, [props], [content])

ParameterTypeRequiredDescription
tagstringYesAny valid HTML5 tag (e.g., 'div', 'button', 'input').
propsObjectNoAttributes, event listeners, and reactive bindings.
contentanyNoText, Nodes, Arrays, or Reactive Functions.

Example:

javascript
const myButton = $.html('button', { class: 'btn-primary' }, 'Click me');

2. Global Tag Helpers

To avoid repetitive $.html calls, SigPro automatically exposes common tags to the global window object. This allows for a clean, declarative syntax.

javascript
// Instead of $.html('div', ...), just use:
+div({ id: 'wrapper' }, [
+  h1("Welcome"),
+  p("This is SigPro.")
+]);

3. Handling Properties & Attributes

SigPro distinguishes between static attributes and reactive bindings using the $ prefix.

Static vs. Reactive Attributes

  • Static: Applied once during creation.
  • Reactive ($): Automatically updates the DOM when the signal changes.
PropertySyntaxResult
Attribute{ id: 'main' }id="main"
Event{ onclick: fn }Adds an event listener.
Reactive Attr{ $class: $theme }Updates class whenever $theme() changes.
Boolean Attr{ $disabled: $isBusy }Toggles the disabled attribute automatically.

4. Two-Way Data Binding

For form inputs, SigPro provides a powerful shortcut using $value or $checked. It automatically handles the event listening and the value synchronization.

javascript
const $text = $("Type here...");
+
+input({ 
+  type: 'text', 
+  $value: $text // Syncs input -> signal and signal -> input
+});
+
+p(["You typed: ", $text]);

5. Reactive Content (Dynamic Children)

The content argument is incredibly flexible. If you pass a function, SigPro treats it as a reactive "portal" that re-renders only that specific part of the DOM.

Text & Nodes

javascript
const $count = $(0);
+
+// Text node updates surgically
+div(["Count: ", $count]); 
+
+// Conditional rendering with a function
+div(() => {
+  return $count() > 10 
+    ? h1("High Score!") 
+    : p("Keep going...");
+});

The "Guillotine" (Performance Tip)

When a reactive function in the content returns a new Node, SigPro uses replaceWith() to swap the old node for the new one. This ensures that:

  1. The update is nearly instantaneous.
  2. The old node is correctly garbage-collected.

6. Summary: Content Types

InputBehavior
String / NumberAppended as a TextNode.
HTMLElementAppended directly to the parent.
ArrayEach item is processed and appended in order.
Function () => ...Creates a live reactive zone that updates automatically.
+ + + + \ No newline at end of file diff --git a/docs/api/mount.html b/docs/api/mount.html new file mode 100644 index 0000000..c30d41c --- /dev/null +++ b/docs/api/mount.html @@ -0,0 +1,52 @@ + + + + + + Application Mounter: $.mount | SigPro + + + + + + + + + + + + + + +
Skip to content

Application Mounter: $.mount

The $.mount function is the entry point of your reactive world. It takes a SigPro component (or a plain DOM node) and injects it into the real document.

1. Syntax: $.mount(node, [target])

ParameterTypeDefaultDescription
nodeHTMLElement or FunctionRequiredThe component or element to render.
targetstring or HTMLElementdocument.bodyWhere to mount the app (CSS selector or Element).

2. Usage Scenarios

A. The "Clean Slate" (Main Entry)

In a modern app (like our main.js example), you usually want to control the entire page. By default, $.mount clears the target's existing HTML before mounting.

javascript
// src/main.js
+import { $ } from 'SigPro';
+import App from './App.js';
+
+$.mount(App); // Mounts to <body> by default

B. Targeting a Specific Container

If you have an existing HTML structure and only want SigPro to manage a specific part (like a #root div), pass a CSS selector or a reference.

html
<div id="sidebar"></div>
+<div id="app-root"></div>
javascript
// Local mount to a specific ID
+$.mount(MyComponent, '#app-root');
+
+// Or using a direct DOM reference
+const sidebar = document.getElementById('sidebar');
+$.mount(SidebarComponent, sidebar);

3. Mounting with Pure HTML

One of SigPro's strengths is that it works perfectly alongside "Old School" HTML. You can create a reactive "island" inside a static page.

javascript
// A small reactive widget in a static .js file
+const CounterWidget = () => {
+  const $c = $(0);
+  return button({ onclick: () => $c(v => v + 1) }, [
+    "Clicks: ", $c
+  ]);
+};
+
+// Mount it into an existing div in your HTML
+$.mount(CounterWidget, '#counter-container');

4. How it Works (The "Wipe" Logic)

When $.mount is called, it performs two critical steps:

  1. Clearance: It sets target.innerHTML = ''. This ensures no "zombie" HTML from previous renders or static placeholders interferes with your app.
  2. Injection: It appends your component. If you passed a Function, it executes it first to get the DOM node.

5. Global vs. Local Scope

Global (The "Framework" Way)

In a standard Vite/ESM project, you initialize SigPro globally in main.js. This makes the $ and the tag helpers (div, button, etc.) available everywhere in your project.

javascript
// main.js - Global Initialization
+import 'SigPro'; 
+
+// Now any other file can just use:
+$.mount(() => h1("Global App"));

Local (The "Library" Way)

If you are worried about polluting the global window object, you can import and use SigPro locally within a specific module.

javascript
// widget.js - Local usage
+import { $ } from 'SigPro';
+
+const myNode = $.html('div', 'Local Widget');
+$.mount(myNode, '#widget-target');

Summary Cheat Sheet

GoalCode
Mount to body$.mount(App)
Mount to ID$.mount(App, '#id')
Mount to Element$.mount(App, myElement)
Reactive Widget$.mount(() => div("Hi"), '#widget')
+ + + + \ No newline at end of file diff --git a/docs/api/pages.html b/docs/api/pages.html deleted file mode 100644 index 8f95432..0000000 --- a/docs/api/pages.html +++ /dev/null @@ -1,405 +0,0 @@ - - - - - - Pages API 📄 | SigPro - - - - - - - - - - - - - - -
Skip to content

Pages API 📄

Pages in SigPro are special components designed for route-based navigation with automatic cleanup. When you navigate away from a page, all signals, effects, and event listeners created within that page are automatically cleaned up - no memory leaks, no manual cleanup needed.

$.page(setupFunction)

Creates a page with automatic cleanup of all signals and effects when navigated away.

javascript
import { $, html } from 'sigpro';
-
-export default $.page(() => {
-  // All signals and effects created here
-  // will be automatically cleaned up on navigation
-  const count = $(0);
-  
-  $.effect(() => {
-    console.log(`Count: ${count()}`);
-  });
-  
-  return html`
-    <div>
-      <h1>My Page</h1>
-      <p>Count: ${count}</p>
-      <button @click=${() => count(c => c + 1)}>+</button>
-    </div>
-  `;
-});

📋 API Reference

ParameterTypeDescription
setupFunctionFunctionFunction that returns the page content. Receives context object with params and onUnmount

Context Object Properties

PropertyTypeDescription
paramsObjectRoute parameters passed to the page
onUnmountFunctionRegister cleanup callbacks (alternative to automatic cleanup)

🎯 Basic Usage

Simple Page

javascript
// pages/home.js
-import { $, html } from 'sigpro';
-
-export default $.page(() => {
-  const title = $('Welcome to SigPro');
-  
-  return html`
-    <div class="home-page">
-      <h1>${title}</h1>
-      <p>This page will clean itself up when you navigate away.</p>
-    </div>
-  `;
-});

Page with Route Parameters

javascript
// pages/user.js
-import { $, html } from 'sigpro';
-
-export default $.page(({ params }) => {
-  // Access route parameters
-  const userId = params.id;
-  const userData = $(null);
-  const loading = $(false);
-  
-  // Auto-cleaned effect
-  $.effect(() => {
-    loading(true);
-    $.fetch(`/api/users/${userId}`, null, loading)
-      .then(data => userData(data));
-  });
-  
-  return html`
-    <div>
-      ${() => loading() ? html`
-        <div class="spinner">Loading...</div>
-      ` : html`
-        <h1>User Profile: ${userData()?.name}</h1>
-        <p>Email: ${userData()?.email}</p>
-      `}
-    </div>
-  `;
-});

🧹 Automatic Cleanup

The magic of $.page is automatic cleanup. Everything created inside the page is tracked and cleaned up:

javascript
export default $.page(() => {
-  // ✅ Signals are auto-cleaned
-  const count = $(0);
-  const user = $(null);
-  
-  // ✅ Effects are auto-cleaned
-  $.effect(() => {
-    document.title = `Count: ${count()}`;
-  });
-  
-  // ✅ Event listeners are auto-cleaned
-  window.addEventListener('resize', handleResize);
-  
-  // ✅ Intervals and timeouts are auto-cleaned
-  const interval = setInterval(() => {
-    refreshData();
-  }, 5000);
-  
-  return html`<div>Page content</div>`;
-});
-// When navigating away: all signals, effects, listeners, intervals STOP

📝 Manual Cleanup with onUnmount

Sometimes you need custom cleanup logic. Use onUnmount for that:

javascript
export default $.page(({ onUnmount }) => {
-  // WebSocket connection
-  const socket = new WebSocket('wss://api.example.com');
-  
-  socket.onmessage = (event) => {
-    updateData(JSON.parse(event.data));
-  };
-  
-  // Manual cleanup
-  onUnmount(() => {
-    socket.close();
-    console.log('WebSocket closed');
-  });
-  
-  return html`<div>Real-time updates</div>`;
-});

🔄 Integration with Router

Pages are designed to work seamlessly with $.router:

javascript
import { $, html } from 'sigpro';
-import HomePage from './pages/Home.js';
-import UserPage from './pages/User.js';
-import SettingsPage from './pages/Settings.js';
-
-const routes = [
-  { path: '/', component: HomePage },
-  { path: '/user/:id', component: UserPage },
-  { path: '/settings', component: SettingsPage },
-];
-
-// Mount router
-document.body.appendChild($.router(routes));

💡 Practical Examples

Example 1: Data Fetching Page

javascript
// pages/posts.js
-export default $.page(({ params }) => {
-  const posts = $([]);
-  const loading = $(true);
-  const error = $(null);
-  
-  $.effect(() => {
-    fetch('/api/posts')
-      .then(res => res.json())
-      .then(data => {
-        posts(data);
-        loading(false);
-      })
-      .catch(err => {
-        error(err.message);
-        loading(false);
-      });
-  });
-  
-  return html`
-    <div class="posts-page">
-      <h1>Blog Posts</h1>
-      
-      ${() => loading() ? html`
-        <div class="loading">Loading posts...</div>
-      ` : error() ? html`
-        <div class="error">Error: ${error()}</div>
-      ` : html`
-        <div class="posts-grid">
-          ${posts().map(post => html`
-            <article class="post-card">
-              <h2>${post.title}</h2>
-              <p>${post.excerpt}</p>
-              <a href="#/post/${post.id}">Read more</a>
-            </article>
-          `)}
-        </div>
-      `}
-    </div>
-  `;
-});

Example 2: Real-time Dashboard

javascript
// pages/dashboard.js
-export default $.page(({ onUnmount }) => {
-  const metrics = $({
-    cpu: 0,
-    memory: 0,
-    requests: 0
-  });
-  
-  // Auto-refresh data
-  const refreshInterval = setInterval(async () => {
-    const data = await $.fetch('/api/metrics');
-    if (data) metrics(data);
-  }, 5000);
-  
-  // Manual cleanup for interval
-  onUnmount(() => clearInterval(refreshInterval));
-  
-  // Live clock
-  const currentTime = $(new Date());
-  const clockInterval = setInterval(() => {
-    currentTime(new Date());
-  }, 1000);
-  
-  onUnmount(() => clearInterval(clockInterval));
-  
-  return html`
-    <div class="dashboard">
-      <h1>System Dashboard</h1>
-      
-      <div class="time">
-        Last updated: ${() => currentTime().toLocaleTimeString()}
-      </div>
-      
-      <div class="metrics-grid">
-        <div class="metric-card">
-          <h3>CPU Usage</h3>
-          <p class="metric-value">${() => metrics().cpu}%</p>
-        </div>
-        <div class="metric-card">
-          <h3>Memory Usage</h3>
-          <p class="metric-value">${() => metrics().memory}%</p>
-        </div>
-        <div class="metric-card">
-          <h3>Requests/min</h3>
-          <p class="metric-value">${() => metrics().requests}</p>
-        </div>
-      </div>
-    </div>
-  `;
-});

Example 3: Multi-step Form

javascript
// pages/checkout.js
-export default $.page(({ onUnmount }) => {
-  const step = $(1);
-  const formData = $({
-    email: '',
-    address: '',
-    payment: ''
-  });
-  
-  // Warn user before leaving
-  const handleBeforeUnload = (e) => {
-    if (step() < 3) {
-      e.preventDefault();
-      e.returnValue = '';
-    }
-  };
-  
-  window.addEventListener('beforeunload', handleBeforeUnload);
-  onUnmount(() => {
-    window.removeEventListener('beforeunload', handleBeforeUnload);
-  });
-  
-  const nextStep = () => step(s => Math.min(s + 1, 3));
-  const prevStep = () => step(s => Math.max(s - 1, 1));
-  
-  return html`
-    <div class="checkout">
-      <h1>Checkout - Step ${step} of 3</h1>
-      
-      ${() => {
-        switch(step()) {
-          case 1:
-            return html`
-              <div class="step">
-                <h2>Email</h2>
-                <input 
-                  type="email" 
-                  :value=${() => formData().email}
-                  @input=${(e) => formData({...formData(), email: e.target.value})}
-                />
-              </div>
-            `;
-          case 2:
-            return html`
-              <div class="step">
-                <h2>Address</h2>
-                <textarea 
-                  :value=${() => formData().address}
-                  @input=${(e) => formData({...formData(), address: e.target.value})}
-                ></textarea>
-              </div>
-            `;
-          case 3:
-            return html`
-              <div class="step">
-                <h2>Payment</h2>
-                <input 
-                  type="text" 
-                  placeholder="Card number"
-                  :value=${() => formData().payment}
-                  @input=${(e) => formData({...formData(), payment: e.target.value})}
-                />
-              </div>
-            `;
-        }
-      }}
-      
-      <div class="buttons">
-        ${() => step() > 1 ? html`
-          <button @click=${prevStep}>Previous</button>
-        ` : ''}
-        
-        ${() => step() < 3 ? html`
-          <button @click=${nextStep}>Next</button>
-        ` : html`
-          <button @click=${submitOrder}>Place Order</button>
-        `}
-      </div>
-    </div>
-  `;
-});

Example 4: Page with Tabs

javascript
// pages/profile.js
-export default $.page(({ params }) => {
-  const activeTab = $('overview');
-  const userData = $(null);
-  
-  // Load user data
-  $.effect(() => {
-    $.fetch(`/api/users/${params.id}`)
-      .then(data => userData(data));
-  });
-  
-  const tabs = {
-    overview: () => html`
-      <div>
-        <h3>Overview</h3>
-        <p>Username: ${userData()?.username}</p>
-        <p>Member since: ${userData()?.joined}</p>
-      </div>
-    `,
-    posts: () => html`
-      <div>
-        <h3>Posts</h3>
-        ${userData()?.posts.map(post => html`
-          <div class="post">${post.title}</div>
-        `)}
-      </div>
-    `,
-    settings: () => html`
-      <div>
-        <h3>Settings</h3>
-        <label>
-          <input type="checkbox" :checked=${userData()?.emailNotifications} />
-          Email notifications
-        </label>
-      </div>
-    `
-  };
-  
-  return html`
-    <div class="profile-page">
-      <h1>${() => userData()?.name}</h1>
-      
-      <div class="tabs">
-        ${Object.keys(tabs).map(tab => html`
-          <button 
-            class:active=${() => activeTab() === tab}
-            @click=${() => activeTab(tab)}
-          >
-            ${tab.charAt(0).toUpperCase() + tab.slice(1)}
-          </button>
-        `)}
-      </div>
-      
-      <div class="tab-content">
-        ${() => tabs[activeTab()]()}
-      </div>
-    </div>
-  `;
-});

🎯 Advanced Patterns

Page with Nested Routes

javascript
// pages/settings/index.js
-export default $.page(({ params }) => {
-  const section = params.section || 'general';
-  
-  const sections = {
-    general: () => import('./general.js').then(m => m.default),
-    security: () => import('./security.js').then(m => m.default),
-    notifications: () => import('./notifications.js').then(m => m.default)
-  };
-  
-  const currentSection = $(null);
-  
-  $.effect(() => {
-    sections[section]().then(comp => currentSection(comp));
-  });
-  
-  return html`
-    <div class="settings">
-      <nav>
-        <a href="#/settings/general">General</a>
-        <a href="#/settings/security">Security</a>
-        <a href="#/settings/notifications">Notifications</a>
-      </nav>
-      
-      <div class="content">
-        ${currentSection}
-      </div>
-    </div>
-  `;
-});

Page with Authentication

javascript
// pages/dashboard.js
-export default $.page(({ onUnmount }) => {
-  const isAuthenticated = $(false);
-  const authCheck = $.effect(() => {
-    const token = localStorage.getItem('token');
-    isAuthenticated(!!token);
-  });
-  
-  // Redirect if not authenticated
-  $.effect(() => {
-    if (!isAuthenticated()) {
-      $.router.go('/login');
-    }
-  });
-  
-  return html`
-    <div class="dashboard">
-      <h1>Protected Dashboard</h1>
-      <!-- Protected content -->
-    </div>
-  `;
-});

📊 Summary

FeatureDescription
Automatic CleanupAll signals, effects, and resources auto-cleaned on navigation
Memory SafeNo memory leaks, even with complex nested effects
Router IntegrationDesigned to work perfectly with $.router
ParametersAccess route parameters via params object
Manual CleanuponUnmount for custom cleanup needs
Zero ConfigurationJust wrap your page in $.page() and it works

Pro Tip: Always wrap route-based views in $.page() to ensure proper cleanup. This prevents memory leaks and ensures your app stays performant even after many navigation changes.

- - - - \ No newline at end of file diff --git a/docs/api/quick.html b/docs/api/quick.html index bd4880f..bfbd9bd 100644 --- a/docs/api/quick.html +++ b/docs/api/quick.html @@ -6,212 +6,26 @@ Quick API Reference ⚡ | SigPro - + - + -
Skip to content

Quick API Reference ⚡

A comprehensive reference for all SigPro APIs. Everything you need to build reactive web applications with signals and web components.

📋 API Functions Reference

FunctionDescriptionExample
$(initialValue)Creates a reactive signal (getter/setter)const count = $(0)
$(computedFn)Creates a computed signalconst full = $(() => first() + last())
$.effect(fn)Runs effect when dependencies change$.effect(() => console.log(count()))
$.page(setupFn)Creates a page with automatic cleanup$.page(() => html
Page
)
$.component(tagName, setupFn, attrs, useShadow)Creates reactive Web Component$.component('my-menu', setup, ['items'])
$.router(routes)Creates a hash-based router$.router([{path:'/', component:Home}])
$.router.go(path)Navigates to a route$.router.go('/user/42')
$.fetch(url, data, loadingSignal)Fetch wrapper with loading stateconst data = await $.fetch('/api', data, loading)
$.storage(key, initialValue, storageType)Persistent signal (local/sessionStorage)const theme = $.storage('theme', 'light')
html`...`Template literal for reactive HTMLhtml`<div>${count}</div>`

Signal Methods

MethodDescriptionExample
signal()Gets current valuecount()
signal(newValue)Sets new valuecount(5)
signal(prev => new)Updates using previous valuecount(c => c + 1)

Component Context Properties

PropertyDescriptionExample
propsReactive component propertiesprops.title()
slot(name)Accesses slot contentslot() or slot('footer')
emit(event, data)Dispatches custom eventemit('update', value)
onUnmount(cb)Registers cleanup callbackonUnmount(() => clearInterval(timer))

Page Context Properties

PropertyDescriptionExample
paramsRoute parametersparams.id, params.slug
onUnmount(cb)Registers cleanup callbackonUnmount(() => clearInterval(timer))

HTML Directives

DirectiveDescriptionExample
@eventEvent listener@click=${handler}
:propertyTwo-way binding:value=${signal}
?attributeBoolean attribute?disabled=${signal}
.propertyDOM property binding.scrollTop=${value}
class:nameConditional classclass:active=${isActive}

📡 Signals - $(initialValue)

Creates a reactive value that notifies dependents when changed.

PatternExampleDescription
Basic Signalconst count = $(0)Create signal with initial value
Gettercount()Read current value
Settercount(5)Set new value directly
Updatercount(prev => prev + 1)Update based on previous value
Computedconst full = $(() => first() + last())Auto-updating derived signal

Examples

javascript
// Basic signal
-const count = $(0);
-console.log(count()); // 0
-count(5);
-count(c => c + 1); // 6
-
-// Computed signal
-const firstName = $('John');
-const lastName = $('Doe');
-const fullName = $(() => `${firstName()} ${lastName()}`);
-console.log(fullName()); // "John Doe"
-firstName('Jane'); // fullName auto-updates to "Jane Doe"

🔄 Effects - $.effect(fn)

Executes a function and automatically re-runs when its dependencies change.

PatternExampleDescription
Basic Effect$.effect(() => console.log(count()))Run effect on dependency changes
Cleanup$.effect(() => { timer = setInterval(...); return () => clearInterval(timer) })Return cleanup function
Stop Effectconst stop = $.effect(...); stop()Manually stop an effect

Examples

javascript
// Auto-running effect
-const count = $(0);
-$.effect(() => {
-  console.log(`Count is: ${count()}`);
-}); // Logs immediately and whenever count changes
-
-// Effect with cleanup
-const userId = $(1);
-$.effect(() => {
-  const id = userId();
-  const timer = setInterval(() => fetchUser(id), 5000);
-  return () => clearInterval(timer); // Cleanup before re-run
-});

📄 Pages - $.page(setupFunction)

Creates a page with automatic cleanup of all signals and effects when navigated away.

javascript
// pages/about.js
-import { $, html } from 'sigpro';
-
-export default $.page(() => {
-  const count = $(0);
-  
-  // Auto-cleaned on navigation
-  $.effect(() => {
-    document.title = `Count: ${count()}`;
-  });
-  
-  return html`
-    <div>
-      <h1>About Page</h1>
-      <p>Count: ${count}</p>
-      <button @click=${() => count(c => c + 1)}>+</button>
-    </div>
-  `;
-});

With Parameters

javascript
export default $.page(({ params, onUnmount }) => {
-  const userId = params.id;
-  
-  // Manual cleanup if needed
-  const interval = setInterval(() => refresh(), 10000);
-  onUnmount(() => clearInterval(interval));
-  
-  return html`<div>User: ${userId}</div>`;
-});

🧩 Components - $.component(tagName, setup, observedAttributes, useShadowDOM)

Creates Custom Elements with reactive properties.

Parameters

ParameterTypeDefaultDescription
tagNamestringrequiredCustom element tag (must include hyphen)
setupFunctionFunctionrequiredFunction that renders the component
observedAttributesstring[][]Attributes to observe for changes
useShadowDOMbooleanfalsetrue = Shadow DOM (encapsulated), false = Light DOM

Light DOM Example (Default)

javascript
// button.js - inherits global styles
-$.component('my-button', (props, { slot, emit }) => {
-  return html`
-    <button 
-      class="px-4 py-2 bg-blue-500 text-white rounded"
-      @click=${() => emit('click')}
-    >
-      ${slot()}
-    </button>
-  `;
-}, ['variant']); // Observe 'variant' attribute

Shadow DOM Example

javascript
// calendar.js - encapsulated styles
-$.component('my-calendar', (props) => {
-  return html`
-    <style>
-      /* These styles are isolated */
-      .calendar {
-        background: white;
-        border-radius: 8px;
-        box-shadow: 0 2px 10px rgba(0,0,0,0.1);
-      }
-    </style>
-    <div class="calendar">
-      ${renderCalendar(props.date())}
-    </div>
-  `;
-}, ['date'], true); // true = use Shadow DOM

🌐 Router - $.router(routes)

Creates a hash-based router with automatic page cleanup.

Route Definition

javascript
const routes = [
-  // Simple routes
-  { path: '/', component: HomePage },
-  { path: '/about', component: AboutPage },
-  
-  // Routes with parameters
-  { path: '/user/:id', component: UserPage },
-  { path: '/user/:id/posts/:pid', component: PostPage },
-  
-  // RegExp routes for advanced matching
-  { path: /^\/posts\/(?<id>\d+)$/, component: PostPage },
-];

Usage

javascript
import { $, html } from 'sigpro';
-import Home from './pages/Home.js';
-import User from './pages/User.js';
-
-const router = $.router([
-  { path: '/', component: Home },
-  { path: '/user/:id', component: User },
-]);
-
-// Navigation
-$.router.go('/user/42');
-$.router.go('about'); // Same as '/about'
-
-// In templates
-html`
-  <nav>
-    <a href="#/">Home</a>
-    <a href="#/user/42">Profile</a>
-    <button @click=${() => $.router.go('/contact')}>
-      Contact
-    </button>
-  </nav>
-`;

📦 Storage - $.storage(key, initialValue, [storage])

Persistent signal that syncs with localStorage or sessionStorage.

javascript
// localStorage (default)
-const theme = $.storage('theme', 'light');
-const user = $.storage('user', null);
-const settings = $.storage('settings', { notifications: true });
-
-// sessionStorage
-const tempData = $.storage('temp', {}, sessionStorage);
-
-// Usage like a normal signal
-theme('dark'); // Auto-saves to localStorage
-console.log(theme()); // 'dark' (even after page refresh)

🌐 Fetch - $.fetch(url, data, [loading])

Simple fetch wrapper with automatic JSON handling.

javascript
const loading = $(false);
-
-async function loadUser(id) {
-  const user = await $.fetch(`/api/users/${id}`, null, loading);
-  if (user) userData(user);
-}
-
-// In template
-html`
-  <div>
-    ${() => loading() ? html`<spinner></spinner>` : html`
-      <p>${userData()?.name}</p>
-    `}
-  </div>
-`;

🎨 Template Literals - html`...`

Creates reactive DOM fragments with directives.

Directives Reference

DirectiveExampleDescription
Event@click=${handler}Add event listener
Two-way binding:value=${signal}Bind signal to input value
Boolean attribute?disabled=${signal}Toggle boolean attribute
Property.scrollTop=${value}Set DOM property directly
Class toggleclass:active=${isActive}Toggle class conditionally

Examples

javascript
const text = $('');
-const isDisabled = $(false);
-const activeTab = $('home');
-
-html`
-  <!-- Event binding -->
-  <button @click=${() => count(c => c + 1)}>+</button>
-  
-  <!-- Two-way binding -->
-  <input :value=${text} />
-  <p>You typed: ${text}</p>
-  
-  <!-- Boolean attributes -->
-  <button ?disabled=${isDisabled}>Submit</button>
-  
-  <!-- Class toggles -->
-  <div class:active=${activeTab() === 'home'}>
-    Home content
-  </div>
-  
-  <!-- Property binding -->
-  <div .scrollTop=${scrollPosition}></div>
-`;

🎯 Complete Component Example

javascript
import { $, html } from 'sigpro';
-
-// Create a component
-$.component('user-profile', (props, { slot, emit }) => {
-  // Reactive state
-  const user = $(null);
-  const loading = $(false);
-  
-  // Load user data when userId changes
-  $.effect(() => {
-    const id = props.userId();
-    if (id) {
-      loading(true);
-      $.fetch(`/api/users/${id}`, null, loading)
-        .then(data => user(data));
-    }
-  });
-  
-  // Computed value
-  const fullName = $(() => 
-    user() ? `${user().firstName} ${user().lastName}` : ''
-  );
-  
-  // Template
-  return html`
-    <div class="user-profile">
-      ${() => loading() ? html`
-        <div class="spinner">Loading...</div>
-      ` : user() ? html`
-        <h2>${fullName}</h2>
-        <p>Email: ${user().email}</p>
-        <button @click=${() => emit('select', user())}>
-          ${slot('Select')}
-        </button>
-      ` : html`
-        <p>User not found</p>
-      `}
-    </div>
-  `;
-}, ['user-id']); // Observe userId attribute
- +
Skip to content

Quick API Reference ⚡

This is a high-level summary of the SigPro core API. For detailed guides and edge cases, please refer to the specific documentation for each module.

1. Core Reactivity: $( )

The $ function is a polymorphic constructor. It creates Signals (state) or Computed Effects (logic) based on the input type.

UsageInput TypeReturnsDescription
SignalanyFunctionA getter/setter for reactive state.
ComputedFunctionFunctionA read-only signal that auto-updates when its dependencies change.

Example:

javascript
const $count = $(0);             // Signal
+const $double = $(() => $count() * 2); // Computed

2. Rendering Engine: $.html

SigPro uses a hyperscript-style engine to create live DOM nodes.

ArgumentTypeRequiredDescription
tagstringYesStandard HTML tag (e.g., 'div', 'button').
propsObjectNoAttributes (id), Events (onclick), or Reactive Props ($value).
contentanyNoString, Node, Array, or Reactive Function.

Example:

javascript
$.html('button', { onclick: () => alert('Hi!') }, 'Click Me');

3. Global Helpers (Tag Proxies)

To keep your code clean, SigPro automatically exposes common HTML tags to the global scope.

CategoryAvailable Tags
Layoutdiv, section, main, nav, header, footer, span
Typographyh1, h2, h3, p, label, a, li, ul, ol
Formsinput, button, form, select, option
Mediaimg, video, audio, canvas

Example:

javascript
// No imports needed!
+div([ 
+  h1("Title"), 
+  button("Ok") 
+]);

4. Mounting & Plugins

Methods to initialize your application and extend the engine.

MethodSignatureDescription
$.mount(node, target)Wipes the target (default: body) and renders the component.
$.plugin(source)Registers a function or loads external .js scripts as plugins.

Example:

javascript
$.plugin([UI, Router]);
+$.mount(App, '#root');

5. Reactive Syntax Cheat Sheet

FeatureSyntaxDescription
Text Bindingp(["Value: ", $sig])Updates text content automatically.
Attributesdiv({ id: $sig })Static attribute assignment.
Reactive Attrdiv({ $class: $sig })Attribute updates when $sig changes.
Two-way Bindinginput({ $value: $sig })Syncs input value and signal automatically.
Conditionaldiv(() => $sig() > 0 ? "Yes" : "No")Re-renders only the content when the condition changes.

Summary Table

FeatureSigPro ApproachBenefit
Update LogicFine-grained (Surgical)Blazing fast updates.
DOMNative NodesZero abstraction cost.
SyntaxPure JavaScriptNo build-tool lock-in.
FootprintModularLoad only what you use.
+ \ No newline at end of file diff --git a/docs/api/routing.html b/docs/api/routing.html deleted file mode 100644 index 5fce056..0000000 --- a/docs/api/routing.html +++ /dev/null @@ -1,628 +0,0 @@ - - - - - - Routing API 🌐 | SigPro - - - - - - - - - - - - - - -
Skip to content

Routing API 🌐

SigPro includes a simple yet powerful hash-based router designed for Single Page Applications (SPAs). It works everywhere with zero server configuration and integrates seamlessly with $.page for automatic cleanup.

Why Hash-Based Routing?

Hash routing (#/about) works everywhere - no server configuration needed. Perfect for:

  • Static sites and SPAs
  • GitHub Pages, Netlify, any static hosting
  • Local development without a server
  • Projects that need to work immediately

$.router(routes)

Creates a hash-based router that renders the matching component and handles navigation.

javascript
import { $, html } from 'sigpro';
-import HomePage from './pages/Home.js';
-import AboutPage from './pages/About.js';
-import UserPage from './pages/User.js';
-
-const routes = [
-  { path: '/', component: HomePage },
-  { path: '/about', component: AboutPage },
-  { path: '/user/:id', component: UserPage },
-];
-
-// Mount the router
-document.body.appendChild($.router(routes));

📋 API Reference

$.router(routes)

ParameterTypeDescription
routesArray<Route>Array of route configurations

Returns: HTMLDivElement - Container that renders the current page

$.router.go(path)

ParameterTypeDescription
pathstringRoute path to navigate to (automatically adds leading slash)

Route Object

PropertyTypeDescription
pathstring or RegExpRoute pattern to match
componentFunctionFunction that returns page content (receives params)

🎯 Route Patterns

String Paths (Simple Routes)

javascript
const routes = [
-  // Static routes
-  { path: '/', component: HomePage },
-  { path: '/about', component: AboutPage },
-  { path: '/contact', component: ContactPage },
-  
-  // Routes with parameters
-  { path: '/user/:id', component: UserPage },
-  { path: '/user/:id/posts', component: UserPostsPage },
-  { path: '/user/:id/posts/:postId', component: PostPage },
-  { path: '/search/:query/page/:num', component: SearchPage },
-];

RegExp Paths (Advanced Routing)

javascript
const routes = [
-  // Match numeric IDs only
-  { path: /^\/users\/(?<id>\d+)$/, component: UserPage },
-  
-  // Match product slugs (letters, numbers, hyphens)
-  { path: /^\/products\/(?<slug>[a-z0-9-]+)$/, component: ProductPage },
-  
-  // Match blog posts by year/month
-  { path: /^\/blog\/(?<year>\d{4})\/(?<month>\d{2})$/, component: BlogArchive },
-  
-  // Match optional language prefix
-  { path: /^\/(?<lang>en|es|fr)?\/?about$/, component: AboutPage },
-  
-  // Match UUID format
-  { path: /^\/items\/(?<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/, 
-    component: ItemPage },
-];

📦 Basic Examples

Simple Router Setup

javascript
// main.js
-import { $, html } from 'sigpro';
-import Home from './pages/Home.js';
-import About from './pages/About.js';
-import Contact from './pages/Contact.js';
-
-const routes = [
-  { path: '/', component: Home },
-  { path: '/about', component: About },
-  { path: '/contact', component: Contact },
-];
-
-const router = $.router(routes);
-
-// Mount to DOM
-document.body.appendChild(router);

Page Components with Parameters

javascript
// pages/User.js
-import { $, html } from 'sigpro';
-
-export default (params) => $.page(() => {
-  // /user/42 → params = { id: '42' }
-  // /user/john/posts/123 → params = { id: 'john', postId: '123' }
-  const userId = params.id;
-  const userData = $(null);
-  
-  $.effect(() => {
-    fetch(`/api/users/${userId}`)
-      .then(res => res.json())
-      .then(data => userData(data));
-  });
-  
-  return html`
-    <div class="user-page">
-      <h1>User Profile: ${userId}</h1>
-      ${() => userData() ? html`
-        <p>Name: ${userData().name}</p>
-        <p>Email: ${userData().email}</p>
-      ` : html`<p>Loading...</p>`}
-    </div>
-  `;
-});
javascript
import { $, html } from 'sigpro';
-
-// In templates
-const NavBar = () => html`
-  <nav>
-    <a href="#/">Home</a>
-    <a href="#/about">About</a>
-    <a href="#/contact">Contact</a>
-    <a href="#/user/42">Profile</a>
-    <a href="#/search/js/page/1">Search</a>
-    
-    <!-- Programmatic navigation -->
-    <button @click=${() => $.router.go('/about')}>
-      Go to About
-    </button>
-    
-    <button @click=${() => $.router.go('contact')}>
-      Go to Contact (auto-adds leading slash)
-    </button>
-  </nav>
-`;

🚀 Advanced Examples

Complete Application with Layout

javascript
// App.js
-import { $, html } from 'sigpro';
-import HomePage from './pages/Home.js';
-import AboutPage from './pages/About.js';
-import UserPage from './pages/User.js';
-import SettingsPage from './pages/Settings.js';
-import NotFound from './pages/NotFound.js';
-
-// Layout component with navigation
-const Layout = (content) => html`
-  <div class="app">
-    <header class="header">
-      <h1>My SigPro App</h1>
-      <nav class="nav">
-        <a href="#/" class:active=${() => isActive('/')}>Home</a>
-        <a href="#/about" class:active=${() => isActive('/about')}>About</a>
-        <a href="#/user/42" class:active=${() => isActive('/user/42')}>Profile</a>
-        <a href="#/settings" class:active=${() => isActive('/settings')}>Settings</a>
-      </nav>
-    </header>
-    
-    <main class="main">
-      ${content}
-    </main>
-    
-    <footer class="footer">
-      <p>© 2024 SigPro App</p>
-    </footer>
-  </div>
-`;
-
-// Helper to check active route
-const isActive = (path) => {
-  const current = window.location.hash.replace(/^#/, '') || '/';
-  return current === path;
-};
-
-// Routes with layout
-const routes = [
-  { path: '/', component: (params) => Layout(HomePage(params)) },
-  { path: '/about', component: (params) => Layout(AboutPage(params)) },
-  { path: '/user/:id', component: (params) => Layout(UserPage(params)) },
-  { path: '/settings', component: (params) => Layout(SettingsPage(params)) },
-  { path: '/:path(.*)', component: (params) => Layout(NotFound(params)) }, // Catch-all
-];
-
-// Create and mount router
-const router = $.router(routes);
-document.body.appendChild(router);

Nested Routes

javascript
// pages/Settings.js (parent route)
-import { $, html } from 'sigpro';
-import SettingsGeneral from './settings/General.js';
-import SettingsSecurity from './settings/Security.js';
-import SettingsNotifications from './settings/Notifications.js';
-
-export default (params) => $.page(() => {
-  const section = params.section || 'general';
-  
-  const sections = {
-    general: SettingsGeneral,
-    security: SettingsSecurity,
-    notifications: SettingsNotifications
-  };
-  
-  const CurrentSection = sections[section];
-  
-  return html`
-    <div class="settings">
-      <h1>Settings</h1>
-      
-      <div class="settings-layout">
-        <nav class="settings-sidebar">
-          <a href="#/settings/general" class:active=${() => section === 'general'}>
-            General
-          </a>
-          <a href="#/settings/security" class:active=${() => section === 'security'}>
-            Security
-          </a>
-          <a href="#/settings/notifications" class:active=${() => section === 'notifications'}>
-            Notifications
-          </a>
-        </nav>
-        
-        <div class="settings-content">
-          ${CurrentSection(params)}
-        </div>
-      </div>
-    </div>
-  `;
-});
-
-// pages/settings/General.js
-export default (params) => $.page(() => {
-  return html`
-    <div>
-      <h2>General Settings</h2>
-      <form>...</form>
-    </div>
-  `;
-});
-
-// Main router with nested routes
-const routes = [
-  { path: '/', component: HomePage },
-  { path: '/settings/:section?', component: SettingsPage }, // Optional section param
-];

Protected Routes (Authentication)

javascript
// auth.js
-import { $ } from 'sigpro';
-
-const isAuthenticated = $(false);
-const user = $(null);
-
-export const checkAuth = async () => {
-  const token = localStorage.getItem('token');
-  if (token) {
-    try {
-      const response = await fetch('/api/verify');
-      if (response.ok) {
-        const userData = await response.json();
-        user(userData);
-        isAuthenticated(true);
-        return true;
-      }
-    } catch (e) {
-      // Handle error
-    }
-  }
-  isAuthenticated(false);
-  user(null);
-  return false;
-};
-
-export const requireAuth = (component) => (params) => {
-  if (isAuthenticated()) {
-    return component(params);
-  }
-  // Redirect to login
-  $.router.go('/login');
-  return null;
-};
-
-export { isAuthenticated, user };
javascript
// pages/Dashboard.js (protected route)
-import { $, html } from 'sigpro';
-import { requireAuth, user } from '../auth.js';
-
-const Dashboard = (params) => $.page(() => {
-  return html`
-    <div class="dashboard">
-      <h1>Welcome, ${() => user()?.name}!</h1>
-      <p>This is your protected dashboard.</p>
-    </div>
-  `;
-});
-
-export default requireAuth(Dashboard);
javascript
// main.js with protected routes
-import { $, html } from 'sigpro';
-import { checkAuth } from './auth.js';
-import HomePage from './pages/Home.js';
-import LoginPage from './pages/Login.js';
-import DashboardPage from './pages/Dashboard.js';
-import AdminPage from './pages/Admin.js';
-
-// Check auth on startup
-checkAuth();
-
-const routes = [
-  { path: '/', component: HomePage },
-  { path: '/login', component: LoginPage },
-  { path: '/dashboard', component: DashboardPage }, // Protected
-  { path: '/admin', component: AdminPage }, // Protected
-];
-
-document.body.appendChild($.router(routes));

Route Transitions

javascript
// with-transitions.js
-import { $, html } from 'sigpro';
-
-export const createRouterWithTransitions = (routes) => {
-  const transitioning = $(false);
-  const currentView = $(null);
-  const nextView = $(null);
-  
-  const container = document.createElement('div');
-  container.style.display = 'contents';
-  
-  const renderWithTransition = async (newView) => {
-    if (currentView() === newView) return;
-    
-    transitioning(true);
-    nextView(newView);
-    
-    // Fade out
-    container.style.transition = 'opacity 0.2s';
-    container.style.opacity = '0';
-    
-    await new Promise(resolve => setTimeout(resolve, 200));
-    
-    // Update content
-    container.replaceChildren(newView);
-    currentView(newView);
-    
-    // Fade in
-    container.style.opacity = '1';
-    
-    await new Promise(resolve => setTimeout(resolve, 200));
-    transitioning(false);
-    container.style.transition = '';
-  };
-  
-  const router = $.router(routes.map(route => ({
-    ...route,
-    component: (params) => {
-      const view = route.component(params);
-      renderWithTransition(view);
-      return document.createComment('router-placeholder');
-    }
-  })));
-  
-  return router;
-};
javascript
// with-breadcrumbs.js
-import { $, html } from 'sigpro';
-
-export const createBreadcrumbs = (routes) => {
-  const breadcrumbs = $([]);
-  
-  const updateBreadcrumbs = (path) => {
-    const parts = path.split('/').filter(Boolean);
-    const crumbs = [];
-    let currentPath = '';
-    
-    parts.forEach((part, index) => {
-      currentPath += `/${part}`;
-      
-      // Find matching route
-      const route = routes.find(r => {
-        if (r.path.includes(':')) {
-          const pattern = r.path.replace(/:[^/]+/g, part);
-          return pattern === currentPath;
-        }
-        return r.path === currentPath;
-      });
-      
-      crumbs.push({
-        path: currentPath,
-        label: route?.name || part.charAt(0).toUpperCase() + part.slice(1),
-        isLast: index === parts.length - 1
-      });
-    });
-    
-    breadcrumbs(crumbs);
-  };
-  
-  // Listen to route changes
-  window.addEventListener('hashchange', () => {
-    const path = window.location.hash.replace(/^#/, '') || '/';
-    updateBreadcrumbs(path);
-  });
-  
-  // Initial update
-  updateBreadcrumbs(window.location.hash.replace(/^#/, '') || '/');
-  
-  return breadcrumbs;
-};
javascript
// Usage in layout
-import { createBreadcrumbs } from './with-breadcrumbs.js';
-
-const breadcrumbs = createBreadcrumbs(routes);
-
-const Layout = (content) => html`
-  <div class="app">
-    <nav class="breadcrumbs">
-      ${() => breadcrumbs().map(crumb => html`
-        ${!crumb.isLast ? html`
-          <a href="#${crumb.path}">${crumb.label}</a>
-          <span class="separator">/</span>
-        ` : html`
-          <span class="current">${crumb.label}</span>
-        `}
-      `)}
-    </nav>
-    
-    <main>
-      ${content}
-    </main>
-  </div>
-`;

Query Parameters

javascript
// with-query-params.js
-export const getQueryParams = () => {
-  const hash = window.location.hash;
-  const queryStart = hash.indexOf('?');
-  if (queryStart === -1) return {};
-  
-  const queryString = hash.slice(queryStart + 1);
-  const params = new URLSearchParams(queryString);
-  const result = {};
-  
-  for (const [key, value] of params) {
-    result[key] = value;
-  }
-  
-  return result;
-};
-
-export const updateQueryParams = (params) => {
-  const hash = window.location.hash.split('?')[0];
-  const queryString = new URLSearchParams(params).toString();
-  window.location.hash = queryString ? `${hash}?${queryString}` : hash;
-};
javascript
// Search page with query params
-import { $, html } from 'sigpro';
-import { getQueryParams, updateQueryParams } from './with-query-params.js';
-
-export default (params) => $.page(() => {
-  // Get initial query from URL
-  const queryParams = getQueryParams();
-  const searchQuery = $(queryParams.q || '');
-  const page = $(parseInt(queryParams.page) || 1);
-  const results = $([]);
-  
-  // Update URL when search changes
-  $.effect(() => {
-    updateQueryParams({
-      q: searchQuery() || undefined,
-      page: page() > 1 ? page() : undefined
-    });
-  });
-  
-  // Fetch results when search or page changes
-  $.effect(() => {
-    if (searchQuery()) {
-      fetch(`/api/search?q=${searchQuery()}&page=${page()}`)
-        .then(res => res.json())
-        .then(data => results(data));
-    }
-  });
-  
-  return html`
-    <div class="search-page">
-      <h1>Search</h1>
-      
-      <input
-        type="search"
-        :value=${searchQuery}
-        placeholder="Search..."
-        @input=${(e) => {
-          searchQuery(e.target.value);
-          page(1); // Reset to first page on new search
-        }}
-      />
-      
-      <div class="results">
-        ${results().map(item => html`
-          <div class="result">${item.title}</div>
-        `)}
-      </div>
-      
-      ${() => results().length ? html`
-        <div class="pagination">
-          <button 
-            ?disabled=${() => page() <= 1}
-            @click=${() => page(p => p - 1)}
-          >
-            Previous
-          </button>
-          
-          <span>Page ${page}</span>
-          
-          <button 
-            ?disabled=${() => results().length < 10}
-            @click=${() => page(p => p + 1)}
-          >
-            Next
-          </button>
-        </div>
-      ` : ''}
-    </div>
-  `;
-});

Lazy Loading Routes

javascript
// lazy.js
-export const lazy = (loader) => {
-  let component = null;
-  
-  return async (params) => {
-    if (!component) {
-      const module = await loader();
-      component = module.default;
-    }
-    return component(params);
-  };
-};
javascript
// main.js with lazy loading
-import { $, html } from 'sigpro';
-import { lazy } from './lazy.js';
-import Layout from './Layout.js';
-
-const routes = [
-  { path: '/', component: lazy(() => import('./pages/Home.js')) },
-  { path: '/about', component: lazy(() => import('./pages/About.js')) },
-  { path: '/dashboard', component: lazy(() => import('./pages/Dashboard.js')) },
-  { 
-    path: '/admin', 
-    component: lazy(() => import('./pages/Admin.js')),
-    // Show loading state
-    loading: () => html`<div class="loading">Loading admin panel...</div>`
-  },
-];
-
-// Wrap with layout
-const routesWithLayout = routes.map(route => ({
-  ...route,
-  component: (params) => Layout(route.component(params))
-}));
-
-document.body.appendChild($.router(routesWithLayout));

Route Guards / Middleware

javascript
// middleware.js
-export const withGuard = (component, guard) => (params) => {
-  const result = guard(params);
-  if (result === true) {
-    return component(params);
-  } else if (typeof result === 'string') {
-    $.router.go(result);
-    return null;
-  }
-  return result; // Custom component (e.g., AccessDenied)
-};
-
-// Guards
-export const roleGuard = (requiredRole) => (params) => {
-  const userRole = localStorage.getItem('userRole');
-  if (userRole === requiredRole) return true;
-  if (!userRole) return '/login';
-  return AccessDeniedPage(params);
-};
-
-export const authGuard = () => (params) => {
-  const token = localStorage.getItem('token');
-  return token ? true : '/login';
-};
-
-export const pendingChangesGuard = (hasPendingChanges) => (params) => {
-  if (hasPendingChanges()) {
-    return ConfirmLeavePage(params);
-  }
-  return true;
-};
javascript
// Usage
-import { withGuard, authGuard, roleGuard } from './middleware.js';
-
-const routes = [
-  { path: '/', component: HomePage },
-  { path: '/profile', component: withGuard(ProfilePage, authGuard()) },
-  { 
-    path: '/admin', 
-    component: withGuard(AdminPage, roleGuard('admin')) 
-  },
-];

📊 Route Matching Priority

Routes are matched in the order they are defined. More specific routes should come first:

javascript
const routes = [
-  // More specific first
-  { path: '/user/:id/edit', component: EditUserPage },
-  { path: '/user/:id/posts', component: UserPostsPage },
-  { path: '/user/:id', component: UserPage },
-  
-  // Static routes
-  { path: '/about', component: AboutPage },
-  { path: '/contact', component: ContactPage },
-  
-  // Catch-all last
-  { path: '/:path(.*)', component: NotFoundPage },
-];

🎯 Complete Example

javascript
// main.js - Complete application
-import { $, html } from 'sigpro';
-import { lazy } from './utils/lazy.js';
-import { withGuard, authGuard } from './utils/middleware.js';
-import Layout from './components/Layout.js';
-
-// Lazy load pages
-const HomePage = lazy(() => import('./pages/Home.js'));
-const AboutPage = lazy(() => import('./pages/About.js'));
-const LoginPage = lazy(() => import('./pages/Login.js'));
-const DashboardPage = lazy(() => import('./pages/Dashboard.js'));
-const UserPage = lazy(() => import('./pages/User.js'));
-const SettingsPage = lazy(() => import('./pages/Settings.js'));
-const NotFoundPage = lazy(() => import('./pages/NotFound.js'));
-
-// Route configuration
-const routes = [
-  { path: '/', component: HomePage, name: 'Home' },
-  { path: '/about', component: AboutPage, name: 'About' },
-  { path: '/login', component: LoginPage, name: 'Login' },
-  { 
-    path: '/dashboard', 
-    component: withGuard(DashboardPage, authGuard()),
-    name: 'Dashboard'
-  },
-  { 
-    path: '/user/:id', 
-    component: UserPage,
-    name: 'User Profile'
-  },
-  { 
-    path: '/settings/:section?', 
-    component: withGuard(SettingsPage, authGuard()),
-    name: 'Settings'
-  },
-  { path: '/:path(.*)', component: NotFoundPage, name: 'Not Found' },
-];
-
-// Wrap all routes with layout
-const routesWithLayout = routes.map(route => ({
-  ...route,
-  component: (params) => Layout(route.component(params))
-}));
-
-// Create and mount router
-const router = $.router(routesWithLayout);
-document.body.appendChild(router);
-
-// Navigation helper (available globally)
-window.navigate = $.router.go;

📊 Summary

FeatureDescription
Hash-basedWorks everywhere, no server config
Route Parameters:param syntax for dynamic segments
RegExp SupportAdvanced pattern matching
Query ParametersSupport for ?key=value in URLs
Programmatic Navigation$.router.go(path)
Auto-cleanupWorks with $.page for memory management
Zero DependenciesPure vanilla JavaScript
Lazy Loading ReadyEasy code splitting

Pro Tip: Order matters in route definitions - put more specific routes (with parameters) before static ones, and always include a catch-all route (404) at the end.

- - - - \ No newline at end of file diff --git a/docs/api/signals.html b/docs/api/signals.html deleted file mode 100644 index c0f7e32..0000000 --- a/docs/api/signals.html +++ /dev/null @@ -1,683 +0,0 @@ - - - - - - Signals API 📡 | SigPro - - - - - - - - - - - - - - -
Skip to content

Signals API 📡

Signals are the heart of SigPro's reactivity system. They are reactive values that automatically track dependencies and notify subscribers when they change. This enables fine-grained updates without virtual DOM diffing.

Core Concepts

What is a Signal?

A signal is a function that holds a value and notifies dependents when that value changes. Signals can be:

  • Basic signals - Hold simple values (numbers, strings, objects)
  • Computed signals - Derive values from other signals
  • Persistent signals - Automatically sync with localStorage/sessionStorage

How Reactivity Works

SigPro uses automatic dependency tracking:

  1. When you read a signal inside an effect, the effect becomes a subscriber
  2. When the signal's value changes, all subscribers are notified
  3. Updates are batched using microtasks for optimal performance
  4. Only the exact nodes that depend on changed values are updated

$(initialValue)

Creates a reactive signal. The behavior changes based on the type of initialValue:

  • If initialValue is a function, creates a computed signal
  • Otherwise, creates a basic signal
javascript
import { $ } from 'sigpro';
-
-// Basic signal
-const count = $(0);
-
-// Computed signal
-const firstName = $('John');
-const lastName = $('Doe');
-const fullName = $(() => `${firstName()} ${lastName()}`);

📋 API Reference

Basic Signals

PatternExampleDescription
Createconst count = $(0)Create signal with initial value
Getcount()Read current value
Setcount(5)Set new value directly
Updatecount(prev => prev + 1)Update based on previous value

Computed Signals

PatternExampleDescription
Createconst total = $(() => price() * quantity())Derive value from other signals
Gettotal()Read computed value (auto-updates)

Signal Methods

MethodDescriptionExample
signal()Gets current valuecount()
signal(newValue)Sets new valuecount(5)
signal(prev => new)Updates using previous valuecount(c => c + 1)

🎯 Basic Examples

Counter Signal

javascript
import { $ } from 'sigpro';
-
-const count = $(0);
-
-console.log(count()); // 0
-
-count(5);
-console.log(count()); // 5
-
-count(prev => prev + 1);
-console.log(count()); // 6

Object Signal

javascript
import { $ } from 'sigpro';
-
-const user = $({
-  name: 'John',
-  age: 30,
-  email: 'john@example.com'
-});
-
-// Read
-console.log(user().name); // 'John'
-
-// Update (immutable pattern)
-user({
-  ...user(),
-  age: 31
-});
-
-// Partial update with function
-user(prev => ({
-  ...prev,
-  email: 'john.doe@example.com'
-}));

Array Signal

javascript
import { $ } from 'sigpro';
-
-const todos = $(['Learn SigPro', 'Build an app']);
-
-// Add item
-todos([...todos(), 'Deploy to production']);
-
-// Remove item
-todos(todos().filter((_, i) => i !== 1));
-
-// Update item
-todos(todos().map((todo, i) => 
-  i === 0 ? 'Master SigPro' : todo
-));

🔄 Computed Signals

Computed signals automatically update when their dependencies change:

javascript
import { $ } from 'sigpro';
-
-const price = $(10);
-const quantity = $(2);
-const tax = $(0.21);
-
-// Computed signals
-const subtotal = $(() => price() * quantity());
-const taxAmount = $(() => subtotal() * tax());
-const total = $(() => subtotal() + taxAmount());
-
-console.log(total()); // 24.2
-
-price(15);
-console.log(total()); // 36.3 (automatically updated)
-
-quantity(3);
-console.log(total()); // 54.45 (automatically updated)

Computed with Multiple Dependencies

javascript
import { $ } from 'sigpro';
-
-const firstName = $('John');
-const lastName = $('Doe');
-const prefix = $('Mr.');
-
-const fullName = $(() => {
-  // Computed signals can contain logic
-  const name = `${firstName()} ${lastName()}`;
-  return prefix() ? `${prefix()} ${name}` : name;
-});
-
-console.log(fullName()); // 'Mr. John Doe'
-
-prefix('');
-console.log(fullName()); // 'John Doe'

Computed with Conditional Logic

javascript
import { $ } from 'sigpro';
-
-const user = $({ role: 'admin', permissions: [] });
-const isAdmin = $(() => user().role === 'admin');
-const hasPermission = $(() => 
-  isAdmin() || user().permissions.includes('edit')
-);
-
-console.log(hasPermission()); // true
-
-user({ role: 'user', permissions: ['view'] });
-console.log(hasPermission()); // false (can't edit)
-
-user({ role: 'user', permissions: ['view', 'edit'] });
-console.log(hasPermission()); // true (now has permission)

🧮 Advanced Signal Patterns

Derived State Pattern

javascript
import { $ } from 'sigpro';
-
-// Shopping cart example
-const cart = $([
-  { id: 1, name: 'Product 1', price: 10, quantity: 2 },
-  { id: 2, name: 'Product 2', price: 15, quantity: 1 },
-]);
-
-// Derived values
-const itemCount = $(() => 
-  cart().reduce((sum, item) => sum + item.quantity, 0)
-);
-
-const subtotal = $(() => 
-  cart().reduce((sum, item) => sum + (item.price * item.quantity), 0)
-);
-
-const tax = $(() => subtotal() * 0.21);
-const total = $(() => subtotal() + tax());
-
-// Update cart
-cart([
-  ...cart(),
-  { id: 3, name: 'Product 3', price: 20, quantity: 1 }
-]);
-
-// All derived values auto-update
-console.log(itemCount()); // 4
-console.log(total()); // (10*2 + 15*1 + 20*1) * 1.21 = 78.65

Validation Pattern

javascript
import { $ } from 'sigpro';
-
-const email = $('');
-const password = $('');
-const confirmPassword = $('');
-
-// Validation signals
-const isEmailValid = $(() => {
-  const value = email();
-  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
-});
-
-const isPasswordValid = $(() => {
-  const value = password();
-  return value.length >= 8;
-});
-
-const doPasswordsMatch = $(() => 
-  password() === confirmPassword()
-);
-
-const isFormValid = $(() => 
-  isEmailValid() && isPasswordValid() && doPasswordsMatch()
-);
-
-// Update form
-email('user@example.com');
-password('secure123');
-confirmPassword('secure123');
-
-console.log(isFormValid()); // true
-
-// Validation messages
-const emailError = $(() => 
-  email() && !isEmailValid() ? 'Invalid email format' : ''
-);

Filtering and Search Pattern

javascript
import { $ } from 'sigpro';
-
-const items = $([
-  { id: 1, name: 'Apple', category: 'fruit' },
-  { id: 2, name: 'Banana', category: 'fruit' },
-  { id: 3, name: 'Carrot', category: 'vegetable' },
-  { id: 4, name: 'Date', category: 'fruit' },
-]);
-
-const searchTerm = $('');
-const categoryFilter = $('all');
-
-// Filtered items (computed)
-const filteredItems = $(() => {
-  let result = items();
-  
-  // Apply search filter
-  if (searchTerm()) {
-    const term = searchTerm().toLowerCase();
-    result = result.filter(item => 
-      item.name.toLowerCase().includes(term)
-    );
-  }
-  
-  // Apply category filter
-  if (categoryFilter() !== 'all') {
-    result = result.filter(item => 
-      item.category === categoryFilter()
-    );
-  }
-  
-  return result;
-});
-
-// Stats
-const fruitCount = $(() => 
-  items().filter(item => item.category === 'fruit').length
-);
-
-const vegCount = $(() => 
-  items().filter(item => item.category === 'vegetable').length
-);
-
-// Update filters
-searchTerm('a');
-console.log(filteredItems().map(i => i.name)); // ['Apple', 'Banana', 'Carrot', 'Date']
-
-categoryFilter('fruit');
-console.log(filteredItems().map(i => i.name)); // ['Apple', 'Banana', 'Date']

Pagination Pattern

javascript
import { $ } from 'sigpro';
-
-const allItems = $([...Array(100).keys()].map(i => `Item ${i + 1}`));
-const currentPage = $(1);
-const itemsPerPage = $(10);
-
-// Paginated items (computed)
-const paginatedItems = $(() => {
-  const start = (currentPage() - 1) * itemsPerPage();
-  const end = start + itemsPerPage();
-  return allItems().slice(start, end);
-});
-
-// Pagination metadata
-const totalPages = $(() => 
-  Math.ceil(allItems().length / itemsPerPage())
-);
-
-const hasNextPage = $(() => 
-  currentPage() < totalPages()
-);
-
-const hasPrevPage = $(() => 
-  currentPage() > 1
-);
-
-const pageRange = $(() => {
-  const current = currentPage();
-  const total = totalPages();
-  const delta = 2;
-  
-  let range = [];
-  for (let i = Math.max(2, current - delta); 
-       i <= Math.min(total - 1, current + delta); 
-       i++) {
-    range.push(i);
-  }
-  
-  if (current - delta > 2) range = ['...', ...range];
-  if (current + delta < total - 1) range = [...range, '...'];
-  
-  return [1, ...range, total];
-});
-
-// Navigation
-const nextPage = () => {
-  if (hasNextPage()) currentPage(c => c + 1);
-};
-
-const prevPage = () => {
-  if (hasPrevPage()) currentPage(c => c - 1);
-};
-
-const goToPage = (page) => {
-  if (page >= 1 && page <= totalPages()) {
-    currentPage(page);
-  }
-};

🔧 Advanced Signal Features

Signal Equality Comparison

Signals use Object.is for change detection. Only notify subscribers when values are actually different:

javascript
import { $ } from 'sigpro';
-
-const count = $(0);
-
-// These won't trigger updates:
-count(0); // Same value
-count(prev => prev); // Returns same value
-
-// These will trigger updates:
-count(1); // Different value
-count(prev => prev + 0); // Still 0? Actually returns 0? Wait...
-// Be careful with functional updates!

Batch Updates

Multiple signal updates are batched into a single microtask:

javascript
import { $ } from 'sigpro';
-
-const firstName = $('John');
-const lastName = $('Doe');
-const fullName = $(() => `${firstName()} ${lastName()}`);
-
-$.effect(() => {
-  console.log('Full name:', fullName());
-});
-// Logs: 'Full name: John Doe'
-
-// Multiple updates in same tick - only one effect run!
-firstName('Jane');
-lastName('Smith');
-// Only logs once: 'Full name: Jane Smith'

Infinite Loop Protection

SigPro includes protection against infinite reactive loops:

javascript
import { $ } from 'sigpro';
-
-const a = $(1);
-const b = $(2);
-
-// This would create a loop, but SigPro prevents it
-$.effect(() => {
-  a(b()); // Reading b
-  b(a()); // Reading a - loop detected!
-});
-// Throws: "SigPro: Infinite reactive loop detected."

📊 Performance Characteristics

OperationComplexityNotes
Signal readO(1)Direct value access
Signal writeO(n)n = number of subscribers
Computed readO(1) or O(m)m = computation complexity
Effect runO(s)s = number of signal reads

🎯 Best Practices

1. Keep Signals Focused

javascript
// ❌ Avoid large monolithic signals
-const state = $({
-  user: null,
-  posts: [],
-  theme: 'light',
-  notifications: []
-});
-
-// ✅ Split into focused signals
-const user = $(null);
-const posts = $([]);
-const theme = $('light');
-const notifications = $([]);

2. Use Computed for Derived State

javascript
// ❌ Don't compute in templates/effects
-$.effect(() => {
-  const total = items().reduce((sum, i) => sum + i.price, 0);
-  updateUI(total);
-});
-
-// ✅ Compute with signals
-const total = $(() => items().reduce((sum, i) => sum + i.price, 0));
-$.effect(() => updateUI(total()));

3. Immutable Updates

javascript
// ❌ Don't mutate objects/arrays
-const user = $({ name: 'John' });
-user().name = 'Jane'; // Won't trigger updates!
-
-// ✅ Create new objects/arrays
-user({ ...user(), name: 'Jane' });
-
-// ❌ Don't mutate arrays
-const todos = $(['a', 'b']);
-todos().push('c'); // Won't trigger updates!
-
-// ✅ Create new arrays
-todos([...todos(), 'c']);

4. Functional Updates for Dependencies

javascript
// ❌ Avoid if new value depends on current
-count(count() + 1);
-
-// ✅ Use functional update
-count(prev => prev + 1);

5. Clean Up Effects

javascript
import { $ } from 'sigpro';
-
-const userId = $(1);
-
-// Effects auto-clean in pages, but you can stop manually
-const stop = $.effect(() => {
-  fetchUser(userId());
-});
-
-// Later, if needed
-stop();

🚀 Real-World Examples

Form State Management

javascript
import { $ } from 'sigpro';
-
-// Form state
-const formData = $({
-  username: '',
-  email: '',
-  age: '',
-  newsletter: false
-});
-
-// Touched fields (for validation UI)
-const touched = $({
-  username: false,
-  email: false,
-  age: false
-});
-
-// Validation rules
-const validations = {
-  username: (value) => 
-    value.length >= 3 ? null : 'Username must be at least 3 characters',
-  email: (value) => 
-    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : 'Invalid email',
-  age: (value) => 
-    !value || (value >= 18 && value <= 120) ? null : 'Age must be 18-120'
-};
-
-// Validation signals
-const errors = $(() => {
-  const data = formData();
-  const result = {};
-  
-  Object.keys(validations).forEach(field => {
-    const error = validations[field](data[field]);
-    if (error) result[field] = error;
-  });
-  
-  return result;
-});
-
-const isValid = $(() => Object.keys(errors()).length === 0);
-
-// Field helpers
-const fieldProps = (field) => ({
-  value: formData()[field],
-  error: touched()[field] ? errors()[field] : null,
-  onChange: (e) => {
-    const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
-    formData({
-      ...formData(),
-      [field]: value
-    });
-  },
-  onBlur: () => {
-    touched({
-      ...touched(),
-      [field]: true
-    });
-  }
-});
-
-// Form submission
-const submitAttempts = $(0);
-const isSubmitting = $(false);
-
-const handleSubmit = async () => {
-  submitAttempts(s => s + 1);
-  
-  if (!isValid()) {
-    // Mark all fields as touched to show errors
-    touched(Object.keys(formData()).reduce((acc, field) => ({
-      ...acc,
-      [field]: true
-    }), {}));
-    return;
-  }
-  
-  isSubmitting(true);
-  try {
-    await saveForm(formData());
-    // Reset form on success
-    formData({ username: '', email: '', age: '', newsletter: false });
-    touched({ username: false, email: false, age: false });
-  } finally {
-    isSubmitting(false);
-  }
-};

Todo App with Filters

javascript
import { $ } from 'sigpro';
-
-// State
-const todos = $([
-  { id: 1, text: 'Learn SigPro', completed: true },
-  { id: 2, text: 'Build an app', completed: false },
-  { id: 3, text: 'Write docs', completed: false }
-]);
-
-const filter = $('all'); // 'all', 'active', 'completed'
-const newTodoText = $('');
-
-// Computed values
-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 activeCount = $(() => 
-  todos().filter(t => !t.completed).length
-);
-
-const completedCount = $(() => 
-  todos().filter(t => t.completed).length
-);
-
-const hasCompleted = $(() => completedCount() > 0);
-
-// Actions
-const addTodo = () => {
-  const text = newTodoText().trim();
-  if (text) {
-    todos([
-      ...todos(),
-      {
-        id: Date.now(),
-        text,
-        completed: false
-      }
-    ]);
-    newTodoText('');
-  }
-};
-
-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));
-};
-
-const clearCompleted = () => {
-  todos(todos().filter(todo => !todo.completed));
-};
-
-const toggleAll = () => {
-  const allCompleted = activeCount() === 0;
-  todos(todos().map(todo => ({
-    ...todo,
-    completed: !allCompleted
-  })));
-};

Shopping Cart

javascript
import { $ } from 'sigpro';
-
-// Products catalog
-const products = $([
-  { id: 1, name: 'Laptop', price: 999, stock: 5 },
-  { id: 2, name: 'Mouse', price: 29, stock: 20 },
-  { id: 3, name: 'Keyboard', price: 79, stock: 10 },
-  { id: 4, name: 'Monitor', price: 299, stock: 3 }
-]);
-
-// Cart state
-const cart = $({});
-const selectedProduct = $(null);
-const quantity = $(1);
-
-// Computed cart values
-const cartItems = $(() => {
-  const items = [];
-  Object.entries(cart()).forEach(([productId, qty]) => {
-    const product = products().find(p => p.id === parseInt(productId));
-    if (product) {
-      items.push({
-        ...product,
-        quantity: qty,
-        subtotal: product.price * qty
-      });
-    }
-  });
-  return items;
-});
-
-const itemCount = $(() => 
-  cartItems().reduce((sum, item) => sum + item.quantity, 0)
-);
-
-const subtotal = $(() => 
-  cartItems().reduce((sum, item) => sum + item.subtotal, 0)
-);
-
-const tax = $(() => subtotal() * 0.10);
-const shipping = $(() => subtotal() > 100 ? 0 : 10);
-const total = $(() => subtotal() + tax() + shipping());
-
-const isCartEmpty = $(() => itemCount() === 0);
-
-// Cart actions
-const addToCart = (product, qty = 1) => {
-  const currentQty = cart()[product.id] || 0;
-  const newQty = currentQty + qty;
-  
-  if (newQty <= product.stock) {
-    cart({
-      ...cart(),
-      [product.id]: newQty
-    });
-    return true;
-  }
-  return false;
-};
-
-const updateQuantity = (productId, newQty) => {
-  const product = products().find(p => p.id === productId);
-  if (newQty <= product.stock) {
-    if (newQty <= 0) {
-      removeFromCart(productId);
-    } else {
-      cart({
-        ...cart(),
-        [productId]: newQty
-      });
-    }
-  }
-};
-
-const removeFromCart = (productId) => {
-  const newCart = { ...cart() };
-  delete newCart[productId];
-  cart(newCart);
-};
-
-const clearCart = () => cart({});
-
-// Stock management
-const productStock = (productId) => {
-  const product = products().find(p => p.id === productId);
-  if (!product) return 0;
-  const inCart = cart()[productId] || 0;
-  return product.stock - inCart;
-};
-
-const isInStock = (productId, qty = 1) => {
-  return productStock(productId) >= qty;
-};

📈 Debugging Signals

Logging Signal Changes

javascript
import { $ } from 'sigpro';
-
-// Wrap a signal to log changes
-const withLogging = (signal, name) => {
-  return (...args) => {
-    if (args.length) {
-      const oldValue = signal();
-      const result = signal(...args);
-      console.log(`${name}:`, oldValue, '->', signal());
-      return result;
-    }
-    return signal();
-  };
-};
-
-// Usage
-const count = withLogging($(0), 'count');
-count(5); // Logs: "count: 0 -> 5"

Signal Inspector

javascript
import { $ } from 'sigpro';
-
-// Create an inspectable signal
-const createInspector = () => {
-  const signals = new Map();
-  
-  const createSignal = (initialValue, name) => {
-    const signal = $(initialValue);
-    signals.set(signal, { name, subscribers: new Set() });
-    
-    // Wrap to track subscribers
-    const wrapped = (...args) => {
-      if (!args.length && activeEffect) {
-        const info = signals.get(wrapped);
-        info.subscribers.add(activeEffect);
-      }
-      return signal(...args);
-    };
-    
-    return wrapped;
-  };
-  
-  const getInfo = () => {
-    const info = {};
-    signals.forEach((data, signal) => {
-      info[data.name] = {
-        subscribers: data.subscribers.size,
-        value: signal()
-      };
-    });
-    return info;
-  };
-  
-  return { createSignal, getInfo };
-};
-
-// Usage
-const inspector = createInspector();
-const count = inspector.createSignal(0, 'count');
-const doubled = inspector.createSignal(() => count() * 2, 'doubled');
-
-console.log(inspector.getInfo());
-// { count: { subscribers: 0, value: 0 }, doubled: { subscribers: 0, value: 0 } }

📊 Summary

FeatureDescription
Basic SignalsHold values and notify on change
Computed SignalsAuto-updating derived values
Automatic TrackingDependencies tracked automatically
Batch UpdatesMultiple updates batched in microtask
Infinite Loop ProtectionPrevents reactive cycles
Zero DependenciesPure vanilla JavaScript

Pro Tip: Signals are the foundation of reactivity in SigPro. Master them, and you've mastered 80% of the library!

- - - - \ No newline at end of file diff --git a/docs/api/storage.html b/docs/api/storage.html deleted file mode 100644 index 44df3fe..0000000 --- a/docs/api/storage.html +++ /dev/null @@ -1,820 +0,0 @@ - - - - - - Storage API 💾 | SigPro - - - - - - - - - - - - - - -
Skip to content

Storage API 💾

SigPro provides persistent signals that automatically synchronize with browser storage APIs. This allows you to create reactive state that survives page reloads and browser sessions with zero additional code.

Core Concepts

What is Persistent Storage?

Persistent signals are special signals that:

  • Initialize from storage (localStorage/sessionStorage) if a saved value exists
  • Auto-save whenever the signal value changes
  • Handle JSON serialization automatically
  • Clean up when set to null or undefined

Storage Types

StoragePersistenceUse Case
localStorageForever (until cleared)User preferences, themes, saved data
sessionStorageUntil tab/window closesForm drafts, temporary state

$.storage(key, initialValue, [storage])

Creates a persistent signal that syncs with browser storage.

javascript
import { $ } from 'sigpro';
-
-// localStorage (default)
-const theme = $.storage('theme', 'light');
-const user = $.storage('user', null);
-const settings = $.storage('settings', { notifications: true });
-
-// sessionStorage
-const draft = $.storage('draft', '', sessionStorage);
-const formData = $.storage('form', {}, sessionStorage);

📋 API Reference

Parameters

ParameterTypeDefaultDescription
keystringrequiredStorage key name
initialValueanyrequiredDefault value if none stored
storageStoragelocalStorageStorage type (localStorage or sessionStorage)

Returns

ReturnDescription
FunctionSignal function (getter/setter) with persistence

🎯 Basic Examples

Theme Preference

javascript
import { $, html } from 'sigpro';
-
-// Persistent theme signal
-const theme = $.storage('theme', 'light');
-
-// Apply theme to document
-$.effect(() => {
-  document.body.className = `theme-${theme()}`;
-});
-
-// Toggle theme
-const toggleTheme = () => {
-  theme(t => t === 'light' ? 'dark' : 'light');
-};
-
-// Template
-html`
-  <div>
-    <p>Current theme: ${theme}</p>
-    <button @click=${toggleTheme}>
-      Toggle Theme
-    </button>
-  </div>
-`;

User Preferences

javascript
import { $ } from 'sigpro';
-
-// Complex preferences object
-const preferences = $.storage('preferences', {
-  language: 'en',
-  fontSize: 'medium',
-  notifications: true,
-  compactView: false,
-  sidebarOpen: true
-});
-
-// Update single preference
-const setPreference = (key, value) => {
-  preferences({
-    ...preferences(),
-    [key]: value
-  });
-};
-
-// Usage
-setPreference('language', 'es');
-setPreference('fontSize', 'large');
-console.log(preferences().language); // 'es'

Form Draft

javascript
import { $, html } from 'sigpro';
-
-// Session-based draft (clears when tab closes)
-const draft = $.storage('contact-form', {
-  name: '',
-  email: '',
-  message: ''
-}, sessionStorage);
-
-// Auto-save on input
-const handleInput = (field, value) => {
-  draft({
-    ...draft(),
-    [field]: value
-  });
-};
-
-// Clear draft after submit
-const handleSubmit = async () => {
-  await submitForm(draft());
-  draft(null); // Clears from storage
-};
-
-// Template
-html`
-  <form @submit=${handleSubmit}>
-    <input
-      type="text"
-      :value=${() => draft().name}
-      @input=${(e) => handleInput('name', e.target.value)}
-      placeholder="Name"
-    />
-    <input
-      type="email"
-      :value=${() => draft().email}
-      @input=${(e) => handleInput('email', e.target.value)}
-      placeholder="Email"
-    />
-    <textarea
-      :value=${() => draft().message}
-      @input=${(e) => handleInput('message', e.target.value)}
-      placeholder="Message"
-    ></textarea>
-    <button type="submit">Send</button>
-  </form>
-`;

🚀 Advanced Examples

Authentication State

javascript
import { $, html } from 'sigpro';
-
-// Persistent auth state
-const auth = $.storage('auth', {
-  token: null,
-  user: null,
-  expiresAt: null
-});
-
-// Computed helpers
-const isAuthenticated = $(() => {
-  const { token, expiresAt } = auth();
-  if (!token || !expiresAt) return false;
-  return new Date(expiresAt) > new Date();
-});
-
-const user = $(() => auth().user);
-
-// Login function
-const login = async (email, password) => {
-  const response = await fetch('/api/login', {
-    method: 'POST',
-    body: JSON.stringify({ email, password })
-  });
-  
-  if (response.ok) {
-    const { token, user, expiresIn } = await response.json();
-    auth({
-      token,
-      user,
-      expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString()
-    });
-    return true;
-  }
-  return false;
-};
-
-// Logout
-const logout = () => {
-  auth(null); // Clear from storage
-};
-
-// Auto-refresh token
-$.effect(() => {
-  if (!isAuthenticated()) return;
-  
-  const { expiresAt } = auth();
-  const expiresIn = new Date(expiresAt) - new Date();
-  const refreshTime = expiresIn - 60000; // 1 minute before expiry
-  
-  if (refreshTime > 0) {
-    const timer = setTimeout(refreshToken, refreshTime);
-    return () => clearTimeout(timer);
-  }
-});
-
-// Navigation guard
-$.effect(() => {
-  if (!isAuthenticated() && window.location.pathname !== '/login') {
-    $.router.go('/login');
-  }
-});

Multi-tab Synchronization

javascript
import { $ } from 'sigpro';
-
-// Storage key for cross-tab communication
-const STORAGE_KEY = 'app-state';
-
-// Create persistent signal
-const appState = $.storage(STORAGE_KEY, {
-  count: 0,
-  lastUpdated: null
-});
-
-// Listen for storage events (changes from other tabs)
-window.addEventListener('storage', (event) => {
-  if (event.key === STORAGE_KEY && event.newValue) {
-    try {
-      // Update signal without triggering save loop
-      const newValue = JSON.parse(event.newValue);
-      appState(newValue);
-    } catch (e) {
-      console.error('Failed to parse storage event:', e);
-    }
-  }
-});
-
-// Update state (syncs across all tabs)
-const increment = () => {
-  appState({
-    count: appState().count + 1,
-    lastUpdated: new Date().toISOString()
-  });
-};
-
-// Tab counter
-const tabCount = $(1);
-
-// Track number of tabs open
-window.addEventListener('storage', (event) => {
-  if (event.key === 'tab-heartbeat') {
-    tabCount(parseInt(event.newValue) || 1);
-  }
-});
-
-// Send heartbeat
-setInterval(() => {
-  localStorage.setItem('tab-heartbeat', tabCount());
-}, 1000);

Settings Manager

javascript
import { $, html } from 'sigpro';
-
-// Settings schema
-const settingsSchema = {
-  theme: {
-    type: 'select',
-    options: ['light', 'dark', 'system'],
-    default: 'system'
-  },
-  fontSize: {
-    type: 'range',
-    min: 12,
-    max: 24,
-    default: 16
-  },
-  notifications: {
-    type: 'checkbox',
-    default: true
-  },
-  language: {
-    type: 'select',
-    options: ['en', 'es', 'fr', 'de'],
-    default: 'en'
-  }
-};
-
-// Persistent settings
-const settings = $.storage('app-settings', 
-  Object.entries(settingsSchema).reduce((acc, [key, config]) => ({
-    ...acc,
-    [key]: config.default
-  }), {})
-);
-
-// Settings component
-const SettingsPanel = () => {
-  return html`
-    <div class="settings-panel">
-      <h2>Settings</h2>
-      
-      ${Object.entries(settingsSchema).map(([key, config]) => {
-        switch(config.type) {
-          case 'select':
-            return html`
-              <div class="setting">
-                <label>${key}:</label>
-                <select 
-                  :value=${() => settings()[key]}
-                  @change=${(e) => updateSetting(key, e.target.value)}
-                >
-                  ${config.options.map(opt => html`
-                    <option value="${opt}" ?selected=${() => settings()[key] === opt}>
-                      ${opt}
-                    </option>
-                  `)}
-                </select>
-              </div>
-            `;
-            
-          case 'range':
-            return html`
-              <div class="setting">
-                <label>${key}: ${() => settings()[key]}</label>
-                <input
-                  type="range"
-                  min="${config.min}"
-                  max="${config.max}"
-                  :value=${() => settings()[key]}
-                  @input=${(e) => updateSetting(key, parseInt(e.target.value))}
-                />
-              </div>
-            `;
-            
-          case 'checkbox':
-            return html`
-              <div class="setting">
-                <label>
-                  <input
-                    type="checkbox"
-                    :checked=${() => settings()[key]}
-                    @change=${(e) => updateSetting(key, e.target.checked)}
-                  />
-                  ${key}
-                </label>
-              </div>
-            `;
-        }
-      })}
-      
-      <button @click=${resetDefaults}>Reset to Defaults</button>
-    </div>
-  `;
-};
-
-// Helper functions
-const updateSetting = (key, value) => {
-  settings({
-    ...settings(),
-    [key]: value
-  });
-};
-
-const resetDefaults = () => {
-  const defaults = Object.entries(settingsSchema).reduce((acc, [key, config]) => ({
-    ...acc,
-    [key]: config.default
-  }), {});
-  settings(defaults);
-};
-
-// Apply settings globally
-$.effect(() => {
-  const { theme, fontSize } = settings();
-  
-  // Apply theme
-  document.documentElement.setAttribute('data-theme', theme);
-  
-  // Apply font size
-  document.documentElement.style.fontSize = `${fontSize}px`;
-});

Shopping Cart Persistence

javascript
import { $, html } from 'sigpro';
-
-// Persistent shopping cart
-const cart = $.storage('shopping-cart', {
-  items: [],
-  lastUpdated: null
-});
-
-// Computed values
-const cartItems = $(() => cart().items);
-const itemCount = $(() => cartItems().reduce((sum, item) => sum + item.quantity, 0));
-const subtotal = $(() => cartItems().reduce((sum, item) => sum + (item.price * item.quantity), 0));
-const tax = $(() => subtotal() * 0.1);
-const total = $(() => subtotal() + tax());
-
-// Cart actions
-const addToCart = (product, quantity = 1) => {
-  const existing = cartItems().findIndex(item => item.id === product.id);
-  
-  if (existing >= 0) {
-    // Update quantity
-    const newItems = [...cartItems()];
-    newItems[existing] = {
-      ...newItems[existing],
-      quantity: newItems[existing].quantity + quantity
-    };
-    
-    cart({
-      items: newItems,
-      lastUpdated: new Date().toISOString()
-    });
-  } else {
-    // Add new item
-    cart({
-      items: [...cartItems(), { ...product, quantity }],
-      lastUpdated: new Date().toISOString()
-    });
-  }
-};
-
-const removeFromCart = (productId) => {
-  cart({
-    items: cartItems().filter(item => item.id !== productId),
-    lastUpdated: new Date().toISOString()
-  });
-};
-
-const updateQuantity = (productId, quantity) => {
-  if (quantity <= 0) {
-    removeFromCart(productId);
-  } else {
-    const newItems = cartItems().map(item =>
-      item.id === productId ? { ...item, quantity } : item
-    );
-    
-    cart({
-      items: newItems,
-      lastUpdated: new Date().toISOString()
-    });
-  }
-};
-
-const clearCart = () => {
-  cart({
-    items: [],
-    lastUpdated: new Date().toISOString()
-  });
-};
-
-// Cart expiration (7 days)
-const CART_EXPIRY_DAYS = 7;
-
-$.effect(() => {
-  const lastUpdated = cart().lastUpdated;
-  if (lastUpdated) {
-    const expiryDate = new Date(lastUpdated);
-    expiryDate.setDate(expiryDate.getDate() + CART_EXPIRY_DAYS);
-    
-    if (new Date() > expiryDate) {
-      clearCart();
-    }
-  }
-});
-
-// Cart display component
-const CartDisplay = () => html`
-  <div class="cart">
-    <h3>Shopping Cart (${itemCount} items)</h3>
-    
-    ${cartItems().map(item => html`
-      <div class="cart-item">
-        <span>${item.name}</span>
-        <span>$${item.price} x ${item.quantity}</span>
-        <span>$${item.price * item.quantity}</span>
-        <button @click=${() => removeFromCart(item.id)}>Remove</button>
-        <input
-          type="number"
-          min="1"
-          :value=${item.quantity}
-          @change=${(e) => updateQuantity(item.id, parseInt(e.target.value))}
-        />
-      </div>
-    `)}
-    
-    <div class="cart-totals">
-      <p>Subtotal: $${subtotal}</p>
-      <p>Tax (10%): $${tax}</p>
-      <p><strong>Total: $${total}</strong></p>
-    </div>
-    
-    ${() => cartItems().length > 0 ? html`
-      <button @click=${checkout}>Checkout</button>
-      <button @click=${clearCart}>Clear Cart</button>
-    ` : html`
-      <p>Your cart is empty</p>
-    `}
-  </div>
-`;

Recent Searches History

javascript
import { $, html } from 'sigpro';
-
-// Persistent search history (max 10 items)
-const searchHistory = $.storage('search-history', []);
-
-// Add search to history
-const addSearch = (query) => {
-  if (!query.trim()) return;
-  
-  const current = searchHistory();
-  const newHistory = [
-    { query, timestamp: new Date().toISOString() },
-    ...current.filter(item => item.query !== query)
-  ].slice(0, 10); // Keep only last 10
-  
-  searchHistory(newHistory);
-};
-
-// Clear history
-const clearHistory = () => {
-  searchHistory([]);
-};
-
-// Remove specific item
-const removeFromHistory = (query) => {
-  searchHistory(searchHistory().filter(item => item.query !== query));
-};
-
-// Search component
-const SearchWithHistory = () => {
-  const searchInput = $('');
-  
-  const handleSearch = () => {
-    const query = searchInput();
-    if (query) {
-      addSearch(query);
-      performSearch(query);
-      searchInput('');
-    }
-  };
-  
-  return html`
-    <div class="search-container">
-      <div class="search-box">
-        <input
-          type="search"
-          :value=${searchInput}
-          @keydown.enter=${handleSearch}
-          placeholder="Search..."
-        />
-        <button @click=${handleSearch}>Search</button>
-      </div>
-      
-      ${() => searchHistory().length > 0 ? html`
-        <div class="search-history">
-          <h4>Recent Searches</h4>
-          ${searchHistory().map(item => html`
-            <div class="history-item">
-              <button 
-                class="history-query"
-                @click=${() => {
-                  searchInput(item.query);
-                  handleSearch();
-                }}
-              >
-                🔍 ${item.query}
-              </button>
-              <small>${new Date(item.timestamp).toLocaleString()}</small>
-              <button 
-                class="remove-btn"
-                @click=${() => removeFromHistory(item.query)}
-              >
-
-              </button>
-            </div>
-          `)}
-          <button class="clear-btn" @click=${clearHistory}>
-            Clear History
-          </button>
-        </div>
-      ` : ''}
-    </div>
-  `;
-};

Multiple Profiles / Accounts

javascript
import { $, html } from 'sigpro';
-
-// Profile manager
-const profiles = $.storage('user-profiles', {
-  current: 'default',
-  list: {
-    default: {
-      name: 'Default',
-      theme: 'light',
-      preferences: {}
-    }
-  }
-});
-
-// Switch profile
-const switchProfile = (profileId) => {
-  profiles({
-    ...profiles(),
-    current: profileId
-  });
-};
-
-// Create profile
-const createProfile = (name) => {
-  const id = `profile-${Date.now()}`;
-  profiles({
-    current: id,
-    list: {
-      ...profiles().list,
-      [id]: {
-        name,
-        theme: 'light',
-        preferences: {},
-        createdAt: new Date().toISOString()
-      }
-    }
-  });
-  return id;
-};
-
-// Delete profile
-const deleteProfile = (profileId) => {
-  if (profileId === 'default') return; // Can't delete default
-  
-  const newList = { ...profiles().list };
-  delete newList[profileId];
-  
-  profiles({
-    current: 'default',
-    list: newList
-  });
-};
-
-// Get current profile data
-const currentProfile = $(() => {
-  const { current, list } = profiles();
-  return list[current] || list.default;
-});
-
-// Profile-aware settings
-const profileTheme = $(() => currentProfile().theme);
-const profilePreferences = $(() => currentProfile().preferences);
-
-// Update profile data
-const updateCurrentProfile = (updates) => {
-  const { current, list } = profiles();
-  profiles({
-    current,
-    list: {
-      ...list,
-      [current]: {
-        ...list[current],
-        ...updates
-      }
-    }
-  });
-};
-
-// Profile selector component
-const ProfileSelector = () => html`
-  <div class="profile-selector">
-    <select 
-      :value=${() => profiles().current}
-      @change=${(e) => switchProfile(e.target.value)}
-    >
-      ${Object.entries(profiles().list).map(([id, profile]) => html`
-        <option value="${id}">${profile.name}</option>
-      `)}
-    </select>
-    
-    <button @click=${() => {
-      const name = prompt('Enter profile name:');
-      if (name) createProfile(name);
-    }}>
-      New Profile
-    </button>
-  </div>
-`;

🛡️ Error Handling

Storage Errors

javascript
import { $ } from 'sigpro';
-
-// Safe storage wrapper
-const safeStorage = (key, initialValue, storage = localStorage) => {
-  try {
-    return $.storage(key, initialValue, storage);
-  } catch (error) {
-    console.warn(`Storage failed for ${key}, using in-memory fallback:`, error);
-    return $(initialValue);
-  }
-};
-
-// Usage with fallback
-const theme = safeStorage('theme', 'light');
-const user = safeStorage('user', null);

Quota Exceeded Handling

javascript
import { $ } from 'sigpro';
-
-const createManagedStorage = (key, initialValue, maxSize = 1024 * 100) => { // 100KB limit
-  const signal = $.storage(key, initialValue);
-  
-  // Monitor size
-  const size = $(0);
-  
-  $.effect(() => {
-    try {
-      const value = signal();
-      const json = JSON.stringify(value);
-      const bytes = new Blob([json]).size;
-      
-      size(bytes);
-      
-      if (bytes > maxSize) {
-        console.warn(`Storage for ${key} exceeded ${maxSize} bytes`);
-        // Could implement cleanup strategy here
-      }
-    } catch (e) {
-      console.error('Size check failed:', e);
-    }
-  });
-  
-  return { signal, size };
-};
-
-// Usage
-const { signal: largeData, size } = createManagedStorage('app-data', {}, 50000);

📊 Storage Limits

Storage TypeTypical LimitNotes
localStorage5-10MBVaries by browser
sessionStorage5-10MBCleared when tab closes
cookies4KBNot recommended for SigPro

🎯 Best Practices

1. Validate Stored Data

javascript
import { $ } from 'sigpro';
-
-// Schema validation
-const createValidatedStorage = (key, schema, defaultValue, storage) => {
-  const signal = $.storage(key, defaultValue, storage);
-  
-  // Wrap to validate on read/write
-  const validated = (...args) => {
-    if (args.length) {
-      // Validate before writing
-      const value = args[0];
-      if (typeof value === 'function') {
-        // Handle functional updates
-        return validated(validated());
-      }
-      
-      // Basic validation
-      const isValid = Object.keys(schema).every(key => {
-        const validator = schema[key];
-        return !validator || validator(value[key]);
-      });
-      
-      if (!isValid) {
-        console.warn('Invalid data, skipping storage write');
-        return signal();
-      }
-    }
-    
-    return signal(...args);
-  };
-  
-  return validated;
-};
-
-// Usage
-const userSchema = {
-  name: v => v && v.length > 0,
-  age: v => v >= 18 && v <= 120,
-  email: v => /@/.test(v)
-};
-
-const user = createValidatedStorage('user', userSchema, {
-  name: '',
-  age: 25,
-  email: ''
-});

2. Handle Versioning

javascript
import { $ } from 'sigpro';
-
-const VERSION = 2;
-
-const createVersionedStorage = (key, migrations, storage) => {
-  const raw = $.storage(key, { version: VERSION, data: {} }, storage);
-  
-  const migrate = (data) => {
-    let current = data;
-    const currentVersion = current.version || 1;
-    
-    for (let v = currentVersion; v < VERSION; v++) {
-      const migrator = migrations[v];
-      if (migrator) {
-        current = migrator(current);
-      }
-    }
-    
-    return current;
-  };
-  
-  // Migrate if needed
-  const stored = raw();
-  if (stored.version !== VERSION) {
-    const migrated = migrate(stored);
-    raw(migrated);
-  }
-  
-  return raw;
-};
-
-// Usage
-const migrations = {
-  1: (old) => ({
-    version: 2,
-    data: {
-      ...old.data,
-      preferences: old.preferences || {}
-    }
-  })
-};
-
-const settings = createVersionedStorage('app-settings', migrations);

3. Encrypt Sensitive Data

javascript
import { $ } from 'sigpro';
-
-// Simple encryption (use proper crypto in production)
-const encrypt = (text) => {
-  return btoa(text); // Base64 - NOT secure, just example
-};
-
-const decrypt = (text) => {
-  try {
-    return atob(text);
-  } catch {
-    return null;
-  }
-};
-
-const createSecureStorage = (key, initialValue, storage) => {
-  const encryptedKey = `enc_${key}`;
-  const signal = $.storage(encryptedKey, null, storage);
-  
-  const secure = (...args) => {
-    if (args.length) {
-      // Encrypt before storing
-      const value = args[0];
-      const encrypted = encrypt(JSON.stringify(value));
-      return signal(encrypted);
-    }
-    
-    // Decrypt when reading
-    const encrypted = signal();
-    if (!encrypted) return initialValue;
-    
-    try {
-      const decrypted = decrypt(encrypted);
-      return decrypted ? JSON.parse(decrypted) : initialValue;
-    } catch {
-      return initialValue;
-    }
-  };
-  
-  return secure;
-};
-
-// Usage
-const secureToken = createSecureStorage('auth-token', null);
-secureToken('sensitive-data-123'); // Stored encrypted

📈 Performance Considerations

OperationCostNotes
Initial readO(1)Single storage read
WriteO(1) + JSON.stringifyAuto-save on change
Large objectsO(n)Stringify/parse overhead
Multiple keysO(k)k = number of keys

Pro Tip: Use sessionStorage for temporary data like form drafts, and localStorage for persistent user preferences. Always validate data when reading from storage to handle corrupted values gracefully.

- - - - \ No newline at end of file diff --git a/docs/api/tags.html b/docs/api/tags.html new file mode 100644 index 0000000..2ebf256 --- /dev/null +++ b/docs/api/tags.html @@ -0,0 +1,79 @@ + + + + + + Global Tag Helpers | SigPro + + + + + + + + + + + + + + +
Skip to content

Global Tag Helpers

In SigPro, you don't need to write $.html('div', ...) every time. To keep your code clean and readable, the engine automatically generates global helper functions for all standard HTML tags.

1. How it Works

When SigPro initializes, it runs a proxy loop that creates a function for every common HTML tag and attaches it to the window object.

  • Traditional: $.html('button', { onclick: ... }, 'Click')
  • SigPro Style: button({ onclick: ... }, 'Click')

This approach gives you a "DSL" (Domain Specific Language) that feels like HTML but is actually pure JavaScript.


2. The Global Registry

The following tags are available globally by default:

CategoryAvailable Functions
Layoutdiv, span, section, main, nav, header, footer, article, aside
Typographyh1, h2, h3, p, ul, ol, li, a, label, strong, em
Formsform, input, button, select, option, textarea
Tabletable, thead, tbody, tr, th, td
Mediaimg, video, audio, canvas, svg

3. Usage Patterns

The tag functions are highly flexible and accept arguments in different orders to suit your coding style.

A. Attributes + Content

The most common pattern.

javascript
div({ class: 'card' }, [
+  h1("Title"),
+  p("Description")
+]);

B. Content Only

If you don't need attributes, you can skip the object entirely.

javascript
div([
+  h1("Just Content"),
+  p("No attributes object needed here.")
+]);

C. Simple Text

For elements that only contain a string.

javascript
button("Submit"); // Equivalent to <button>Submit</button>

4. Reactive Tags

Since these helpers are just wrappers around $.html, they support full reactivity out of the box.

javascript
const $loading = $(true);
+
+div([
+  $loading() ? span("Loading...") : h1("Data Ready!"),
+  button({ 
+    $disabled: $loading, // Reactive attribute
+    onclick: () => $loading(false) 
+  }, "Stop Loading")
+]);

5. Under the Hood

If you are curious about how this happens without a compiler, here is the logic inside the SigPro core:

javascript
const tags = ['div', 'span', 'p', 'button', ...];
+
+tags.forEach(tag => {
+  window[tag] = (props, content) => $.html(tag, props, content);
+});

Because these are attached to window, they are available in any file in your project as soon as SigPro is loaded, making your components look like this:

javascript
// No imports required for tags!
+export default () => 
+  section({ id: 'hero' }, [
+    h1("Fast. Atomic. Simple."),
+    p("Built with SigPro.")
+  ]);

6. Full Comparison: SigPro vs. Standard HTML

To better understand the translation, here is a complete example of a User Card component. Notice how SigPro attributes with the $ prefix map to reactive behavior, while standard attributes remain static.

javascript
const $online = $(true);
+
+export const UserCard = () => (
+  div({ class: 'user-card' }, [
+    img({ src: 'avatar.png', alt: 'User' }),
+    
+    div({ class: 'info' }, [
+      h2("John Doe"),
+      p({ 
+        $class: () => $online() ? 'status-on' : 'status-off' 
+      }, [
+        "Status: ", 
+        () => $online() ? "Online" : "Offline"
+      ])
+    ]),
+    
+    button({ 
+      onclick: () => $online(!$online()) 
+    }, "Toggle Status")
+  ])
+);
html
<div class="user-card">
+  <img src="avatar.png" alt="User">
+  
+  <div class="info">
+    <h2>John Doe</h2>
+    <p class="status-on">
+      Status: Online
+    </p>
+  </div>
+  
+  <button>Toggle Status</button>
+</div>

What is happening here?

  1. Structure: The hierarchy is identical. div([...]) in JS translates directly to nested tags in HTML.
  2. Attributes: class is set once. $class is "live"; SigPro listens to the $online signal and updates the class name without re-rendering the whole card.
  3. Content: The array [...] in SigPro is the equivalent of the children inside an HTML tag.
  4. Reactivity: The function () => $online() ? ... creates a TextNode in the HTML that changes its text content surgically whenever the signal toggles.

💡 Best Practices

  1. Destructuring: If you prefer not to rely on global variables, you can destructure them from window or $ (though in SigPro, using them globally is the intended "clean" way).
  2. Custom Tags: If you need a tag that isn't in the default list (like a Web Component), you can still use the base engine: $.html('my-custom-element', { ... }).
+ + + + \ No newline at end of file diff --git a/docs/assets/api__.md.BVVMY-2O.js b/docs/assets/api__.md.BVVMY-2O.js new file mode 100644 index 0000000..87cc389 --- /dev/null +++ b/docs/assets/api__.md.BVVMY-2O.js @@ -0,0 +1,27 @@ +import{_ as i,o as t,c as a,ae as e}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"The Reactive Core: $( )","description":"","frontmatter":{},"headers":[],"relativePath":"api/$.md","filePath":"api/$.md"}'),n={name:"api/$.md"};function l(h,s,p,r,k,d){return t(),a("div",null,[...s[0]||(s[0]=[e(`

The Reactive Core: $( )

The $ function is the heart of SigPro. It is a Unified Reactive Constructor that handles state, derivations, and side effects through a single, consistent interface.

1. The Constructor: $( input )

Depending on what you pass into $( ), SigPro creates a different type of reactive primitive:

Input TypeResultInternal Behavior
Value (String, Number, Object...)SignalCreates a piece of mutable state.
FunctionComputed / EffectCreates a derived value that tracks dependencies.

2. Signal (State)

A Signal is a "box" that holds a value. It provides a getter/setter function to interact with that value.

  • When to use: For data that changes over time (counters, user input, toggle states, API data).
  • Syntax: const $state = $(initialValue);

Example:

javascript
const $name = $("Alice");
+
+// Read the value (Getter)
+console.log($name()); // "Alice"
+
+// Update the value (Setter)
+$name("Bob"); 
+
+// Update based on previous value
+$name(current => current + " Smith");

3. Computed (Derived State)

When you pass a function to $( ) that returns a value, SigPro creates a Computed Signal. It automatically tracks which signals are used inside it and re-runs only when they change.

  • When to use: For values that depend on other signals (totals, filtered lists, formatted strings).
  • Syntax: const $derived = $(() => logic);

Example:

javascript
const $price = $(100);
+const $qty = $(2);
+
+// Automatically tracks $price and $qty
+const $total = $(() => $price() * $qty());
+
+console.log($total()); // 200
+
+$qty(3); // $total updates to 300 automatically

4. Effects (Side Effects)

An Effect is a function passed to $( ) that does not return a value (or returns undefined). SigPro treats this as a subscription that performs an action whenever its dependencies change.

  • When to use: For DOM manipulations, logging, or syncing with external APIs (LocalStorage, Fetch).
  • Syntax: $(() => { action });

Example:

javascript
const $theme = $("light");
+
+// This effect runs every time $theme changes
+$(() => {
+  document.body.className = $theme();
+  console.log("Theme updated to:", $theme());
+});
+
+$theme("dark"); // Logs: Theme updated to: dark

5. Summary Table: Usage Guide

PrimitiveLogic TypeReturns Value?Typical Use Case
SignalStaticYes (Mutable)const $user = $("Guest")
ComputedRead-onlyYes (Automatic)const $isLoggedIn = $(() => $user() !== "Guest")
EffectImperativeNo$(() => localStorage.setItem('user', $user()))

💡 Pro Tip: Naming Convention

In SigPro, we use the $ prefix (e.g., $count) for variables that hold a reactive function. This makes it easy to distinguish between a standard variable and a reactive one at a glance:

javascript
let count = 0;   // Static
+const $count = $(0); // Reactive (Function)
`,30)])])}const c=i(n,[["render",l]]);export{g as __pageData,c as default}; diff --git a/docs/assets/api__.md.BVVMY-2O.lean.js b/docs/assets/api__.md.BVVMY-2O.lean.js new file mode 100644 index 0000000..727d251 --- /dev/null +++ b/docs/assets/api__.md.BVVMY-2O.lean.js @@ -0,0 +1 @@ +import{_ as i,o as t,c as a,ae as e}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"The Reactive Core: $( )","description":"","frontmatter":{},"headers":[],"relativePath":"api/$.md","filePath":"api/$.md"}'),n={name:"api/$.md"};function l(h,s,p,r,k,d){return t(),a("div",null,[...s[0]||(s[0]=[e("",30)])])}const c=i(n,[["render",l]]);export{g as __pageData,c as default}; diff --git a/docs/assets/api_components.md.BlFwj17l.js b/docs/assets/api_components.md.BlFwj17l.js deleted file mode 100644 index 8386adc..0000000 --- a/docs/assets/api_components.md.BlFwj17l.js +++ /dev/null @@ -1,571 +0,0 @@ -import{_ as i,o as a,c as n,ae as h}from"./chunks/framework.C8AWLET_.js";const F=JSON.parse('{"title":"Components API 🧩","description":"","frontmatter":{},"headers":[],"relativePath":"api/components.md","filePath":"api/components.md"}'),t={name:"api/components.md"};function l(k,s,p,e,E,r){return a(),n("div",null,[...s[0]||(s[0]=[h(`

Components API 🧩

Components in SigPro are native Web Components built on the Custom Elements standard. They provide a way to create reusable, encapsulated pieces of UI with reactive properties and automatic cleanup.

$.component(tagName, setupFunction, observedAttributes, useShadowDOM)

Creates a custom element with reactive properties and automatic dependency tracking.

javascript
import { $, html } from 'sigpro';
-
-$.component('my-button', (props, { slot, emit }) => {
-  return html\`
-    <button 
-      class="btn"
-      @click=\${() => emit('click')}
-    >
-      \${slot()}
-    </button>
-  \`;
-}, ['variant']); // Observe the 'variant' attribute

📋 API Reference

Parameters

ParameterTypeDefaultDescription
tagNamestringrequiredCustom element tag name (must include a hyphen, e.g., my-button)
setupFunctionFunctionrequiredFunction that returns the component's template
observedAttributesstring[][]Attributes to observe for changes (become reactive props)
useShadowDOMbooleanfalsetrue = Shadow DOM (encapsulated), false = Light DOM (inherits styles)

Setup Function Parameters

The setup function receives two arguments:

  1. props - Object containing reactive signals for each observed attribute
  2. context - Object with helper methods and properties

Context Object Properties

PropertyTypeDescription
slot(name)FunctionReturns array of child nodes for the specified slot
emit(name, detail)FunctionDispatches a custom event
select(selector)FunctionQuery selector within component's root
selectAll(selector)FunctionQuery selector all within component's root
hostHTMLElementReference to the custom element instance
rootNodeComponent's root (shadow root or element itself)
onUnmount(callback)FunctionRegister cleanup function

🏠 Light DOM vs Shadow DOM

Light DOM (useShadowDOM = false) - Default

The component inherits global styles from the application. Perfect for components that should integrate with your site's design system.

javascript
// Button that uses global Tailwind CSS
-$.component('tw-button', (props, { slot, emit }) => {
-  const variant = props.variant() || 'primary';
-  
-  const variants = {
-    primary: 'bg-blue-500 hover:bg-blue-600 text-white',
-    secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
-    outline: 'border border-blue-500 text-blue-500 hover:bg-blue-50'
-  };
-  
-  return html\`
-    <button 
-      class="px-4 py-2 rounded font-semibold transition-colors \${variants[variant]}"
-      @click=\${() => emit('click')}
-    >
-      \${slot()}
-    </button>
-  \`;
-}, ['variant']);

Shadow DOM (useShadowDOM = true) - Encapsulated

The component encapsulates its styles completely. External styles don't affect it, and its styles don't leak out.

javascript
// Calendar with encapsulated styles
-$.component('ui-calendar', (props) => {
-  return html\`
-    <style>
-      /* These styles won't affect the rest of the page */
-      .calendar {
-        font-family: system-ui, sans-serif;
-        background: white;
-        border-radius: 12px;
-        padding: 20px;
-        box-shadow: 0 4px 12px rgba(0,0,0,0.1);
-      }
-      .day {
-        aspect-ratio: 1;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        cursor: pointer;
-        border-radius: 50%;
-      }
-      .day.selected {
-        background: #2196f3;
-        color: white;
-      }
-    </style>
-    
-    <div class="calendar">
-      \${renderCalendar(props.date())}
-    </div>
-  \`;
-}, ['date'], true); // true = use Shadow DOM

🎯 Basic Examples

Simple Counter Component

javascript
// counter.js
-$.component('my-counter', (props) => {
-  const count = $(0);
-  
-  return html\`
-    <div class="counter">
-      <p>Count: \${count}</p>
-      <button @click=\${() => count(c => c + 1)}>+</button>
-      <button @click=\${() => count(c => c - 1)}>-</button>
-      <button @click=\${() => count(0)}>Reset</button>
-    </div>
-  \`;
-});

Usage:

html
<my-counter></my-counter>

Component with Props

javascript
// greeting.js
-$.component('my-greeting', (props) => {
-  const name = props.name() || 'World';
-  const greeting = $(() => \`Hello, \${name}!\`);
-  
-  return html\`
-    <div class="greeting">
-      <h1>\${greeting}</h1>
-      <p>This is a greeting component.</p>
-    </div>
-  \`;
-}, ['name']); // Observe the 'name' attribute

Usage:

html
<my-greeting name="John"></my-greeting>
-<my-greeting name="Jane"></my-greeting>

Component with Events

javascript
// toggle.js
-$.component('my-toggle', (props, { emit }) => {
-  const isOn = $(props.initial() === 'on');
-  
-  const toggle = () => {
-    isOn(!isOn());
-    emit('toggle', { isOn: isOn() });
-    emit(isOn() ? 'on' : 'off');
-  };
-  
-  return html\`
-    <button 
-      class="toggle \${() => isOn() ? 'active' : ''}"
-      @click=\${toggle}
-    >
-      \${() => isOn() ? 'ON' : 'OFF'}
-    </button>
-  \`;
-}, ['initial']);

Usage:

html
<my-toggle 
-  initial="off"
-  @toggle=\${(e) => console.log('Toggled:', e.detail)}
-  @on=\${() => console.log('Turned on')}
-  @off=\${() => console.log('Turned off')}
-></my-toggle>

🎨 Advanced Examples

Form Input Component

javascript
// form-input.js
-$.component('form-input', (props, { emit }) => {
-  const value = $(props.value() || '');
-  const error = $(null);
-  const touched = $(false);
-  
-  // Validation effect
-  $.effect(() => {
-    if (props.pattern() && touched()) {
-      const regex = new RegExp(props.pattern());
-      const isValid = regex.test(value());
-      error(isValid ? null : props.errorMessage() || 'Invalid input');
-      emit('validate', { isValid, value: value() });
-    }
-  });
-  
-  const handleInput = (e) => {
-    value(e.target.value);
-    emit('update', e.target.value);
-  };
-  
-  const handleBlur = () => {
-    touched(true);
-  };
-  
-  return html\`
-    <div class="form-group">
-      \${props.label() ? html\`
-        <label class="form-label">
-          \${props.label()}
-          \${props.required() ? html\`<span class="required">*</span>\` : ''}
-        </label>
-      \` : ''}
-      
-      <input
-        type="\${props.type() || 'text'}"
-        class="form-control \${() => error() ? 'is-invalid' : ''}"
-        :value=\${value}
-        @input=\${handleInput}
-        @blur=\${handleBlur}
-        placeholder="\${props.placeholder() || ''}"
-        ?disabled=\${props.disabled}
-        ?required=\${props.required}
-      />
-      
-      \${() => error() ? html\`
-        <div class="error-message">\${error()}</div>
-      \` : ''}
-      
-      \${props.helpText() ? html\`
-        <small class="help-text">\${props.helpText()}</small>
-      \` : ''}
-    </div>
-  \`;
-}, ['label', 'type', 'value', 'placeholder', 'disabled', 'required', 'pattern', 'errorMessage', 'helpText']);

Usage:

html
<form-input
-  label="Email"
-  type="email"
-  required
-  pattern="^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"
-  errorMessage="Please enter a valid email"
-  @update=\${(e) => formData.email = e.detail}
-  @validate=\${(e) => setEmailValid(e.detail.isValid)}
->
-</form-input>
javascript
// modal.js
-$.component('my-modal', (props, { slot, emit, onUnmount }) => {
-  const isOpen = $(false);
-  
-  // Handle escape key
-  const handleKeydown = (e) => {
-    if (e.key === 'Escape' && isOpen()) {
-      close();
-    }
-  };
-  
-  $.effect(() => {
-    if (isOpen()) {
-      document.addEventListener('keydown', handleKeydown);
-      document.body.style.overflow = 'hidden';
-    } else {
-      document.removeEventListener('keydown', handleKeydown);
-      document.body.style.overflow = '';
-    }
-  });
-  
-  // Cleanup on unmount
-  onUnmount(() => {
-    document.removeEventListener('keydown', handleKeydown);
-    document.body.style.overflow = '';
-  });
-  
-  const open = () => {
-    isOpen(true);
-    emit('open');
-  };
-  
-  const close = () => {
-    isOpen(false);
-    emit('close');
-  };
-  
-  // Expose methods to parent
-  props.open = open;
-  props.close = close;
-  
-  return html\`
-    <div>
-      <!-- Trigger button -->
-      <button 
-        class="modal-trigger"
-        @click=\${open}
-      >
-        \${slot('trigger') || 'Open Modal'}
-      </button>
-      
-      <!-- Modal overlay -->
-      \${() => isOpen() ? html\`
-        <div class="modal-overlay" @click=\${close}>
-          <div class="modal-content" @click.stop>
-            <div class="modal-header">
-              <h3>\${props.title() || 'Modal'}</h3>
-              <button class="close-btn" @click=\${close}>&times;</button>
-            </div>
-            <div class="modal-body">
-              \${slot('body')}
-            </div>
-            <div class="modal-footer">
-              \${slot('footer') || html\`
-                <button @click=\${close}>Close</button>
-              \`}
-            </div>
-          </div>
-        </div>
-      \` : ''}
-    </div>
-  \`;
-}, ['title'], false);

Usage:

html
<my-modal title="Confirm Delete">
-  <button slot="trigger">Delete Item</button>
-  
-  <div slot="body">
-    <p>Are you sure you want to delete this item?</p>
-    <p class="warning">This action cannot be undone.</p>
-  </div>
-  
-  <div slot="footer">
-    <button class="cancel" @click=\${close}>Cancel</button>
-    <button class="delete" @click=\${handleDelete}>Delete</button>
-  </div>
-</my-modal>

Data Table Component

javascript
// data-table.js
-$.component('data-table', (props, { emit }) => {
-  const data = $(props.data() || []);
-  const columns = $(props.columns() || []);
-  const sortColumn = $(null);
-  const sortDirection = $('asc');
-  const filterText = $('');
-  
-  // Computed: filtered and sorted data
-  const processedData = $(() => {
-    let result = [...data()];
-    
-    // Filter
-    if (filterText()) {
-      const search = filterText().toLowerCase();
-      result = result.filter(row => 
-        Object.values(row).some(val => 
-          String(val).toLowerCase().includes(search)
-        )
-      );
-    }
-    
-    // Sort
-    if (sortColumn()) {
-      const col = sortColumn();
-      const direction = sortDirection() === 'asc' ? 1 : -1;
-      
-      result.sort((a, b) => {
-        if (a[col] < b[col]) return -direction;
-        if (a[col] > b[col]) return direction;
-        return 0;
-      });
-    }
-    
-    return result;
-  });
-  
-  const handleSort = (col) => {
-    if (sortColumn() === col) {
-      sortDirection(sortDirection() === 'asc' ? 'desc' : 'asc');
-    } else {
-      sortColumn(col);
-      sortDirection('asc');
-    }
-    emit('sort', { column: col, direction: sortDirection() });
-  };
-  
-  return html\`
-    <div class="data-table">
-      <!-- Search input -->
-      <div class="table-toolbar">
-        <input
-          type="search"
-          :value=\${filterText}
-          placeholder="Search..."
-          class="search-input"
-        />
-        <span class="record-count">
-          \${() => \`\${processedData().length} of \${data().length} records\`}
-        </span>
-      </div>
-      
-      <!-- Table -->
-      <table>
-        <thead>
-          <tr>
-            \${columns().map(col => html\`
-              <th 
-                @click=\${() => handleSort(col.field)}
-                class:sortable=\${true}
-                class:sorted=\${() => sortColumn() === col.field}
-              >
-                \${col.label}
-                \${() => sortColumn() === col.field ? html\`
-                  <span class="sort-icon">
-                    \${sortDirection() === 'asc' ? '↑' : '↓'}
-                  </span>
-                \` : ''}
-              </th>
-            \`)}
-          </tr>
-        </thead>
-        <tbody>
-          \${() => processedData().map(row => html\`
-            <tr @click=\${() => emit('row-click', row)}>
-              \${columns().map(col => html\`
-                <td>\${row[col.field]}</td>
-              \`)}
-            </tr>
-          \`)}
-        </tbody>
-      </table>
-      
-      <!-- Empty state -->
-      \${() => processedData().length === 0 ? html\`
-        <div class="empty-state">
-          No data found
-        </div>
-      \` : ''}
-    </div>
-  \`;
-}, ['data', 'columns']);

Usage:

javascript
const userColumns = [
-  { field: 'id', label: 'ID' },
-  { field: 'name', label: 'Name' },
-  { field: 'email', label: 'Email' },
-  { field: 'role', label: 'Role' }
-];
-
-const userData = [
-  { id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' },
-  { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' }
-];
html
<data-table 
-  .data=\${userData}
-  .columns=\${userColumns}
-  @row-click=\${(e) => console.log('Row clicked:', e.detail)}
->
-</data-table>

Tabs Component

javascript
// tabs.js
-$.component('my-tabs', (props, { slot, emit }) => {
-  const activeTab = $(props.active() || 0);
-  
-  // Get all tab headers from slots
-  const tabs = $(() => {
-    const headers = slot('tab');
-    return headers.map((node, index) => ({
-      index,
-      title: node.textContent,
-      content: slot(\`panel-\${index}\`)[0]
-    }));
-  });
-  
-  $.effect(() => {
-    emit('change', { index: activeTab(), tab: tabs()[activeTab()] });
-  });
-  
-  return html\`
-    <div class="tabs">
-      <div class="tab-headers">
-        \${tabs().map(tab => html\`
-          <button
-            class="tab-header \${() => activeTab() === tab.index ? 'active' : ''}"
-            @click=\${() => activeTab(tab.index)}
-          >
-            \${tab.title}
-          </button>
-        \`)}
-      </div>
-      
-      <div class="tab-panels">
-        \${tabs().map(tab => html\`
-          <div 
-            class="tab-panel"
-            style="display: \${() => activeTab() === tab.index ? 'block' : 'none'}"
-          >
-            \${tab.content}
-          </div>
-        \`)}
-      </div>
-    </div>
-  \`;
-}, ['active']);

Usage:

html
<my-tabs @change=\${(e) => console.log('Tab changed:', e.detail)}>
-  <div slot="tab">Profile</div>
-  <div slot="panel-0">
-    <h3>Profile Settings</h3>
-    <form>...</form>
-  </div>
-  
-  <div slot="tab">Security</div>
-  <div slot="panel-1">
-    <h3>Security Settings</h3>
-    <form>...</form>
-  </div>
-  
-  <div slot="tab">Notifications</div>
-  <div slot="panel-2">
-    <h3>Notification Preferences</h3>
-    <form>...</form>
-  </div>
-</my-tabs>

Component with External Data

javascript
// user-profile.js
-$.component('user-profile', (props, { emit, onUnmount }) => {
-  const user = $(null);
-  const loading = $(false);
-  const error = $(null);
-  
-  // Fetch user data when userId changes
-  $.effect(() => {
-    const userId = props.userId();
-    if (!userId) return;
-    
-    loading(true);
-    error(null);
-    
-    const controller = new AbortController();
-    
-    fetch(\`/api/users/\${userId}\`, { signal: controller.signal })
-      .then(res => res.json())
-      .then(data => {
-        user(data);
-        emit('loaded', data);
-      })
-      .catch(err => {
-        if (err.name !== 'AbortError') {
-          error(err.message);
-          emit('error', err);
-        }
-      })
-      .finally(() => loading(false));
-    
-    // Cleanup: abort fetch if component unmounts or userId changes
-    onUnmount(() => controller.abort());
-  });
-  
-  return html\`
-    <div class="user-profile">
-      \${() => loading() ? html\`
-        <div class="spinner">Loading...</div>
-      \` : error() ? html\`
-        <div class="error">Error: \${error()}</div>
-      \` : user() ? html\`
-        <div class="user-info">
-          <img src="\${user().avatar}" class="avatar" />
-          <h2>\${user().name}</h2>
-          <p>\${user().email}</p>
-          <p>Member since: \${new Date(user().joined).toLocaleDateString()}</p>
-        </div>
-      \` : html\`
-        <div class="no-user">No user selected</div>
-      \`}
-    </div>
-  \`;
-}, ['user-id']);

📦 Component Libraries

Building a Reusable Component Library

javascript
// components/index.js
-import { $, html } from 'sigpro';
-
-// Button component
-export const Button = $.component('ui-button', (props, { slot, emit }) => {
-  const variant = props.variant() || 'primary';
-  const size = props.size() || 'md';
-  
-  const sizes = {
-    sm: 'px-2 py-1 text-sm',
-    md: 'px-4 py-2',
-    lg: 'px-6 py-3 text-lg'
-  };
-  
-  const variants = {
-    primary: 'bg-blue-500 hover:bg-blue-600 text-white',
-    secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
-    danger: 'bg-red-500 hover:bg-red-600 text-white'
-  };
-  
-  return html\`
-    <button
-      class="rounded font-semibold transition-colors \${sizes[size]} \${variants[variant]}"
-      ?disabled=\${props.disabled}
-      @click=\${() => emit('click')}
-    >
-      \${slot()}
-    </button>
-  \`;
-}, ['variant', 'size', 'disabled']);
-
-// Card component
-export const Card = $.component('ui-card', (props, { slot }) => {
-  return html\`
-    <div class="card border rounded-lg shadow-sm overflow-hidden">
-      \${props.title() ? html\`
-        <div class="card-header bg-gray-50 px-4 py-3 border-b">
-          <h3 class="font-semibold">\${props.title()}</h3>
-        </div>
-      \` : ''}
-      
-      <div class="card-body p-4">
-        \${slot()}
-      </div>
-      
-      \${props.footer() ? html\`
-        <div class="card-footer bg-gray-50 px-4 py-3 border-t">
-          \${slot('footer')}
-        </div>
-      \` : ''}
-    </div>
-  \`;
-}, ['title']);
-
-// Badge component
-export const Badge = $.component('ui-badge', (props, { slot }) => {
-  const type = props.type() || 'default';
-  
-  const types = {
-    default: 'bg-gray-100 text-gray-800',
-    success: 'bg-green-100 text-green-800',
-    warning: 'bg-yellow-100 text-yellow-800',
-    error: 'bg-red-100 text-red-800',
-    info: 'bg-blue-100 text-blue-800'
-  };
-  
-  return html\`
-    <span class="inline-block px-2 py-1 text-xs font-semibold rounded \${types[type]}">
-      \${slot()}
-    </span>
-  \`;
-}, ['type']);
-
-export { $, html };

Usage:

javascript
import { Button, Card, Badge } from './components/index.js';
-
-// Use components anywhere
-const app = html\`
-  <div>
-    <Card title="Welcome">
-      <p>This is a card component</p>
-      <div slot="footer">
-        <Button variant="primary" @click=\${handleClick}>
-          Save Changes
-        </Button>
-        <Badge type="success">New</Badge>
-      </div>
-    </Card>
-  </div>
-\`;

🎯 Decision Guide: Light DOM vs Shadow DOM

Use Light DOM (false) when...Use Shadow DOM (true) when...
Component is part of your main appBuilding a UI library for others
Using global CSS (Tailwind, Bootstrap)Creating embeddable widgets
Need to inherit theme variablesStyles must be pixel-perfect everywhere
Working with existing design systemComponent has complex, specific styles
Quick prototypingDistributing to different projects
Form elements that should match siteNeed style isolation/encapsulation

📊 Summary

FeatureDescription
Native Web ComponentsBuilt on Custom Elements standard
Reactive PropsObserved attributes become signals
Two Rendering ModesLight DOM (default) or Shadow DOM
Automatic CleanupEffects and listeners cleaned up on disconnect
Event SystemCustom events with emit()
Slot SupportFull slot API for content projection
Zero DependenciesPure vanilla JavaScript

Pro Tip: Start with Light DOM components for app-specific UI, and use Shadow DOM when building components that need to work identically across different projects or websites.

`,64)])])}const g=i(t,[["render",l]]);export{F as __pageData,g as default}; diff --git a/docs/assets/api_components.md.BlFwj17l.lean.js b/docs/assets/api_components.md.BlFwj17l.lean.js deleted file mode 100644 index f21251e..0000000 --- a/docs/assets/api_components.md.BlFwj17l.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as i,o as a,c as n,ae as h}from"./chunks/framework.C8AWLET_.js";const F=JSON.parse('{"title":"Components API 🧩","description":"","frontmatter":{},"headers":[],"relativePath":"api/components.md","filePath":"api/components.md"}'),t={name:"api/components.md"};function l(k,s,p,e,E,r){return a(),n("div",null,[...s[0]||(s[0]=[h("",64)])])}const g=i(t,[["render",l]]);export{F as __pageData,g as default}; diff --git a/docs/assets/api_effects.md.Br_yStBS.js b/docs/assets/api_effects.md.Br_yStBS.js deleted file mode 100644 index 8d2bef7..0000000 --- a/docs/assets/api_effects.md.Br_yStBS.js +++ /dev/null @@ -1,763 +0,0 @@ -import{_ as i,o as a,c as n,ae as h}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Effects API 🔄","description":"","frontmatter":{},"headers":[],"relativePath":"api/effects.md","filePath":"api/effects.md"}'),k={name:"api/effects.md"};function l(t,s,p,e,E,r){return a(),n("div",null,[...s[0]||(s[0]=[h(`

Effects API 🔄

Effects are the bridge between reactive signals and side effects in your application. They automatically track signal dependencies and re-run whenever those signals change, enabling everything from DOM updates to data fetching and localStorage synchronization.

Core Concepts

What is an Effect?

An effect is a function that:

  • Runs immediately when created
  • Tracks all signals read during its execution
  • Re-runs automatically when any tracked signal changes
  • Can return a cleanup function that runs before the next execution or when the effect is stopped

How Effects Work

  1. When an effect runs, it sets itself as the activeEffect
  2. Any signal read during execution adds the effect to its subscribers
  3. When a signal changes, it queues all its subscribers
  4. Effects are batched and run in the next microtask
  5. If an effect returns a function, it's stored as a cleanup handler

$.effect(effectFn)

Creates a reactive effect that automatically tracks dependencies and re-runs when they change.

javascript
import { $ } from 'sigpro';
-
-const count = $(0);
-
-$.effect(() => {
-  console.log(\`Count is: \${count()}\`);
-});
-// Logs: "Count is: 0"
-
-count(1);
-// Logs: "Count is: 1"

📋 API Reference

PatternExampleDescription
Basic Effect$.effect(() => console.log(count()))Run on dependency changes
With Cleanup$.effect(() => { timer = setInterval(...); return () => clearInterval(timer) })Return cleanup function
Stop Effectconst stop = $.effect(...); stop()Manually stop an effect

Effect Object (Internal)

Property/MethodDescription
dependenciesSet of signal subscriber sets this effect belongs to
cleanupHandlersSet of cleanup functions to run before next execution
run()Executes the effect and tracks dependencies
stop()Stops the effect and runs all cleanup handlers

🎯 Basic Examples

Console Logging

javascript
import { $ } from 'sigpro';
-
-const name = $('World');
-const count = $(0);
-
-$.effect(() => {
-  console.log(\`Hello \${name()}! Count is \${count()}\`);
-});
-// Logs: "Hello World! Count is 0"
-
-name('John');
-// Logs: "Hello John! Count is 0"
-
-count(5);
-// Logs: "Hello John! Count is 5"

DOM Updates

javascript
import { $ } from 'sigpro';
-
-const count = $(0);
-const element = document.getElementById('counter');
-
-$.effect(() => {
-  element.textContent = \`Count: \${count()}\`;
-});
-
-// Updates DOM automatically when count changes
-count(10); // Element text becomes "Count: 10"

Document Title

javascript
import { $ } from 'sigpro';
-
-const page = $('home');
-const unreadCount = $(0);
-
-$.effect(() => {
-  const base = page() === 'home' ? 'Home' : 'Dashboard';
-  const unread = unreadCount() > 0 ? \` (\${unreadCount()})\` : '';
-  document.title = \`\${base}\${unread} - My App\`;
-});
-
-page('dashboard'); // Title: "Dashboard - My App"
-unreadCount(3);    // Title: "Dashboard (3) - My App"

🧹 Effects with Cleanup

Cleanup functions are essential for managing resources like intervals, event listeners, and subscriptions.

Basic Cleanup

javascript
import { $ } from 'sigpro';
-
-const userId = $(1);
-
-$.effect(() => {
-  const id = userId();
-  console.log(\`Setting up timer for user \${id}\`);
-  
-  const timer = setInterval(() => {
-    console.log(\`Polling user \${id}...\`);
-  }, 1000);
-  
-  // Cleanup runs before next effect execution
-  return () => {
-    console.log(\`Cleaning up timer for user \${id}\`);
-    clearInterval(timer);
-  };
-});
-// Sets up timer for user 1
-
-userId(2);
-// Cleans up timer for user 1
-// Sets up timer for user 2

Event Listener Cleanup

javascript
import { $ } from 'sigpro';
-
-const isListening = $(false);
-
-$.effect(() => {
-  if (!isListening()) return;
-  
-  const handleClick = (e) => {
-    console.log('Window clicked:', e.clientX, e.clientY);
-  };
-  
-  window.addEventListener('click', handleClick);
-  console.log('Click listener added');
-  
-  return () => {
-    window.removeEventListener('click', handleClick);
-    console.log('Click listener removed');
-  };
-});
-
-isListening(true);  // Adds listener
-isListening(false); // Removes listener
-isListening(true);  // Adds listener again

WebSocket Connection

javascript
import { $ } from 'sigpro';
-
-const room = $('general');
-const messages = $([]);
-
-$.effect(() => {
-  const currentRoom = room();
-  console.log(\`Connecting to room: \${currentRoom}\`);
-  
-  const ws = new WebSocket(\`wss://chat.example.com/\${currentRoom}\`);
-  
-  ws.onmessage = (event) => {
-    messages([...messages(), JSON.parse(event.data)]);
-  };
-  
-  ws.onerror = (error) => {
-    console.error('WebSocket error:', error);
-  };
-  
-  // Cleanup: close connection when room changes
-  return () => {
-    console.log(\`Disconnecting from room: \${currentRoom}\`);
-    ws.close();
-  };
-});
-
-room('random'); // Closes 'general' connection, opens 'random'

⏱️ Effect Timing and Batching

Microtask Batching

Effects are batched using queueMicrotask for optimal performance:

javascript
import { $ } from 'sigpro';
-
-const a = $(1);
-const b = $(2);
-const c = $(3);
-
-$.effect(() => {
-  console.log('Effect ran with:', a(), b(), c());
-});
-// Logs immediately: "Effect ran with: 1 2 3"
-
-// Multiple updates in same tick - only one effect run!
-a(10);
-b(20);
-c(30);
-// Only logs once: "Effect ran with: 10 20 30"

Async Effects

Effects can be asynchronous, but be careful with dependency tracking:

javascript
import { $ } from 'sigpro';
-
-const userId = $(1);
-const userData = $(null);
-
-$.effect(() => {
-  const id = userId();
-  console.log(\`Fetching user \${id}...\`);
-  
-  // Only id() is tracked (synchronous part)
-  fetch(\`/api/users/\${id}\`)
-    .then(res => res.json())
-    .then(data => {
-      // This runs later - no dependency tracking here!
-      userData(data);
-    });
-});
-
-userId(2); // Triggers effect again, cancels previous fetch

Effect with AbortController

For proper async cleanup with fetch:

javascript
import { $ } from 'sigpro';
-
-const userId = $(1);
-const userData = $(null);
-const loading = $(false);
-
-$.effect(() => {
-  const id = userId();
-  const controller = new AbortController();
-  
-  loading(true);
-  
-  fetch(\`/api/users/\${id}\`, { signal: controller.signal })
-    .then(res => res.json())
-    .then(data => {
-      userData(data);
-      loading(false);
-    })
-    .catch(err => {
-      if (err.name !== 'AbortError') {
-        console.error('Fetch error:', err);
-        loading(false);
-      }
-    });
-  
-  // Cleanup: abort fetch if userId changes before completion
-  return () => {
-    controller.abort();
-  };
-});

🎨 Advanced Effect Patterns

Debounced Effects

javascript
import { $ } from 'sigpro';
-
-const searchTerm = $('');
-const results = $([]);
-let debounceTimeout;
-
-$.effect(() => {
-  const term = searchTerm();
-  
-  // Clear previous timeout
-  clearTimeout(debounceTimeout);
-  
-  // Don't search if term is too short
-  if (term.length < 3) {
-    results([]);
-    return;
-  }
-  
-  // Debounce search
-  debounceTimeout = setTimeout(async () => {
-    console.log('Searching for:', term);
-    const data = await fetch(\`/api/search?q=\${term}\`).then(r => r.json());
-    results(data);
-  }, 300);
-  
-  // Cleanup on effect re-run
-  return () => clearTimeout(debounceTimeout);
-});

Throttled Effects

javascript
import { $ } from 'sigpro';
-
-const scrollPosition = $(0);
-let lastRun = 0;
-let rafId = null;
-
-$.effect(() => {
-  const pos = scrollPosition();
-  
-  // Throttle with requestAnimationFrame
-  if (rafId) cancelAnimationFrame(rafId);
-  
-  rafId = requestAnimationFrame(() => {
-    console.log('Scroll position:', pos);
-    updateScrollUI(pos);
-    lastRun = Date.now();
-    rafId = null;
-  });
-  
-  return () => {
-    if (rafId) {
-      cancelAnimationFrame(rafId);
-      rafId = null;
-    }
-  };
-});
-
-// Even with many updates, effect runs at most once per frame
-for (let i = 0; i < 100; i++) {
-  scrollPosition(i);
-}

Conditional Effects

javascript
import { $ } from 'sigpro';
-
-const isEnabled = $(false);
-const value = $(0);
-const threshold = $(10);
-
-$.effect(() => {
-  // Effect only runs when isEnabled is true
-  if (!isEnabled()) return;
-  
-  console.log(\`Monitoring value: \${value()}, threshold: \${threshold()}\`);
-  
-  if (value() > threshold()) {
-    alert(\`Value \${value()} exceeded threshold \${threshold()}!\`);
-  }
-});
-
-isEnabled(true);  // Effect starts monitoring
-value(15);        // Triggers alert
-isEnabled(false); // Effect stops (still runs, but condition prevents logic)

Effect with Multiple Cleanups

javascript
import { $ } from 'sigpro';
-
-const config = $({ theme: 'light', notifications: true });
-
-$.effect(() => {
-  const { theme, notifications } = config();
-  const cleanups = [];
-  
-  // Setup theme
-  document.body.className = \`theme-\${theme}\`;
-  cleanups.push(() => {
-    document.body.classList.remove(\`theme-\${theme}\`);
-  });
-  
-  // Setup notifications
-  if (notifications) {
-    const handler = (e) => console.log('Notification:', e.detail);
-    window.addEventListener('notification', handler);
-    cleanups.push(() => {
-      window.removeEventListener('notification', handler);
-    });
-  }
-  
-  // Return combined cleanup
-  return () => {
-    cleanups.forEach(cleanup => cleanup());
-  };
-});

🎯 Effects in Components

Component Lifecycle

javascript
import { $, html } from 'sigpro';
-
-$.component('timer-display', () => {
-  const seconds = $(0);
-  
-  // Effect for timer - automatically cleaned up when component unmounts
-  $.effect(() => {
-    const interval = setInterval(() => {
-      seconds(s => s + 1);
-    }, 1000);
-    
-    return () => clearInterval(interval);
-  });
-  
-  return html\`
-    <div>
-      <h2>Timer: \${seconds}s</h2>
-    </div>
-  \`;
-});

Effects with Props

javascript
import { $, html } from 'sigpro';
-
-$.component('data-viewer', (props) => {
-  const data = $(null);
-  const error = $(null);
-  
-  // Effect reacts to prop changes
-  $.effect(() => {
-    const url = props.url();
-    if (!url) return;
-    
-    const controller = new AbortController();
-    
-    fetch(url, { signal: controller.signal })
-      .then(res => res.json())
-      .then(data)
-      .catch(err => {
-        if (err.name !== 'AbortError') {
-          error(err.message);
-        }
-      });
-    
-    return () => controller.abort();
-  });
-  
-  return html\`
-    <div>
-      \${() => {
-        if (error()) return html\`<div class="error">\${error()}</div>\`;
-        if (!data()) return html\`<div>Loading...</div>\`;
-        return html\`<pre>\${JSON.stringify(data(), null, 2)}</pre>\`;
-      }}
-    </div>
-  \`;
-}, ['url']);

🔧 Effect Management

Stopping Effects

javascript
import { $ } from 'sigpro';
-
-const count = $(0);
-
-// Start effect
-const stopEffect = $.effect(() => {
-  console.log('Count:', count());
-});
-
-count(1); // Logs: "Count: 1"
-count(2); // Logs: "Count: 2"
-
-// Stop the effect
-stopEffect();
-
-count(3); // No logging - effect is stopped

Conditional Effect Stopping

javascript
import { $ } from 'sigpro';
-
-const isActive = $(true);
-const count = $(0);
-
-let currentEffect = null;
-
-$.effect(() => {
-  if (isActive()) {
-    // Start or restart the monitoring effect
-    if (currentEffect) currentEffect();
-    
-    currentEffect = $.effect(() => {
-      console.log('Monitoring count:', count());
-    });
-  } else {
-    // Stop monitoring
-    if (currentEffect) {
-      currentEffect();
-      currentEffect = null;
-    }
-  }
-});

Nested Effects

javascript
import { $ } from 'sigpro';
-
-const user = $({ id: 1, name: 'John' });
-const settings = $({ theme: 'dark' });
-
-$.effect(() => {
-  console.log('User changed:', user().name);
-  
-  // Nested effect - tracks settings independently
-  $.effect(() => {
-    console.log('Settings changed:', settings().theme);
-  });
-  
-  // When user changes, the nested effect is recreated
-});

🚀 Real-World Examples

Auto-saving Form

javascript
import { $ } from 'sigpro';
-
-const formData = $({
-  title: '',
-  content: '',
-  tags: []
-});
-
-const lastSaved = $(null);
-const saveStatus = $('idle'); // 'idle', 'saving', 'saved', 'error'
-let saveTimeout;
-
-$.effect(() => {
-  const data = formData();
-  
-  // Clear previous timeout
-  clearTimeout(saveTimeout);
-  
-  // Don't save empty form
-  if (!data.title && !data.content) {
-    saveStatus('idle');
-    return;
-  }
-  
-  saveStatus('saving');
-  
-  // Debounce save
-  saveTimeout = setTimeout(async () => {
-    try {
-      await fetch('/api/posts', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify(data)
-      });
-      saveStatus('saved');
-      lastSaved(new Date());
-    } catch (error) {
-      saveStatus('error');
-      console.error('Auto-save failed:', error);
-    }
-  }, 1000);
-  
-  return () => clearTimeout(saveTimeout);
-});
-
-// UI feedback
-const statusMessage = $(() => {
-  const status = saveStatus();
-  const saved = lastSaved();
-  
-  if (status === 'saving') return 'Saving...';
-  if (status === 'error') return 'Save failed';
-  if (status === 'saved' && saved) {
-    return \`Last saved: \${saved().toLocaleTimeString()}\`;
-  }
-  return '';
-});

Real-time Search with Debounce

javascript
import { $ } from 'sigpro';
-
-const searchInput = $('');
-const searchResults = $([]);
-const searchStatus = $('idle'); // 'idle', 'searching', 'results', 'no-results', 'error'
-let searchTimeout;
-let abortController = null;
-
-$.effect(() => {
-  const query = searchInput().trim();
-  
-  // Clear previous timeout
-  clearTimeout(searchTimeout);
-  
-  // Cancel previous request
-  if (abortController) {
-    abortController.abort();
-    abortController = null;
-  }
-  
-  // Don't search for short queries
-  if (query.length < 2) {
-    searchResults([]);
-    searchStatus('idle');
-    return;
-  }
-  
-  searchStatus('searching');
-  
-  // Debounce search
-  searchTimeout = setTimeout(async () => {
-    abortController = new AbortController();
-    
-    try {
-      const response = await fetch(\`/api/search?q=\${encodeURIComponent(query)}\`, {
-        signal: abortController.signal
-      });
-      
-      const data = await response.json();
-      
-      if (!abortController.signal.aborted) {
-        searchResults(data);
-        searchStatus(data.length ? 'results' : 'no-results');
-        abortController = null;
-      }
-    } catch (error) {
-      if (error.name !== 'AbortError') {
-        console.error('Search failed:', error);
-        searchStatus('error');
-      }
-    }
-  }, 300);
-  
-  return () => {
-    clearTimeout(searchTimeout);
-    if (abortController) {
-      abortController.abort();
-      abortController = null;
-    }
-  };
-});

Analytics Tracking

javascript
import { $ } from 'sigpro';
-
-// Analytics configuration
-const analyticsEnabled = $(true);
-const currentPage = $('/');
-const userProperties = $({});
-
-// Track page views
-$.effect(() => {
-  if (!analyticsEnabled()) return;
-  
-  const page = currentPage();
-  const properties = userProperties();
-  
-  console.log('Track page view:', page, properties);
-  
-  // Send to analytics
-  gtag('config', 'GA-MEASUREMENT-ID', {
-    page_path: page,
-    ...properties
-  });
-});
-
-// Track user interactions
-const trackEvent = (eventName, properties = {}) => {
-  $.effect(() => {
-    if (!analyticsEnabled()) return;
-    
-    console.log('Track event:', eventName, properties);
-    gtag('event', eventName, properties);
-  });
-};
-
-// Usage
-currentPage('/dashboard');
-userProperties({ userId: 123, plan: 'premium' });
-trackEvent('button_click', { buttonId: 'signup' });

Keyboard Shortcuts

javascript
import { $ } from 'sigpro';
-
-const shortcuts = $({
-  'ctrl+s': { handler: null, description: 'Save' },
-  'ctrl+z': { handler: null, description: 'Undo' },
-  'ctrl+shift+z': { handler: null, description: 'Redo' },
-  'escape': { handler: null, description: 'Close modal' }
-});
-
-const pressedKeys = new Set();
-
-$.effect(() => {
-  const handleKeyDown = (e) => {
-    const key = e.key.toLowerCase();
-    const ctrl = e.ctrlKey ? 'ctrl+' : '';
-    const shift = e.shiftKey ? 'shift+' : '';
-    const alt = e.altKey ? 'alt+' : '';
-    const meta = e.metaKey ? 'meta+' : '';
-    
-    const combo = \`\${ctrl}\${shift}\${alt}\${meta}\${key}\`.replace(/\\+$/, '');
-    
-    const shortcut = shortcuts()[combo];
-    if (shortcut?.handler) {
-      e.preventDefault();
-      shortcut.handler();
-    }
-  };
-  
-  window.addEventListener('keydown', handleKeyDown);
-  
-  return () => window.removeEventListener('keydown', handleKeyDown);
-});
-
-// Register shortcuts
-shortcuts({
-  ...shortcuts(),
-  'ctrl+s': {
-    handler: () => saveDocument(),
-    description: 'Save document'
-  },
-  'ctrl+z': {
-    handler: () => undo(),
-    description: 'Undo'
-  }
-});

Infinite Scroll

javascript
import { $ } from 'sigpro';
-
-const posts = $([]);
-const page = $(1);
-const hasMore = $(true);
-const loading = $(false);
-let observer = null;
-
-// Load more posts
-const loadMore = async () => {
-  if (loading() || !hasMore()) return;
-  
-  loading(true);
-  try {
-    const response = await fetch(\`/api/posts?page=\${page()}\`);
-    const newPosts = await response.json();
-    
-    if (newPosts.length === 0) {
-      hasMore(false);
-    } else {
-      posts([...posts(), ...newPosts]);
-      page(p => p + 1);
-    }
-  } finally {
-    loading(false);
-  }
-};
-
-// Setup intersection observer for infinite scroll
-$.effect(() => {
-  const sentinel = document.getElementById('sentinel');
-  if (!sentinel) return;
-  
-  observer = new IntersectionObserver(
-    (entries) => {
-      if (entries[0].isIntersecting && !loading() && hasMore()) {
-        loadMore();
-      }
-    },
-    { threshold: 0.1 }
-  );
-  
-  observer.observe(sentinel);
-  
-  return () => {
-    if (observer) {
-      observer.disconnect();
-      observer = null;
-    }
-  };
-});
-
-// Initial load
-loadMore();

📊 Performance Considerations

PatternPerformance ImpactBest Practice
Multiple signal readsO(n) per effectGroup related signals
Deep object accessMinimalUse computed signals
Large arraysO(n) for iterationMemoize with computed
Frequent updatesBatchedLet batching work
Heavy computationsBlockingUse Web Workers

🎯 Best Practices

1. Keep Effects Focused

javascript
// ❌ Avoid doing too much in one effect
-$.effect(() => {
-  updateUI(count());           // UI update
-  saveToStorage(count());      // Storage
-  sendAnalytics(count());      // Analytics
-  validate(count());           // Validation
-});
-
-// ✅ Split into focused effects
-$.effect(() => updateUI(count()));
-$.effect(() => saveToStorage(count()));
-$.effect(() => sendAnalytics(count()));
-$.effect(() => validate(count()));

2. Always Clean Up

javascript
// ❌ Missing cleanup
-$.effect(() => {
-  const timer = setInterval(() => {}, 1000);
-  // Memory leak!
-});
-
-// ✅ Proper cleanup
-$.effect(() => {
-  const timer = setInterval(() => {}, 1000);
-  return () => clearInterval(timer);
-});

3. Avoid Writing to Signals in Effects

javascript
import { $ } from 'sigpro';
-
-const a = $(1);
-const b = $(2);
-
-// ❌ Avoid - can cause loops
-$.effect(() => {
-  a(b()); // Writing to a while reading b
-});
-
-// ✅ Use computed signals instead
-const sum = $(() => a() + b());

4. Use Conditional Logic Carefully

javascript
// ❌ Condition affects dependency tracking
-$.effect(() => {
-  if (condition()) {
-    console.log(a()); // Only tracks a when condition is true
-  }
-});
-
-// ✅ Track all dependencies explicitly
-$.effect(() => {
-  const cond = condition(); // Track condition
-  if (cond) {
-    console.log(a()); // Track a
-  }
-});

5. Memoize Expensive Computations

javascript
import { $ } from 'sigpro';
-
-const items = $([]);
-
-// ❌ Expensive computation runs on every effect
-$.effect(() => {
-  const total = items().reduce((sum, i) => sum + i.price, 0);
-  updateTotal(total);
-});
-
-// ✅ Memoize with computed signal
-const total = $(() => items().reduce((sum, i) => sum + i.price, 0));
-$.effect(() => updateTotal(total()));

🔍 Debugging Effects

Logging Effect Runs

javascript
import { $ } from 'sigpro';
-
-const withLogging = (effectFn, name) => {
-  return $.effect(() => {
-    console.log(\`[\${name}] Running...\`);
-    const start = performance.now();
-    
-    const result = effectFn();
-    
-    const duration = performance.now() - start;
-    console.log(\`[\${name}] Completed in \${duration.toFixed(2)}ms\`);
-    
-    return result;
-  });
-};
-
-// Usage
-withLogging(() => {
-  console.log('Count:', count());
-}, 'count-effect');

Effect Inspector

javascript
import { $ } from 'sigpro';
-
-const createEffectInspector = () => {
-  const effects = new Map();
-  let id = 0;
-  
-  const trackedEffect = (fn, name = \`effect-\${++id}\`) => {
-    const info = {
-      name,
-      runs: 0,
-      lastRun: null,
-      duration: 0,
-      dependencies: new Set()
-    };
-    
-    const wrapped = () => {
-      info.runs++;
-      info.lastRun = new Date();
-      const start = performance.now();
-      
-      const result = fn();
-      
-      info.duration = performance.now() - start;
-      return result;
-    };
-    
-    const stop = $.effect(wrapped);
-    effects.set(stop, info);
-    
-    return stop;
-  };
-  
-  const getReport = () => {
-    const report = {};
-    effects.forEach((info, stop) => {
-      report[info.name] = {
-        runs: info.runs,
-        lastRun: info.lastRun,
-        avgDuration: info.duration / info.runs
-      };
-    });
-    return report;
-  };
-  
-  return { trackedEffect, getReport };
-};
-
-// Usage
-const inspector = createEffectInspector();
-inspector.trackedEffect(() => {
-  console.log('Count:', count());
-}, 'counter-effect');

📊 Summary

FeatureDescription
Automatic TrackingDependencies tracked automatically
Cleanup FunctionsReturn function to clean up resources
Batch UpdatesMultiple changes batched in microtask
Manual StopCan stop effects with returned function
Nested EffectsEffects can contain other effects
Auto-cleanupEffects in pages/components auto-cleaned

Pro Tip: Effects are the perfect place for side effects like DOM updates, data fetching, and subscriptions. Keep them focused and always clean up resources!

`,94)])])}const y=i(k,[["render",l]]);export{g as __pageData,y as default}; diff --git a/docs/assets/api_effects.md.Br_yStBS.lean.js b/docs/assets/api_effects.md.Br_yStBS.lean.js deleted file mode 100644 index 8ad5dff..0000000 --- a/docs/assets/api_effects.md.Br_yStBS.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as i,o as a,c as n,ae as h}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Effects API 🔄","description":"","frontmatter":{},"headers":[],"relativePath":"api/effects.md","filePath":"api/effects.md"}'),k={name:"api/effects.md"};function l(t,s,p,e,E,r){return a(),n("div",null,[...s[0]||(s[0]=[h("",94)])])}const y=i(k,[["render",l]]);export{g as __pageData,y as default}; diff --git a/docs/assets/api_fetch.md.DQLBJSoq.js b/docs/assets/api_fetch.md.DQLBJSoq.js deleted file mode 100644 index ffcb610..0000000 --- a/docs/assets/api_fetch.md.DQLBJSoq.js +++ /dev/null @@ -1,849 +0,0 @@ -import{_ as i,o as a,c as n,ae as h}from"./chunks/framework.C8AWLET_.js";const d=JSON.parse('{"title":"Fetch API 🌐","description":"","frontmatter":{},"headers":[],"relativePath":"api/fetch.md","filePath":"api/fetch.md"}'),l={name:"api/fetch.md"};function k(t,s,p,e,E,F){return a(),n("div",null,[...s[0]||(s[0]=[h(`

Fetch API 🌐

SigPro provides a simple, lightweight wrapper around the native Fetch API that integrates seamlessly with signals for loading state management. It's designed for common use cases with sensible defaults.

Core Concepts

What is $.fetch?

A ultra-simple fetch wrapper that:

  • Automatically handles JSON serialization and parsing
  • Integrates with signals for loading state
  • Returns null on error (no try/catch needed for basic usage)
  • Works great with effects for reactive data fetching

$.fetch(url, data, [loading])

Makes a POST request with JSON data and optional loading signal.

javascript
import { $ } from 'sigpro';
-
-const loading = $(false);
-
-async function loadUser() {
-  const user = await $.fetch('/api/user', { id: 123 }, loading);
-  if (user) {
-    console.log('User loaded:', user);
-  }
-}

📋 API Reference

Parameters

ParameterTypeDescription
urlstringEndpoint URL
dataObjectData to send (automatically JSON.stringify'd)
loadingFunction (optional)Signal function to track loading state

Returns

ReturnDescription
Promise<Object|null>Parsed JSON response or null on error

🎯 Basic Examples

Simple Data Fetching

javascript
import { $ } from 'sigpro';
-
-const userData = $(null);
-
-async function fetchUser(id) {
-  const data = await $.fetch('/api/user', { id });
-  if (data) {
-    userData(data);
-  }
-}
-
-fetchUser(123);

With Loading State

javascript
import { $, html } from 'sigpro';
-
-const user = $(null);
-const loading = $(false);
-
-async function loadUser(id) {
-  const data = await $.fetch('/api/user', { id }, loading);
-  if (data) user(data);
-}
-
-// In your template
-html\`
-  <div>
-    \${() => loading() ? html\`
-      <div class="spinner">Loading...</div>
-    \` : user() ? html\`
-      <div>
-        <h2>\${user().name}</h2>
-        <p>Email: \${user().email}</p>
-      </div>
-    \` : html\`
-      <p>No user found</p>
-    \`}
-  </div>
-\`;

In an Effect

javascript
import { $ } from 'sigpro';
-
-const userId = $(1);
-const user = $(null);
-const loading = $(false);
-
-$.effect(() => {
-  const id = userId();
-  if (id) {
-    $.fetch(\`/api/users/\${id}\`, null, loading).then(data => {
-      if (data) user(data);
-    });
-  }
-});
-
-userId(2); // Automatically fetches new user

🚀 Advanced Examples

User Profile with Loading States

javascript
import { $, html } from 'sigpro';
-
-const Profile = () => {
-  const userId = $(1);
-  const user = $(null);
-  const loading = $(false);
-  const error = $(null);
-
-  const fetchUser = async (id) => {
-    error(null);
-    const data = await $.fetch('/api/user', { id }, loading);
-    if (data) {
-      user(data);
-    } else {
-      error('Failed to load user');
-    }
-  };
-
-  // Fetch when userId changes
-  $.effect(() => {
-    fetchUser(userId());
-  });
-
-  return html\`
-    <div class="profile">
-      <div class="user-selector">
-        <button @click=\${() => userId(1)}>User 1</button>
-        <button @click=\${() => userId(2)}>User 2</button>
-        <button @click=\${() => userId(3)}>User 3</button>
-      </div>
-
-      \${() => {
-        if (loading()) {
-          return html\`<div class="spinner">Loading profile...</div>\`;
-        }
-        
-        if (error()) {
-          return html\`<div class="error">\${error()}</div>\`;
-        }
-        
-        if (user()) {
-          return html\`
-            <div class="user-info">
-              <h2>\${user().name}</h2>
-              <p>Email: \${user().email}</p>
-              <p>Role: \${user().role}</p>
-              <p>Joined: \${new Date(user().joined).toLocaleDateString()}</p>
-            </div>
-          \`;
-        }
-        
-        return html\`<p>Select a user</p>\`;
-      }}
-    </div>
-  \`;
-};

Todo List with API

javascript
import { $, html } from 'sigpro';
-
-const TodoApp = () => {
-  const todos = $([]);
-  const loading = $(false);
-  const newTodo = $('');
-  const filter = $('all'); // 'all', 'active', 'completed'
-
-  // Load todos
-  const loadTodos = async () => {
-    const data = await $.fetch('/api/todos', {}, loading);
-    if (data) todos(data);
-  };
-
-  // Add todo
-  const addTodo = async () => {
-    if (!newTodo().trim()) return;
-    
-    const todo = await $.fetch('/api/todos', {
-      text: newTodo(),
-      completed: false
-    });
-    
-    if (todo) {
-      todos([...todos(), todo]);
-      newTodo('');
-    }
-  };
-
-  // Toggle todo
-  const toggleTodo = async (id, completed) => {
-    const updated = await $.fetch(\`/api/todos/\${id}\`, {
-      completed: !completed
-    });
-    
-    if (updated) {
-      todos(todos().map(t => 
-        t.id === id ? updated : t
-      ));
-    }
-  };
-
-  // Delete todo
-  const deleteTodo = async (id) => {
-    const result = await $.fetch(\`/api/todos/\${id}/delete\`, {});
-    if (result) {
-      todos(todos().filter(t => t.id !== id));
-    }
-  };
-
-  // Filtered todos
-  const filteredTodos = $(() => {
-    const currentFilter = filter();
-    if (currentFilter === 'all') return todos();
-    if (currentFilter === 'active') {
-      return todos().filter(t => !t.completed);
-    }
-    return todos().filter(t => t.completed);
-  });
-
-  // Load on mount
-  loadTodos();
-
-  return html\`
-    <div class="todo-app">
-      <h1>Todo List</h1>
-      
-      <div class="add-todo">
-        <input
-          type="text"
-          :value=\${newTodo}
-          @keydown.enter=\${addTodo}
-          placeholder="Add a new todo..."
-        />
-        <button @click=\${addTodo}>Add</button>
-      </div>
-      
-      <div class="filters">
-        <button 
-          class:active=\${() => filter() === 'all'}
-          @click=\${() => filter('all')}
-        >
-          All
-        </button>
-        <button 
-          class:active=\${() => filter() === 'active'}
-          @click=\${() => filter('active')}
-        >
-          Active
-        </button>
-        <button 
-          class:active=\${() => filter() === 'completed'}
-          @click=\${() => filter('completed')}
-        >
-          Completed
-        </button>
-      </div>
-      
-      \${() => loading() ? html\`
-        <div class="spinner">Loading todos...</div>
-      ) : html\`
-        <ul class="todo-list">
-          \${filteredTodos().map(todo => html\`
-            <li class="todo-item">
-              <input
-                type="checkbox"
-                :checked=\${todo.completed}
-                @change=\${() => toggleTodo(todo.id, todo.completed)}
-              />
-              <span class:completed=\${todo.completed}>\${todo.text}</span>
-              <button @click=\${() => deleteTodo(todo.id)}>🗑️</button>
-            </li>
-          \`)}
-        </ul>
-      \`}
-    </div>
-  \`;
-};

Infinite Scroll with Pagination

javascript
import { $, html } from 'sigpro';
-
-const InfiniteScroll = () => {
-  const posts = $([]);
-  const page = $(1);
-  const loading = $(false);
-  const hasMore = $(true);
-  const error = $(null);
-
-  const loadMore = async () => {
-    if (loading() || !hasMore()) return;
-    
-    const data = await $.fetch('/api/posts', { 
-      page: page(),
-      limit: 10 
-    }, loading);
-    
-    if (data) {
-      if (data.posts.length === 0) {
-        hasMore(false);
-      } else {
-        posts([...posts(), ...data.posts]);
-        page(p => p + 1);
-      }
-    } else {
-      error('Failed to load posts');
-    }
-  };
-
-  // Intersection Observer for infinite scroll
-  $.effect(() => {
-    const observer = new IntersectionObserver(
-      (entries) => {
-        if (entries[0].isIntersecting) {
-          loadMore();
-        }
-      },
-      { threshold: 0.1 }
-    );
-    
-    const sentinel = document.getElementById('sentinel');
-    if (sentinel) observer.observe(sentinel);
-    
-    return () => observer.disconnect();
-  });
-
-  // Initial load
-  loadMore();
-
-  return html\`
-    <div class="infinite-scroll">
-      <h1>Posts</h1>
-      
-      <div class="posts">
-        \${posts().map(post => html\`
-          <article class="post">
-            <h2>\${post.title}</h2>
-            <p>\${post.body}</p>
-            <small>By \${post.author}</small>
-          </article>
-        \`)}
-      </div>
-      
-      <div id="sentinel" class="sentinel">
-        \${() => {
-          if (loading()) {
-            return html\`<div class="spinner">Loading more...</div>\`;
-          }
-          if (error()) {
-            return html\`<div class="error">\${error()}</div>\`;
-          }
-          if (!hasMore()) {
-            return html\`<div class="end">No more posts</div>\`;
-          }
-          return '';
-        }}
-      </div>
-    </div>
-  \`;
-};

Search with Debounce

javascript
import { $, html } from 'sigpro';
-
-const SearchComponent = () => {
-  const query = $('');
-  const results = $([]);
-  const loading = $(false);
-  const error = $(null);
-  let searchTimeout;
-
-  const performSearch = async (searchQuery) => {
-    if (!searchQuery.trim()) {
-      results([]);
-      return;
-    }
-    
-    const data = await $.fetch('/api/search', { 
-      q: searchQuery 
-    }, loading);
-    
-    if (data) {
-      results(data);
-    } else {
-      error('Search failed');
-    }
-  };
-
-  // Debounced search
-  $.effect(() => {
-    const searchQuery = query();
-    
-    clearTimeout(searchTimeout);
-    
-    if (searchQuery.length < 2) {
-      results([]);
-      return;
-    }
-    
-    searchTimeout = setTimeout(() => {
-      performSearch(searchQuery);
-    }, 300);
-    
-    return () => clearTimeout(searchTimeout);
-  });
-
-  return html\`
-    <div class="search">
-      <div class="search-box">
-        <input
-          type="search"
-          :value=\${query}
-          placeholder="Search..."
-          class="search-input"
-        />
-        \${() => loading() ? html\`
-          <span class="spinner-small">⌛</span>
-        ) : ''}
-      </div>
-      
-      \${() => {
-        if (error()) {
-          return html\`<div class="error">\${error()}</div>\`;
-        }
-        
-        if (results().length > 0) {
-          return html\`
-            <ul class="results">
-              \${results().map(item => html\`
-                <li class="result-item">
-                  <h3>\${item.title}</h3>
-                  <p>\${item.description}</p>
-                </li>
-              \`)}
-            </ul>
-          \`;
-        }
-        
-        if (query().length >= 2 && !loading()) {
-          return html\`<p class="no-results">No results found</p>\`;
-        }
-        
-        return '';
-      }}
-    </div>
-  \`;
-};

Form Submission

javascript
import { $, html } from 'sigpro';
-
-const ContactForm = () => {
-  const formData = $({
-    name: '',
-    email: '',
-    message: ''
-  });
-  
-  const submitting = $(false);
-  const submitError = $(null);
-  const submitSuccess = $(false);
-
-  const handleSubmit = async (e) => {
-    e.preventDefault();
-    
-    submitError(null);
-    submitSuccess(false);
-    
-    const result = await $.fetch('/api/contact', formData(), submitting);
-    
-    if (result) {
-      submitSuccess(true);
-      formData({ name: '', email: '', message: '' });
-    } else {
-      submitError('Failed to send message. Please try again.');
-    }
-  };
-
-  const updateField = (field, value) => {
-    formData({
-      ...formData(),
-      [field]: value
-    });
-  };
-
-  return html\`
-    <form class="contact-form" @submit=\${handleSubmit}>
-      <h2>Contact Us</h2>
-      
-      <div class="form-group">
-        <label for="name">Name:</label>
-        <input
-          type="text"
-          id="name"
-          :value=\${() => formData().name}
-          @input=\${(e) => updateField('name', e.target.value)}
-          required
-          ?disabled=\${submitting}
-        />
-      </div>
-      
-      <div class="form-group">
-        <label for="email">Email:</label>
-        <input
-          type="email"
-          id="email"
-          :value=\${() => formData().email}
-          @input=\${(e) => updateField('email', e.target.value)}
-          required
-          ?disabled=\${submitting}
-        />
-      </div>
-      
-      <div class="form-group">
-        <label for="message">Message:</label>
-        <textarea
-          id="message"
-          :value=\${() => formData().message}
-          @input=\${(e) => updateField('message', e.target.value)}
-          required
-          rows="5"
-          ?disabled=\${submitting}
-        ></textarea>
-      </div>
-      
-      \${() => {
-        if (submitting()) {
-          return html\`<div class="submitting">Sending...</div>\`;
-        }
-        
-        if (submitError()) {
-          return html\`<div class="error">\${submitError()}</div>\`;
-        }
-        
-        if (submitSuccess()) {
-          return html\`<div class="success">Message sent successfully!</div>\`;
-        }
-        
-        return '';
-      }}
-      
-      <button 
-        type="submit" 
-        ?disabled=\${submitting}
-      >
-        Send Message
-      </button>
-    </form>
-  \`;
-};

Real-time Dashboard with Multiple Endpoints

javascript
import { $, html } from 'sigpro';
-
-const Dashboard = () => {
-  // Multiple data streams
-  const metrics = $({});
-  const alerts = $([]);
-  const logs = $([]);
-  
-  const loading = $({
-    metrics: false,
-    alerts: false,
-    logs: false
-  });
-
-  const refreshInterval = $(5000); // 5 seconds
-
-  const fetchMetrics = async () => {
-    const data = await $.fetch('/api/metrics', {}, loading().metrics);
-    if (data) metrics(data);
-  };
-
-  const fetchAlerts = async () => {
-    const data = await $.fetch('/api/alerts', {}, loading().alerts);
-    if (data) alerts(data);
-  };
-
-  const fetchLogs = async () => {
-    const data = await $.fetch('/api/logs', { 
-      limit: 50 
-    }, loading().logs);
-    if (data) logs(data);
-  };
-
-  // Auto-refresh all data
-  $.effect(() => {
-    fetchMetrics();
-    fetchAlerts();
-    fetchLogs();
-    
-    const interval = setInterval(() => {
-      fetchMetrics();
-      fetchAlerts();
-    }, refreshInterval());
-    
-    return () => clearInterval(interval);
-  });
-
-  return html\`
-    <div class="dashboard">
-      <header>
-        <h1>System Dashboard</h1>
-        <div class="refresh-control">
-          <label>
-            Refresh interval:
-            <select :value=\${refreshInterval} @change=\${(e) => refreshInterval(parseInt(e.target.value))}>
-              <option value="2000">2 seconds</option>
-              <option value="5000">5 seconds</option>
-              <option value="10000">10 seconds</option>
-              <option value="30000">30 seconds</option>
-            </select>
-          </label>
-        </div>
-      </header>
-      
-      <div class="dashboard-grid">
-        <!-- Metrics Panel -->
-        <div class="panel metrics">
-          <h2>System Metrics</h2>
-          \${() => loading().metrics ? html\`
-            <div class="spinner">Loading metrics...</div>
-          ) : html\`
-            <div class="metrics-grid">
-              <div class="metric">
-                <label>CPU</label>
-                <span>\${metrics().cpu || 0}%</span>
-              </div>
-              <div class="metric">
-                <label>Memory</label>
-                <span>\${metrics().memory || 0}%</span>
-              </div>
-              <div class="metric">
-                <label>Requests</label>
-                <span>\${metrics().requests || 0}/s</span>
-              </div>
-            </div>
-          \`}
-        </div>
-        
-        <!-- Alerts Panel -->
-        <div class="panel alerts">
-          <h2>Active Alerts</h2>
-          \${() => loading().alerts ? html\`
-            <div class="spinner">Loading alerts...</div>
-          ) : alerts().length > 0 ? html\`
-            <ul>
-              \${alerts().map(alert => html\`
-                <li class="alert \${alert.severity}">
-                  <strong>\${alert.type}</strong>
-                  <p>\${alert.message}</p>
-                  <small>\${new Date(alert.timestamp).toLocaleTimeString()}</small>
-                </li>
-              \`)}
-            </ul>
-          ) : html\`
-            <p class="no-data">No active alerts</p>
-          \`}
-        </div>
-        
-        <!-- Logs Panel -->
-        <div class="panel logs">
-          <h2>Recent Logs</h2>
-          \${() => loading().logs ? html\`
-            <div class="spinner">Loading logs...</div>
-          ) : html\`
-            <ul>
-              \${logs().map(log => html\`
-                <li class="log \${log.level}">
-                  <span class="timestamp">\${new Date(log.timestamp).toLocaleTimeString()}</span>
-                  <span class="message">\${log.message}</span>
-                </li>
-              \`)}
-            </ul>
-          \`}
-        </div>
-      </div>
-    </div>
-  \`;
-};

File Upload

javascript
import { $, html } from 'sigpro';
-
-const FileUploader = () => {
-  const files = $([]);
-  const uploading = $(false);
-  const uploadProgress = $({});
-  const uploadResults = $([]);
-
-  const handleFileSelect = (e) => {
-    files([...e.target.files]);
-  };
-
-  const uploadFiles = async () => {
-    if (files().length === 0) return;
-    
-    uploading(true);
-    uploadResults([]);
-    
-    for (const file of files()) {
-      const formData = new FormData();
-      formData.append('file', file);
-      
-      // Track progress for this file
-      uploadProgress({
-        ...uploadProgress(),
-        [file.name]: 0
-      });
-      
-      try {
-        // Custom fetch for FormData
-        const response = await fetch('/api/upload', {
-          method: 'POST',
-          body: formData
-        });
-        
-        const result = await response.json();
-        
-        uploadResults([
-          ...uploadResults(),
-          { file: file.name, success: true, result }
-        ]);
-      } catch (error) {
-        uploadResults([
-          ...uploadResults(),
-          { file: file.name, success: false, error: error.message }
-        ]);
-      }
-      
-      uploadProgress({
-        ...uploadProgress(),
-        [file.name]: 100
-      });
-    }
-    
-    uploading(false);
-  };
-
-  return html\`
-    <div class="file-uploader">
-      <h2>Upload Files</h2>
-      
-      <input
-        type="file"
-        multiple
-        @change=\${handleFileSelect}
-        ?disabled=\${uploading}
-      />
-      
-      \${() => files().length > 0 ? html\`
-        <div class="file-list">
-          <h3>Selected Files:</h3>
-          <ul>
-            \${files().map(file => html\`
-              <li>
-                \${file.name} (\${(file.size / 1024).toFixed(2)} KB)
-                \${() => uploadProgress()[file.name] ? html\`
-                  <progress value="\${uploadProgress()[file.name]}" max="100"></progress>
-                ) : ''}
-              </li>
-            \`)}
-          </ul>
-          
-          <button 
-            @click=\${uploadFiles}
-            ?disabled=\${uploading}
-          >
-            \${() => uploading() ? 'Uploading...' : 'Upload Files'}
-          </button>
-        </div>
-      \` : ''}
-      
-      \${() => uploadResults().length > 0 ? html\`
-        <div class="upload-results">
-          <h3>Upload Results:</h3>
-          <ul>
-            \${uploadResults().map(result => html\`
-              <li class="\${result.success ? 'success' : 'error'}">
-                \${result.file}: 
-                \${result.success ? 'Uploaded successfully' : \`Failed: \${result.error}\`}
-              </li>
-            \`)}
-          </ul>
-        </div>
-      \` : ''}
-    </div>
-  \`;
-};

Retry Logic

javascript
import { $ } from 'sigpro';
-
-// Enhanced fetch with retry
-const fetchWithRetry = async (url, data, loading, maxRetries = 3) => {
-  let lastError;
-  
-  for (let attempt = 1; attempt <= maxRetries; attempt++) {
-    try {
-      if (loading) loading(true);
-      
-      const result = await $.fetch(url, data);
-      if (result !== null) {
-        return result;
-      }
-      
-      // If we get null but no error, wait and retry
-      if (attempt < maxRetries) {
-        await new Promise(resolve => 
-          setTimeout(resolve, Math.pow(2, attempt) * 1000) // Exponential backoff
-        );
-      }
-    } catch (error) {
-      lastError = error;
-      console.warn(\`Attempt \${attempt} failed:\`, error);
-      
-      if (attempt < maxRetries) {
-        await new Promise(resolve => 
-          setTimeout(resolve, Math.pow(2, attempt) * 1000)
-        );
-      }
-    } finally {
-      if (attempt === maxRetries && loading) {
-        loading(false);
-      }
-    }
-  }
-  
-  console.error('All retry attempts failed:', lastError);
-  return null;
-};
-
-// Usage
-const loading = $(false);
-const data = await fetchWithRetry('/api/unreliable-endpoint', {}, loading, 5);

🎯 Best Practices

1. Always Handle Null Responses

javascript
// ❌ Don't assume success
-const data = await $.fetch('/api/data');
-console.log(data.property); // Might throw if data is null
-
-// ✅ Check for null
-const data = await $.fetch('/api/data');
-if (data) {
-  console.log(data.property);
-} else {
-  showError('Failed to load data');
-}

2. Use with Effects for Reactivity

javascript
// ❌ Manual fetching
-button.addEventListener('click', async () => {
-  const data = await $.fetch('/api/data');
-  updateUI(data);
-});
-
-// ✅ Reactive fetching
-const trigger = $(false);
-
-$.effect(() => {
-  if (trigger()) {
-    $.fetch('/api/data').then(data => {
-      if (data) updateUI(data);
-    });
-  }
-});
-
-trigger(true); // Triggers fetch

3. Combine with Loading Signals

javascript
// ✅ Always show loading state
-const loading = $(false);
-const data = $(null);
-
-async function load() {
-  const result = await $.fetch('/api/data', {}, loading);
-  if (result) data(result);
-}
-
-// In template
-html\`
-  <div>
-    \${() => loading() ? '<Spinner />' : 
-      data() ? '<Data />' : 
-      '<Empty />'}
-  </div>
-\`;

4. Cancel In-flight Requests

javascript
// ✅ Use AbortController with effects
-let controller;
-
-$.effect(() => {
-  if (controller) {
-    controller.abort();
-  }
-  
-  controller = new AbortController();
-  
-  fetch(url, { signal: controller.signal })
-    .then(res => res.json())
-    .then(data => {
-      if (!controller.signal.aborted) {
-        updateData(data);
-      }
-    });
-  
-  return () => controller.abort();
-});

📊 Error Handling

Basic Error Handling

javascript
const data = await $.fetch('/api/data');
-if (!data) {
-  // Handle error (show message, retry, etc.)
-}

With Error Signal

javascript
const data = $(null);
-const error = $(null);
-const loading = $(false);
-
-async function loadData() {
-  error(null);
-  const result = await $.fetch('/api/data', {}, loading);
-  
-  if (result) {
-    data(result);
-  } else {
-    error('Failed to load data');
-  }
-}

Pro Tip: Combine $.fetch with $.effect and loading signals for a complete reactive data fetching solution. The loading signal integration makes it trivial to show loading states in your UI.

`,54)])])}const g=i(l,[["render",k]]);export{d as __pageData,g as default}; diff --git a/docs/assets/api_fetch.md.DQLBJSoq.lean.js b/docs/assets/api_fetch.md.DQLBJSoq.lean.js deleted file mode 100644 index 0a44344..0000000 --- a/docs/assets/api_fetch.md.DQLBJSoq.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as i,o as a,c as n,ae as h}from"./chunks/framework.C8AWLET_.js";const d=JSON.parse('{"title":"Fetch API 🌐","description":"","frontmatter":{},"headers":[],"relativePath":"api/fetch.md","filePath":"api/fetch.md"}'),l={name:"api/fetch.md"};function k(t,s,p,e,E,F){return a(),n("div",null,[...s[0]||(s[0]=[h("",54)])])}const g=i(l,[["render",k]]);export{d as __pageData,g as default}; diff --git a/docs/assets/api_html.md.-lEpgX-Z.js b/docs/assets/api_html.md.-lEpgX-Z.js new file mode 100644 index 0000000..0925874 --- /dev/null +++ b/docs/assets/api_html.md.-lEpgX-Z.js @@ -0,0 +1,22 @@ +import{_ as s,o as i,c as a,ae as e}from"./chunks/framework.C8AWLET_.js";const c=JSON.parse('{"title":"Rendering Engine: $.html","description":"","frontmatter":{},"headers":[],"relativePath":"api/html.md","filePath":"api/html.md"}'),n={name:"api/html.md"};function l(h,t,r,d,p,o){return i(),a("div",null,[...t[0]||(t[0]=[e(`

Rendering Engine: $.html

The $.html function is the architect of your UI. It creates standard HTML elements and wires them directly to your signals without the need for a Virtual DOM.

1. Syntax: $.html(tag, [props], [content])

ParameterTypeRequiredDescription
tagstringYesAny valid HTML5 tag (e.g., 'div', 'button', 'input').
propsObjectNoAttributes, event listeners, and reactive bindings.
contentanyNoText, Nodes, Arrays, or Reactive Functions.

Example:

javascript
const myButton = $.html('button', { class: 'btn-primary' }, 'Click me');

2. Global Tag Helpers

To avoid repetitive $.html calls, SigPro automatically exposes common tags to the global window object. This allows for a clean, declarative syntax.

javascript
// Instead of $.html('div', ...), just use:
+div({ id: 'wrapper' }, [
+  h1("Welcome"),
+  p("This is SigPro.")
+]);

3. Handling Properties & Attributes

SigPro distinguishes between static attributes and reactive bindings using the $ prefix.

Static vs. Reactive Attributes

  • Static: Applied once during creation.
  • Reactive ($): Automatically updates the DOM when the signal changes.
PropertySyntaxResult
Attribute{ id: 'main' }id="main"
Event{ onclick: fn }Adds an event listener.
Reactive Attr{ $class: $theme }Updates class whenever $theme() changes.
Boolean Attr{ $disabled: $isBusy }Toggles the disabled attribute automatically.

4. Two-Way Data Binding

For form inputs, SigPro provides a powerful shortcut using $value or $checked. It automatically handles the event listening and the value synchronization.

javascript
const $text = $("Type here...");
+
+input({ 
+  type: 'text', 
+  $value: $text // Syncs input -> signal and signal -> input
+});
+
+p(["You typed: ", $text]);

5. Reactive Content (Dynamic Children)

The content argument is incredibly flexible. If you pass a function, SigPro treats it as a reactive "portal" that re-renders only that specific part of the DOM.

Text & Nodes

javascript
const $count = $(0);
+
+// Text node updates surgically
+div(["Count: ", $count]); 
+
+// Conditional rendering with a function
+div(() => {
+  return $count() > 10 
+    ? h1("High Score!") 
+    : p("Keep going...");
+});

The "Guillotine" (Performance Tip)

When a reactive function in the content returns a new Node, SigPro uses replaceWith() to swap the old node for the new one. This ensures that:

  1. The update is nearly instantaneous.
  2. The old node is correctly garbage-collected.

6. Summary: Content Types

InputBehavior
String / NumberAppended as a TextNode.
HTMLElementAppended directly to the parent.
ArrayEach item is processed and appended in order.
Function () => ...Creates a live reactive zone that updates automatically.
`,31)])])}const g=s(n,[["render",l]]);export{c as __pageData,g as default}; diff --git a/docs/assets/api_html.md.-lEpgX-Z.lean.js b/docs/assets/api_html.md.-lEpgX-Z.lean.js new file mode 100644 index 0000000..8f9d825 --- /dev/null +++ b/docs/assets/api_html.md.-lEpgX-Z.lean.js @@ -0,0 +1 @@ +import{_ as s,o as i,c as a,ae as e}from"./chunks/framework.C8AWLET_.js";const c=JSON.parse('{"title":"Rendering Engine: $.html","description":"","frontmatter":{},"headers":[],"relativePath":"api/html.md","filePath":"api/html.md"}'),n={name:"api/html.md"};function l(h,t,r,d,p,o){return i(),a("div",null,[...t[0]||(t[0]=[e("",31)])])}const g=s(n,[["render",l]]);export{c as __pageData,g as default}; diff --git a/docs/assets/api_mount.md.eGRwkZvh.js b/docs/assets/api_mount.md.eGRwkZvh.js new file mode 100644 index 0000000..49d4057 --- /dev/null +++ b/docs/assets/api_mount.md.eGRwkZvh.js @@ -0,0 +1,28 @@ +import{_ as i,o as a,c as t,ae as e}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Application Mounter: $.mount","description":"","frontmatter":{},"headers":[],"relativePath":"api/mount.md","filePath":"api/mount.md"}'),n={name:"api/mount.md"};function l(h,s,p,o,r,k){return a(),t("div",null,[...s[0]||(s[0]=[e(`

Application Mounter: $.mount

The $.mount function is the entry point of your reactive world. It takes a SigPro component (or a plain DOM node) and injects it into the real document.

1. Syntax: $.mount(node, [target])

ParameterTypeDefaultDescription
nodeHTMLElement or FunctionRequiredThe component or element to render.
targetstring or HTMLElementdocument.bodyWhere to mount the app (CSS selector or Element).

2. Usage Scenarios

A. The "Clean Slate" (Main Entry)

In a modern app (like our main.js example), you usually want to control the entire page. By default, $.mount clears the target's existing HTML before mounting.

javascript
// src/main.js
+import { $ } from 'SigPro';
+import App from './App.js';
+
+$.mount(App); // Mounts to <body> by default

B. Targeting a Specific Container

If you have an existing HTML structure and only want SigPro to manage a specific part (like a #root div), pass a CSS selector or a reference.

html
<div id="sidebar"></div>
+<div id="app-root"></div>
javascript
// Local mount to a specific ID
+$.mount(MyComponent, '#app-root');
+
+// Or using a direct DOM reference
+const sidebar = document.getElementById('sidebar');
+$.mount(SidebarComponent, sidebar);

3. Mounting with Pure HTML

One of SigPro's strengths is that it works perfectly alongside "Old School" HTML. You can create a reactive "island" inside a static page.

javascript
// A small reactive widget in a static .js file
+const CounterWidget = () => {
+  const $c = $(0);
+  return button({ onclick: () => $c(v => v + 1) }, [
+    "Clicks: ", $c
+  ]);
+};
+
+// Mount it into an existing div in your HTML
+$.mount(CounterWidget, '#counter-container');

4. How it Works (The "Wipe" Logic)

When $.mount is called, it performs two critical steps:

  1. Clearance: It sets target.innerHTML = ''. This ensures no "zombie" HTML from previous renders or static placeholders interferes with your app.
  2. Injection: It appends your component. If you passed a Function, it executes it first to get the DOM node.

5. Global vs. Local Scope

Global (The "Framework" Way)

In a standard Vite/ESM project, you initialize SigPro globally in main.js. This makes the $ and the tag helpers (div, button, etc.) available everywhere in your project.

javascript
// main.js - Global Initialization
+import 'SigPro'; 
+
+// Now any other file can just use:
+$.mount(() => h1("Global App"));

Local (The "Library" Way)

If you are worried about polluting the global window object, you can import and use SigPro locally within a specific module.

javascript
// widget.js - Local usage
+import { $ } from 'SigPro';
+
+const myNode = $.html('div', 'Local Widget');
+$.mount(myNode, '#widget-target');

Summary Cheat Sheet

GoalCode
Mount to body$.mount(App)
Mount to ID$.mount(App, '#id')
Mount to Element$.mount(App, myElement)
Reactive Widget$.mount(() => div("Hi"), '#widget')
`,32)])])}const c=i(n,[["render",l]]);export{g as __pageData,c as default}; diff --git a/docs/assets/api_mount.md.eGRwkZvh.lean.js b/docs/assets/api_mount.md.eGRwkZvh.lean.js new file mode 100644 index 0000000..427b068 --- /dev/null +++ b/docs/assets/api_mount.md.eGRwkZvh.lean.js @@ -0,0 +1 @@ +import{_ as i,o as a,c as t,ae as e}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Application Mounter: $.mount","description":"","frontmatter":{},"headers":[],"relativePath":"api/mount.md","filePath":"api/mount.md"}'),n={name:"api/mount.md"};function l(h,s,p,o,r,k){return a(),t("div",null,[...s[0]||(s[0]=[e("",32)])])}const c=i(n,[["render",l]]);export{g as __pageData,c as default}; diff --git a/docs/assets/api_pages.md.BP19nHXw.js b/docs/assets/api_pages.md.BP19nHXw.js deleted file mode 100644 index d038a11..0000000 --- a/docs/assets/api_pages.md.BP19nHXw.js +++ /dev/null @@ -1,381 +0,0 @@ -import{_ as i,o as a,c as n,ae as h}from"./chunks/framework.C8AWLET_.js";const d=JSON.parse('{"title":"Pages API 📄","description":"","frontmatter":{},"headers":[],"relativePath":"api/pages.md","filePath":"api/pages.md"}'),t={name:"api/pages.md"};function l(k,s,p,e,E,r){return a(),n("div",null,[...s[0]||(s[0]=[h(`

Pages API 📄

Pages in SigPro are special components designed for route-based navigation with automatic cleanup. When you navigate away from a page, all signals, effects, and event listeners created within that page are automatically cleaned up - no memory leaks, no manual cleanup needed.

$.page(setupFunction)

Creates a page with automatic cleanup of all signals and effects when navigated away.

javascript
import { $, html } from 'sigpro';
-
-export default $.page(() => {
-  // All signals and effects created here
-  // will be automatically cleaned up on navigation
-  const count = $(0);
-  
-  $.effect(() => {
-    console.log(\`Count: \${count()}\`);
-  });
-  
-  return html\`
-    <div>
-      <h1>My Page</h1>
-      <p>Count: \${count}</p>
-      <button @click=\${() => count(c => c + 1)}>+</button>
-    </div>
-  \`;
-});

📋 API Reference

ParameterTypeDescription
setupFunctionFunctionFunction that returns the page content. Receives context object with params and onUnmount

Context Object Properties

PropertyTypeDescription
paramsObjectRoute parameters passed to the page
onUnmountFunctionRegister cleanup callbacks (alternative to automatic cleanup)

🎯 Basic Usage

Simple Page

javascript
// pages/home.js
-import { $, html } from 'sigpro';
-
-export default $.page(() => {
-  const title = $('Welcome to SigPro');
-  
-  return html\`
-    <div class="home-page">
-      <h1>\${title}</h1>
-      <p>This page will clean itself up when you navigate away.</p>
-    </div>
-  \`;
-});

Page with Route Parameters

javascript
// pages/user.js
-import { $, html } from 'sigpro';
-
-export default $.page(({ params }) => {
-  // Access route parameters
-  const userId = params.id;
-  const userData = $(null);
-  const loading = $(false);
-  
-  // Auto-cleaned effect
-  $.effect(() => {
-    loading(true);
-    $.fetch(\`/api/users/\${userId}\`, null, loading)
-      .then(data => userData(data));
-  });
-  
-  return html\`
-    <div>
-      \${() => loading() ? html\`
-        <div class="spinner">Loading...</div>
-      \` : html\`
-        <h1>User Profile: \${userData()?.name}</h1>
-        <p>Email: \${userData()?.email}</p>
-      \`}
-    </div>
-  \`;
-});

🧹 Automatic Cleanup

The magic of $.page is automatic cleanup. Everything created inside the page is tracked and cleaned up:

javascript
export default $.page(() => {
-  // ✅ Signals are auto-cleaned
-  const count = $(0);
-  const user = $(null);
-  
-  // ✅ Effects are auto-cleaned
-  $.effect(() => {
-    document.title = \`Count: \${count()}\`;
-  });
-  
-  // ✅ Event listeners are auto-cleaned
-  window.addEventListener('resize', handleResize);
-  
-  // ✅ Intervals and timeouts are auto-cleaned
-  const interval = setInterval(() => {
-    refreshData();
-  }, 5000);
-  
-  return html\`<div>Page content</div>\`;
-});
-// When navigating away: all signals, effects, listeners, intervals STOP

📝 Manual Cleanup with onUnmount

Sometimes you need custom cleanup logic. Use onUnmount for that:

javascript
export default $.page(({ onUnmount }) => {
-  // WebSocket connection
-  const socket = new WebSocket('wss://api.example.com');
-  
-  socket.onmessage = (event) => {
-    updateData(JSON.parse(event.data));
-  };
-  
-  // Manual cleanup
-  onUnmount(() => {
-    socket.close();
-    console.log('WebSocket closed');
-  });
-  
-  return html\`<div>Real-time updates</div>\`;
-});

🔄 Integration with Router

Pages are designed to work seamlessly with $.router:

javascript
import { $, html } from 'sigpro';
-import HomePage from './pages/Home.js';
-import UserPage from './pages/User.js';
-import SettingsPage from './pages/Settings.js';
-
-const routes = [
-  { path: '/', component: HomePage },
-  { path: '/user/:id', component: UserPage },
-  { path: '/settings', component: SettingsPage },
-];
-
-// Mount router
-document.body.appendChild($.router(routes));

💡 Practical Examples

Example 1: Data Fetching Page

javascript
// pages/posts.js
-export default $.page(({ params }) => {
-  const posts = $([]);
-  const loading = $(true);
-  const error = $(null);
-  
-  $.effect(() => {
-    fetch('/api/posts')
-      .then(res => res.json())
-      .then(data => {
-        posts(data);
-        loading(false);
-      })
-      .catch(err => {
-        error(err.message);
-        loading(false);
-      });
-  });
-  
-  return html\`
-    <div class="posts-page">
-      <h1>Blog Posts</h1>
-      
-      \${() => loading() ? html\`
-        <div class="loading">Loading posts...</div>
-      \` : error() ? html\`
-        <div class="error">Error: \${error()}</div>
-      \` : html\`
-        <div class="posts-grid">
-          \${posts().map(post => html\`
-            <article class="post-card">
-              <h2>\${post.title}</h2>
-              <p>\${post.excerpt}</p>
-              <a href="#/post/\${post.id}">Read more</a>
-            </article>
-          \`)}
-        </div>
-      \`}
-    </div>
-  \`;
-});

Example 2: Real-time Dashboard

javascript
// pages/dashboard.js
-export default $.page(({ onUnmount }) => {
-  const metrics = $({
-    cpu: 0,
-    memory: 0,
-    requests: 0
-  });
-  
-  // Auto-refresh data
-  const refreshInterval = setInterval(async () => {
-    const data = await $.fetch('/api/metrics');
-    if (data) metrics(data);
-  }, 5000);
-  
-  // Manual cleanup for interval
-  onUnmount(() => clearInterval(refreshInterval));
-  
-  // Live clock
-  const currentTime = $(new Date());
-  const clockInterval = setInterval(() => {
-    currentTime(new Date());
-  }, 1000);
-  
-  onUnmount(() => clearInterval(clockInterval));
-  
-  return html\`
-    <div class="dashboard">
-      <h1>System Dashboard</h1>
-      
-      <div class="time">
-        Last updated: \${() => currentTime().toLocaleTimeString()}
-      </div>
-      
-      <div class="metrics-grid">
-        <div class="metric-card">
-          <h3>CPU Usage</h3>
-          <p class="metric-value">\${() => metrics().cpu}%</p>
-        </div>
-        <div class="metric-card">
-          <h3>Memory Usage</h3>
-          <p class="metric-value">\${() => metrics().memory}%</p>
-        </div>
-        <div class="metric-card">
-          <h3>Requests/min</h3>
-          <p class="metric-value">\${() => metrics().requests}</p>
-        </div>
-      </div>
-    </div>
-  \`;
-});

Example 3: Multi-step Form

javascript
// pages/checkout.js
-export default $.page(({ onUnmount }) => {
-  const step = $(1);
-  const formData = $({
-    email: '',
-    address: '',
-    payment: ''
-  });
-  
-  // Warn user before leaving
-  const handleBeforeUnload = (e) => {
-    if (step() < 3) {
-      e.preventDefault();
-      e.returnValue = '';
-    }
-  };
-  
-  window.addEventListener('beforeunload', handleBeforeUnload);
-  onUnmount(() => {
-    window.removeEventListener('beforeunload', handleBeforeUnload);
-  });
-  
-  const nextStep = () => step(s => Math.min(s + 1, 3));
-  const prevStep = () => step(s => Math.max(s - 1, 1));
-  
-  return html\`
-    <div class="checkout">
-      <h1>Checkout - Step \${step} of 3</h1>
-      
-      \${() => {
-        switch(step()) {
-          case 1:
-            return html\`
-              <div class="step">
-                <h2>Email</h2>
-                <input 
-                  type="email" 
-                  :value=\${() => formData().email}
-                  @input=\${(e) => formData({...formData(), email: e.target.value})}
-                />
-              </div>
-            \`;
-          case 2:
-            return html\`
-              <div class="step">
-                <h2>Address</h2>
-                <textarea 
-                  :value=\${() => formData().address}
-                  @input=\${(e) => formData({...formData(), address: e.target.value})}
-                ></textarea>
-              </div>
-            \`;
-          case 3:
-            return html\`
-              <div class="step">
-                <h2>Payment</h2>
-                <input 
-                  type="text" 
-                  placeholder="Card number"
-                  :value=\${() => formData().payment}
-                  @input=\${(e) => formData({...formData(), payment: e.target.value})}
-                />
-              </div>
-            \`;
-        }
-      }}
-      
-      <div class="buttons">
-        \${() => step() > 1 ? html\`
-          <button @click=\${prevStep}>Previous</button>
-        \` : ''}
-        
-        \${() => step() < 3 ? html\`
-          <button @click=\${nextStep}>Next</button>
-        \` : html\`
-          <button @click=\${submitOrder}>Place Order</button>
-        \`}
-      </div>
-    </div>
-  \`;
-});

Example 4: Page with Tabs

javascript
// pages/profile.js
-export default $.page(({ params }) => {
-  const activeTab = $('overview');
-  const userData = $(null);
-  
-  // Load user data
-  $.effect(() => {
-    $.fetch(\`/api/users/\${params.id}\`)
-      .then(data => userData(data));
-  });
-  
-  const tabs = {
-    overview: () => html\`
-      <div>
-        <h3>Overview</h3>
-        <p>Username: \${userData()?.username}</p>
-        <p>Member since: \${userData()?.joined}</p>
-      </div>
-    \`,
-    posts: () => html\`
-      <div>
-        <h3>Posts</h3>
-        \${userData()?.posts.map(post => html\`
-          <div class="post">\${post.title}</div>
-        \`)}
-      </div>
-    \`,
-    settings: () => html\`
-      <div>
-        <h3>Settings</h3>
-        <label>
-          <input type="checkbox" :checked=\${userData()?.emailNotifications} />
-          Email notifications
-        </label>
-      </div>
-    \`
-  };
-  
-  return html\`
-    <div class="profile-page">
-      <h1>\${() => userData()?.name}</h1>
-      
-      <div class="tabs">
-        \${Object.keys(tabs).map(tab => html\`
-          <button 
-            class:active=\${() => activeTab() === tab}
-            @click=\${() => activeTab(tab)}
-          >
-            \${tab.charAt(0).toUpperCase() + tab.slice(1)}
-          </button>
-        \`)}
-      </div>
-      
-      <div class="tab-content">
-        \${() => tabs[activeTab()]()}
-      </div>
-    </div>
-  \`;
-});

🎯 Advanced Patterns

Page with Nested Routes

javascript
// pages/settings/index.js
-export default $.page(({ params }) => {
-  const section = params.section || 'general';
-  
-  const sections = {
-    general: () => import('./general.js').then(m => m.default),
-    security: () => import('./security.js').then(m => m.default),
-    notifications: () => import('./notifications.js').then(m => m.default)
-  };
-  
-  const currentSection = $(null);
-  
-  $.effect(() => {
-    sections[section]().then(comp => currentSection(comp));
-  });
-  
-  return html\`
-    <div class="settings">
-      <nav>
-        <a href="#/settings/general">General</a>
-        <a href="#/settings/security">Security</a>
-        <a href="#/settings/notifications">Notifications</a>
-      </nav>
-      
-      <div class="content">
-        \${currentSection}
-      </div>
-    </div>
-  \`;
-});

Page with Authentication

javascript
// pages/dashboard.js
-export default $.page(({ onUnmount }) => {
-  const isAuthenticated = $(false);
-  const authCheck = $.effect(() => {
-    const token = localStorage.getItem('token');
-    isAuthenticated(!!token);
-  });
-  
-  // Redirect if not authenticated
-  $.effect(() => {
-    if (!isAuthenticated()) {
-      $.router.go('/login');
-    }
-  });
-  
-  return html\`
-    <div class="dashboard">
-      <h1>Protected Dashboard</h1>
-      <!-- Protected content -->
-    </div>
-  \`;
-});

📊 Summary

FeatureDescription
Automatic CleanupAll signals, effects, and resources auto-cleaned on navigation
Memory SafeNo memory leaks, even with complex nested effects
Router IntegrationDesigned to work perfectly with $.router
ParametersAccess route parameters via params object
Manual CleanuponUnmount for custom cleanup needs
Zero ConfigurationJust wrap your page in $.page() and it works

Pro Tip: Always wrap route-based views in $.page() to ensure proper cleanup. This prevents memory leaks and ensures your app stays performant even after many navigation changes.

`,41)])])}const g=i(t,[["render",l]]);export{d as __pageData,g as default}; diff --git a/docs/assets/api_pages.md.BP19nHXw.lean.js b/docs/assets/api_pages.md.BP19nHXw.lean.js deleted file mode 100644 index 8a2f4b3..0000000 --- a/docs/assets/api_pages.md.BP19nHXw.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as i,o as a,c as n,ae as h}from"./chunks/framework.C8AWLET_.js";const d=JSON.parse('{"title":"Pages API 📄","description":"","frontmatter":{},"headers":[],"relativePath":"api/pages.md","filePath":"api/pages.md"}'),t={name:"api/pages.md"};function l(k,s,p,e,E,r){return a(),n("div",null,[...s[0]||(s[0]=[h("",41)])])}const g=i(t,[["render",l]]);export{d as __pageData,g as default}; diff --git a/docs/assets/api_quick.md.BDS3ttnt.js b/docs/assets/api_quick.md.BDS3ttnt.js deleted file mode 100644 index 652a1a7..0000000 --- a/docs/assets/api_quick.md.BDS3ttnt.js +++ /dev/null @@ -1,193 +0,0 @@ -import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const E=JSON.parse('{"title":"Quick API Reference ⚡","description":"","frontmatter":{},"headers":[],"relativePath":"api/quick.md","filePath":"api/quick.md"}'),h={name:"api/quick.md"};function l(e,s,p,k,d,r){return a(),t("div",null,[...s[0]||(s[0]=[n(`

Quick API Reference ⚡

A comprehensive reference for all SigPro APIs. Everything you need to build reactive web applications with signals and web components.

📋 API Functions Reference

FunctionDescriptionExample
$(initialValue)Creates a reactive signal (getter/setter)const count = $(0)
$(computedFn)Creates a computed signalconst full = $(() => first() + last())
$.effect(fn)Runs effect when dependencies change$.effect(() => console.log(count()))
$.page(setupFn)Creates a page with automatic cleanup$.page(() => html
Page
)
$.component(tagName, setupFn, attrs, useShadow)Creates reactive Web Component$.component('my-menu', setup, ['items'])
$.router(routes)Creates a hash-based router$.router([{path:'/', component:Home}])
$.router.go(path)Navigates to a route$.router.go('/user/42')
$.fetch(url, data, loadingSignal)Fetch wrapper with loading stateconst data = await $.fetch('/api', data, loading)
$.storage(key, initialValue, storageType)Persistent signal (local/sessionStorage)const theme = $.storage('theme', 'light')
html\`...\`Template literal for reactive HTMLhtml\`<div>\${count}</div>\`

Signal Methods

MethodDescriptionExample
signal()Gets current valuecount()
signal(newValue)Sets new valuecount(5)
signal(prev => new)Updates using previous valuecount(c => c + 1)

Component Context Properties

PropertyDescriptionExample
propsReactive component propertiesprops.title()
slot(name)Accesses slot contentslot() or slot('footer')
emit(event, data)Dispatches custom eventemit('update', value)
onUnmount(cb)Registers cleanup callbackonUnmount(() => clearInterval(timer))

Page Context Properties

PropertyDescriptionExample
paramsRoute parametersparams.id, params.slug
onUnmount(cb)Registers cleanup callbackonUnmount(() => clearInterval(timer))

HTML Directives

DirectiveDescriptionExample
@eventEvent listener@click=\${handler}
:propertyTwo-way binding:value=\${signal}
?attributeBoolean attribute?disabled=\${signal}
.propertyDOM property binding.scrollTop=\${value}
class:nameConditional classclass:active=\${isActive}

📡 Signals - $(initialValue)

Creates a reactive value that notifies dependents when changed.

PatternExampleDescription
Basic Signalconst count = $(0)Create signal with initial value
Gettercount()Read current value
Settercount(5)Set new value directly
Updatercount(prev => prev + 1)Update based on previous value
Computedconst full = $(() => first() + last())Auto-updating derived signal

Examples

javascript
// Basic signal
-const count = $(0);
-console.log(count()); // 0
-count(5);
-count(c => c + 1); // 6
-
-// Computed signal
-const firstName = $('John');
-const lastName = $('Doe');
-const fullName = $(() => \`\${firstName()} \${lastName()}\`);
-console.log(fullName()); // "John Doe"
-firstName('Jane'); // fullName auto-updates to "Jane Doe"

🔄 Effects - $.effect(fn)

Executes a function and automatically re-runs when its dependencies change.

PatternExampleDescription
Basic Effect$.effect(() => console.log(count()))Run effect on dependency changes
Cleanup$.effect(() => { timer = setInterval(...); return () => clearInterval(timer) })Return cleanup function
Stop Effectconst stop = $.effect(...); stop()Manually stop an effect

Examples

javascript
// Auto-running effect
-const count = $(0);
-$.effect(() => {
-  console.log(\`Count is: \${count()}\`);
-}); // Logs immediately and whenever count changes
-
-// Effect with cleanup
-const userId = $(1);
-$.effect(() => {
-  const id = userId();
-  const timer = setInterval(() => fetchUser(id), 5000);
-  return () => clearInterval(timer); // Cleanup before re-run
-});

📄 Pages - $.page(setupFunction)

Creates a page with automatic cleanup of all signals and effects when navigated away.

javascript
// pages/about.js
-import { $, html } from 'sigpro';
-
-export default $.page(() => {
-  const count = $(0);
-  
-  // Auto-cleaned on navigation
-  $.effect(() => {
-    document.title = \`Count: \${count()}\`;
-  });
-  
-  return html\`
-    <div>
-      <h1>About Page</h1>
-      <p>Count: \${count}</p>
-      <button @click=\${() => count(c => c + 1)}>+</button>
-    </div>
-  \`;
-});

With Parameters

javascript
export default $.page(({ params, onUnmount }) => {
-  const userId = params.id;
-  
-  // Manual cleanup if needed
-  const interval = setInterval(() => refresh(), 10000);
-  onUnmount(() => clearInterval(interval));
-  
-  return html\`<div>User: \${userId}</div>\`;
-});

🧩 Components - $.component(tagName, setup, observedAttributes, useShadowDOM)

Creates Custom Elements with reactive properties.

Parameters

ParameterTypeDefaultDescription
tagNamestringrequiredCustom element tag (must include hyphen)
setupFunctionFunctionrequiredFunction that renders the component
observedAttributesstring[][]Attributes to observe for changes
useShadowDOMbooleanfalsetrue = Shadow DOM (encapsulated), false = Light DOM

Light DOM Example (Default)

javascript
// button.js - inherits global styles
-$.component('my-button', (props, { slot, emit }) => {
-  return html\`
-    <button 
-      class="px-4 py-2 bg-blue-500 text-white rounded"
-      @click=\${() => emit('click')}
-    >
-      \${slot()}
-    </button>
-  \`;
-}, ['variant']); // Observe 'variant' attribute

Shadow DOM Example

javascript
// calendar.js - encapsulated styles
-$.component('my-calendar', (props) => {
-  return html\`
-    <style>
-      /* These styles are isolated */
-      .calendar {
-        background: white;
-        border-radius: 8px;
-        box-shadow: 0 2px 10px rgba(0,0,0,0.1);
-      }
-    </style>
-    <div class="calendar">
-      \${renderCalendar(props.date())}
-    </div>
-  \`;
-}, ['date'], true); // true = use Shadow DOM

🌐 Router - $.router(routes)

Creates a hash-based router with automatic page cleanup.

Route Definition

javascript
const routes = [
-  // Simple routes
-  { path: '/', component: HomePage },
-  { path: '/about', component: AboutPage },
-  
-  // Routes with parameters
-  { path: '/user/:id', component: UserPage },
-  { path: '/user/:id/posts/:pid', component: PostPage },
-  
-  // RegExp routes for advanced matching
-  { path: /^\\/posts\\/(?<id>\\d+)$/, component: PostPage },
-];

Usage

javascript
import { $, html } from 'sigpro';
-import Home from './pages/Home.js';
-import User from './pages/User.js';
-
-const router = $.router([
-  { path: '/', component: Home },
-  { path: '/user/:id', component: User },
-]);
-
-// Navigation
-$.router.go('/user/42');
-$.router.go('about'); // Same as '/about'
-
-// In templates
-html\`
-  <nav>
-    <a href="#/">Home</a>
-    <a href="#/user/42">Profile</a>
-    <button @click=\${() => $.router.go('/contact')}>
-      Contact
-    </button>
-  </nav>
-\`;

📦 Storage - $.storage(key, initialValue, [storage])

Persistent signal that syncs with localStorage or sessionStorage.

javascript
// localStorage (default)
-const theme = $.storage('theme', 'light');
-const user = $.storage('user', null);
-const settings = $.storage('settings', { notifications: true });
-
-// sessionStorage
-const tempData = $.storage('temp', {}, sessionStorage);
-
-// Usage like a normal signal
-theme('dark'); // Auto-saves to localStorage
-console.log(theme()); // 'dark' (even after page refresh)

🌐 Fetch - $.fetch(url, data, [loading])

Simple fetch wrapper with automatic JSON handling.

javascript
const loading = $(false);
-
-async function loadUser(id) {
-  const user = await $.fetch(\`/api/users/\${id}\`, null, loading);
-  if (user) userData(user);
-}
-
-// In template
-html\`
-  <div>
-    \${() => loading() ? html\`<spinner></spinner>\` : html\`
-      <p>\${userData()?.name}</p>
-    \`}
-  </div>
-\`;

🎨 Template Literals - html\`...\`

Creates reactive DOM fragments with directives.

Directives Reference

DirectiveExampleDescription
Event@click=\${handler}Add event listener
Two-way binding:value=\${signal}Bind signal to input value
Boolean attribute?disabled=\${signal}Toggle boolean attribute
Property.scrollTop=\${value}Set DOM property directly
Class toggleclass:active=\${isActive}Toggle class conditionally

Examples

javascript
const text = $('');
-const isDisabled = $(false);
-const activeTab = $('home');
-
-html\`
-  <!-- Event binding -->
-  <button @click=\${() => count(c => c + 1)}>+</button>
-  
-  <!-- Two-way binding -->
-  <input :value=\${text} />
-  <p>You typed: \${text}</p>
-  
-  <!-- Boolean attributes -->
-  <button ?disabled=\${isDisabled}>Submit</button>
-  
-  <!-- Class toggles -->
-  <div class:active=\${activeTab() === 'home'}>
-    Home content
-  </div>
-  
-  <!-- Property binding -->
-  <div .scrollTop=\${scrollPosition}></div>
-\`;

🎯 Complete Component Example

javascript
import { $, html } from 'sigpro';
-
-// Create a component
-$.component('user-profile', (props, { slot, emit }) => {
-  // Reactive state
-  const user = $(null);
-  const loading = $(false);
-  
-  // Load user data when userId changes
-  $.effect(() => {
-    const id = props.userId();
-    if (id) {
-      loading(true);
-      $.fetch(\`/api/users/\${id}\`, null, loading)
-        .then(data => user(data));
-    }
-  });
-  
-  // Computed value
-  const fullName = $(() => 
-    user() ? \`\${user().firstName} \${user().lastName}\` : ''
-  );
-  
-  // Template
-  return html\`
-    <div class="user-profile">
-      \${() => loading() ? html\`
-        <div class="spinner">Loading...</div>
-      \` : user() ? html\`
-        <h2>\${fullName}</h2>
-        <p>Email: \${user().email}</p>
-        <button @click=\${() => emit('select', user())}>
-          \${slot('Select')}
-        </button>
-      \` : html\`
-        <p>User not found</p>
-      \`}
-    </div>
-  \`;
-}, ['user-id']); // Observe userId attribute
`,55)])])}const F=i(h,[["render",l]]);export{E as __pageData,F as default}; diff --git a/docs/assets/api_quick.md.BDS3ttnt.lean.js b/docs/assets/api_quick.md.BDS3ttnt.lean.js deleted file mode 100644 index 012ea6d..0000000 --- a/docs/assets/api_quick.md.BDS3ttnt.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const E=JSON.parse('{"title":"Quick API Reference ⚡","description":"","frontmatter":{},"headers":[],"relativePath":"api/quick.md","filePath":"api/quick.md"}'),h={name:"api/quick.md"};function l(e,s,p,k,d,r){return a(),t("div",null,[...s[0]||(s[0]=[n("",55)])])}const F=i(h,[["render",l]]);export{E as __pageData,F as default}; diff --git a/docs/assets/api_quick.md.Cy_XozKR.js b/docs/assets/api_quick.md.Cy_XozKR.js new file mode 100644 index 0000000..49fa511 --- /dev/null +++ b/docs/assets/api_quick.md.Cy_XozKR.js @@ -0,0 +1,7 @@ +import{_ as e,o as s,c as i,ae as a}from"./chunks/framework.C8AWLET_.js";const c=JSON.parse('{"title":"Quick API Reference ⚡","description":"","frontmatter":{},"headers":[],"relativePath":"api/quick.md","filePath":"api/quick.md"}'),l={name:"api/quick.md"};function n(d,t,o,r,h,g){return s(),i("div",null,[...t[0]||(t[0]=[a(`

Quick API Reference ⚡

This is a high-level summary of the SigPro core API. For detailed guides and edge cases, please refer to the specific documentation for each module.

1. Core Reactivity: $( )

The $ function is a polymorphic constructor. It creates Signals (state) or Computed Effects (logic) based on the input type.

UsageInput TypeReturnsDescription
SignalanyFunctionA getter/setter for reactive state.
ComputedFunctionFunctionA read-only signal that auto-updates when its dependencies change.

Example:

javascript
const $count = $(0);             // Signal
+const $double = $(() => $count() * 2); // Computed

2. Rendering Engine: $.html

SigPro uses a hyperscript-style engine to create live DOM nodes.

ArgumentTypeRequiredDescription
tagstringYesStandard HTML tag (e.g., 'div', 'button').
propsObjectNoAttributes (id), Events (onclick), or Reactive Props ($value).
contentanyNoString, Node, Array, or Reactive Function.

Example:

javascript
$.html('button', { onclick: () => alert('Hi!') }, 'Click Me');

3. Global Helpers (Tag Proxies)

To keep your code clean, SigPro automatically exposes common HTML tags to the global scope.

CategoryAvailable Tags
Layoutdiv, section, main, nav, header, footer, span
Typographyh1, h2, h3, p, label, a, li, ul, ol
Formsinput, button, form, select, option
Mediaimg, video, audio, canvas

Example:

javascript
// No imports needed!
+div([ 
+  h1("Title"), 
+  button("Ok") 
+]);

4. Mounting & Plugins

Methods to initialize your application and extend the engine.

MethodSignatureDescription
$.mount(node, target)Wipes the target (default: body) and renders the component.
$.plugin(source)Registers a function or loads external .js scripts as plugins.

Example:

javascript
$.plugin([UI, Router]);
+$.mount(App, '#root');

5. Reactive Syntax Cheat Sheet

FeatureSyntaxDescription
Text Bindingp(["Value: ", $sig])Updates text content automatically.
Attributesdiv({ id: $sig })Static attribute assignment.
Reactive Attrdiv({ $class: $sig })Attribute updates when $sig changes.
Two-way Bindinginput({ $value: $sig })Syncs input value and signal automatically.
Conditionaldiv(() => $sig() > 0 ? "Yes" : "No")Re-renders only the content when the condition changes.

Summary Table

FeatureSigPro ApproachBenefit
Update LogicFine-grained (Surgical)Blazing fast updates.
DOMNative NodesZero abstraction cost.
SyntaxPure JavaScriptNo build-tool lock-in.
FootprintModularLoad only what you use.
`,31)])])}const k=e(l,[["render",n]]);export{c as __pageData,k as default}; diff --git a/docs/assets/api_quick.md.Cy_XozKR.lean.js b/docs/assets/api_quick.md.Cy_XozKR.lean.js new file mode 100644 index 0000000..53ba9b5 --- /dev/null +++ b/docs/assets/api_quick.md.Cy_XozKR.lean.js @@ -0,0 +1 @@ +import{_ as e,o as s,c as i,ae as a}from"./chunks/framework.C8AWLET_.js";const c=JSON.parse('{"title":"Quick API Reference ⚡","description":"","frontmatter":{},"headers":[],"relativePath":"api/quick.md","filePath":"api/quick.md"}'),l={name:"api/quick.md"};function n(d,t,o,r,h,g){return s(),i("div",null,[...t[0]||(t[0]=[a("",31)])])}const k=e(l,[["render",n]]);export{c as __pageData,k as default}; diff --git a/docs/assets/api_routing.md.7SNAZXtp.js b/docs/assets/api_routing.md.7SNAZXtp.js deleted file mode 100644 index f44d17d..0000000 --- a/docs/assets/api_routing.md.7SNAZXtp.js +++ /dev/null @@ -1,604 +0,0 @@ -import{_ as i,o as a,c as n,ae as h}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Routing API 🌐","description":"","frontmatter":{},"headers":[],"relativePath":"api/routing.md","filePath":"api/routing.md"}'),t={name:"api/routing.md"};function k(p,s,l,e,E,r){return a(),n("div",null,[...s[0]||(s[0]=[h(`

Routing API 🌐

SigPro includes a simple yet powerful hash-based router designed for Single Page Applications (SPAs). It works everywhere with zero server configuration and integrates seamlessly with $.page for automatic cleanup.

Why Hash-Based Routing?

Hash routing (#/about) works everywhere - no server configuration needed. Perfect for:

  • Static sites and SPAs
  • GitHub Pages, Netlify, any static hosting
  • Local development without a server
  • Projects that need to work immediately

$.router(routes)

Creates a hash-based router that renders the matching component and handles navigation.

javascript
import { $, html } from 'sigpro';
-import HomePage from './pages/Home.js';
-import AboutPage from './pages/About.js';
-import UserPage from './pages/User.js';
-
-const routes = [
-  { path: '/', component: HomePage },
-  { path: '/about', component: AboutPage },
-  { path: '/user/:id', component: UserPage },
-];
-
-// Mount the router
-document.body.appendChild($.router(routes));

📋 API Reference

$.router(routes)

ParameterTypeDescription
routesArray<Route>Array of route configurations

Returns: HTMLDivElement - Container that renders the current page

$.router.go(path)

ParameterTypeDescription
pathstringRoute path to navigate to (automatically adds leading slash)

Route Object

PropertyTypeDescription
pathstring or RegExpRoute pattern to match
componentFunctionFunction that returns page content (receives params)

🎯 Route Patterns

String Paths (Simple Routes)

javascript
const routes = [
-  // Static routes
-  { path: '/', component: HomePage },
-  { path: '/about', component: AboutPage },
-  { path: '/contact', component: ContactPage },
-  
-  // Routes with parameters
-  { path: '/user/:id', component: UserPage },
-  { path: '/user/:id/posts', component: UserPostsPage },
-  { path: '/user/:id/posts/:postId', component: PostPage },
-  { path: '/search/:query/page/:num', component: SearchPage },
-];

RegExp Paths (Advanced Routing)

javascript
const routes = [
-  // Match numeric IDs only
-  { path: /^\\/users\\/(?<id>\\d+)$/, component: UserPage },
-  
-  // Match product slugs (letters, numbers, hyphens)
-  { path: /^\\/products\\/(?<slug>[a-z0-9-]+)$/, component: ProductPage },
-  
-  // Match blog posts by year/month
-  { path: /^\\/blog\\/(?<year>\\d{4})\\/(?<month>\\d{2})$/, component: BlogArchive },
-  
-  // Match optional language prefix
-  { path: /^\\/(?<lang>en|es|fr)?\\/?about$/, component: AboutPage },
-  
-  // Match UUID format
-  { path: /^\\/items\\/(?<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/, 
-    component: ItemPage },
-];

📦 Basic Examples

Simple Router Setup

javascript
// main.js
-import { $, html } from 'sigpro';
-import Home from './pages/Home.js';
-import About from './pages/About.js';
-import Contact from './pages/Contact.js';
-
-const routes = [
-  { path: '/', component: Home },
-  { path: '/about', component: About },
-  { path: '/contact', component: Contact },
-];
-
-const router = $.router(routes);
-
-// Mount to DOM
-document.body.appendChild(router);

Page Components with Parameters

javascript
// pages/User.js
-import { $, html } from 'sigpro';
-
-export default (params) => $.page(() => {
-  // /user/42 → params = { id: '42' }
-  // /user/john/posts/123 → params = { id: 'john', postId: '123' }
-  const userId = params.id;
-  const userData = $(null);
-  
-  $.effect(() => {
-    fetch(\`/api/users/\${userId}\`)
-      .then(res => res.json())
-      .then(data => userData(data));
-  });
-  
-  return html\`
-    <div class="user-page">
-      <h1>User Profile: \${userId}</h1>
-      \${() => userData() ? html\`
-        <p>Name: \${userData().name}</p>
-        <p>Email: \${userData().email}</p>
-      \` : html\`<p>Loading...</p>\`}
-    </div>
-  \`;
-});
javascript
import { $, html } from 'sigpro';
-
-// In templates
-const NavBar = () => html\`
-  <nav>
-    <a href="#/">Home</a>
-    <a href="#/about">About</a>
-    <a href="#/contact">Contact</a>
-    <a href="#/user/42">Profile</a>
-    <a href="#/search/js/page/1">Search</a>
-    
-    <!-- Programmatic navigation -->
-    <button @click=\${() => $.router.go('/about')}>
-      Go to About
-    </button>
-    
-    <button @click=\${() => $.router.go('contact')}>
-      Go to Contact (auto-adds leading slash)
-    </button>
-  </nav>
-\`;

🚀 Advanced Examples

Complete Application with Layout

javascript
// App.js
-import { $, html } from 'sigpro';
-import HomePage from './pages/Home.js';
-import AboutPage from './pages/About.js';
-import UserPage from './pages/User.js';
-import SettingsPage from './pages/Settings.js';
-import NotFound from './pages/NotFound.js';
-
-// Layout component with navigation
-const Layout = (content) => html\`
-  <div class="app">
-    <header class="header">
-      <h1>My SigPro App</h1>
-      <nav class="nav">
-        <a href="#/" class:active=\${() => isActive('/')}>Home</a>
-        <a href="#/about" class:active=\${() => isActive('/about')}>About</a>
-        <a href="#/user/42" class:active=\${() => isActive('/user/42')}>Profile</a>
-        <a href="#/settings" class:active=\${() => isActive('/settings')}>Settings</a>
-      </nav>
-    </header>
-    
-    <main class="main">
-      \${content}
-    </main>
-    
-    <footer class="footer">
-      <p>© 2024 SigPro App</p>
-    </footer>
-  </div>
-\`;
-
-// Helper to check active route
-const isActive = (path) => {
-  const current = window.location.hash.replace(/^#/, '') || '/';
-  return current === path;
-};
-
-// Routes with layout
-const routes = [
-  { path: '/', component: (params) => Layout(HomePage(params)) },
-  { path: '/about', component: (params) => Layout(AboutPage(params)) },
-  { path: '/user/:id', component: (params) => Layout(UserPage(params)) },
-  { path: '/settings', component: (params) => Layout(SettingsPage(params)) },
-  { path: '/:path(.*)', component: (params) => Layout(NotFound(params)) }, // Catch-all
-];
-
-// Create and mount router
-const router = $.router(routes);
-document.body.appendChild(router);

Nested Routes

javascript
// pages/Settings.js (parent route)
-import { $, html } from 'sigpro';
-import SettingsGeneral from './settings/General.js';
-import SettingsSecurity from './settings/Security.js';
-import SettingsNotifications from './settings/Notifications.js';
-
-export default (params) => $.page(() => {
-  const section = params.section || 'general';
-  
-  const sections = {
-    general: SettingsGeneral,
-    security: SettingsSecurity,
-    notifications: SettingsNotifications
-  };
-  
-  const CurrentSection = sections[section];
-  
-  return html\`
-    <div class="settings">
-      <h1>Settings</h1>
-      
-      <div class="settings-layout">
-        <nav class="settings-sidebar">
-          <a href="#/settings/general" class:active=\${() => section === 'general'}>
-            General
-          </a>
-          <a href="#/settings/security" class:active=\${() => section === 'security'}>
-            Security
-          </a>
-          <a href="#/settings/notifications" class:active=\${() => section === 'notifications'}>
-            Notifications
-          </a>
-        </nav>
-        
-        <div class="settings-content">
-          \${CurrentSection(params)}
-        </div>
-      </div>
-    </div>
-  \`;
-});
-
-// pages/settings/General.js
-export default (params) => $.page(() => {
-  return html\`
-    <div>
-      <h2>General Settings</h2>
-      <form>...</form>
-    </div>
-  \`;
-});
-
-// Main router with nested routes
-const routes = [
-  { path: '/', component: HomePage },
-  { path: '/settings/:section?', component: SettingsPage }, // Optional section param
-];

Protected Routes (Authentication)

javascript
// auth.js
-import { $ } from 'sigpro';
-
-const isAuthenticated = $(false);
-const user = $(null);
-
-export const checkAuth = async () => {
-  const token = localStorage.getItem('token');
-  if (token) {
-    try {
-      const response = await fetch('/api/verify');
-      if (response.ok) {
-        const userData = await response.json();
-        user(userData);
-        isAuthenticated(true);
-        return true;
-      }
-    } catch (e) {
-      // Handle error
-    }
-  }
-  isAuthenticated(false);
-  user(null);
-  return false;
-};
-
-export const requireAuth = (component) => (params) => {
-  if (isAuthenticated()) {
-    return component(params);
-  }
-  // Redirect to login
-  $.router.go('/login');
-  return null;
-};
-
-export { isAuthenticated, user };
javascript
// pages/Dashboard.js (protected route)
-import { $, html } from 'sigpro';
-import { requireAuth, user } from '../auth.js';
-
-const Dashboard = (params) => $.page(() => {
-  return html\`
-    <div class="dashboard">
-      <h1>Welcome, \${() => user()?.name}!</h1>
-      <p>This is your protected dashboard.</p>
-    </div>
-  \`;
-});
-
-export default requireAuth(Dashboard);
javascript
// main.js with protected routes
-import { $, html } from 'sigpro';
-import { checkAuth } from './auth.js';
-import HomePage from './pages/Home.js';
-import LoginPage from './pages/Login.js';
-import DashboardPage from './pages/Dashboard.js';
-import AdminPage from './pages/Admin.js';
-
-// Check auth on startup
-checkAuth();
-
-const routes = [
-  { path: '/', component: HomePage },
-  { path: '/login', component: LoginPage },
-  { path: '/dashboard', component: DashboardPage }, // Protected
-  { path: '/admin', component: AdminPage }, // Protected
-];
-
-document.body.appendChild($.router(routes));

Route Transitions

javascript
// with-transitions.js
-import { $, html } from 'sigpro';
-
-export const createRouterWithTransitions = (routes) => {
-  const transitioning = $(false);
-  const currentView = $(null);
-  const nextView = $(null);
-  
-  const container = document.createElement('div');
-  container.style.display = 'contents';
-  
-  const renderWithTransition = async (newView) => {
-    if (currentView() === newView) return;
-    
-    transitioning(true);
-    nextView(newView);
-    
-    // Fade out
-    container.style.transition = 'opacity 0.2s';
-    container.style.opacity = '0';
-    
-    await new Promise(resolve => setTimeout(resolve, 200));
-    
-    // Update content
-    container.replaceChildren(newView);
-    currentView(newView);
-    
-    // Fade in
-    container.style.opacity = '1';
-    
-    await new Promise(resolve => setTimeout(resolve, 200));
-    transitioning(false);
-    container.style.transition = '';
-  };
-  
-  const router = $.router(routes.map(route => ({
-    ...route,
-    component: (params) => {
-      const view = route.component(params);
-      renderWithTransition(view);
-      return document.createComment('router-placeholder');
-    }
-  })));
-  
-  return router;
-};
javascript
// with-breadcrumbs.js
-import { $, html } from 'sigpro';
-
-export const createBreadcrumbs = (routes) => {
-  const breadcrumbs = $([]);
-  
-  const updateBreadcrumbs = (path) => {
-    const parts = path.split('/').filter(Boolean);
-    const crumbs = [];
-    let currentPath = '';
-    
-    parts.forEach((part, index) => {
-      currentPath += \`/\${part}\`;
-      
-      // Find matching route
-      const route = routes.find(r => {
-        if (r.path.includes(':')) {
-          const pattern = r.path.replace(/:[^/]+/g, part);
-          return pattern === currentPath;
-        }
-        return r.path === currentPath;
-      });
-      
-      crumbs.push({
-        path: currentPath,
-        label: route?.name || part.charAt(0).toUpperCase() + part.slice(1),
-        isLast: index === parts.length - 1
-      });
-    });
-    
-    breadcrumbs(crumbs);
-  };
-  
-  // Listen to route changes
-  window.addEventListener('hashchange', () => {
-    const path = window.location.hash.replace(/^#/, '') || '/';
-    updateBreadcrumbs(path);
-  });
-  
-  // Initial update
-  updateBreadcrumbs(window.location.hash.replace(/^#/, '') || '/');
-  
-  return breadcrumbs;
-};
javascript
// Usage in layout
-import { createBreadcrumbs } from './with-breadcrumbs.js';
-
-const breadcrumbs = createBreadcrumbs(routes);
-
-const Layout = (content) => html\`
-  <div class="app">
-    <nav class="breadcrumbs">
-      \${() => breadcrumbs().map(crumb => html\`
-        \${!crumb.isLast ? html\`
-          <a href="#\${crumb.path}">\${crumb.label}</a>
-          <span class="separator">/</span>
-        \` : html\`
-          <span class="current">\${crumb.label}</span>
-        \`}
-      \`)}
-    </nav>
-    
-    <main>
-      \${content}
-    </main>
-  </div>
-\`;

Query Parameters

javascript
// with-query-params.js
-export const getQueryParams = () => {
-  const hash = window.location.hash;
-  const queryStart = hash.indexOf('?');
-  if (queryStart === -1) return {};
-  
-  const queryString = hash.slice(queryStart + 1);
-  const params = new URLSearchParams(queryString);
-  const result = {};
-  
-  for (const [key, value] of params) {
-    result[key] = value;
-  }
-  
-  return result;
-};
-
-export const updateQueryParams = (params) => {
-  const hash = window.location.hash.split('?')[0];
-  const queryString = new URLSearchParams(params).toString();
-  window.location.hash = queryString ? \`\${hash}?\${queryString}\` : hash;
-};
javascript
// Search page with query params
-import { $, html } from 'sigpro';
-import { getQueryParams, updateQueryParams } from './with-query-params.js';
-
-export default (params) => $.page(() => {
-  // Get initial query from URL
-  const queryParams = getQueryParams();
-  const searchQuery = $(queryParams.q || '');
-  const page = $(parseInt(queryParams.page) || 1);
-  const results = $([]);
-  
-  // Update URL when search changes
-  $.effect(() => {
-    updateQueryParams({
-      q: searchQuery() || undefined,
-      page: page() > 1 ? page() : undefined
-    });
-  });
-  
-  // Fetch results when search or page changes
-  $.effect(() => {
-    if (searchQuery()) {
-      fetch(\`/api/search?q=\${searchQuery()}&page=\${page()}\`)
-        .then(res => res.json())
-        .then(data => results(data));
-    }
-  });
-  
-  return html\`
-    <div class="search-page">
-      <h1>Search</h1>
-      
-      <input
-        type="search"
-        :value=\${searchQuery}
-        placeholder="Search..."
-        @input=\${(e) => {
-          searchQuery(e.target.value);
-          page(1); // Reset to first page on new search
-        }}
-      />
-      
-      <div class="results">
-        \${results().map(item => html\`
-          <div class="result">\${item.title}</div>
-        \`)}
-      </div>
-      
-      \${() => results().length ? html\`
-        <div class="pagination">
-          <button 
-            ?disabled=\${() => page() <= 1}
-            @click=\${() => page(p => p - 1)}
-          >
-            Previous
-          </button>
-          
-          <span>Page \${page}</span>
-          
-          <button 
-            ?disabled=\${() => results().length < 10}
-            @click=\${() => page(p => p + 1)}
-          >
-            Next
-          </button>
-        </div>
-      \` : ''}
-    </div>
-  \`;
-});

Lazy Loading Routes

javascript
// lazy.js
-export const lazy = (loader) => {
-  let component = null;
-  
-  return async (params) => {
-    if (!component) {
-      const module = await loader();
-      component = module.default;
-    }
-    return component(params);
-  };
-};
javascript
// main.js with lazy loading
-import { $, html } from 'sigpro';
-import { lazy } from './lazy.js';
-import Layout from './Layout.js';
-
-const routes = [
-  { path: '/', component: lazy(() => import('./pages/Home.js')) },
-  { path: '/about', component: lazy(() => import('./pages/About.js')) },
-  { path: '/dashboard', component: lazy(() => import('./pages/Dashboard.js')) },
-  { 
-    path: '/admin', 
-    component: lazy(() => import('./pages/Admin.js')),
-    // Show loading state
-    loading: () => html\`<div class="loading">Loading admin panel...</div>\`
-  },
-];
-
-// Wrap with layout
-const routesWithLayout = routes.map(route => ({
-  ...route,
-  component: (params) => Layout(route.component(params))
-}));
-
-document.body.appendChild($.router(routesWithLayout));

Route Guards / Middleware

javascript
// middleware.js
-export const withGuard = (component, guard) => (params) => {
-  const result = guard(params);
-  if (result === true) {
-    return component(params);
-  } else if (typeof result === 'string') {
-    $.router.go(result);
-    return null;
-  }
-  return result; // Custom component (e.g., AccessDenied)
-};
-
-// Guards
-export const roleGuard = (requiredRole) => (params) => {
-  const userRole = localStorage.getItem('userRole');
-  if (userRole === requiredRole) return true;
-  if (!userRole) return '/login';
-  return AccessDeniedPage(params);
-};
-
-export const authGuard = () => (params) => {
-  const token = localStorage.getItem('token');
-  return token ? true : '/login';
-};
-
-export const pendingChangesGuard = (hasPendingChanges) => (params) => {
-  if (hasPendingChanges()) {
-    return ConfirmLeavePage(params);
-  }
-  return true;
-};
javascript
// Usage
-import { withGuard, authGuard, roleGuard } from './middleware.js';
-
-const routes = [
-  { path: '/', component: HomePage },
-  { path: '/profile', component: withGuard(ProfilePage, authGuard()) },
-  { 
-    path: '/admin', 
-    component: withGuard(AdminPage, roleGuard('admin')) 
-  },
-];

📊 Route Matching Priority

Routes are matched in the order they are defined. More specific routes should come first:

javascript
const routes = [
-  // More specific first
-  { path: '/user/:id/edit', component: EditUserPage },
-  { path: '/user/:id/posts', component: UserPostsPage },
-  { path: '/user/:id', component: UserPage },
-  
-  // Static routes
-  { path: '/about', component: AboutPage },
-  { path: '/contact', component: ContactPage },
-  
-  // Catch-all last
-  { path: '/:path(.*)', component: NotFoundPage },
-];

🎯 Complete Example

javascript
// main.js - Complete application
-import { $, html } from 'sigpro';
-import { lazy } from './utils/lazy.js';
-import { withGuard, authGuard } from './utils/middleware.js';
-import Layout from './components/Layout.js';
-
-// Lazy load pages
-const HomePage = lazy(() => import('./pages/Home.js'));
-const AboutPage = lazy(() => import('./pages/About.js'));
-const LoginPage = lazy(() => import('./pages/Login.js'));
-const DashboardPage = lazy(() => import('./pages/Dashboard.js'));
-const UserPage = lazy(() => import('./pages/User.js'));
-const SettingsPage = lazy(() => import('./pages/Settings.js'));
-const NotFoundPage = lazy(() => import('./pages/NotFound.js'));
-
-// Route configuration
-const routes = [
-  { path: '/', component: HomePage, name: 'Home' },
-  { path: '/about', component: AboutPage, name: 'About' },
-  { path: '/login', component: LoginPage, name: 'Login' },
-  { 
-    path: '/dashboard', 
-    component: withGuard(DashboardPage, authGuard()),
-    name: 'Dashboard'
-  },
-  { 
-    path: '/user/:id', 
-    component: UserPage,
-    name: 'User Profile'
-  },
-  { 
-    path: '/settings/:section?', 
-    component: withGuard(SettingsPage, authGuard()),
-    name: 'Settings'
-  },
-  { path: '/:path(.*)', component: NotFoundPage, name: 'Not Found' },
-];
-
-// Wrap all routes with layout
-const routesWithLayout = routes.map(route => ({
-  ...route,
-  component: (params) => Layout(route.component(params))
-}));
-
-// Create and mount router
-const router = $.router(routesWithLayout);
-document.body.appendChild(router);
-
-// Navigation helper (available globally)
-window.navigate = $.router.go;

📊 Summary

FeatureDescription
Hash-basedWorks everywhere, no server config
Route Parameters:param syntax for dynamic segments
RegExp SupportAdvanced pattern matching
Query ParametersSupport for ?key=value in URLs
Programmatic Navigation$.router.go(path)
Auto-cleanupWorks with $.page for memory management
Zero DependenciesPure vanilla JavaScript
Lazy Loading ReadyEasy code splitting

Pro Tip: Order matters in route definitions - put more specific routes (with parameters) before static ones, and always include a catch-all route (404) at the end.

`,60)])])}const F=i(t,[["render",k]]);export{g as __pageData,F as default}; diff --git a/docs/assets/api_routing.md.7SNAZXtp.lean.js b/docs/assets/api_routing.md.7SNAZXtp.lean.js deleted file mode 100644 index 37f6f1a..0000000 --- a/docs/assets/api_routing.md.7SNAZXtp.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as i,o as a,c as n,ae as h}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Routing API 🌐","description":"","frontmatter":{},"headers":[],"relativePath":"api/routing.md","filePath":"api/routing.md"}'),t={name:"api/routing.md"};function k(p,s,l,e,E,r){return a(),n("div",null,[...s[0]||(s[0]=[h("",60)])])}const F=i(t,[["render",k]]);export{g as __pageData,F as default}; diff --git a/docs/assets/api_signals.md.CrW68-BA.js b/docs/assets/api_signals.md.CrW68-BA.js deleted file mode 100644 index acf5267..0000000 --- a/docs/assets/api_signals.md.CrW68-BA.js +++ /dev/null @@ -1,659 +0,0 @@ -import{_ as i,o as a,c as n,ae as h}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Signals API 📡","description":"","frontmatter":{},"headers":[],"relativePath":"api/signals.md","filePath":"api/signals.md"}'),k={name:"api/signals.md"};function l(t,s,p,e,E,r){return a(),n("div",null,[...s[0]||(s[0]=[h(`

Signals API 📡

Signals are the heart of SigPro's reactivity system. They are reactive values that automatically track dependencies and notify subscribers when they change. This enables fine-grained updates without virtual DOM diffing.

Core Concepts

What is a Signal?

A signal is a function that holds a value and notifies dependents when that value changes. Signals can be:

  • Basic signals - Hold simple values (numbers, strings, objects)
  • Computed signals - Derive values from other signals
  • Persistent signals - Automatically sync with localStorage/sessionStorage

How Reactivity Works

SigPro uses automatic dependency tracking:

  1. When you read a signal inside an effect, the effect becomes a subscriber
  2. When the signal's value changes, all subscribers are notified
  3. Updates are batched using microtasks for optimal performance
  4. Only the exact nodes that depend on changed values are updated

$(initialValue)

Creates a reactive signal. The behavior changes based on the type of initialValue:

  • If initialValue is a function, creates a computed signal
  • Otherwise, creates a basic signal
javascript
import { $ } from 'sigpro';
-
-// Basic signal
-const count = $(0);
-
-// Computed signal
-const firstName = $('John');
-const lastName = $('Doe');
-const fullName = $(() => \`\${firstName()} \${lastName()}\`);

📋 API Reference

Basic Signals

PatternExampleDescription
Createconst count = $(0)Create signal with initial value
Getcount()Read current value
Setcount(5)Set new value directly
Updatecount(prev => prev + 1)Update based on previous value

Computed Signals

PatternExampleDescription
Createconst total = $(() => price() * quantity())Derive value from other signals
Gettotal()Read computed value (auto-updates)

Signal Methods

MethodDescriptionExample
signal()Gets current valuecount()
signal(newValue)Sets new valuecount(5)
signal(prev => new)Updates using previous valuecount(c => c + 1)

🎯 Basic Examples

Counter Signal

javascript
import { $ } from 'sigpro';
-
-const count = $(0);
-
-console.log(count()); // 0
-
-count(5);
-console.log(count()); // 5
-
-count(prev => prev + 1);
-console.log(count()); // 6

Object Signal

javascript
import { $ } from 'sigpro';
-
-const user = $({
-  name: 'John',
-  age: 30,
-  email: 'john@example.com'
-});
-
-// Read
-console.log(user().name); // 'John'
-
-// Update (immutable pattern)
-user({
-  ...user(),
-  age: 31
-});
-
-// Partial update with function
-user(prev => ({
-  ...prev,
-  email: 'john.doe@example.com'
-}));

Array Signal

javascript
import { $ } from 'sigpro';
-
-const todos = $(['Learn SigPro', 'Build an app']);
-
-// Add item
-todos([...todos(), 'Deploy to production']);
-
-// Remove item
-todos(todos().filter((_, i) => i !== 1));
-
-// Update item
-todos(todos().map((todo, i) => 
-  i === 0 ? 'Master SigPro' : todo
-));

🔄 Computed Signals

Computed signals automatically update when their dependencies change:

javascript
import { $ } from 'sigpro';
-
-const price = $(10);
-const quantity = $(2);
-const tax = $(0.21);
-
-// Computed signals
-const subtotal = $(() => price() * quantity());
-const taxAmount = $(() => subtotal() * tax());
-const total = $(() => subtotal() + taxAmount());
-
-console.log(total()); // 24.2
-
-price(15);
-console.log(total()); // 36.3 (automatically updated)
-
-quantity(3);
-console.log(total()); // 54.45 (automatically updated)

Computed with Multiple Dependencies

javascript
import { $ } from 'sigpro';
-
-const firstName = $('John');
-const lastName = $('Doe');
-const prefix = $('Mr.');
-
-const fullName = $(() => {
-  // Computed signals can contain logic
-  const name = \`\${firstName()} \${lastName()}\`;
-  return prefix() ? \`\${prefix()} \${name}\` : name;
-});
-
-console.log(fullName()); // 'Mr. John Doe'
-
-prefix('');
-console.log(fullName()); // 'John Doe'

Computed with Conditional Logic

javascript
import { $ } from 'sigpro';
-
-const user = $({ role: 'admin', permissions: [] });
-const isAdmin = $(() => user().role === 'admin');
-const hasPermission = $(() => 
-  isAdmin() || user().permissions.includes('edit')
-);
-
-console.log(hasPermission()); // true
-
-user({ role: 'user', permissions: ['view'] });
-console.log(hasPermission()); // false (can't edit)
-
-user({ role: 'user', permissions: ['view', 'edit'] });
-console.log(hasPermission()); // true (now has permission)

🧮 Advanced Signal Patterns

Derived State Pattern

javascript
import { $ } from 'sigpro';
-
-// Shopping cart example
-const cart = $([
-  { id: 1, name: 'Product 1', price: 10, quantity: 2 },
-  { id: 2, name: 'Product 2', price: 15, quantity: 1 },
-]);
-
-// Derived values
-const itemCount = $(() => 
-  cart().reduce((sum, item) => sum + item.quantity, 0)
-);
-
-const subtotal = $(() => 
-  cart().reduce((sum, item) => sum + (item.price * item.quantity), 0)
-);
-
-const tax = $(() => subtotal() * 0.21);
-const total = $(() => subtotal() + tax());
-
-// Update cart
-cart([
-  ...cart(),
-  { id: 3, name: 'Product 3', price: 20, quantity: 1 }
-]);
-
-// All derived values auto-update
-console.log(itemCount()); // 4
-console.log(total()); // (10*2 + 15*1 + 20*1) * 1.21 = 78.65

Validation Pattern

javascript
import { $ } from 'sigpro';
-
-const email = $('');
-const password = $('');
-const confirmPassword = $('');
-
-// Validation signals
-const isEmailValid = $(() => {
-  const value = email();
-  return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value);
-});
-
-const isPasswordValid = $(() => {
-  const value = password();
-  return value.length >= 8;
-});
-
-const doPasswordsMatch = $(() => 
-  password() === confirmPassword()
-);
-
-const isFormValid = $(() => 
-  isEmailValid() && isPasswordValid() && doPasswordsMatch()
-);
-
-// Update form
-email('user@example.com');
-password('secure123');
-confirmPassword('secure123');
-
-console.log(isFormValid()); // true
-
-// Validation messages
-const emailError = $(() => 
-  email() && !isEmailValid() ? 'Invalid email format' : ''
-);

Filtering and Search Pattern

javascript
import { $ } from 'sigpro';
-
-const items = $([
-  { id: 1, name: 'Apple', category: 'fruit' },
-  { id: 2, name: 'Banana', category: 'fruit' },
-  { id: 3, name: 'Carrot', category: 'vegetable' },
-  { id: 4, name: 'Date', category: 'fruit' },
-]);
-
-const searchTerm = $('');
-const categoryFilter = $('all');
-
-// Filtered items (computed)
-const filteredItems = $(() => {
-  let result = items();
-  
-  // Apply search filter
-  if (searchTerm()) {
-    const term = searchTerm().toLowerCase();
-    result = result.filter(item => 
-      item.name.toLowerCase().includes(term)
-    );
-  }
-  
-  // Apply category filter
-  if (categoryFilter() !== 'all') {
-    result = result.filter(item => 
-      item.category === categoryFilter()
-    );
-  }
-  
-  return result;
-});
-
-// Stats
-const fruitCount = $(() => 
-  items().filter(item => item.category === 'fruit').length
-);
-
-const vegCount = $(() => 
-  items().filter(item => item.category === 'vegetable').length
-);
-
-// Update filters
-searchTerm('a');
-console.log(filteredItems().map(i => i.name)); // ['Apple', 'Banana', 'Carrot', 'Date']
-
-categoryFilter('fruit');
-console.log(filteredItems().map(i => i.name)); // ['Apple', 'Banana', 'Date']

Pagination Pattern

javascript
import { $ } from 'sigpro';
-
-const allItems = $([...Array(100).keys()].map(i => \`Item \${i + 1}\`));
-const currentPage = $(1);
-const itemsPerPage = $(10);
-
-// Paginated items (computed)
-const paginatedItems = $(() => {
-  const start = (currentPage() - 1) * itemsPerPage();
-  const end = start + itemsPerPage();
-  return allItems().slice(start, end);
-});
-
-// Pagination metadata
-const totalPages = $(() => 
-  Math.ceil(allItems().length / itemsPerPage())
-);
-
-const hasNextPage = $(() => 
-  currentPage() < totalPages()
-);
-
-const hasPrevPage = $(() => 
-  currentPage() > 1
-);
-
-const pageRange = $(() => {
-  const current = currentPage();
-  const total = totalPages();
-  const delta = 2;
-  
-  let range = [];
-  for (let i = Math.max(2, current - delta); 
-       i <= Math.min(total - 1, current + delta); 
-       i++) {
-    range.push(i);
-  }
-  
-  if (current - delta > 2) range = ['...', ...range];
-  if (current + delta < total - 1) range = [...range, '...'];
-  
-  return [1, ...range, total];
-});
-
-// Navigation
-const nextPage = () => {
-  if (hasNextPage()) currentPage(c => c + 1);
-};
-
-const prevPage = () => {
-  if (hasPrevPage()) currentPage(c => c - 1);
-};
-
-const goToPage = (page) => {
-  if (page >= 1 && page <= totalPages()) {
-    currentPage(page);
-  }
-};

🔧 Advanced Signal Features

Signal Equality Comparison

Signals use Object.is for change detection. Only notify subscribers when values are actually different:

javascript
import { $ } from 'sigpro';
-
-const count = $(0);
-
-// These won't trigger updates:
-count(0); // Same value
-count(prev => prev); // Returns same value
-
-// These will trigger updates:
-count(1); // Different value
-count(prev => prev + 0); // Still 0? Actually returns 0? Wait...
-// Be careful with functional updates!

Batch Updates

Multiple signal updates are batched into a single microtask:

javascript
import { $ } from 'sigpro';
-
-const firstName = $('John');
-const lastName = $('Doe');
-const fullName = $(() => \`\${firstName()} \${lastName()}\`);
-
-$.effect(() => {
-  console.log('Full name:', fullName());
-});
-// Logs: 'Full name: John Doe'
-
-// Multiple updates in same tick - only one effect run!
-firstName('Jane');
-lastName('Smith');
-// Only logs once: 'Full name: Jane Smith'

Infinite Loop Protection

SigPro includes protection against infinite reactive loops:

javascript
import { $ } from 'sigpro';
-
-const a = $(1);
-const b = $(2);
-
-// This would create a loop, but SigPro prevents it
-$.effect(() => {
-  a(b()); // Reading b
-  b(a()); // Reading a - loop detected!
-});
-// Throws: "SigPro: Infinite reactive loop detected."

📊 Performance Characteristics

OperationComplexityNotes
Signal readO(1)Direct value access
Signal writeO(n)n = number of subscribers
Computed readO(1) or O(m)m = computation complexity
Effect runO(s)s = number of signal reads

🎯 Best Practices

1. Keep Signals Focused

javascript
// ❌ Avoid large monolithic signals
-const state = $({
-  user: null,
-  posts: [],
-  theme: 'light',
-  notifications: []
-});
-
-// ✅ Split into focused signals
-const user = $(null);
-const posts = $([]);
-const theme = $('light');
-const notifications = $([]);

2. Use Computed for Derived State

javascript
// ❌ Don't compute in templates/effects
-$.effect(() => {
-  const total = items().reduce((sum, i) => sum + i.price, 0);
-  updateUI(total);
-});
-
-// ✅ Compute with signals
-const total = $(() => items().reduce((sum, i) => sum + i.price, 0));
-$.effect(() => updateUI(total()));

3. Immutable Updates

javascript
// ❌ Don't mutate objects/arrays
-const user = $({ name: 'John' });
-user().name = 'Jane'; // Won't trigger updates!
-
-// ✅ Create new objects/arrays
-user({ ...user(), name: 'Jane' });
-
-// ❌ Don't mutate arrays
-const todos = $(['a', 'b']);
-todos().push('c'); // Won't trigger updates!
-
-// ✅ Create new arrays
-todos([...todos(), 'c']);

4. Functional Updates for Dependencies

javascript
// ❌ Avoid if new value depends on current
-count(count() + 1);
-
-// ✅ Use functional update
-count(prev => prev + 1);

5. Clean Up Effects

javascript
import { $ } from 'sigpro';
-
-const userId = $(1);
-
-// Effects auto-clean in pages, but you can stop manually
-const stop = $.effect(() => {
-  fetchUser(userId());
-});
-
-// Later, if needed
-stop();

🚀 Real-World Examples

Form State Management

javascript
import { $ } from 'sigpro';
-
-// Form state
-const formData = $({
-  username: '',
-  email: '',
-  age: '',
-  newsletter: false
-});
-
-// Touched fields (for validation UI)
-const touched = $({
-  username: false,
-  email: false,
-  age: false
-});
-
-// Validation rules
-const validations = {
-  username: (value) => 
-    value.length >= 3 ? null : 'Username must be at least 3 characters',
-  email: (value) => 
-    /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value) ? null : 'Invalid email',
-  age: (value) => 
-    !value || (value >= 18 && value <= 120) ? null : 'Age must be 18-120'
-};
-
-// Validation signals
-const errors = $(() => {
-  const data = formData();
-  const result = {};
-  
-  Object.keys(validations).forEach(field => {
-    const error = validations[field](data[field]);
-    if (error) result[field] = error;
-  });
-  
-  return result;
-});
-
-const isValid = $(() => Object.keys(errors()).length === 0);
-
-// Field helpers
-const fieldProps = (field) => ({
-  value: formData()[field],
-  error: touched()[field] ? errors()[field] : null,
-  onChange: (e) => {
-    const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
-    formData({
-      ...formData(),
-      [field]: value
-    });
-  },
-  onBlur: () => {
-    touched({
-      ...touched(),
-      [field]: true
-    });
-  }
-});
-
-// Form submission
-const submitAttempts = $(0);
-const isSubmitting = $(false);
-
-const handleSubmit = async () => {
-  submitAttempts(s => s + 1);
-  
-  if (!isValid()) {
-    // Mark all fields as touched to show errors
-    touched(Object.keys(formData()).reduce((acc, field) => ({
-      ...acc,
-      [field]: true
-    }), {}));
-    return;
-  }
-  
-  isSubmitting(true);
-  try {
-    await saveForm(formData());
-    // Reset form on success
-    formData({ username: '', email: '', age: '', newsletter: false });
-    touched({ username: false, email: false, age: false });
-  } finally {
-    isSubmitting(false);
-  }
-};

Todo App with Filters

javascript
import { $ } from 'sigpro';
-
-// State
-const todos = $([
-  { id: 1, text: 'Learn SigPro', completed: true },
-  { id: 2, text: 'Build an app', completed: false },
-  { id: 3, text: 'Write docs', completed: false }
-]);
-
-const filter = $('all'); // 'all', 'active', 'completed'
-const newTodoText = $('');
-
-// Computed values
-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 activeCount = $(() => 
-  todos().filter(t => !t.completed).length
-);
-
-const completedCount = $(() => 
-  todos().filter(t => t.completed).length
-);
-
-const hasCompleted = $(() => completedCount() > 0);
-
-// Actions
-const addTodo = () => {
-  const text = newTodoText().trim();
-  if (text) {
-    todos([
-      ...todos(),
-      {
-        id: Date.now(),
-        text,
-        completed: false
-      }
-    ]);
-    newTodoText('');
-  }
-};
-
-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));
-};
-
-const clearCompleted = () => {
-  todos(todos().filter(todo => !todo.completed));
-};
-
-const toggleAll = () => {
-  const allCompleted = activeCount() === 0;
-  todos(todos().map(todo => ({
-    ...todo,
-    completed: !allCompleted
-  })));
-};

Shopping Cart

javascript
import { $ } from 'sigpro';
-
-// Products catalog
-const products = $([
-  { id: 1, name: 'Laptop', price: 999, stock: 5 },
-  { id: 2, name: 'Mouse', price: 29, stock: 20 },
-  { id: 3, name: 'Keyboard', price: 79, stock: 10 },
-  { id: 4, name: 'Monitor', price: 299, stock: 3 }
-]);
-
-// Cart state
-const cart = $({});
-const selectedProduct = $(null);
-const quantity = $(1);
-
-// Computed cart values
-const cartItems = $(() => {
-  const items = [];
-  Object.entries(cart()).forEach(([productId, qty]) => {
-    const product = products().find(p => p.id === parseInt(productId));
-    if (product) {
-      items.push({
-        ...product,
-        quantity: qty,
-        subtotal: product.price * qty
-      });
-    }
-  });
-  return items;
-});
-
-const itemCount = $(() => 
-  cartItems().reduce((sum, item) => sum + item.quantity, 0)
-);
-
-const subtotal = $(() => 
-  cartItems().reduce((sum, item) => sum + item.subtotal, 0)
-);
-
-const tax = $(() => subtotal() * 0.10);
-const shipping = $(() => subtotal() > 100 ? 0 : 10);
-const total = $(() => subtotal() + tax() + shipping());
-
-const isCartEmpty = $(() => itemCount() === 0);
-
-// Cart actions
-const addToCart = (product, qty = 1) => {
-  const currentQty = cart()[product.id] || 0;
-  const newQty = currentQty + qty;
-  
-  if (newQty <= product.stock) {
-    cart({
-      ...cart(),
-      [product.id]: newQty
-    });
-    return true;
-  }
-  return false;
-};
-
-const updateQuantity = (productId, newQty) => {
-  const product = products().find(p => p.id === productId);
-  if (newQty <= product.stock) {
-    if (newQty <= 0) {
-      removeFromCart(productId);
-    } else {
-      cart({
-        ...cart(),
-        [productId]: newQty
-      });
-    }
-  }
-};
-
-const removeFromCart = (productId) => {
-  const newCart = { ...cart() };
-  delete newCart[productId];
-  cart(newCart);
-};
-
-const clearCart = () => cart({});
-
-// Stock management
-const productStock = (productId) => {
-  const product = products().find(p => p.id === productId);
-  if (!product) return 0;
-  const inCart = cart()[productId] || 0;
-  return product.stock - inCart;
-};
-
-const isInStock = (productId, qty = 1) => {
-  return productStock(productId) >= qty;
-};

📈 Debugging Signals

Logging Signal Changes

javascript
import { $ } from 'sigpro';
-
-// Wrap a signal to log changes
-const withLogging = (signal, name) => {
-  return (...args) => {
-    if (args.length) {
-      const oldValue = signal();
-      const result = signal(...args);
-      console.log(\`\${name}:\`, oldValue, '->', signal());
-      return result;
-    }
-    return signal();
-  };
-};
-
-// Usage
-const count = withLogging($(0), 'count');
-count(5); // Logs: "count: 0 -> 5"

Signal Inspector

javascript
import { $ } from 'sigpro';
-
-// Create an inspectable signal
-const createInspector = () => {
-  const signals = new Map();
-  
-  const createSignal = (initialValue, name) => {
-    const signal = $(initialValue);
-    signals.set(signal, { name, subscribers: new Set() });
-    
-    // Wrap to track subscribers
-    const wrapped = (...args) => {
-      if (!args.length && activeEffect) {
-        const info = signals.get(wrapped);
-        info.subscribers.add(activeEffect);
-      }
-      return signal(...args);
-    };
-    
-    return wrapped;
-  };
-  
-  const getInfo = () => {
-    const info = {};
-    signals.forEach((data, signal) => {
-      info[data.name] = {
-        subscribers: data.subscribers.size,
-        value: signal()
-      };
-    });
-    return info;
-  };
-  
-  return { createSignal, getInfo };
-};
-
-// Usage
-const inspector = createInspector();
-const count = inspector.createSignal(0, 'count');
-const doubled = inspector.createSignal(() => count() * 2, 'doubled');
-
-console.log(inspector.getInfo());
-// { count: { subscribers: 0, value: 0 }, doubled: { subscribers: 0, value: 0 } }

📊 Summary

FeatureDescription
Basic SignalsHold values and notify on change
Computed SignalsAuto-updating derived values
Automatic TrackingDependencies tracked automatically
Batch UpdatesMultiple updates batched in microtask
Infinite Loop ProtectionPrevents reactive cycles
Zero DependenciesPure vanilla JavaScript

Pro Tip: Signals are the foundation of reactivity in SigPro. Master them, and you've mastered 80% of the library!

`,82)])])}const y=i(k,[["render",l]]);export{g as __pageData,y as default}; diff --git a/docs/assets/api_signals.md.CrW68-BA.lean.js b/docs/assets/api_signals.md.CrW68-BA.lean.js deleted file mode 100644 index 089705f..0000000 --- a/docs/assets/api_signals.md.CrW68-BA.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as i,o as a,c as n,ae as h}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Signals API 📡","description":"","frontmatter":{},"headers":[],"relativePath":"api/signals.md","filePath":"api/signals.md"}'),k={name:"api/signals.md"};function l(t,s,p,e,E,r){return a(),n("div",null,[...s[0]||(s[0]=[h("",82)])])}const y=i(k,[["render",l]]);export{g as __pageData,y as default}; diff --git a/docs/assets/api_storage.md.COEWBXHk.js b/docs/assets/api_storage.md.COEWBXHk.js deleted file mode 100644 index 430118a..0000000 --- a/docs/assets/api_storage.md.COEWBXHk.js +++ /dev/null @@ -1,796 +0,0 @@ -import{_ as i,o as a,c as n,ae as h}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Storage API 💾","description":"","frontmatter":{},"headers":[],"relativePath":"api/storage.md","filePath":"api/storage.md"}'),k={name:"api/storage.md"};function l(t,s,p,e,E,r){return a(),n("div",null,[...s[0]||(s[0]=[h(`

Storage API 💾

SigPro provides persistent signals that automatically synchronize with browser storage APIs. This allows you to create reactive state that survives page reloads and browser sessions with zero additional code.

Core Concepts

What is Persistent Storage?

Persistent signals are special signals that:

  • Initialize from storage (localStorage/sessionStorage) if a saved value exists
  • Auto-save whenever the signal value changes
  • Handle JSON serialization automatically
  • Clean up when set to null or undefined

Storage Types

StoragePersistenceUse Case
localStorageForever (until cleared)User preferences, themes, saved data
sessionStorageUntil tab/window closesForm drafts, temporary state

$.storage(key, initialValue, [storage])

Creates a persistent signal that syncs with browser storage.

javascript
import { $ } from 'sigpro';
-
-// localStorage (default)
-const theme = $.storage('theme', 'light');
-const user = $.storage('user', null);
-const settings = $.storage('settings', { notifications: true });
-
-// sessionStorage
-const draft = $.storage('draft', '', sessionStorage);
-const formData = $.storage('form', {}, sessionStorage);

📋 API Reference

Parameters

ParameterTypeDefaultDescription
keystringrequiredStorage key name
initialValueanyrequiredDefault value if none stored
storageStoragelocalStorageStorage type (localStorage or sessionStorage)

Returns

ReturnDescription
FunctionSignal function (getter/setter) with persistence

🎯 Basic Examples

Theme Preference

javascript
import { $, html } from 'sigpro';
-
-// Persistent theme signal
-const theme = $.storage('theme', 'light');
-
-// Apply theme to document
-$.effect(() => {
-  document.body.className = \`theme-\${theme()}\`;
-});
-
-// Toggle theme
-const toggleTheme = () => {
-  theme(t => t === 'light' ? 'dark' : 'light');
-};
-
-// Template
-html\`
-  <div>
-    <p>Current theme: \${theme}</p>
-    <button @click=\${toggleTheme}>
-      Toggle Theme
-    </button>
-  </div>
-\`;

User Preferences

javascript
import { $ } from 'sigpro';
-
-// Complex preferences object
-const preferences = $.storage('preferences', {
-  language: 'en',
-  fontSize: 'medium',
-  notifications: true,
-  compactView: false,
-  sidebarOpen: true
-});
-
-// Update single preference
-const setPreference = (key, value) => {
-  preferences({
-    ...preferences(),
-    [key]: value
-  });
-};
-
-// Usage
-setPreference('language', 'es');
-setPreference('fontSize', 'large');
-console.log(preferences().language); // 'es'

Form Draft

javascript
import { $, html } from 'sigpro';
-
-// Session-based draft (clears when tab closes)
-const draft = $.storage('contact-form', {
-  name: '',
-  email: '',
-  message: ''
-}, sessionStorage);
-
-// Auto-save on input
-const handleInput = (field, value) => {
-  draft({
-    ...draft(),
-    [field]: value
-  });
-};
-
-// Clear draft after submit
-const handleSubmit = async () => {
-  await submitForm(draft());
-  draft(null); // Clears from storage
-};
-
-// Template
-html\`
-  <form @submit=\${handleSubmit}>
-    <input
-      type="text"
-      :value=\${() => draft().name}
-      @input=\${(e) => handleInput('name', e.target.value)}
-      placeholder="Name"
-    />
-    <input
-      type="email"
-      :value=\${() => draft().email}
-      @input=\${(e) => handleInput('email', e.target.value)}
-      placeholder="Email"
-    />
-    <textarea
-      :value=\${() => draft().message}
-      @input=\${(e) => handleInput('message', e.target.value)}
-      placeholder="Message"
-    ></textarea>
-    <button type="submit">Send</button>
-  </form>
-\`;

🚀 Advanced Examples

Authentication State

javascript
import { $, html } from 'sigpro';
-
-// Persistent auth state
-const auth = $.storage('auth', {
-  token: null,
-  user: null,
-  expiresAt: null
-});
-
-// Computed helpers
-const isAuthenticated = $(() => {
-  const { token, expiresAt } = auth();
-  if (!token || !expiresAt) return false;
-  return new Date(expiresAt) > new Date();
-});
-
-const user = $(() => auth().user);
-
-// Login function
-const login = async (email, password) => {
-  const response = await fetch('/api/login', {
-    method: 'POST',
-    body: JSON.stringify({ email, password })
-  });
-  
-  if (response.ok) {
-    const { token, user, expiresIn } = await response.json();
-    auth({
-      token,
-      user,
-      expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString()
-    });
-    return true;
-  }
-  return false;
-};
-
-// Logout
-const logout = () => {
-  auth(null); // Clear from storage
-};
-
-// Auto-refresh token
-$.effect(() => {
-  if (!isAuthenticated()) return;
-  
-  const { expiresAt } = auth();
-  const expiresIn = new Date(expiresAt) - new Date();
-  const refreshTime = expiresIn - 60000; // 1 minute before expiry
-  
-  if (refreshTime > 0) {
-    const timer = setTimeout(refreshToken, refreshTime);
-    return () => clearTimeout(timer);
-  }
-});
-
-// Navigation guard
-$.effect(() => {
-  if (!isAuthenticated() && window.location.pathname !== '/login') {
-    $.router.go('/login');
-  }
-});

Multi-tab Synchronization

javascript
import { $ } from 'sigpro';
-
-// Storage key for cross-tab communication
-const STORAGE_KEY = 'app-state';
-
-// Create persistent signal
-const appState = $.storage(STORAGE_KEY, {
-  count: 0,
-  lastUpdated: null
-});
-
-// Listen for storage events (changes from other tabs)
-window.addEventListener('storage', (event) => {
-  if (event.key === STORAGE_KEY && event.newValue) {
-    try {
-      // Update signal without triggering save loop
-      const newValue = JSON.parse(event.newValue);
-      appState(newValue);
-    } catch (e) {
-      console.error('Failed to parse storage event:', e);
-    }
-  }
-});
-
-// Update state (syncs across all tabs)
-const increment = () => {
-  appState({
-    count: appState().count + 1,
-    lastUpdated: new Date().toISOString()
-  });
-};
-
-// Tab counter
-const tabCount = $(1);
-
-// Track number of tabs open
-window.addEventListener('storage', (event) => {
-  if (event.key === 'tab-heartbeat') {
-    tabCount(parseInt(event.newValue) || 1);
-  }
-});
-
-// Send heartbeat
-setInterval(() => {
-  localStorage.setItem('tab-heartbeat', tabCount());
-}, 1000);

Settings Manager

javascript
import { $, html } from 'sigpro';
-
-// Settings schema
-const settingsSchema = {
-  theme: {
-    type: 'select',
-    options: ['light', 'dark', 'system'],
-    default: 'system'
-  },
-  fontSize: {
-    type: 'range',
-    min: 12,
-    max: 24,
-    default: 16
-  },
-  notifications: {
-    type: 'checkbox',
-    default: true
-  },
-  language: {
-    type: 'select',
-    options: ['en', 'es', 'fr', 'de'],
-    default: 'en'
-  }
-};
-
-// Persistent settings
-const settings = $.storage('app-settings', 
-  Object.entries(settingsSchema).reduce((acc, [key, config]) => ({
-    ...acc,
-    [key]: config.default
-  }), {})
-);
-
-// Settings component
-const SettingsPanel = () => {
-  return html\`
-    <div class="settings-panel">
-      <h2>Settings</h2>
-      
-      \${Object.entries(settingsSchema).map(([key, config]) => {
-        switch(config.type) {
-          case 'select':
-            return html\`
-              <div class="setting">
-                <label>\${key}:</label>
-                <select 
-                  :value=\${() => settings()[key]}
-                  @change=\${(e) => updateSetting(key, e.target.value)}
-                >
-                  \${config.options.map(opt => html\`
-                    <option value="\${opt}" ?selected=\${() => settings()[key] === opt}>
-                      \${opt}
-                    </option>
-                  \`)}
-                </select>
-              </div>
-            \`;
-            
-          case 'range':
-            return html\`
-              <div class="setting">
-                <label>\${key}: \${() => settings()[key]}</label>
-                <input
-                  type="range"
-                  min="\${config.min}"
-                  max="\${config.max}"
-                  :value=\${() => settings()[key]}
-                  @input=\${(e) => updateSetting(key, parseInt(e.target.value))}
-                />
-              </div>
-            \`;
-            
-          case 'checkbox':
-            return html\`
-              <div class="setting">
-                <label>
-                  <input
-                    type="checkbox"
-                    :checked=\${() => settings()[key]}
-                    @change=\${(e) => updateSetting(key, e.target.checked)}
-                  />
-                  \${key}
-                </label>
-              </div>
-            \`;
-        }
-      })}
-      
-      <button @click=\${resetDefaults}>Reset to Defaults</button>
-    </div>
-  \`;
-};
-
-// Helper functions
-const updateSetting = (key, value) => {
-  settings({
-    ...settings(),
-    [key]: value
-  });
-};
-
-const resetDefaults = () => {
-  const defaults = Object.entries(settingsSchema).reduce((acc, [key, config]) => ({
-    ...acc,
-    [key]: config.default
-  }), {});
-  settings(defaults);
-};
-
-// Apply settings globally
-$.effect(() => {
-  const { theme, fontSize } = settings();
-  
-  // Apply theme
-  document.documentElement.setAttribute('data-theme', theme);
-  
-  // Apply font size
-  document.documentElement.style.fontSize = \`\${fontSize}px\`;
-});

Shopping Cart Persistence

javascript
import { $, html } from 'sigpro';
-
-// Persistent shopping cart
-const cart = $.storage('shopping-cart', {
-  items: [],
-  lastUpdated: null
-});
-
-// Computed values
-const cartItems = $(() => cart().items);
-const itemCount = $(() => cartItems().reduce((sum, item) => sum + item.quantity, 0));
-const subtotal = $(() => cartItems().reduce((sum, item) => sum + (item.price * item.quantity), 0));
-const tax = $(() => subtotal() * 0.1);
-const total = $(() => subtotal() + tax());
-
-// Cart actions
-const addToCart = (product, quantity = 1) => {
-  const existing = cartItems().findIndex(item => item.id === product.id);
-  
-  if (existing >= 0) {
-    // Update quantity
-    const newItems = [...cartItems()];
-    newItems[existing] = {
-      ...newItems[existing],
-      quantity: newItems[existing].quantity + quantity
-    };
-    
-    cart({
-      items: newItems,
-      lastUpdated: new Date().toISOString()
-    });
-  } else {
-    // Add new item
-    cart({
-      items: [...cartItems(), { ...product, quantity }],
-      lastUpdated: new Date().toISOString()
-    });
-  }
-};
-
-const removeFromCart = (productId) => {
-  cart({
-    items: cartItems().filter(item => item.id !== productId),
-    lastUpdated: new Date().toISOString()
-  });
-};
-
-const updateQuantity = (productId, quantity) => {
-  if (quantity <= 0) {
-    removeFromCart(productId);
-  } else {
-    const newItems = cartItems().map(item =>
-      item.id === productId ? { ...item, quantity } : item
-    );
-    
-    cart({
-      items: newItems,
-      lastUpdated: new Date().toISOString()
-    });
-  }
-};
-
-const clearCart = () => {
-  cart({
-    items: [],
-    lastUpdated: new Date().toISOString()
-  });
-};
-
-// Cart expiration (7 days)
-const CART_EXPIRY_DAYS = 7;
-
-$.effect(() => {
-  const lastUpdated = cart().lastUpdated;
-  if (lastUpdated) {
-    const expiryDate = new Date(lastUpdated);
-    expiryDate.setDate(expiryDate.getDate() + CART_EXPIRY_DAYS);
-    
-    if (new Date() > expiryDate) {
-      clearCart();
-    }
-  }
-});
-
-// Cart display component
-const CartDisplay = () => html\`
-  <div class="cart">
-    <h3>Shopping Cart (\${itemCount} items)</h3>
-    
-    \${cartItems().map(item => html\`
-      <div class="cart-item">
-        <span>\${item.name}</span>
-        <span>$\${item.price} x \${item.quantity}</span>
-        <span>$\${item.price * item.quantity}</span>
-        <button @click=\${() => removeFromCart(item.id)}>Remove</button>
-        <input
-          type="number"
-          min="1"
-          :value=\${item.quantity}
-          @change=\${(e) => updateQuantity(item.id, parseInt(e.target.value))}
-        />
-      </div>
-    \`)}
-    
-    <div class="cart-totals">
-      <p>Subtotal: $\${subtotal}</p>
-      <p>Tax (10%): $\${tax}</p>
-      <p><strong>Total: $\${total}</strong></p>
-    </div>
-    
-    \${() => cartItems().length > 0 ? html\`
-      <button @click=\${checkout}>Checkout</button>
-      <button @click=\${clearCart}>Clear Cart</button>
-    \` : html\`
-      <p>Your cart is empty</p>
-    \`}
-  </div>
-\`;

Recent Searches History

javascript
import { $, html } from 'sigpro';
-
-// Persistent search history (max 10 items)
-const searchHistory = $.storage('search-history', []);
-
-// Add search to history
-const addSearch = (query) => {
-  if (!query.trim()) return;
-  
-  const current = searchHistory();
-  const newHistory = [
-    { query, timestamp: new Date().toISOString() },
-    ...current.filter(item => item.query !== query)
-  ].slice(0, 10); // Keep only last 10
-  
-  searchHistory(newHistory);
-};
-
-// Clear history
-const clearHistory = () => {
-  searchHistory([]);
-};
-
-// Remove specific item
-const removeFromHistory = (query) => {
-  searchHistory(searchHistory().filter(item => item.query !== query));
-};
-
-// Search component
-const SearchWithHistory = () => {
-  const searchInput = $('');
-  
-  const handleSearch = () => {
-    const query = searchInput();
-    if (query) {
-      addSearch(query);
-      performSearch(query);
-      searchInput('');
-    }
-  };
-  
-  return html\`
-    <div class="search-container">
-      <div class="search-box">
-        <input
-          type="search"
-          :value=\${searchInput}
-          @keydown.enter=\${handleSearch}
-          placeholder="Search..."
-        />
-        <button @click=\${handleSearch}>Search</button>
-      </div>
-      
-      \${() => searchHistory().length > 0 ? html\`
-        <div class="search-history">
-          <h4>Recent Searches</h4>
-          \${searchHistory().map(item => html\`
-            <div class="history-item">
-              <button 
-                class="history-query"
-                @click=\${() => {
-                  searchInput(item.query);
-                  handleSearch();
-                }}
-              >
-                🔍 \${item.query}
-              </button>
-              <small>\${new Date(item.timestamp).toLocaleString()}</small>
-              <button 
-                class="remove-btn"
-                @click=\${() => removeFromHistory(item.query)}
-              >
-
-              </button>
-            </div>
-          \`)}
-          <button class="clear-btn" @click=\${clearHistory}>
-            Clear History
-          </button>
-        </div>
-      \` : ''}
-    </div>
-  \`;
-};

Multiple Profiles / Accounts

javascript
import { $, html } from 'sigpro';
-
-// Profile manager
-const profiles = $.storage('user-profiles', {
-  current: 'default',
-  list: {
-    default: {
-      name: 'Default',
-      theme: 'light',
-      preferences: {}
-    }
-  }
-});
-
-// Switch profile
-const switchProfile = (profileId) => {
-  profiles({
-    ...profiles(),
-    current: profileId
-  });
-};
-
-// Create profile
-const createProfile = (name) => {
-  const id = \`profile-\${Date.now()}\`;
-  profiles({
-    current: id,
-    list: {
-      ...profiles().list,
-      [id]: {
-        name,
-        theme: 'light',
-        preferences: {},
-        createdAt: new Date().toISOString()
-      }
-    }
-  });
-  return id;
-};
-
-// Delete profile
-const deleteProfile = (profileId) => {
-  if (profileId === 'default') return; // Can't delete default
-  
-  const newList = { ...profiles().list };
-  delete newList[profileId];
-  
-  profiles({
-    current: 'default',
-    list: newList
-  });
-};
-
-// Get current profile data
-const currentProfile = $(() => {
-  const { current, list } = profiles();
-  return list[current] || list.default;
-});
-
-// Profile-aware settings
-const profileTheme = $(() => currentProfile().theme);
-const profilePreferences = $(() => currentProfile().preferences);
-
-// Update profile data
-const updateCurrentProfile = (updates) => {
-  const { current, list } = profiles();
-  profiles({
-    current,
-    list: {
-      ...list,
-      [current]: {
-        ...list[current],
-        ...updates
-      }
-    }
-  });
-};
-
-// Profile selector component
-const ProfileSelector = () => html\`
-  <div class="profile-selector">
-    <select 
-      :value=\${() => profiles().current}
-      @change=\${(e) => switchProfile(e.target.value)}
-    >
-      \${Object.entries(profiles().list).map(([id, profile]) => html\`
-        <option value="\${id}">\${profile.name}</option>
-      \`)}
-    </select>
-    
-    <button @click=\${() => {
-      const name = prompt('Enter profile name:');
-      if (name) createProfile(name);
-    }}>
-      New Profile
-    </button>
-  </div>
-\`;

🛡️ Error Handling

Storage Errors

javascript
import { $ } from 'sigpro';
-
-// Safe storage wrapper
-const safeStorage = (key, initialValue, storage = localStorage) => {
-  try {
-    return $.storage(key, initialValue, storage);
-  } catch (error) {
-    console.warn(\`Storage failed for \${key}, using in-memory fallback:\`, error);
-    return $(initialValue);
-  }
-};
-
-// Usage with fallback
-const theme = safeStorage('theme', 'light');
-const user = safeStorage('user', null);

Quota Exceeded Handling

javascript
import { $ } from 'sigpro';
-
-const createManagedStorage = (key, initialValue, maxSize = 1024 * 100) => { // 100KB limit
-  const signal = $.storage(key, initialValue);
-  
-  // Monitor size
-  const size = $(0);
-  
-  $.effect(() => {
-    try {
-      const value = signal();
-      const json = JSON.stringify(value);
-      const bytes = new Blob([json]).size;
-      
-      size(bytes);
-      
-      if (bytes > maxSize) {
-        console.warn(\`Storage for \${key} exceeded \${maxSize} bytes\`);
-        // Could implement cleanup strategy here
-      }
-    } catch (e) {
-      console.error('Size check failed:', e);
-    }
-  });
-  
-  return { signal, size };
-};
-
-// Usage
-const { signal: largeData, size } = createManagedStorage('app-data', {}, 50000);

📊 Storage Limits

Storage TypeTypical LimitNotes
localStorage5-10MBVaries by browser
sessionStorage5-10MBCleared when tab closes
cookies4KBNot recommended for SigPro

🎯 Best Practices

1. Validate Stored Data

javascript
import { $ } from 'sigpro';
-
-// Schema validation
-const createValidatedStorage = (key, schema, defaultValue, storage) => {
-  const signal = $.storage(key, defaultValue, storage);
-  
-  // Wrap to validate on read/write
-  const validated = (...args) => {
-    if (args.length) {
-      // Validate before writing
-      const value = args[0];
-      if (typeof value === 'function') {
-        // Handle functional updates
-        return validated(validated());
-      }
-      
-      // Basic validation
-      const isValid = Object.keys(schema).every(key => {
-        const validator = schema[key];
-        return !validator || validator(value[key]);
-      });
-      
-      if (!isValid) {
-        console.warn('Invalid data, skipping storage write');
-        return signal();
-      }
-    }
-    
-    return signal(...args);
-  };
-  
-  return validated;
-};
-
-// Usage
-const userSchema = {
-  name: v => v && v.length > 0,
-  age: v => v >= 18 && v <= 120,
-  email: v => /@/.test(v)
-};
-
-const user = createValidatedStorage('user', userSchema, {
-  name: '',
-  age: 25,
-  email: ''
-});

2. Handle Versioning

javascript
import { $ } from 'sigpro';
-
-const VERSION = 2;
-
-const createVersionedStorage = (key, migrations, storage) => {
-  const raw = $.storage(key, { version: VERSION, data: {} }, storage);
-  
-  const migrate = (data) => {
-    let current = data;
-    const currentVersion = current.version || 1;
-    
-    for (let v = currentVersion; v < VERSION; v++) {
-      const migrator = migrations[v];
-      if (migrator) {
-        current = migrator(current);
-      }
-    }
-    
-    return current;
-  };
-  
-  // Migrate if needed
-  const stored = raw();
-  if (stored.version !== VERSION) {
-    const migrated = migrate(stored);
-    raw(migrated);
-  }
-  
-  return raw;
-};
-
-// Usage
-const migrations = {
-  1: (old) => ({
-    version: 2,
-    data: {
-      ...old.data,
-      preferences: old.preferences || {}
-    }
-  })
-};
-
-const settings = createVersionedStorage('app-settings', migrations);

3. Encrypt Sensitive Data

javascript
import { $ } from 'sigpro';
-
-// Simple encryption (use proper crypto in production)
-const encrypt = (text) => {
-  return btoa(text); // Base64 - NOT secure, just example
-};
-
-const decrypt = (text) => {
-  try {
-    return atob(text);
-  } catch {
-    return null;
-  }
-};
-
-const createSecureStorage = (key, initialValue, storage) => {
-  const encryptedKey = \`enc_\${key}\`;
-  const signal = $.storage(encryptedKey, null, storage);
-  
-  const secure = (...args) => {
-    if (args.length) {
-      // Encrypt before storing
-      const value = args[0];
-      const encrypted = encrypt(JSON.stringify(value));
-      return signal(encrypted);
-    }
-    
-    // Decrypt when reading
-    const encrypted = signal();
-    if (!encrypted) return initialValue;
-    
-    try {
-      const decrypted = decrypt(encrypted);
-      return decrypted ? JSON.parse(decrypted) : initialValue;
-    } catch {
-      return initialValue;
-    }
-  };
-  
-  return secure;
-};
-
-// Usage
-const secureToken = createSecureStorage('auth-token', null);
-secureToken('sensitive-data-123'); // Stored encrypted

📈 Performance Considerations

OperationCostNotes
Initial readO(1)Single storage read
WriteO(1) + JSON.stringifyAuto-save on change
Large objectsO(n)Stringify/parse overhead
Multiple keysO(k)k = number of keys

Pro Tip: Use sessionStorage for temporary data like form drafts, and localStorage for persistent user preferences. Always validate data when reading from storage to handle corrupted values gracefully.

`,54)])])}const F=i(k,[["render",l]]);export{g as __pageData,F as default}; diff --git a/docs/assets/api_storage.md.COEWBXHk.lean.js b/docs/assets/api_storage.md.COEWBXHk.lean.js deleted file mode 100644 index 2b86ef1..0000000 --- a/docs/assets/api_storage.md.COEWBXHk.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as i,o as a,c as n,ae as h}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Storage API 💾","description":"","frontmatter":{},"headers":[],"relativePath":"api/storage.md","filePath":"api/storage.md"}'),k={name:"api/storage.md"};function l(t,s,p,e,E,r){return a(),n("div",null,[...s[0]||(s[0]=[h("",54)])])}const F=i(k,[["render",l]]);export{g as __pageData,F as default}; diff --git a/docs/assets/api_tags.md.33XeBTH-.js b/docs/assets/api_tags.md.33XeBTH-.js new file mode 100644 index 0000000..08ffed5 --- /dev/null +++ b/docs/assets/api_tags.md.33XeBTH-.js @@ -0,0 +1,55 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const E=JSON.parse('{"title":"Global Tag Helpers","description":"","frontmatter":{},"headers":[],"relativePath":"api/tags.md","filePath":"api/tags.md"}'),e={name:"api/tags.md"};function l(h,s,p,k,r,d){return a(),t("div",null,[...s[0]||(s[0]=[n(`

Global Tag Helpers

In SigPro, you don't need to write $.html('div', ...) every time. To keep your code clean and readable, the engine automatically generates global helper functions for all standard HTML tags.

1. How it Works

When SigPro initializes, it runs a proxy loop that creates a function for every common HTML tag and attaches it to the window object.

  • Traditional: $.html('button', { onclick: ... }, 'Click')
  • SigPro Style: button({ onclick: ... }, 'Click')

This approach gives you a "DSL" (Domain Specific Language) that feels like HTML but is actually pure JavaScript.


2. The Global Registry

The following tags are available globally by default:

CategoryAvailable Functions
Layoutdiv, span, section, main, nav, header, footer, article, aside
Typographyh1, h2, h3, p, ul, ol, li, a, label, strong, em
Formsform, input, button, select, option, textarea
Tabletable, thead, tbody, tr, th, td
Mediaimg, video, audio, canvas, svg

3. Usage Patterns

The tag functions are highly flexible and accept arguments in different orders to suit your coding style.

A. Attributes + Content

The most common pattern.

javascript
div({ class: 'card' }, [
+  h1("Title"),
+  p("Description")
+]);

B. Content Only

If you don't need attributes, you can skip the object entirely.

javascript
div([
+  h1("Just Content"),
+  p("No attributes object needed here.")
+]);

C. Simple Text

For elements that only contain a string.

javascript
button("Submit"); // Equivalent to <button>Submit</button>

4. Reactive Tags

Since these helpers are just wrappers around $.html, they support full reactivity out of the box.

javascript
const $loading = $(true);
+
+div([
+  $loading() ? span("Loading...") : h1("Data Ready!"),
+  button({ 
+    $disabled: $loading, // Reactive attribute
+    onclick: () => $loading(false) 
+  }, "Stop Loading")
+]);

5. Under the Hood

If you are curious about how this happens without a compiler, here is the logic inside the SigPro core:

javascript
const tags = ['div', 'span', 'p', 'button', ...];
+
+tags.forEach(tag => {
+  window[tag] = (props, content) => $.html(tag, props, content);
+});

Because these are attached to window, they are available in any file in your project as soon as SigPro is loaded, making your components look like this:

javascript
// No imports required for tags!
+export default () => 
+  section({ id: 'hero' }, [
+    h1("Fast. Atomic. Simple."),
+    p("Built with SigPro.")
+  ]);

6. Full Comparison: SigPro vs. Standard HTML

To better understand the translation, here is a complete example of a User Card component. Notice how SigPro attributes with the $ prefix map to reactive behavior, while standard attributes remain static.

javascript
const $online = $(true);
+
+export const UserCard = () => (
+  div({ class: 'user-card' }, [
+    img({ src: 'avatar.png', alt: 'User' }),
+    
+    div({ class: 'info' }, [
+      h2("John Doe"),
+      p({ 
+        $class: () => $online() ? 'status-on' : 'status-off' 
+      }, [
+        "Status: ", 
+        () => $online() ? "Online" : "Offline"
+      ])
+    ]),
+    
+    button({ 
+      onclick: () => $online(!$online()) 
+    }, "Toggle Status")
+  ])
+);
html
<div class="user-card">
+  <img src="avatar.png" alt="User">
+  
+  <div class="info">
+    <h2>John Doe</h2>
+    <p class="status-on">
+      Status: Online
+    </p>
+  </div>
+  
+  <button>Toggle Status</button>
+</div>

What is happening here?

  1. Structure: The hierarchy is identical. div([...]) in JS translates directly to nested tags in HTML.
  2. Attributes: class is set once. $class is "live"; SigPro listens to the $online signal and updates the class name without re-rendering the whole card.
  3. Content: The array [...] in SigPro is the equivalent of the children inside an HTML tag.
  4. Reactivity: The function () => $online() ? ... creates a TextNode in the HTML that changes its text content surgically whenever the signal toggles.

💡 Best Practices

  1. Destructuring: If you prefer not to rely on global variables, you can destructure them from window or $ (though in SigPro, using them globally is the intended "clean" way).
  2. Custom Tags: If you need a tag that isn't in the default list (like a Web Component), you can still use the base engine: $.html('my-custom-element', { ... }).
`,41)])])}const g=i(e,[["render",l]]);export{E as __pageData,g as default}; diff --git a/docs/assets/api_tags.md.33XeBTH-.lean.js b/docs/assets/api_tags.md.33XeBTH-.lean.js new file mode 100644 index 0000000..e1b1295 --- /dev/null +++ b/docs/assets/api_tags.md.33XeBTH-.lean.js @@ -0,0 +1 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const E=JSON.parse('{"title":"Global Tag Helpers","description":"","frontmatter":{},"headers":[],"relativePath":"api/tags.md","filePath":"api/tags.md"}'),e={name:"api/tags.md"};function l(h,s,p,k,r,d){return a(),t("div",null,[...s[0]||(s[0]=[n("",41)])])}const g=i(e,[["render",l]]);export{E as __pageData,g as default}; diff --git a/docs/assets/guide_getting-started.md.BeQpK3vd.js b/docs/assets/guide_getting-started.md.BeQpK3vd.js deleted file mode 100644 index 95e5ed5..0000000 --- a/docs/assets/guide_getting-started.md.BeQpK3vd.js +++ /dev/null @@ -1,172 +0,0 @@ -import{_ as i,o as a,c as n,ae as t}from"./chunks/framework.C8AWLET_.js";const E=JSON.parse('{"title":"Getting Started with SigPro 🚀","description":"","frontmatter":{},"headers":[],"relativePath":"guide/getting-started.md","filePath":"guide/getting-started.md"}'),l={name:"guide/getting-started.md"};function h(p,s,e,k,r,d){return a(),n("div",null,[...s[0]||(s[0]=[t(`

Getting Started with SigPro 🚀

Welcome to SigPro! This guide will help you get up and running with the library in minutes. SigPro is a minimalist reactive library that embraces the web platform - no compilation, no virtual DOM, just pure JavaScript and intelligent reactivity.

📦 Installation

Choose your preferred installation method:

bash
# Using npm
-npm install sigpro
-
-# Using bun
-bun add sigpro
-
-# Or simply copy sigpro.js to your project
-# (yes, it's that simple!)

🎯 Core Imports

javascript
import { $, html } from 'sigpro';

That's it! Just two imports to unlock the entire reactive system:

  • $ - Creates reactive signals (the heart of reactivity)
  • html - Template literal tag for reactive DOM rendering

🧠 Understanding the Basics

Signals - The Reactive Heart

Signals are reactive values that automatically track dependencies and update when changed:

javascript
// Create a signal with initial value
-const count = $(0);
-
-// Read value (with auto dependency tracking)
-console.log(count()); // 0
-
-// Set new value
-count(5);
-
-// Update using previous value
-count(prev => prev + 1); // 6
-
-// Create computed signals (auto-updating)
-const firstName = $('John');
-const lastName = $('Doe');
-const fullName = $(() => \`\${firstName()} \${lastName()}\`);
-console.log(fullName()); // "John Doe"
-firstName('Jane'); // fullName() now returns "Jane Doe"

Effects - Automatic Reactions

Effects automatically run and re-run when their signal dependencies change:

javascript
const count = $(0);
-
-$.effect(() => {
-  console.log(\`Count is: \${count()}\`);
-});
-// Logs: "Count is: 0"
-
-count(1);
-// Logs: "Count is: 1"
-
-// Effects can return cleanup functions
-$.effect(() => {
-  const id = count();
-  const timer = setInterval(() => {
-    console.log(\`Polling with count: \${id}\`);
-  }, 1000);
-  
-  // Cleanup runs before next effect execution
-  return () => clearInterval(timer);
-});

Rendering with html

The html tag creates reactive DOM fragments:

javascript
const count = $(0);
-const isActive = $(true);
-
-const fragment = html\`
-  <div class="counter">
-    <h2>Count: \${count}</h2>
-    
-    <!-- Event binding -->
-    <button @click=\${() => count(c => c + 1)}>
-      Increment
-    </button>
-    
-    <!-- Boolean attributes -->
-    <button ?disabled=\${() => !isActive()}>
-      Submit
-    </button>
-  </div>
-\`;
-
-document.body.appendChild(fragment);

🎨 Your First Reactive App

Let's build a simple todo app to see SigPro in action:

javascript
import { $, html } from 'sigpro';
-
-// Create a simple todo app
-function TodoApp() {
-  // Reactive state
-  const todos = $(['Learn SigPro', 'Build something awesome']);
-  const newTodo = $('');
-  
-  // Computed value
-  const todoCount = $(() => todos().length);
-  
-  // Add todo function
-  const addTodo = () => {
-    if (newTodo().trim()) {
-      todos([...todos(), newTodo()]);
-      newTodo('');
-    }
-  };
-  
-  // Remove todo function
-  const removeTodo = (index) => {
-    todos(todos().filter((_, i) => i !== index));
-  };
-  
-  // Return reactive template
-  return html\`
-    <div style="max-width: 400px; margin: 2rem auto; font-family: system-ui;">
-      <h1>📝 Todo App</h1>
-      
-      <!-- Input form -->
-      <div style="display: flex; gap: 8px; margin-bottom: 16px;">
-        <input 
-          type="text" 
-          :value=\${newTodo}
-          placeholder="Add a new todo..."
-          style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"
-          @keydown.enter=\${addTodo}
-        />
-        <button 
-          @click=\${addTodo}
-          style="padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;"
-        >
-          Add
-        </button>
-      </div>
-      
-      <!-- Todo count -->
-      <p>Total todos: \${todoCount}</p>
-      
-      <!-- Todo list -->
-      <ul style="list-style: none; padding: 0;">
-        \${() => todos().map((todo, index) => html\`
-          <li style="display: flex; justify-content: space-between; align-items: center; padding: 8px; margin: 4px 0; background: #f5f5f5; border-radius: 4px;">
-            <span>\${todo}</span>
-            <button 
-              @click=\${() => removeTodo(index)}
-              style="padding: 4px 8px; background: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer;"
-            >
-
-            </button>
-          </li>
-        \`)}
-      </ul>
-    </div>
-  \`;
-}
-
-// Mount the app
-document.body.appendChild(TodoApp());

🎯 Key Concepts

1. Signal Patterns

PatternExampleUse Case
Basic signalconst count = $(0)Simple values
Computed$( () => first() + last() )Derived values
Signal updatecount(5)Direct set
Functional updatecount(prev => prev + 1)Based on previous

2. Effect Patterns

javascript
// Basic effect
-$.effect(() => console.log(count()));
-
-// Effect with cleanup
-$.effect(() => {
-  const timer = setInterval(() => {}, 1000);
-  return () => clearInterval(timer);
-});
-
-// Stopping an effect
-const stop = $.effect(() => {});
-stop(); // Effect won't run again

3. HTML Directives

DirectiveExampleDescription
@event@click=\${handler}Event listeners
:property:value=\${signal}Two-way binding
?attribute?disabled=\${signal}Boolean attributes
.property.scrollTop=\${value}DOM properties

💡 Pro Tips for Beginners

1. Start Simple

javascript
// Begin with basic signals
-const name = $('World');
-html\`<h1>Hello, \${name}!</h1>\`;

2. Use Computed Signals for Derived State

javascript
// ❌ Don't compute in template
-html\`<p>Total: \${items().length * price()}</p>\`;
-
-// ✅ Compute with signals
-const total = $(() => items().length * price());
-html\`<p>Total: \${total}</p>\`;

3. Leverage Effects for Side Effects

javascript
// Auto-save to localStorage
-$.effect(() => {
-  localStorage.setItem('draft', JSON.stringify(draft()));
-});

🔧 VS Code Setup

For the best development experience, install these VS Code extensions:

  • lit-html - Adds syntax highlighting for html tagged templates
  • Prettier - Automatically formats your template literals
javascript
// With lit-html extension, you get full syntax highlighting!
-html\`
-  <div style="color: #ff4444; background: linear-gradient(45deg, blue, green)">
-    <h1>Beautiful highlighted template</h1>
-  </div>
-\`

📁 Project Structure

Here's a recommended structure for larger apps:

my-sigpro-app/
-├── index.html
-├── main.js
-├── components/
-│   ├── Button.js
-│   ├── TodoList.js
-│   └── TodoItem.js
-├── pages/
-│   ├── HomePage.js
-│   └── AboutPage.js
-└── utils/
-    └── helpers.js

Example main.js:

javascript
import { $, html } from 'sigpro';
-import HomePage from './pages/HomePage.js';
-
-// Mount your app
-document.body.appendChild(HomePage());

🎓 Summary

You've learned:

  • ✅ How to install SigPro
  • ✅ Core concepts: signals, effects, and reactive rendering
  • ✅ Built a complete todo app
  • ✅ Key patterns and best practices
  • ✅ How to structure larger applications

Remember: SigPro embraces the web platform. You're writing vanilla JavaScript with superpowers—no compilation, no lock-in, just clean, maintainable code that will work for years to come.

"Stop fighting the platform. Start building with it."

Happy coding! 🎉

`,51)])])}const o=i(l,[["render",h]]);export{E as __pageData,o as default}; diff --git a/docs/assets/guide_getting-started.md.BeQpK3vd.lean.js b/docs/assets/guide_getting-started.md.BeQpK3vd.lean.js deleted file mode 100644 index 7e0ddbd..0000000 --- a/docs/assets/guide_getting-started.md.BeQpK3vd.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as i,o as a,c as n,ae as t}from"./chunks/framework.C8AWLET_.js";const E=JSON.parse('{"title":"Getting Started with SigPro 🚀","description":"","frontmatter":{},"headers":[],"relativePath":"guide/getting-started.md","filePath":"guide/getting-started.md"}'),l={name:"guide/getting-started.md"};function h(p,s,e,k,r,d){return a(),n("div",null,[...s[0]||(s[0]=[t("",51)])])}const o=i(l,[["render",h]]);export{E as __pageData,o as default}; diff --git a/docs/assets/guide_getting-started.md.D_gqopPp.js b/docs/assets/guide_getting-started.md.D_gqopPp.js new file mode 100644 index 0000000..3f5d489 --- /dev/null +++ b/docs/assets/guide_getting-started.md.D_gqopPp.js @@ -0,0 +1,26 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Getting Started","description":"","frontmatter":{},"headers":[],"relativePath":"guide/getting-started.md","filePath":"guide/getting-started.md"}'),e={name:"guide/getting-started.md"};function l(h,s,p,k,r,d){return a(),t("div",null,[...s[0]||(s[0]=[n(`

Getting Started

SigPro is a lightweight, atomic reactive engine designed to build modern web interfaces with zero overhead. It focuses on high performance through fine-grained reactivity.

1. Installation

You can install SigPro via your favorite package manager:

bash
npm install SigPro
bash
pnpm add SigPro
bash
yarn add SigPro
bash
bun add SigPro

2. Basic Usage

The core of SigPro is the $ function, which creates reactive state (Signals) and computed effects.

Create a main.js file and try this:

javascript
import { $ } from 'SigPro';
+
+// 1. Create a reactive signal
+const $name = $("World");
+
+// 2. Define a reactive component
+const App = () => div({ class: 'container' }, [
+  h1(["Hello, ", $name, "!"]),
+  
+  input({ 
+    type: 'text', 
+    $value: $name, // Two-way binding
+    placeholder: 'Enter your name...' 
+  }),
+  
+  button({ 
+    onclick: () => $name("SigPro") 
+  }, "Set to SigPro")
+]);
+
+// 3. Mount the application
+$.mount(App, '#app');

3. How it Works

SigPro doesn't use a Virtual DOM. Instead, it creates real DOM nodes and binds them directly to your data:

  1. Signals: $(value) creates a getter/setter function.
  2. Reactivity: When you pass a signal or a function to a DOM element, SigPro automatically creates a subscription.
  3. Fine-Grained Updates: Only the specific text node or attribute linked to the signal updates when the value changes.

4. Global Tags

By default, SigPro exports common HTML tags to the global scope (window) when initialized. This allows you to write clean, declarative UI without importing every single tag:

javascript
// Instead of $.html('div', ...), just use:
+div([
+  h1("Clean Syntax"),
+  p("No more boilerplate.")
+]);
`,15)])])}const c=i(e,[["render",l]]);export{g as __pageData,c as default}; diff --git a/docs/assets/guide_getting-started.md.D_gqopPp.lean.js b/docs/assets/guide_getting-started.md.D_gqopPp.lean.js new file mode 100644 index 0000000..c3be89b --- /dev/null +++ b/docs/assets/guide_getting-started.md.D_gqopPp.lean.js @@ -0,0 +1 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Getting Started","description":"","frontmatter":{},"headers":[],"relativePath":"guide/getting-started.md","filePath":"guide/getting-started.md"}'),e={name:"guide/getting-started.md"};function l(h,s,p,k,r,d){return a(),t("div",null,[...s[0]||(s[0]=[n("",15)])])}const c=i(e,[["render",l]]);export{g as __pageData,c as default}; diff --git a/docs/assets/guide_why.md.DXchYMN-.js b/docs/assets/guide_why.md.DXchYMN-.js deleted file mode 100644 index 9f92563..0000000 --- a/docs/assets/guide_why.md.DXchYMN-.js +++ /dev/null @@ -1,23 +0,0 @@ -import{_ as s,o as a,c as i,ae as e}from"./chunks/framework.C8AWLET_.js";const c=JSON.parse('{"title":"Why SigPro? ❓","description":"","frontmatter":{},"headers":[],"relativePath":"guide/why.md","filePath":"guide/why.md"}'),n={name:"guide/why.md"};function r(l,t,o,h,d,p){return a(),i("div",null,[...t[0]||(t[0]=[e(`

Why SigPro? ❓

After years of building applications with React, Vue, and Svelte—investing countless hours mastering their unique mental models, build tools, and update cycles—I kept circling back to the same realization: no matter how sophisticated the framework, it all eventually compiles down to HTML, CSS, and vanilla JavaScript. The web platform has evolved tremendously, yet many libraries continue to reinvent the wheel, creating parallel universes with their own rules, their own syntaxes, and their own steep learning curves.

SigPro is my answer to a simple question: Why fight the platform when we can embrace it?

🌐 The Web Platform Is Finally Ready

Modern browsers now offer powerful primitives that make true reactivity possible without virtual DOM diffing, without compilers, and without lock-in:

Browser PrimitiveWhat It Enables
Custom ElementsCreate reusable components with native browser APIs
Shadow DOMEncapsulate styles and markup without preprocessors
CSS Custom PropertiesDynamic theming without CSS-in-JS
Microtask QueuesEfficient update batching without complex scheduling

🎯 The SigPro Philosophy

SigPro strips away the complexity, delivering a reactive programming model that feels familiar but stays remarkably close to vanilla JS:

  • No JSX transformations - Just template literals
  • No template compilers - The browser parses your HTML
  • No proprietary syntax to learn - Just functions and signals
  • No build step required - Works directly in the browser
javascript
// Just vanilla JavaScript with signals
-import { $, html } from 'sigpro';
-
-const count = $(0);
-
-document.body.appendChild(html\`
-  <div>
-    <p>Count: \${count}</p>
-    <button @click=\${() => count(c => c + 1)}>
-      Increment
-    </button>
-  </div>
-\`);

📊 Comparative

MetricSigProSolidSvelteVueReact
Bundle Size (gzip)🥇 5.2KB🥈 15KB🥉 16.6KB20.4KB43.9KB
Time to Interactive🥇 0.8s🥈 1.3s🥉 1.4s1.6s2.3s
Initial Render (ms)🥇 124ms🥈 198ms🥉 287ms298ms452ms
Update Performance (ms)🥇 4ms🥈 5ms🥈 5ms🥉 7ms18ms
Dependencies🥇 0🥇 0🥇 0🥈 2🥉 5
Compilation Required🥇 No🥇 No🥈 Yes🥇 No🥇 No
Browser Native🥇 Yes🥈 Partial🥉 Partial🥉 PartialNo
Framework Lock-in🥇 None🥈 Medium🥉 High🥈 Medium🥉 High
Longevity (standards-based)🥇 10+ years🥈 5 years🥉 3 years🥈 5 years🥈 5 years

🔑 Core Principles

SigPro is built on four fundamental principles:

📡 True Reactivity

Automatic dependency tracking with no manual subscriptions. When a signal changes, only the exact DOM nodes that depend on it update—surgically, efficiently, instantly.

Surgical Updates

No virtual DOM diffing. No tree reconciliation. Just direct DOM updates where and when needed. The result is predictable performance that scales with your content, not your component count.

🧩 Web Standards

Built on Custom Elements, not a custom rendering engine. Your components are real web components that work in any framework—or none at all.

🔬 Predictable

No magic, just signals and effects. What you see is what you get. The debugging experience is straightforward because there's no framework layer between your code and the browser.

🎨 The Development Experience

javascript
// With VS Code + lit-html extension, you get:
-// ✅ Syntax highlighting
-// ✅ Color previews
-// ✅ Auto-formatting
-// ✅ IntelliSense
-
-html\`
-  <div style="color: #ff4444; background: linear-gradient(45deg, blue, green)">
-    <h1>Beautiful highlighted template</h1>
-  </div>
-\`

⏱️ Built for the Long Term

What emerged is a library that proves we've reached a turning point: the web is finally mature enough that we don't need to abstract it anymore. We can build reactive, component-based applications using virtually pure JavaScript, leveraging the platform's latest advances instead of working against them.

The result isn't just smaller bundles or faster rendering—it's code that will still run 10 years from now, in any browser, without maintenance.

📈 The Verdict

While other frameworks build parallel universes with proprietary syntax and compilation steps, SigPro embraces the web platform. SigPro isn't just another framework—it's a return to fundamentals, showing that the dream of simple, powerful reactivity is now achievable with the tools browsers give us out of the box.

"Stop fighting the platform. Start building with it."

🚀 Ready to Start?

Get Started with SigProView on GitHubnpm Package

`,32)])])}const k=s(n,[["render",r]]);export{c as __pageData,k as default}; diff --git a/docs/assets/guide_why.md.DXchYMN-.lean.js b/docs/assets/guide_why.md.DXchYMN-.lean.js deleted file mode 100644 index 763900c..0000000 --- a/docs/assets/guide_why.md.DXchYMN-.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as s,o as a,c as i,ae as e}from"./chunks/framework.C8AWLET_.js";const c=JSON.parse('{"title":"Why SigPro? ❓","description":"","frontmatter":{},"headers":[],"relativePath":"guide/why.md","filePath":"guide/why.md"}'),n={name:"guide/why.md"};function r(l,t,o,h,d,p){return a(),i("div",null,[...t[0]||(t[0]=[e("",32)])])}const k=s(n,[["render",r]]);export{c as __pageData,k as default}; diff --git a/docs/assets/guide_why.md.lyU7T5_c.js b/docs/assets/guide_why.md.lyU7T5_c.js new file mode 100644 index 0000000..7e0cea4 --- /dev/null +++ b/docs/assets/guide_why.md.lyU7T5_c.js @@ -0,0 +1,7 @@ +import{_ as e,o as i,c as a,ae as s}from"./chunks/framework.C8AWLET_.js";const c=JSON.parse('{"title":"Why SigPro?","description":"","frontmatter":{},"headers":[],"relativePath":"guide/why.md","filePath":"guide/why.md"}'),l={name:"guide/why.md"};function n(r,t,o,h,d,g){return i(),a("div",null,[...t[0]||(t[0]=[s(`

Why SigPro?

After years of building applications with React, Vue, and Svelte—investing countless hours mastering unique mental models, proprietary syntaxes, and complex build tools—we reached a realization: the web platform has evolved, but frameworks have become layers of abstraction that often move us further away from the browser.

SigPro is the answer to a simple question: Why fight the platform when we can embrace it?

The Modern Web is Ready

SigPro bypasses the overhead of the Virtual DOM and heavy compilers by using modern browser primitives. It treats the DOM as a first-class citizen, not as a side effect of a state change.

Browser PrimitiveWhat It Enables
Closures & ProxiesAutomatic dependency tracking without heavy overhead.
ES ModulesNative modularity and lazy loading without complex bundlers.
Direct DOM APIsSurgical updates that are faster than any reconciliation algorithm.
Microtask QueuesBatching updates efficiently to ensure 60fps performance.

The SigPro Philosophy

SigPro strips away the complexity, delivering a reactive programming model that feels like a framework but stays remarkably close to Vanilla JS:

  • No JSX transformations – Pure JavaScript functions.
  • No Virtual DOM – Direct, fine-grained DOM manipulation.
  • No proprietary syntax – If you know JS, you know SigPro.
  • Zero Build Step Required – It can run directly in the browser via ESM.
javascript
// Pure, Atomic, Reactive.
+const $count = $(0);
+
+const Counter = () => div([
+  p(["Count: ", $count]),
+  button({ onclick: () => $count(c => c + 1) }, "Increment")
+]);

Performance Comparison

SigPro isn't just lighter; it's architecturally faster because it skips the "diffing" phase entirely.

MetricSigProSolidJSSvelteVueReact
Bundle Size (gzip)🥇 < 2KB🥈 7KB🥉 16KB20KB45KB
ArchitectureAtomicAtomicCompiledV-DOMV-DOM
Initial Render🥇 Fastest🥈 Fast🥉 FastAverageSlow
Update Perf🥇 Surgical🥇 Surgical🥈 Fast🥉 AverageSlow
Dependencies🥇 0🥇 0🥇 0🥈 2🥉 5+
Build Step🥇 Optional🥈 Required🥈 Required🥇 Optional🥈 Required

🔑 Core Principles

SigPro is built on four fundamental pillars:

📡 Atomic Reactivity

Automatic dependency tracking with no manual subscriptions. When a signal changes, only the exact text nodes or attributes that depend on it update—instantly and surgically.

⚡ Surgical DOM Updates

No Virtual DOM diffing. No tree reconciliation. We don't guess what changed; we know exactly where the update needs to happen. Performance scales with your data, not the size of your component tree.

🧩 Plugin-First Architecture

The core is a tiny, powerful engine. Need Routing? Fetching? Global UI? Just plug it in. This keeps your production bundles "pay-only-for-what-you-use."

🔬 Predictable & Transparent

There is no "magic" hidden in a black-box compiler. What you write is what the browser executes. Debugging is straightforward because there is no framework layer between your code and the DevTools.


"SigPro returns the joy of web development by making the browser the hero again."

`,28)])])}const y=e(l,[["render",n]]);export{c as __pageData,y as default}; diff --git a/docs/assets/guide_why.md.lyU7T5_c.lean.js b/docs/assets/guide_why.md.lyU7T5_c.lean.js new file mode 100644 index 0000000..cd2cec3 --- /dev/null +++ b/docs/assets/guide_why.md.lyU7T5_c.lean.js @@ -0,0 +1 @@ +import{_ as e,o as i,c as a,ae as s}from"./chunks/framework.C8AWLET_.js";const c=JSON.parse('{"title":"Why SigPro?","description":"","frontmatter":{},"headers":[],"relativePath":"guide/why.md","filePath":"guide/why.md"}'),l={name:"guide/why.md"};function n(r,t,o,h,d,g){return i(),a("div",null,[...t[0]||(t[0]=[s("",28)])])}const y=e(l,[["render",n]]);export{c as __pageData,y as default}; diff --git a/docs/assets/index.md.BWH7zN4c.js b/docs/assets/index.md.BWH7zN4c.js new file mode 100644 index 0000000..d0f80bb --- /dev/null +++ b/docs/assets/index.md.BWH7zN4c.js @@ -0,0 +1,17 @@ +import{_ as s,o as a,c as t,ae as e}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"SigPro","text":"Atomic Unified Reactive Engine","tagline":"Fine-grained reactivity, built-in routing, and modular plugins. All under 2KB.","image":{"src":"/logo.png","alt":"SigPro Logo"},"actions":[{"theme":"brand","text":"Get Started","link":"/guide/getting-started"},{"theme":"alt","text":"View on GitHub","link":"https://git.natxocc.com/sigpro/"}]},"features":[{"title":"Atomic Reactivity","details":"Powered by Signals. Only updates what changes. No Virtual DOM overhead, no heavy re-renders."},{"title":"Zero Dependencies","details":"Written in pure Vanilla JS. Maximum performance with the smallest footprint possible."},{"title":"Modular Ecosystem","details":"Official plugins for UI components, dynamic Routing, Fetch, and Storage. Load only what you need."}]},"headers":[],"relativePath":"index.md","filePath":"index.md"}'),n={name:"index.md"};function l(h,i,p,r,k,o){return a(),t("div",null,[...i[0]||(i[0]=[e(`

Why SigPro?

SigPro isn't just another framework; it's a high-performance engine. It strips away the complexity of massive bundles and returns to the essence of the web, enhanced with reactive superpowers.

The Core in Action

javascript
import { $ } from 'sigpro2';
+
+// A reactive state Signal
+const $count = $(0);
+
+// A Computed signal that updates automatically
+const $double = $(() => $count() * 2);
+
+// UI that breathes with your data
+const Counter = () => div([
+  h1(["Count: ", $count]),
+  p(["Double: ", $double]),
+  button({ onclick: () => $count(c => c + 1) }, "Increment")
+]);
+
+$.mount(Counter);

Key Features

⚡️ Fine-Grained Reactivity

Unlike frameworks that diff complex trees (V-DOM), SigPro binds your signals directly to real DOM text nodes and attributes. If the data changes, the node changes. Period.

🔌 Polymorphic Plugin System

Extend core capabilities in a single line. Add global UI helpers, routing, or state persistence seamlessly.

javascript
import { UI, Router } from 'sigpro/plugins';
+$.plugin([UI, Router]);

📂 File-Based Routing

With our dedicated Vite plugin, manage your routes simply by creating files in src/pages/. It supports native Lazy Loading out of the box for lightning-fast initial loads.


Quick Install

bash
npm install sigpro
bash
pnpm add sigpro
bash
yarn add sigpro
bash
bun add sigpro

Community & Support

SigPro is an open-source project. Whether you want to contribute, report a bug, or just talk about reactivity, join us on our official repository.

Built with ❤️ by NatxoCC
`,20)])])}const c=s(n,[["render",l]]);export{g as __pageData,c as default}; diff --git a/docs/assets/index.md.BWH7zN4c.lean.js b/docs/assets/index.md.BWH7zN4c.lean.js new file mode 100644 index 0000000..e18d5ca --- /dev/null +++ b/docs/assets/index.md.BWH7zN4c.lean.js @@ -0,0 +1 @@ +import{_ as s,o as a,c as t,ae as e}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"SigPro","text":"Atomic Unified Reactive Engine","tagline":"Fine-grained reactivity, built-in routing, and modular plugins. All under 2KB.","image":{"src":"/logo.png","alt":"SigPro Logo"},"actions":[{"theme":"brand","text":"Get Started","link":"/guide/getting-started"},{"theme":"alt","text":"View on GitHub","link":"https://git.natxocc.com/sigpro/"}]},"features":[{"title":"Atomic Reactivity","details":"Powered by Signals. Only updates what changes. No Virtual DOM overhead, no heavy re-renders."},{"title":"Zero Dependencies","details":"Written in pure Vanilla JS. Maximum performance with the smallest footprint possible."},{"title":"Modular Ecosystem","details":"Official plugins for UI components, dynamic Routing, Fetch, and Storage. Load only what you need."}]},"headers":[],"relativePath":"index.md","filePath":"index.md"}'),n={name:"index.md"};function l(h,i,p,r,k,o){return a(),t("div",null,[...i[0]||(i[0]=[e("",20)])])}const c=s(n,[["render",l]]);export{g as __pageData,c as default}; diff --git a/docs/assets/index.md.uvMJmU4o.js b/docs/assets/index.md.uvMJmU4o.js deleted file mode 100644 index 925d34b..0000000 --- a/docs/assets/index.md.uvMJmU4o.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as e,o as i,c as a,ae as n}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse(`{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"SigPro","text":"Reactivity for the Web Platform","tagline":"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.","image":{"src":"/logo.svg","alt":"SigPro"},"actions":[{"theme":"brand","text":"Get Started","link":"/guide/getting-started"}]},"features":[{"title":"⚡ 3KB gzipped","details":"Minimal footprint with maximum impact. No heavy dependencies, just pure reactivity."},{"title":"🎯 Native Web Components","details":"Built on Custom Elements and Shadow DOM. Leverage the platform, don't fight it."},{"title":"🔄 Signal-based Reactivity","details":"Fine-grained updates without virtual DOM diffing. Just intelligent, automatic reactivity."}]},"headers":[],"relativePath":"index.md","filePath":"index.md"}`),s={name:"index.md"};function o(r,t,l,d,p,c){return i(),a("div",null,[...t[0]||(t[0]=[n('

npm versionbundle sizelicense

"Stop fighting the platform. Start building with it."

',2)])])}const u=e(s,[["render",o]]);export{g as __pageData,u as default}; diff --git a/docs/assets/index.md.uvMJmU4o.lean.js b/docs/assets/index.md.uvMJmU4o.lean.js deleted file mode 100644 index 26e303b..0000000 --- a/docs/assets/index.md.uvMJmU4o.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as e,o as i,c as a,ae as n}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse(`{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"SigPro","text":"Reactivity for the Web Platform","tagline":"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.","image":{"src":"/logo.svg","alt":"SigPro"},"actions":[{"theme":"brand","text":"Get Started","link":"/guide/getting-started"}]},"features":[{"title":"⚡ 3KB gzipped","details":"Minimal footprint with maximum impact. No heavy dependencies, just pure reactivity."},{"title":"🎯 Native Web Components","details":"Built on Custom Elements and Shadow DOM. Leverage the platform, don't fight it."},{"title":"🔄 Signal-based Reactivity","details":"Fine-grained updates without virtual DOM diffing. Just intelligent, automatic reactivity."}]},"headers":[],"relativePath":"index.md","filePath":"index.md"}`),s={name:"index.md"};function o(r,t,l,d,p,c){return i(),a("div",null,[...t[0]||(t[0]=[n("",2)])])}const u=e(s,[["render",o]]);export{g as __pageData,u as default}; diff --git a/docs/assets/plugins_core.debug.md.CVHw_PN0.js b/docs/assets/plugins_core.debug.md.CVHw_PN0.js new file mode 100644 index 0000000..f78710c --- /dev/null +++ b/docs/assets/plugins_core.debug.md.CVHw_PN0.js @@ -0,0 +1,27 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Development Tool: _debug","description":"","frontmatter":{},"headers":[],"relativePath":"plugins/core.debug.md","filePath":"plugins/core.debug.md"}'),e={name:"plugins/core.debug.md"};function l(h,s,p,k,r,o){return a(),t("div",null,[...s[0]||(s[0]=[n(`

Development Tool: _debug

The Debug Plugin is a lightweight reactive listener. Once attached to a signal or a computed function, it automatically monitors changes, compares values, and formats the output in the browser console.

1. Core Features

  • Reactive Tracking: Automatically logs whenever the tracked signal updates.
  • Visual Grouping: Uses styled console groups to keep your dev tools organized.
  • Object Inspection: Automatically uses console.table() when the signal contains an object or array.
  • Efficient Comparison: Uses Object.is to prevent redundant logging if the value hasn't actually changed.

2. Installation

To use _debug, you only need the SigPro core. Register the plugin in your main.js. You can conditionally load it so it only runs during development.

javascript
import { $ } from 'sigpro';
+import { Debug } from 'sigpro/plugins';
+
+// Only load Debug in development mode
+const plugins = [];
+if (import.meta.env.DEV) plugins.push(Debug);
+
+$.plugin(plugins).then(() => {
+  import('./App.js').then(app => $.mount(app.default));
+});
bash
npm install sigpro
bash
pnpm add sigpro
bash
yarn add sigpro
bash
bun add sigpro

3. Basic Usage

Call _debug anywhere in your component. It stays active in the background, watching the signal's lifecycle.

javascript
export default () => {
+  const $count = $(0);
+  const $user = $({ name: "Guest", role: "Viewer" });
+
+  // Start tracking
+  _debug($count, "Main Counter");
+  _debug($user, "User Session");
+
+  return div([
+    button({ onclick: () => $count(c => c + 1) }, "Increment"),
+    button({ onclick: () => $user({ name: "Admin", role: "Super" }) }, "Promote")
+  ]);
+};

4. Console Output Breakdown

When a signal changes, the console displays a structured block:

  1. Header: A styled badge with the name (e.g., SigPro Debug: Main Counter).
  2. Previous Value: The value before the update (in red).
  3. Current Value: The new value (in green).
  4. Table View: If the value is an object, a formatted table appears automatically.

5. Debugging Computed Values

You can also debug computed functions to see exactly when derived state is recalculated.

javascript
const $price = $(100);
+const $tax = $(0.21);
+const $total = $(() => $price() * (1 + $tax()));
+
+// Monitor the result of the calculation
+_debug($total, "Final Invoice Total");

6. Why use _debug?

  1. Clean Logic: No need to scatter console.log inside your reactive functions.
  2. State History: Instantly see the "Before" and "After" of any user action.
  3. No-Noise: It only logs when a real change occurs, keeping the console clean.
  4. Deep Inspection: The automatic console.table makes debugging large API responses much faster.
`,24)])])}const E=i(e,[["render",l]]);export{g as __pageData,E as default}; diff --git a/docs/assets/plugins_core.debug.md.CVHw_PN0.lean.js b/docs/assets/plugins_core.debug.md.CVHw_PN0.lean.js new file mode 100644 index 0000000..add7b6e --- /dev/null +++ b/docs/assets/plugins_core.debug.md.CVHw_PN0.lean.js @@ -0,0 +1 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Development Tool: _debug","description":"","frontmatter":{},"headers":[],"relativePath":"plugins/core.debug.md","filePath":"plugins/core.debug.md"}'),e={name:"plugins/core.debug.md"};function l(h,s,p,k,r,o){return a(),t("div",null,[...s[0]||(s[0]=[n("",24)])])}const E=i(e,[["render",l]]);export{g as __pageData,E as default}; diff --git a/docs/assets/plugins_core.fetch.md.BIc8aMQh.js b/docs/assets/plugins_core.fetch.md.BIc8aMQh.js new file mode 100644 index 0000000..43f233f --- /dev/null +++ b/docs/assets/plugins_core.fetch.md.BIc8aMQh.js @@ -0,0 +1,30 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const o=JSON.parse('{"title":"Data Fetching: _fetch","description":"","frontmatter":{},"headers":[],"relativePath":"plugins/core.fetch.md","filePath":"plugins/core.fetch.md"}'),e={name:"plugins/core.fetch.md"};function h(l,s,p,k,r,d){return a(),t("div",null,[...s[0]||(s[0]=[n(`

Data Fetching: _fetch

The Fetch Plugin provides a reactive wrapper around the native browser Fetch API. Instead of managing complex async/await flows within your UI, _fetch returns a "Reactive Tripod" (Data, Loading, and Error) that your components can listen to automatically.

1. Core Concept

When you call _fetch, it returns three signals immediately. Your UI declares how to react to these signals as they change from their initial state to the final response.

  • $data: Initialized as null. Automatically holds the JSON response on success.
  • $loading: Initialized as true. Flips to false once the request settles.
  • $error: Initialized as null. Holds the error message if the request fails.

2. Installation

Register the Fetch plugin in your main.js. By convention, we load it alongside the UI and Router to have the full SigPro ecosystem ready.

javascript
import { $ } from 'sigpro';
+import { Fetch } from 'sigpro/plugins';
+
+$.plugin([Fetch]).then(() => {
+  // Now _fetch() is available globally
+  import('./App.js').then(app => $.mount(app.default));
+});

3. Basic Usage

Use _fetch inside your component to get live updates. The UI updates surgically whenever a signal changes.

javascript
export default () => {
+  const { $data, $loading, $error } = _fetch('https://api.github.com/users/octocat');
+
+  return div({ class: 'p-6 flex flex-col gap-4' }, [
+    h1("Profile Details"),
+    
+    // 1. Loading State (using SigPro UI button)
+    () => $loading() && _button({ $loading: true }, "Fetching..."),
+
+    // 2. Error State
+    () => $error() && div({ class: 'alert alert-error' }, $error()),
+
+    // 3. Success State
+    () => $data() && div({ class: 'card bg-base-200 p-4' }, [
+      img({ src: $data().avatar_url, class: 'w-16 rounded-full' }),
+      h2($data().name),
+      p($data().bio)
+    ])
+  ]);
+};

4. Advanced Configuration

_fetch accepts the same RequestInit options as the standard fetch() (methods, headers, body, etc.).

javascript
const { $data, $loading } = _fetch('/api/v1/update', {
+  method: 'PATCH',
+  headers: { 'Content-Type': 'application/json' },
+  body: JSON.stringify({ status: 'active' })
+});

5. Why use _fetch instead of native Fetch?

  1. Declarative UI: You define the "Loading", "Error", and "Success" templates once, and they swap automatically.
  2. No useEffect required: Since SigPro is natively reactive, you don't need lifecycle hooks to trigger re-renders; the signals handle it.
  3. Consistency: It follows the same _prefix pattern as the rest of the official plugin ecosystem.
  4. Automatic JSON Parsing: It assumes JSON by default and handles 404/500 errors by populating the $error signal.
`,20)])])}const c=i(e,[["render",h]]);export{o as __pageData,c as default}; diff --git a/docs/assets/plugins_core.fetch.md.BIc8aMQh.lean.js b/docs/assets/plugins_core.fetch.md.BIc8aMQh.lean.js new file mode 100644 index 0000000..bf0184a --- /dev/null +++ b/docs/assets/plugins_core.fetch.md.BIc8aMQh.lean.js @@ -0,0 +1 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const o=JSON.parse('{"title":"Data Fetching: _fetch","description":"","frontmatter":{},"headers":[],"relativePath":"plugins/core.fetch.md","filePath":"plugins/core.fetch.md"}'),e={name:"plugins/core.fetch.md"};function h(l,s,p,k,r,d){return a(),t("div",null,[...s[0]||(s[0]=[n("",20)])])}const c=i(e,[["render",h]]);export{o as __pageData,c as default}; diff --git a/docs/assets/plugins_core.router.md.bGFltJyy.js b/docs/assets/plugins_core.router.md.bGFltJyy.js new file mode 100644 index 0000000..1a6b6bc --- /dev/null +++ b/docs/assets/plugins_core.router.md.bGFltJyy.js @@ -0,0 +1,31 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const E=JSON.parse('{"title":"Navigation Plugin: Router","description":"","frontmatter":{},"headers":[],"relativePath":"plugins/core.router.md","filePath":"plugins/core.router.md"}'),e={name:"plugins/core.router.md"};function h(l,s,p,k,r,d){return a(),t("div",null,[...s[0]||(s[0]=[n(`

Navigation Plugin: Router

The SigPro Router handles URL changes via hashes (#) and maps them to components. It supports dynamic parameters (like :id) and asynchronous loading for heavy pages.

1. Core Features

  • Hash-based: Works everywhere without special server configuration.
  • Lazy Loading: Pages are only downloaded when the user visits the route.
  • Reactive: The view updates automatically when the hash changes.
  • Dynamic Routes: Supports paths like /user/:id.

2. Installation

The Router is usually included in the official plugins package.

bash
npm install -D tailwindcss @tailwindcss/vite daisyui@next
bash
pnpm add -D tailwindcss @tailwindcss/vite daisyui@next
bash
yarn add -D tailwindcss @tailwindcss/vite daisyui@next
bash
bun add -d tailwindcss @tailwindcss/vite daisyui@next

3. Setting Up Routes

In your App.js (or a dedicated routes file), define your navigation map.

javascript
const routes = [
+  { path: '/', component: () => h1("Home Page") },
+  { 
+    path: '/admin', 
+    // Lazy Loading: This file is only fetched when needed
+    component: () => import('./pages/Admin.js') 
+  },
+  { path: '/user/:id', component: (params) => h2(\`User ID: \${params.id}\`) },
+  { path: '*', component: () => div("404 - Page Not Found") }
+];
+
+export default () => div([
+  _navbar({ title: "My App" }),
+  _router(routes) // The router is now a global tag
+]);

4. Navigation (_router.go)

To move between pages programmatically (e.g., inside an onclick event), use the global _router.go helper.

javascript
_button({ 
+  onclick: () => _router.go('/admin') 
+}, "Go to Admin")

5. How it Works (Under the Hood)

The router tracks the window.location.hash and uses a reactive signal to trigger a re-render of the specific area where _router(routes) is placed.

  1. Match: It filters your route array to find the best fit.
  2. Resolve: * If it's a standard function, it executes it immediately.
    • If it's a Promise (via import()), it shows a loading state and swaps the content once the module arrives.
  3. Inject: It replaces the previous DOM node with the new page content surgically.

6. Integration with UI Components

Since you are using the UI Plugin, you can easily create active states in your navigation menus by checking the current hash.

javascript
// Example of a reactive sidebar menu
+_menu({
+  items: [
+    { 
+      label: 'Dashboard', 
+      active: () => window.location.hash === '#/', 
+      onclick: () => _router.go('/') 
+    },
+    { 
+      label: 'Settings', 
+      active: () => window.location.hash === '#/settings', 
+      onclick: () => _router.go('/settings') 
+    }
+  ]
+})
`,24)])])}const g=i(e,[["render",h]]);export{E as __pageData,g as default}; diff --git a/docs/assets/plugins_core.router.md.bGFltJyy.lean.js b/docs/assets/plugins_core.router.md.bGFltJyy.lean.js new file mode 100644 index 0000000..121e5fd --- /dev/null +++ b/docs/assets/plugins_core.router.md.bGFltJyy.lean.js @@ -0,0 +1 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const E=JSON.parse('{"title":"Navigation Plugin: Router","description":"","frontmatter":{},"headers":[],"relativePath":"plugins/core.router.md","filePath":"plugins/core.router.md"}'),e={name:"plugins/core.router.md"};function h(l,s,p,k,r,d){return a(),t("div",null,[...s[0]||(s[0]=[n("",24)])])}const g=i(e,[["render",h]]);export{E as __pageData,g as default}; diff --git a/docs/assets/plugins_core.storage.md.Bgu1q6YH.js b/docs/assets/plugins_core.storage.md.Bgu1q6YH.js new file mode 100644 index 0000000..f69bdda --- /dev/null +++ b/docs/assets/plugins_core.storage.md.Bgu1q6YH.js @@ -0,0 +1,29 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Persistence Tool: _storage","description":"","frontmatter":{},"headers":[],"relativePath":"plugins/core.storage.md","filePath":"plugins/core.storage.md"}'),e={name:"plugins/core.storage.md"};function h(l,s,p,k,r,o){return a(),t("div",null,[...s[0]||(s[0]=[n(`

Persistence Tool: _storage

The Storage plugin synchronizes a signal with a specific key in your browser's localStorage. It handles both the initial hydration (loading data when the app starts) and automatic saving whenever the signal's value changes.

1. Core Concept

When you "attach" a signal to _storage, two things happen:

  1. Hydration: The plugin checks if the key already exists in localStorage. If it does, it parses the JSON and updates the signal immediately.
  2. Reactive Sync: It creates a reactive watcher that stringifies and saves the signal's value to the disk every time it is updated.

2. Installation

Register the Storage plugin in your main.js. Since this is a logic-only plugin, it doesn't require any CSS or UI dependencies.

javascript
import { $ } from 'sigpro';
+import { Storage } from 'sigpro/plugins';
+
+$.plugin(Storage).then(() => {
+  import('./App.js').then(app => $.mount(app.default));
+});
bash
npm install sigpro
bash
pnpm add sigpro
bash
yarn add sigpro
bash
bun add sigpro

3. Basic Usage

You can wrap any signal with _storage. It is common practice to do this right after creating the signal.

javascript
export default () => {
+  // 1. Create a signal with a default value
+  const $theme = $( 'light' );
+
+  // 2. Persist it. If 'user_theme' exists in localStorage, 
+  // $theme will be updated to that value instantly.
+  _storage($theme, 'user_theme');
+
+  return div({ class: () => \`app-\${$theme()}\` }, [
+    h1(\`Current Theme: \${$theme()}\`),
+    button({ 
+      onclick: () => $theme(t => t === 'light' ? 'dark' : 'light') 
+    }, "Toggle Theme")
+  ]);
+};

4. Complex Data (Objects & Arrays)

Since the plugin uses JSON.parse and JSON.stringify internally, it works perfectly with complex state structures.

javascript
const $settings = $({ 
+  notifications: true, 
+  fontSize: 16 
+});
+
+// Automatically saves the whole object whenever any property changes
+_storage($settings, 'app_settings');

5. Why use _storage?

  1. Zero Boilerplate: You don't need to manually write localStorage.getItem or setItem logic inside your components.
  2. Chaining: Because _storage returns the signal, you can persist it inline.
  3. Error Resilience: It includes a built-in try/catch block to prevent your app from crashing if the stored JSON is corrupted.
  4. Surgical Persistence: Only the signals you explicitly mark for storage are saved, keeping your localStorage clean.

6. Pro Tip: Combining with Debug

You can chain plugins to create a fully monitored and persistent state:

javascript
const $score = _storage($(0), 'high_score');
+
+// Now it's saved to disk AND logged to console on every change
+_debug($score, "Game Score");
`,25)])])}const c=i(e,[["render",h]]);export{g as __pageData,c as default}; diff --git a/docs/assets/plugins_core.storage.md.Bgu1q6YH.lean.js b/docs/assets/plugins_core.storage.md.Bgu1q6YH.lean.js new file mode 100644 index 0000000..be7046b --- /dev/null +++ b/docs/assets/plugins_core.storage.md.Bgu1q6YH.lean.js @@ -0,0 +1 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Persistence Tool: _storage","description":"","frontmatter":{},"headers":[],"relativePath":"plugins/core.storage.md","filePath":"plugins/core.storage.md"}'),e={name:"plugins/core.storage.md"};function h(l,s,p,k,r,o){return a(),t("div",null,[...s[0]||(s[0]=[n("",25)])])}const c=i(e,[["render",h]]);export{g as __pageData,c as default}; diff --git a/docs/assets/plugins_core.ui.md.DDLum7rv.js b/docs/assets/plugins_core.ui.md.DDLum7rv.js new file mode 100644 index 0000000..013bc6d --- /dev/null +++ b/docs/assets/plugins_core.ui.md.DDLum7rv.js @@ -0,0 +1,30 @@ +import{_ as i,o as t,c as a,ae as e}from"./chunks/framework.C8AWLET_.js";const c=JSON.parse('{"title":"Official UI Plugin: UI","description":"","frontmatter":{},"headers":[],"relativePath":"plugins/core.ui.md","filePath":"plugins/core.ui.md"}'),n={name:"plugins/core.ui.md"};function l(h,s,p,o,d,r){return t(),a("div",null,[...s[0]||(s[0]=[e(`

Official UI Plugin: UI

The SigPro UI plugin is a high-level component library built on top of the reactive core. It leverages Tailwind CSS v4 for utility styling and daisyUI v5 for semantic components.

1. Prerequisites & Installation

To use these components, you must install the styling engine. SigPro UI provides the logic, but Tailwind and daisyUI provide the visuals.

bash
npm install -D tailwindcss @tailwindcss/vite daisyui@next
bash
pnpm add -D tailwindcss @tailwindcss/vite daisyui@next
bash
yarn add -D tailwindcss @tailwindcss/vite daisyui@next
bash
bun add -d tailwindcss @tailwindcss/vite daisyui@next

Would you like to continue with the Router.md documentation now?

CSS Configuration (app.css)

In Tailwind v4, configuration is handled directly in your CSS. Create a src/app.css file:

css
/* src/app.css */
+@import "tailwindcss";
+
+/* Import daisyUI v5 as a Tailwind v4 plugin */
+@plugin "daisyui";
+
+/* Optional: Configure themes */
+@custom-variant dark (&:where(.dark, [data-theme="dark"], [data-theme="dark"] *)));

2. Initialization

You must import your CSS and register the UI plugin in your entry point. This populates the global scope with reactive component helpers (prefixed with _).

javascript
// main.js
+import './app.css';
+import { $ } from 'sigpro';
+import { UI } from 'sigpro/plugins';
+
+$.plugin(UI).then(() => {
+  // Global components like _button and _input are now ready
+  import('./App.js').then(app => $.mount(app.default));
+});

3. Core Component Tags (_tags)

SigPro UI components are more than just HTML; they are Reactive Functional Components that manage complex states (loading, errors, accessibility) automatically.

A. Action Components (_button)

The _button automatically handles spinners and disabled states based on signals.

PropertyTypeDescription
$loadingsignalIf true, shows a spinner and disables the button.
$disabledsignalManually disables the button (logic-bound).
iconnode/strPrepends an icon to the text.
badgestringAppends a small badge to the button.
javascript
_button({ 
+  $loading: $isSaving, 
+  icon: '💾', 
+  class: 'btn-primary' 
+}, "Save Data")

B. High-Density Forms (_input, _select, _checkbox)

These components wrap the raw input in a fieldset with integrated labels and tooltips.

  • label: Field title displayed above the input.
  • tip: Displays a ? badge that shows a tooltip on hover.
  • $error: A signal that, when populated, turns the input red and displays the message.
  • $value: Two-way binding. Updates the signal on input and the input on signal change.
javascript
_input({
+  label: "Username",
+  tip: "Choose a unique name",
+  $value: $name,
+  $error: $nameError
+})

4. Complex UI Patterns

Reactive Modals (_modal)

The _modal is surgically mounted. If the $open signal is false, the component is completely removed from the DOM, optimizing performance.

javascript
const $showModal = $(false);
+
+_modal({ $open: $showModal, title: "Alert" }, [
+  p("Are you sure you want to proceed?"),
+  _button({ onclick: () => doAction() }, "Confirm")
+])

Designed to work seamlessly with the Router.

ComponentKey Logic
_tabsAccepts an active property (signal or function) to highlight the current tab.
_drawerA responsive sidebar that toggles via an ID or an $open signal.
_navbarStandard top bar with shadow and glass effect support.
_menuVertical navigation list with active state support.

5. Summary Table: UI Globals

Once $.plugin(UI) is active, these tags are available project-wide:

TagCategoryUse Case
_fieldsetLayoutGrouping related inputs with a legend.
_accordionContentCollapsible sections (FAQs).
_badgeFeedbackStatus indicators (Success, Warning).
_tooltipFeedbackDescriptive text on hover.
_rangeInputReactive slider for numerical values.

What's next?

With the UI ready and styled via Tailwind v4, we can move to the Router.md. We will explain how to link _tabs and _menu to different URL paths for a full SPA experience.

Would you like to start with the Router configuration?

`,40)])])}const g=i(n,[["render",l]]);export{c as __pageData,g as default}; diff --git a/docs/assets/plugins_core.ui.md.DDLum7rv.lean.js b/docs/assets/plugins_core.ui.md.DDLum7rv.lean.js new file mode 100644 index 0000000..69a4077 --- /dev/null +++ b/docs/assets/plugins_core.ui.md.DDLum7rv.lean.js @@ -0,0 +1 @@ +import{_ as i,o as t,c as a,ae as e}from"./chunks/framework.C8AWLET_.js";const c=JSON.parse('{"title":"Official UI Plugin: UI","description":"","frontmatter":{},"headers":[],"relativePath":"plugins/core.ui.md","filePath":"plugins/core.ui.md"}'),n={name:"plugins/core.ui.md"};function l(h,s,p,o,d,r){return t(),a("div",null,[...s[0]||(s[0]=[e("",40)])])}const g=i(n,[["render",l]]);export{c as __pageData,g as default}; diff --git a/docs/assets/plugins_custom.md.D2KGTblR.js b/docs/assets/plugins_custom.md.D2KGTblR.js new file mode 100644 index 0000000..d93a9f0 --- /dev/null +++ b/docs/assets/plugins_custom.md.D2KGTblR.js @@ -0,0 +1,48 @@ +import{_ as i,o as a,c as n,ae as t}from"./chunks/framework.C8AWLET_.js";const o=JSON.parse('{"title":"Creating Custom Plugins","description":"","frontmatter":{},"headers":[],"relativePath":"plugins/custom.md","filePath":"plugins/custom.md"}'),l={name:"plugins/custom.md"};function h(e,s,p,k,r,d){return a(),n("div",null,[...s[0]||(s[0]=[t(`

Creating Custom Plugins

There are two main ways to expose a plugin's functionality: Static/Manual Imports (cleaner for large projects) or Global/Automatic Window Injection (easier for quick scripts and global helpers).

1. The Anatomy of a Plugin

A plugin is a standard JavaScript function. By convention, if a plugin adds a global helper or component, it should be prefixed with an underscore (_).

javascript
// plugins/my-utils.js
+export const MyUtils = ($) => {
+  
+  // 1. Attach to the SigPro instance
+  $.capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
+
+  // 2. Attach to the Window (Global access)
+  window._hello = (name) => div(\`Hello, \${$.capitalize(name)}!\`);
+  
+  // 3. You can also return values if needed
+  return { version: '1.0.0' };
+};

2. Integration Strategies

This approach keeps your global namespace clean. You import the logic only where you need it, but the plugin still initializes the core $ extensions.

javascript
// main.js
+import { $ } from 'sigpro';
+import { MyUtils } from './plugins/my-utils.js';
+
+$.plugin(MyUtils);
+
+// App.js
+export default () => {
+  const name = "sigpro";
+  // $.capitalize was added by the plugin
+  return h1($.capitalize(name)); 
+};

Option B: Automatic Window Injection

If your plugin defines global tags (like _button or _hello), you should attach them to the window object inside the plugin function. This makes them available everywhere without imports.

javascript
// plugins/theme.js
+export const Theme = ($) => {
+  const $dark = $(false);
+
+  window._themeToggle = () => button({
+    onclick: () => $dark(v => !v),
+    class: () => $dark() ? 'bg-black text-white' : 'bg-white text-black'
+  }, "Toggle Mode");
+};
+
+// main.js
+$.plugin(Theme).then(() => {
+   // _themeToggle is now a global function
+   $.mount(App);
+});

3. Asynchronous Plugins

If your plugin needs to load external data or scripts before the app starts, make it async. SigPro will wait for it.

javascript
export const ConfigLoader = async ($) => {
+  const res = await fetch('/config.json');
+  const config = await res.json();
+  
+  $.config = config; // Attach loaded config to SigPro
+};
+
+// Usage
+$.plugin(ConfigLoader).then(() => {
+  console.log("Config loaded:", $.config);
+  $.mount(App);
+});

4. Best Practices for Plugin Authors

RuleDescription
PrefixingUse _ for UI components (_modal) and $. for logic ($.fetch).
IdempotencyEnsure calling $.plugin(MyPlugin) twice doesn't break the app.
EncapsulationUse the $ instance passed as an argument rather than importing it again inside the plugin.
ReactivityAlways use $(...) for internal state so the app stays reactive.

5. Installation

Custom plugins don't require extra packages, but ensure your build tool (Vite/Bun) is configured to handle the module imports.

bash
npm install sigpro
bash
pnpm add sigpro
bash
yarn add sigpro
bash
bun add sigpro
`,24)])])}const E=i(l,[["render",h]]);export{o as __pageData,E as default}; diff --git a/docs/assets/plugins_custom.md.D2KGTblR.lean.js b/docs/assets/plugins_custom.md.D2KGTblR.lean.js new file mode 100644 index 0000000..6535180 --- /dev/null +++ b/docs/assets/plugins_custom.md.D2KGTblR.lean.js @@ -0,0 +1 @@ +import{_ as i,o as a,c as n,ae as t}from"./chunks/framework.C8AWLET_.js";const o=JSON.parse('{"title":"Creating Custom Plugins","description":"","frontmatter":{},"headers":[],"relativePath":"plugins/custom.md","filePath":"plugins/custom.md"}'),l={name:"plugins/custom.md"};function h(e,s,p,k,r,d){return a(),n("div",null,[...s[0]||(s[0]=[t("",24)])])}const E=i(l,[["render",h]]);export{o as __pageData,E as default}; diff --git a/docs/assets/plugins_quick.md.ODjl7edh.js b/docs/assets/plugins_quick.md.ODjl7edh.js new file mode 100644 index 0000000..123478f --- /dev/null +++ b/docs/assets/plugins_quick.md.ODjl7edh.js @@ -0,0 +1,36 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Extending SigPro: $.plugin","description":"","frontmatter":{},"headers":[],"relativePath":"plugins/quick.md","filePath":"plugins/quick.md"}'),e={name:"plugins/quick.md"};function l(h,s,p,k,r,o){return a(),t("div",null,[...s[0]||(s[0]=[n(`

Extending SigPro: $.plugin

The plugin system is the engine's way of growing. It allows you to inject new functionality directly into the $ object or load external resources.

1. How Plugins Work

A plugin in SigPro is simply a function that receives the core instance. When you run $.plugin(MyPlugin), the engine hands over the $ object so the plugin can attach new methods or register global tags (like div(), span(), etc.).

Functional Plugin Example

javascript
// A plugin that adds a simple logger to any signal
+const Logger = ($) => {
+  $.watch = (target, label = "Log") => {
+    $(() => console.log(\`[\${label}]:\`, target()));
+  };
+};
+
+// Activation
+$.plugin(Logger);
+const $count = $(0);
+$.watch($count, "Counter"); // Now available globally via $

2. Initialization Patterns

Since plugins often set up global variables (like the HTML tags), the order of initialization is critical. Here are the two ways to start your app:

This is the most robust way. It ensures all global tags (div, button, etc.) are created before your App code is even read by the browser.

javascript
// main.js
+import { $ } from 'sigpro';
+import { UI, Router } from 'sigpro/plugins';
+
+// 1. Load plugins first
+$.plugin([UI, Router]).then(() => {
+  
+  // 2. Import your app only after the environment is ready
+  import('./App.js').then(appFile => {
+    const MyApp = appFile.default;
+    $.mount(MyApp, '#app');
+  });
+
+});

Option B: Static Start (No Global Tags)

Use this only if you prefer not to use global tags and want to use $.html directly in your components. This allows for standard static imports.

javascript
// main.js
+import { $ } from 'sigpro';
+import { UI } from 'sigpro/plugins';
+import MyApp from './App.js'; // Static import works here
+
+$.plugin(UI);
+$.mount(MyApp, '#app');

Warning: In this mode, if App.js uses div() instead of $.html('div'), it will throw a ReferenceError.


3. Resource Plugins (External Scripts)

You can pass a URL or an Array of URLs. SigPro will inject them as <script> tags and return a Promise that resolves when the scripts are fully loaded and executed.

javascript
// Loading external libraries as plugins
+await $.plugin([
+  'https://cdn.jsdelivr.net/npm/chart.js',
+  'https://cdn.example.com/custom-ui-lib.js'
+]);
+
+console.log("External resources are ready to use!");

4. Polymorphic Loading Reference

The $.plugin method adapts to whatever you throw at it:

Input TypeActionBehavior
FunctionExecutes fn($)Synchronous / Immediate
String (URL)Injects <script src="...">Asynchronous (Returns Promise)
ArrayProcesses each item in the listReturns Promise if any item is Async

💡 Pro Tip: Why the .then()?

Using $.plugin([...]).then(...) is like giving your app a "Pre-flight Check". It guarantees that:

  1. All reactive methods are attached.
  2. Global HTML tags are defined.
  3. External libraries (like Chart.js) are loaded.
  4. The result: Your components are cleaner, smaller, and error-free.
`,28)])])}const E=i(e,[["render",l]]);export{g as __pageData,E as default}; diff --git a/docs/assets/plugins_quick.md.ODjl7edh.lean.js b/docs/assets/plugins_quick.md.ODjl7edh.lean.js new file mode 100644 index 0000000..f909f8f --- /dev/null +++ b/docs/assets/plugins_quick.md.ODjl7edh.lean.js @@ -0,0 +1 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Extending SigPro: $.plugin","description":"","frontmatter":{},"headers":[],"relativePath":"plugins/quick.md","filePath":"plugins/quick.md"}'),e={name:"plugins/quick.md"};function l(h,s,p,k,r,o){return a(),t("div",null,[...s[0]||(s[0]=[n("",28)])])}const E=i(e,[["render",l]]);export{g as __pageData,E as default}; diff --git a/docs/assets/style.DJRheFKp.css b/docs/assets/style.7j_EAAZ2.css similarity index 99% rename from docs/assets/style.DJRheFKp.css rename to docs/assets/style.7j_EAAZ2.css index 8f062c4..600f633 100644 --- a/docs/assets/style.DJRheFKp.css +++ b/docs/assets/style.7j_EAAZ2.css @@ -1 +1 @@ -@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-roman-cyrillic.C5lxZ8CY.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-roman-greek-ext.CqjqNYQ-.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-roman-greek.BBVDIX6e.woff2) format("woff2");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-roman-vietnamese.BjW4sHH5.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-roman-latin-ext.4ZJIpNVo.woff2) format("woff2");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-roman-latin.Di8DUHzh.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-italic-cyrillic-ext.r48I6akx.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-italic-cyrillic.By2_1cv3.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-italic-greek-ext.1u6EdAuj.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-italic-greek.DJ8dCoTZ.woff2) format("woff2");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-italic-vietnamese.BSbpV94h.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-italic-latin-ext.CN1xVJS-.woff2) format("woff2");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-italic-latin.C2AdPX0b.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Punctuation SC;font-weight:400;src:local("PingFang SC Regular"),local("Noto Sans CJK SC"),local("Microsoft YaHei");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:500;src:local("PingFang SC Medium"),local("Noto Sans CJK SC"),local("Microsoft YaHei");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:600;src:local("PingFang SC Semibold"),local("Noto Sans CJK SC Bold"),local("Microsoft YaHei Bold");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:700;src:local("PingFang SC Semibold"),local("Noto Sans CJK SC Bold"),local("Microsoft YaHei Bold");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}:root{--vp-c-white: #ffffff;--vp-c-black: #000000;--vp-c-neutral: var(--vp-c-black);--vp-c-neutral-inverse: var(--vp-c-white)}.dark{--vp-c-neutral: var(--vp-c-white);--vp-c-neutral-inverse: var(--vp-c-black)}:root{--vp-c-gray-1: #dddde3;--vp-c-gray-2: #e4e4e9;--vp-c-gray-3: #ebebef;--vp-c-gray-soft: rgba(142, 150, 170, .14);--vp-c-indigo-1: #3451b2;--vp-c-indigo-2: #3a5ccc;--vp-c-indigo-3: #5672cd;--vp-c-indigo-soft: rgba(100, 108, 255, .14);--vp-c-purple-1: #6f42c1;--vp-c-purple-2: #7e4cc9;--vp-c-purple-3: #8e5cd9;--vp-c-purple-soft: rgba(159, 122, 234, .14);--vp-c-green-1: #18794e;--vp-c-green-2: #299764;--vp-c-green-3: #30a46c;--vp-c-green-soft: rgba(16, 185, 129, .14);--vp-c-yellow-1: #915930;--vp-c-yellow-2: #946300;--vp-c-yellow-3: #9f6a00;--vp-c-yellow-soft: rgba(234, 179, 8, .14);--vp-c-red-1: #b8272c;--vp-c-red-2: #d5393e;--vp-c-red-3: #e0575b;--vp-c-red-soft: rgba(244, 63, 94, .14);--vp-c-sponsor: #db2777}.dark{--vp-c-gray-1: #515c67;--vp-c-gray-2: #414853;--vp-c-gray-3: #32363f;--vp-c-gray-soft: rgba(101, 117, 133, .16);--vp-c-indigo-1: #a8b1ff;--vp-c-indigo-2: #5c73e7;--vp-c-indigo-3: #3e63dd;--vp-c-indigo-soft: rgba(100, 108, 255, .16);--vp-c-purple-1: #c8abfa;--vp-c-purple-2: #a879e6;--vp-c-purple-3: #8e5cd9;--vp-c-purple-soft: rgba(159, 122, 234, .16);--vp-c-green-1: #3dd68c;--vp-c-green-2: #30a46c;--vp-c-green-3: #298459;--vp-c-green-soft: rgba(16, 185, 129, .16);--vp-c-yellow-1: #f9b44e;--vp-c-yellow-2: #da8b17;--vp-c-yellow-3: #a46a0a;--vp-c-yellow-soft: rgba(234, 179, 8, .16);--vp-c-red-1: #f66f81;--vp-c-red-2: #f14158;--vp-c-red-3: #b62a3c;--vp-c-red-soft: rgba(244, 63, 94, .16)}:root{--vp-c-bg: #ffffff;--vp-c-bg-alt: #f6f6f7;--vp-c-bg-elv: #ffffff;--vp-c-bg-soft: #f6f6f7}.dark{--vp-c-bg: #1b1b1f;--vp-c-bg-alt: #161618;--vp-c-bg-elv: #202127;--vp-c-bg-soft: #202127}:root{--vp-c-border: #c2c2c4;--vp-c-divider: #e2e2e3;--vp-c-gutter: #e2e2e3}.dark{--vp-c-border: #3c3f44;--vp-c-divider: #2e2e32;--vp-c-gutter: #000000}:root{--vp-c-text-1: #3c3c43;--vp-c-text-2: #67676c;--vp-c-text-3: #929295}.dark{--vp-c-text-1: #dfdfd6;--vp-c-text-2: #98989f;--vp-c-text-3: #6a6a71}:root{--vp-c-default-1: var(--vp-c-gray-1);--vp-c-default-2: var(--vp-c-gray-2);--vp-c-default-3: var(--vp-c-gray-3);--vp-c-default-soft: var(--vp-c-gray-soft);--vp-c-brand-1: var(--vp-c-indigo-1);--vp-c-brand-2: var(--vp-c-indigo-2);--vp-c-brand-3: var(--vp-c-indigo-3);--vp-c-brand-soft: var(--vp-c-indigo-soft);--vp-c-brand: var(--vp-c-brand-1);--vp-c-tip-1: var(--vp-c-brand-1);--vp-c-tip-2: var(--vp-c-brand-2);--vp-c-tip-3: var(--vp-c-brand-3);--vp-c-tip-soft: var(--vp-c-brand-soft);--vp-c-note-1: var(--vp-c-brand-1);--vp-c-note-2: var(--vp-c-brand-2);--vp-c-note-3: var(--vp-c-brand-3);--vp-c-note-soft: var(--vp-c-brand-soft);--vp-c-success-1: var(--vp-c-green-1);--vp-c-success-2: var(--vp-c-green-2);--vp-c-success-3: var(--vp-c-green-3);--vp-c-success-soft: var(--vp-c-green-soft);--vp-c-important-1: var(--vp-c-purple-1);--vp-c-important-2: var(--vp-c-purple-2);--vp-c-important-3: var(--vp-c-purple-3);--vp-c-important-soft: var(--vp-c-purple-soft);--vp-c-warning-1: var(--vp-c-yellow-1);--vp-c-warning-2: var(--vp-c-yellow-2);--vp-c-warning-3: var(--vp-c-yellow-3);--vp-c-warning-soft: var(--vp-c-yellow-soft);--vp-c-danger-1: var(--vp-c-red-1);--vp-c-danger-2: var(--vp-c-red-2);--vp-c-danger-3: var(--vp-c-red-3);--vp-c-danger-soft: var(--vp-c-red-soft);--vp-c-caution-1: var(--vp-c-red-1);--vp-c-caution-2: var(--vp-c-red-2);--vp-c-caution-3: var(--vp-c-red-3);--vp-c-caution-soft: var(--vp-c-red-soft)}:root{--vp-font-family-base: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--vp-font-family-mono: ui-monospace, "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace;font-optical-sizing:auto}:root:where(:lang(zh)){--vp-font-family-base: "Punctuation SC", "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"}:root{--vp-shadow-1: 0 1px 2px rgba(0, 0, 0, .04), 0 1px 2px rgba(0, 0, 0, .06);--vp-shadow-2: 0 3px 12px rgba(0, 0, 0, .07), 0 1px 4px rgba(0, 0, 0, .07);--vp-shadow-3: 0 12px 32px rgba(0, 0, 0, .1), 0 2px 6px rgba(0, 0, 0, .08);--vp-shadow-4: 0 14px 44px rgba(0, 0, 0, .12), 0 3px 9px rgba(0, 0, 0, .12);--vp-shadow-5: 0 18px 56px rgba(0, 0, 0, .16), 0 4px 12px rgba(0, 0, 0, .16)}:root{--vp-z-index-footer: 10;--vp-z-index-local-nav: 20;--vp-z-index-nav: 30;--vp-z-index-layout-top: 40;--vp-z-index-backdrop: 50;--vp-z-index-sidebar: 60}@media (min-width: 960px){:root{--vp-z-index-sidebar: 25}}:root{--vp-layout-max-width: 1440px}:root{--vp-header-anchor-symbol: "#"}:root{--vp-code-line-height: 1.7;--vp-code-font-size: .875em;--vp-code-color: var(--vp-c-brand-1);--vp-code-link-color: var(--vp-c-brand-1);--vp-code-link-hover-color: var(--vp-c-brand-2);--vp-code-bg: var(--vp-c-default-soft);--vp-code-block-color: var(--vp-c-text-2);--vp-code-block-bg: var(--vp-c-bg-alt);--vp-code-block-divider-color: var(--vp-c-gutter);--vp-code-lang-color: var(--vp-c-text-3);--vp-code-line-highlight-color: var(--vp-c-default-soft);--vp-code-line-number-color: var(--vp-c-text-3);--vp-code-line-diff-add-color: var(--vp-c-success-soft);--vp-code-line-diff-add-symbol-color: var(--vp-c-success-1);--vp-code-line-diff-remove-color: var(--vp-c-danger-soft);--vp-code-line-diff-remove-symbol-color: var(--vp-c-danger-1);--vp-code-line-warning-color: var(--vp-c-warning-soft);--vp-code-line-error-color: var(--vp-c-danger-soft);--vp-code-copy-code-border-color: var(--vp-c-divider);--vp-code-copy-code-bg: var(--vp-c-bg-soft);--vp-code-copy-code-hover-border-color: var(--vp-c-divider);--vp-code-copy-code-hover-bg: var(--vp-c-bg);--vp-code-copy-code-active-text: var(--vp-c-text-2);--vp-code-copy-copied-text-content: "Copied";--vp-code-tab-divider: var(--vp-code-block-divider-color);--vp-code-tab-text-color: var(--vp-c-text-2);--vp-code-tab-bg: var(--vp-code-block-bg);--vp-code-tab-hover-text-color: var(--vp-c-text-1);--vp-code-tab-active-text-color: var(--vp-c-text-1);--vp-code-tab-active-bar-color: var(--vp-c-brand-1)}:lang(es),:lang(pt){--vp-code-copy-copied-text-content: "Copiado"}:lang(fa){--vp-code-copy-copied-text-content: "کپی شد"}:lang(ko){--vp-code-copy-copied-text-content: "복사됨"}:lang(ru){--vp-code-copy-copied-text-content: "Скопировано"}:lang(zh){--vp-code-copy-copied-text-content: "已复制"}:root{--vp-button-brand-border: transparent;--vp-button-brand-text: var(--vp-c-white);--vp-button-brand-bg: var(--vp-c-brand-3);--vp-button-brand-hover-border: transparent;--vp-button-brand-hover-text: var(--vp-c-white);--vp-button-brand-hover-bg: var(--vp-c-brand-2);--vp-button-brand-active-border: transparent;--vp-button-brand-active-text: var(--vp-c-white);--vp-button-brand-active-bg: var(--vp-c-brand-1);--vp-button-alt-border: transparent;--vp-button-alt-text: var(--vp-c-text-1);--vp-button-alt-bg: var(--vp-c-default-3);--vp-button-alt-hover-border: transparent;--vp-button-alt-hover-text: var(--vp-c-text-1);--vp-button-alt-hover-bg: var(--vp-c-default-2);--vp-button-alt-active-border: transparent;--vp-button-alt-active-text: var(--vp-c-text-1);--vp-button-alt-active-bg: var(--vp-c-default-1);--vp-button-sponsor-border: var(--vp-c-text-2);--vp-button-sponsor-text: var(--vp-c-text-2);--vp-button-sponsor-bg: transparent;--vp-button-sponsor-hover-border: var(--vp-c-sponsor);--vp-button-sponsor-hover-text: var(--vp-c-sponsor);--vp-button-sponsor-hover-bg: transparent;--vp-button-sponsor-active-border: var(--vp-c-sponsor);--vp-button-sponsor-active-text: var(--vp-c-sponsor);--vp-button-sponsor-active-bg: transparent}:root{--vp-custom-block-font-size: 14px;--vp-custom-block-code-font-size: 13px;--vp-custom-block-info-border: transparent;--vp-custom-block-info-text: var(--vp-c-text-1);--vp-custom-block-info-bg: var(--vp-c-default-soft);--vp-custom-block-info-code-bg: var(--vp-c-default-soft);--vp-custom-block-note-border: transparent;--vp-custom-block-note-text: var(--vp-c-text-1);--vp-custom-block-note-bg: var(--vp-c-default-soft);--vp-custom-block-note-code-bg: var(--vp-c-default-soft);--vp-custom-block-tip-border: transparent;--vp-custom-block-tip-text: var(--vp-c-text-1);--vp-custom-block-tip-bg: var(--vp-c-tip-soft);--vp-custom-block-tip-code-bg: var(--vp-c-tip-soft);--vp-custom-block-important-border: transparent;--vp-custom-block-important-text: var(--vp-c-text-1);--vp-custom-block-important-bg: var(--vp-c-important-soft);--vp-custom-block-important-code-bg: var(--vp-c-important-soft);--vp-custom-block-warning-border: transparent;--vp-custom-block-warning-text: var(--vp-c-text-1);--vp-custom-block-warning-bg: var(--vp-c-warning-soft);--vp-custom-block-warning-code-bg: var(--vp-c-warning-soft);--vp-custom-block-danger-border: transparent;--vp-custom-block-danger-text: var(--vp-c-text-1);--vp-custom-block-danger-bg: var(--vp-c-danger-soft);--vp-custom-block-danger-code-bg: var(--vp-c-danger-soft);--vp-custom-block-caution-border: transparent;--vp-custom-block-caution-text: var(--vp-c-text-1);--vp-custom-block-caution-bg: var(--vp-c-caution-soft);--vp-custom-block-caution-code-bg: var(--vp-c-caution-soft);--vp-custom-block-details-border: var(--vp-custom-block-info-border);--vp-custom-block-details-text: var(--vp-custom-block-info-text);--vp-custom-block-details-bg: var(--vp-custom-block-info-bg);--vp-custom-block-details-code-bg: var(--vp-custom-block-info-code-bg)}:root{--vp-input-border-color: var(--vp-c-border);--vp-input-bg-color: var(--vp-c-bg-alt);--vp-input-switch-bg-color: var(--vp-c-default-soft)}:root{--vp-nav-height: 64px;--vp-nav-bg-color: var(--vp-c-bg);--vp-nav-screen-bg-color: var(--vp-c-bg);--vp-nav-logo-height: 24px}.hide-nav{--vp-nav-height: 0px}.hide-nav .VPSidebar{--vp-nav-height: 22px}:root{--vp-local-nav-bg-color: var(--vp-c-bg)}:root{--vp-sidebar-width: 272px;--vp-sidebar-bg-color: var(--vp-c-bg-alt)}:root{--vp-backdrop-bg-color: rgba(0, 0, 0, .6)}:root{--vp-home-hero-name-color: var(--vp-c-brand-1);--vp-home-hero-name-background: transparent;--vp-home-hero-image-background-image: none;--vp-home-hero-image-filter: none}:root{--vp-badge-info-border: transparent;--vp-badge-info-text: var(--vp-c-text-2);--vp-badge-info-bg: var(--vp-c-default-soft);--vp-badge-tip-border: transparent;--vp-badge-tip-text: var(--vp-c-tip-1);--vp-badge-tip-bg: var(--vp-c-tip-soft);--vp-badge-warning-border: transparent;--vp-badge-warning-text: var(--vp-c-warning-1);--vp-badge-warning-bg: var(--vp-c-warning-soft);--vp-badge-danger-border: transparent;--vp-badge-danger-text: var(--vp-c-danger-1);--vp-badge-danger-bg: var(--vp-c-danger-soft)}:root{--vp-carbon-ads-text-color: var(--vp-c-text-1);--vp-carbon-ads-poweredby-color: var(--vp-c-text-2);--vp-carbon-ads-bg-color: var(--vp-c-bg-soft);--vp-carbon-ads-hover-text-color: var(--vp-c-brand-1);--vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1)}:root{--vp-local-search-bg: var(--vp-c-bg);--vp-local-search-result-bg: var(--vp-c-bg);--vp-local-search-result-border: var(--vp-c-divider);--vp-local-search-result-selected-bg: var(--vp-c-bg);--vp-local-search-result-selected-border: var(--vp-c-brand-1);--vp-local-search-highlight-bg: var(--vp-c-brand-1);--vp-local-search-highlight-text: var(--vp-c-neutral-inverse)}@media (prefers-reduced-motion: reduce){*,:before,:after{animation-delay:-1ms!important;animation-duration:1ms!important;animation-iteration-count:1!important;background-attachment:initial!important;scroll-behavior:auto!important;transition-duration:0s!important;transition-delay:0s!important}}*,:before,:after{box-sizing:border-box}html{line-height:1.4;font-size:16px;-webkit-text-size-adjust:100%}html.dark{color-scheme:dark}body{margin:0;width:100%;min-width:320px;min-height:100vh;line-height:24px;font-family:var(--vp-font-family-base);font-size:16px;font-weight:400;color:var(--vp-c-text-1);background-color:var(--vp-c-bg);font-synthesis:style;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}main{display:block}h1,h2,h3,h4,h5,h6{margin:0;line-height:24px;font-size:16px;font-weight:400}p{margin:0}strong,b{font-weight:600}a,area,button,[role=button],input,label,select,summary,textarea{touch-action:manipulation}a{color:inherit;text-decoration:inherit}ol,ul{list-style:none;margin:0;padding:0}blockquote{margin:0}pre,code,kbd,samp{font-family:var(--vp-font-family-mono)}img,svg,video,canvas,audio,iframe,embed,object{display:block}figure{margin:0}img,video{max-width:100%;height:auto}button,input,optgroup,select,textarea{border:0;padding:0;line-height:inherit;color:inherit}button{padding:0;font-family:inherit;background-color:transparent;background-image:none}button:enabled,[role=button]:enabled{cursor:pointer}button:focus,button:focus-visible{outline:1px dotted;outline:4px auto -webkit-focus-ring-color}button:focus:not(:focus-visible){outline:none!important}input:focus,textarea:focus,select:focus{outline:none}table{border-collapse:collapse}input{background-color:transparent}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:var(--vp-c-text-3)}input::-ms-input-placeholder,textarea::-ms-input-placeholder{color:var(--vp-c-text-3)}input::placeholder,textarea::placeholder{color:var(--vp-c-text-3)}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=number]{-moz-appearance:textfield}textarea{resize:vertical}select{-webkit-appearance:none}fieldset{margin:0;padding:0}h1,h2,h3,h4,h5,h6,li,p{overflow-wrap:break-word}vite-error-overlay{z-index:9999}mjx-container{overflow-x:auto}mjx-container>svg{display:inline-block;margin:auto}[class^=vpi-],[class*=" vpi-"],.vp-icon{width:1em;height:1em}[class^=vpi-].bg,[class*=" vpi-"].bg,.vp-icon.bg{background-size:100% 100%;background-color:transparent}[class^=vpi-]:not(.bg),[class*=" vpi-"]:not(.bg),.vp-icon:not(.bg){-webkit-mask:var(--icon) no-repeat;mask:var(--icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit}.vpi-align-left{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M21 6H3M15 12H3M17 18H3'/%3E%3C/svg%3E")}.vpi-arrow-right,.vpi-arrow-down,.vpi-arrow-left,.vpi-arrow-up{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5l7 7-7 7'/%3E%3C/svg%3E")}.vpi-chevron-right,.vpi-chevron-down,.vpi-chevron-left,.vpi-chevron-up{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E")}.vpi-chevron-down,.vpi-arrow-down{transform:rotate(90deg)}.vpi-chevron-left,.vpi-arrow-left{transform:rotate(180deg)}.vpi-chevron-up,.vpi-arrow-up{transform:rotate(-90deg)}.vpi-square-pen{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'/%3E%3Cpath d='M18.375 2.625a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4Z'/%3E%3C/svg%3E")}.vpi-plus{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5v14'/%3E%3C/svg%3E")}.vpi-sun{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41'/%3E%3C/svg%3E")}.vpi-moon{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z'/%3E%3C/svg%3E")}.vpi-more-horizontal{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='1'/%3E%3Ccircle cx='19' cy='12' r='1'/%3E%3Ccircle cx='5' cy='12' r='1'/%3E%3C/svg%3E")}.vpi-languages{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m5 8 6 6M4 14l6-6 2-3M2 5h12M7 2h1M22 22l-5-10-5 10M14 18h6'/%3E%3C/svg%3E")}.vpi-heart{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z'/%3E%3C/svg%3E")}.vpi-search{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E")}.vpi-layout-list{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='7' height='7' x='3' y='3' rx='1'/%3E%3Crect width='7' height='7' x='3' y='14' rx='1'/%3E%3Cpath d='M14 4h7M14 9h7M14 15h7M14 20h7'/%3E%3C/svg%3E")}.vpi-delete{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M20 5H9l-7 7 7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2ZM18 9l-6 6M12 9l6 6'/%3E%3C/svg%3E")}.vpi-corner-down-left{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 10-5 5 5 5'/%3E%3Cpath d='M20 4v7a4 4 0 0 1-4 4H4'/%3E%3C/svg%3E")}:root{--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E");--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E")}.visually-hidden{position:absolute;width:1px;height:1px;white-space:nowrap;clip:rect(0 0 0 0);clip-path:inset(50%);overflow:hidden}.custom-block{border:1px solid transparent;border-radius:8px;padding:16px 16px 8px;line-height:24px;font-size:var(--vp-custom-block-font-size);color:var(--vp-c-text-2)}.custom-block.info{border-color:var(--vp-custom-block-info-border);color:var(--vp-custom-block-info-text);background-color:var(--vp-custom-block-info-bg)}.custom-block.info a,.custom-block.info code{color:var(--vp-c-brand-1)}.custom-block.info a:hover,.custom-block.info a:hover>code{color:var(--vp-c-brand-2)}.custom-block.info code{background-color:var(--vp-custom-block-info-code-bg)}.custom-block.note{border-color:var(--vp-custom-block-note-border);color:var(--vp-custom-block-note-text);background-color:var(--vp-custom-block-note-bg)}.custom-block.note a,.custom-block.note code{color:var(--vp-c-brand-1)}.custom-block.note a:hover,.custom-block.note a:hover>code{color:var(--vp-c-brand-2)}.custom-block.note code{background-color:var(--vp-custom-block-note-code-bg)}.custom-block.tip{border-color:var(--vp-custom-block-tip-border);color:var(--vp-custom-block-tip-text);background-color:var(--vp-custom-block-tip-bg)}.custom-block.tip a,.custom-block.tip code{color:var(--vp-c-tip-1)}.custom-block.tip a:hover,.custom-block.tip a:hover>code{color:var(--vp-c-tip-2)}.custom-block.tip code{background-color:var(--vp-custom-block-tip-code-bg)}.custom-block.important{border-color:var(--vp-custom-block-important-border);color:var(--vp-custom-block-important-text);background-color:var(--vp-custom-block-important-bg)}.custom-block.important a,.custom-block.important code{color:var(--vp-c-important-1)}.custom-block.important a:hover,.custom-block.important a:hover>code{color:var(--vp-c-important-2)}.custom-block.important code{background-color:var(--vp-custom-block-important-code-bg)}.custom-block.warning{border-color:var(--vp-custom-block-warning-border);color:var(--vp-custom-block-warning-text);background-color:var(--vp-custom-block-warning-bg)}.custom-block.warning a,.custom-block.warning code{color:var(--vp-c-warning-1)}.custom-block.warning a:hover,.custom-block.warning a:hover>code{color:var(--vp-c-warning-2)}.custom-block.warning code{background-color:var(--vp-custom-block-warning-code-bg)}.custom-block.danger{border-color:var(--vp-custom-block-danger-border);color:var(--vp-custom-block-danger-text);background-color:var(--vp-custom-block-danger-bg)}.custom-block.danger a,.custom-block.danger code{color:var(--vp-c-danger-1)}.custom-block.danger a:hover,.custom-block.danger a:hover>code{color:var(--vp-c-danger-2)}.custom-block.danger code{background-color:var(--vp-custom-block-danger-code-bg)}.custom-block.caution{border-color:var(--vp-custom-block-caution-border);color:var(--vp-custom-block-caution-text);background-color:var(--vp-custom-block-caution-bg)}.custom-block.caution a,.custom-block.caution code{color:var(--vp-c-caution-1)}.custom-block.caution a:hover,.custom-block.caution a:hover>code{color:var(--vp-c-caution-2)}.custom-block.caution code{background-color:var(--vp-custom-block-caution-code-bg)}.custom-block.details{border-color:var(--vp-custom-block-details-border);color:var(--vp-custom-block-details-text);background-color:var(--vp-custom-block-details-bg)}.custom-block.details a{color:var(--vp-c-brand-1)}.custom-block.details a:hover,.custom-block.details a:hover>code{color:var(--vp-c-brand-2)}.custom-block.details code{background-color:var(--vp-custom-block-details-code-bg)}.custom-block-title{font-weight:600}.custom-block p+p{margin:8px 0}.custom-block.details summary{margin:0 0 8px;font-weight:700;cursor:pointer;-webkit-user-select:none;user-select:none}.custom-block.details summary+p{margin:8px 0}.custom-block a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px;transition:opacity .25s}.custom-block a:hover{opacity:.75}.custom-block code{font-size:var(--vp-custom-block-code-font-size)}.custom-block.custom-block th,.custom-block.custom-block blockquote>p{font-size:var(--vp-custom-block-font-size);color:inherit}.dark .vp-code span{color:var(--shiki-dark, inherit)}html:not(.dark) .vp-code span{color:var(--shiki-light, inherit)}.vp-code-group{margin-top:16px}.vp-code-group .tabs{position:relative;display:flex;margin-right:-24px;margin-left:-24px;padding:0 12px;background-color:var(--vp-code-tab-bg);overflow-x:auto;overflow-y:hidden;box-shadow:inset 0 -1px var(--vp-code-tab-divider)}@media (min-width: 640px){.vp-code-group .tabs{margin-right:0;margin-left:0;border-radius:8px 8px 0 0}}.vp-code-group .tabs input{position:fixed;opacity:0;pointer-events:none}.vp-code-group .tabs label{position:relative;display:inline-block;border-bottom:1px solid transparent;padding:0 12px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-code-tab-text-color);white-space:nowrap;cursor:pointer;transition:color .25s}.vp-code-group .tabs label:after{position:absolute;right:8px;bottom:-1px;left:8px;z-index:1;height:2px;border-radius:2px;content:"";background-color:transparent;transition:background-color .25s}.vp-code-group label:hover{color:var(--vp-code-tab-hover-text-color)}.vp-code-group input:checked+label{color:var(--vp-code-tab-active-text-color)}.vp-code-group input:checked+label:after{background-color:var(--vp-code-tab-active-bar-color)}.vp-code-group div[class*=language-],.vp-block{display:none;margin-top:0!important;border-top-left-radius:0!important;border-top-right-radius:0!important}.vp-code-group div[class*=language-].active,.vp-block.active{display:block}.vp-block{padding:20px 24px}.vp-doc h1,.vp-doc h2,.vp-doc h3,.vp-doc h4,.vp-doc h5,.vp-doc h6{position:relative;font-weight:600;outline:none}.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:28px}.vp-doc h2{margin:48px 0 16px;border-top:1px solid var(--vp-c-divider);padding-top:24px;letter-spacing:-.02em;line-height:32px;font-size:24px}.vp-doc h3{margin:32px 0 0;letter-spacing:-.01em;line-height:28px;font-size:20px}.vp-doc h4{margin:24px 0 0;letter-spacing:-.01em;line-height:24px;font-size:18px}.vp-doc .header-anchor{position:absolute;top:0;left:0;margin-left:-.87em;font-weight:500;-webkit-user-select:none;user-select:none;opacity:0;text-decoration:none;transition:color .25s,opacity .25s}.vp-doc .header-anchor:before{content:var(--vp-header-anchor-symbol)}.vp-doc h1:hover .header-anchor,.vp-doc h1 .header-anchor:focus,.vp-doc h2:hover .header-anchor,.vp-doc h2 .header-anchor:focus,.vp-doc h3:hover .header-anchor,.vp-doc h3 .header-anchor:focus,.vp-doc h4:hover .header-anchor,.vp-doc h4 .header-anchor:focus,.vp-doc h5:hover .header-anchor,.vp-doc h5 .header-anchor:focus,.vp-doc h6:hover .header-anchor,.vp-doc h6 .header-anchor:focus{opacity:1}@media (min-width: 768px){.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:32px}}.vp-doc h2 .header-anchor{top:24px}.vp-doc p,.vp-doc summary{margin:16px 0}.vp-doc p{line-height:28px}.vp-doc blockquote{margin:16px 0;border-left:2px solid var(--vp-c-divider);padding-left:16px;transition:border-color .5s;color:var(--vp-c-text-2)}.vp-doc blockquote>p{margin:0;font-size:16px;transition:color .5s}.vp-doc a{font-weight:500;color:var(--vp-c-brand-1);text-decoration:underline;text-underline-offset:2px;transition:color .25s,opacity .25s}.vp-doc a:hover{color:var(--vp-c-brand-2)}.vp-doc strong{font-weight:600}.vp-doc ul,.vp-doc ol{padding-left:1.25rem;margin:16px 0}.vp-doc ul{list-style:disc}.vp-doc ol{list-style:decimal}.vp-doc li+li{margin-top:8px}.vp-doc li>ol,.vp-doc li>ul{margin:8px 0 0}.vp-doc table{display:block;border-collapse:collapse;margin:20px 0;overflow-x:auto}.vp-doc tr{background-color:var(--vp-c-bg);border-top:1px solid var(--vp-c-divider);transition:background-color .5s}.vp-doc tr:nth-child(2n){background-color:var(--vp-c-bg-soft)}.vp-doc th,.vp-doc td{border:1px solid var(--vp-c-divider);padding:8px 16px}.vp-doc th{text-align:left;font-size:14px;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-doc td{font-size:14px}.vp-doc hr{margin:16px 0;border:none;border-top:1px solid var(--vp-c-divider)}.vp-doc .custom-block{margin:16px 0}.vp-doc .custom-block p{margin:8px 0;line-height:24px}.vp-doc .custom-block p:first-child{margin:0}.vp-doc .custom-block div[class*=language-]{margin:8px 0;border-radius:8px}.vp-doc .custom-block div[class*=language-] code{font-weight:400;background-color:transparent}.vp-doc .custom-block .vp-code-group .tabs{margin:0;border-radius:8px 8px 0 0}.vp-doc :not(pre,h1,h2,h3,h4,h5,h6)>code{font-size:var(--vp-code-font-size);color:var(--vp-code-color)}.vp-doc :not(pre)>code{border-radius:4px;padding:3px 6px;background-color:var(--vp-code-bg);transition:color .25s,background-color .5s}.vp-doc a>code{color:var(--vp-code-link-color)}.vp-doc a:hover>code{color:var(--vp-code-link-hover-color)}.vp-doc h1>code,.vp-doc h2>code,.vp-doc h3>code,.vp-doc h4>code{font-size:.9em}.vp-doc div[class*=language-],.vp-block{position:relative;margin:16px -24px;background-color:var(--vp-code-block-bg);overflow-x:auto;transition:background-color .5s}@media (min-width: 640px){.vp-doc div[class*=language-],.vp-block{border-radius:8px;margin:16px 0}}@media (max-width: 639px){.vp-doc li div[class*=language-]{border-radius:8px 0 0 8px}}.vp-doc div[class*=language-]+div[class*=language-],.vp-doc div[class$=-api]+div[class*=language-],.vp-doc div[class*=language-]+div[class$=-api]>div[class*=language-]{margin-top:-8px}.vp-doc [class*=language-] pre,.vp-doc [class*=language-] code{direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}.vp-doc [class*=language-] pre{position:relative;z-index:1;margin:0;padding:20px 0;background:transparent;overflow-x:auto}.vp-doc [class*=language-] code{display:block;padding:0 24px;width:fit-content;min-width:100%;line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-block-color);transition:color .5s}.vp-doc [class*=language-] code .highlighted{background-color:var(--vp-code-line-highlight-color);transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .highlighted.error{background-color:var(--vp-code-line-error-color)}.vp-doc [class*=language-] code .highlighted.warning{background-color:var(--vp-code-line-warning-color)}.vp-doc [class*=language-] code .diff{transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .diff:before{position:absolute;left:10px}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){filter:blur(.095rem);opacity:.4;transition:filter .35s,opacity .35s}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){opacity:.7;transition:filter .35s,opacity .35s}.vp-doc [class*=language-]:hover .has-focused-lines .line:not(.has-focus){filter:blur(0);opacity:1}.vp-doc [class*=language-] code .diff.remove{background-color:var(--vp-code-line-diff-remove-color);opacity:.7}.vp-doc [class*=language-] code .diff.remove:before{content:"-";color:var(--vp-code-line-diff-remove-symbol-color)}.vp-doc [class*=language-] code .diff.add{background-color:var(--vp-code-line-diff-add-color)}.vp-doc [class*=language-] code .diff.add:before{content:"+";color:var(--vp-code-line-diff-add-symbol-color)}.vp-doc div[class*=language-].line-numbers-mode{padding-left:32px}.vp-doc .line-numbers-wrapper{position:absolute;top:0;bottom:0;left:0;z-index:3;border-right:1px solid var(--vp-code-block-divider-color);padding-top:20px;width:32px;text-align:center;font-family:var(--vp-font-family-mono);line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-line-number-color);transition:border-color .5s,color .5s}.vp-doc [class*=language-]>button.copy{direction:ltr;position:absolute;top:12px;right:12px;z-index:3;border:1px solid var(--vp-code-copy-code-border-color);border-radius:4px;width:40px;height:40px;background-color:var(--vp-code-copy-code-bg);opacity:0;cursor:pointer;background-image:var(--vp-icon-copy);background-position:50%;background-size:20px;background-repeat:no-repeat;transition:border-color .25s,background-color .25s,opacity .25s}.vp-doc [class*=language-]:hover>button.copy,.vp-doc [class*=language-]>button.copy:focus{opacity:1}.vp-doc [class*=language-]>button.copy:hover,.vp-doc [class*=language-]>button.copy.copied{border-color:var(--vp-code-copy-code-hover-border-color);background-color:var(--vp-code-copy-code-hover-bg)}.vp-doc [class*=language-]>button.copy.copied,.vp-doc [class*=language-]>button.copy:hover.copied{border-radius:0 4px 4px 0;background-color:var(--vp-code-copy-code-hover-bg);background-image:var(--vp-icon-copied)}.vp-doc [class*=language-]>button.copy.copied:before,.vp-doc [class*=language-]>button.copy:hover.copied:before{position:relative;top:-1px;transform:translate(calc(-100% - 1px));display:flex;justify-content:center;align-items:center;border:1px solid var(--vp-code-copy-code-hover-border-color);border-right:0;border-radius:4px 0 0 4px;padding:0 10px;width:fit-content;height:40px;text-align:center;font-size:12px;font-weight:500;color:var(--vp-code-copy-code-active-text);background-color:var(--vp-code-copy-code-hover-bg);white-space:nowrap;content:var(--vp-code-copy-copied-text-content)}.vp-doc [class*=language-]>span.lang{position:absolute;top:2px;right:8px;z-index:2;font-size:12px;font-weight:500;-webkit-user-select:none;user-select:none;color:var(--vp-code-lang-color);transition:color .4s,opacity .4s}.vp-doc [class*=language-]:hover>button.copy+span.lang,.vp-doc [class*=language-]>button.copy:focus+span.lang{opacity:0}.vp-doc .VPTeamMembers{margin-top:24px}.vp-doc .VPTeamMembers.small.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}.vp-doc .VPTeamMembers.small.count-2 .container,.vp-doc .VPTeamMembers.small.count-3 .container{max-width:100%!important}.vp-doc .VPTeamMembers.medium.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}:is(.vp-external-link-icon,.vp-doc a[href*="://"],.vp-doc a[target=_blank]):not(:is(.no-icon,svg a,:has(img,svg))):after{display:inline-block;margin-top:-1px;margin-left:4px;width:11px;height:11px;background:currentColor;color:var(--vp-c-text-3);flex-shrink:0;--icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E");-webkit-mask-image:var(--icon);mask-image:var(--icon)}.vp-external-link-icon:after{content:""}.external-link-icon-enabled :is(.vp-doc a[href*="://"],.vp-doc a[target=_blank]):not(:is(.no-icon,svg a,:has(img,svg))):after{content:"";color:currentColor}.vp-sponsor{border-radius:16px;overflow:hidden}.vp-sponsor.aside{border-radius:12px}.vp-sponsor-section+.vp-sponsor-section{margin-top:4px}.vp-sponsor-tier{margin:0 0 4px!important;text-align:center;letter-spacing:1px!important;line-height:24px;width:100%;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-sponsor.normal .vp-sponsor-tier{padding:13px 0 11px;font-size:14px}.vp-sponsor.aside .vp-sponsor-tier{padding:9px 0 7px;font-size:12px}.vp-sponsor-grid+.vp-sponsor-tier{margin-top:4px}.vp-sponsor-grid{display:flex;flex-wrap:wrap;gap:4px}.vp-sponsor-grid.xmini .vp-sponsor-grid-link{height:64px}.vp-sponsor-grid.xmini .vp-sponsor-grid-image{max-width:64px;max-height:22px}.vp-sponsor-grid.mini .vp-sponsor-grid-link{height:72px}.vp-sponsor-grid.mini .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.small .vp-sponsor-grid-link{height:96px}.vp-sponsor-grid.small .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.medium .vp-sponsor-grid-link{height:112px}.vp-sponsor-grid.medium .vp-sponsor-grid-image{max-width:120px;max-height:36px}.vp-sponsor-grid.big .vp-sponsor-grid-link{height:184px}.vp-sponsor-grid.big .vp-sponsor-grid-image{max-width:192px;max-height:56px}.vp-sponsor-grid[data-vp-grid="2"] .vp-sponsor-grid-item{width:calc((100% - 4px)/2)}.vp-sponsor-grid[data-vp-grid="3"] .vp-sponsor-grid-item{width:calc((100% - 4px * 2) / 3)}.vp-sponsor-grid[data-vp-grid="4"] .vp-sponsor-grid-item{width:calc((100% - 12px)/4)}.vp-sponsor-grid[data-vp-grid="5"] .vp-sponsor-grid-item{width:calc((100% - 16px)/5)}.vp-sponsor-grid[data-vp-grid="6"] .vp-sponsor-grid-item{width:calc((100% - 4px * 5) / 6)}.vp-sponsor-grid-item{flex-shrink:0;width:100%;background-color:var(--vp-c-bg-soft);transition:background-color .25s}.vp-sponsor-grid-item:hover{background-color:var(--vp-c-default-soft)}.vp-sponsor-grid-item:hover .vp-sponsor-grid-image{filter:grayscale(0) invert(0)}.vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.dark .vp-sponsor-grid-item:hover{background-color:var(--vp-c-white)}.dark .vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.vp-sponsor-grid-link{display:flex}.vp-sponsor-grid-box{display:flex;justify-content:center;align-items:center;width:100%}.vp-sponsor-grid-image{max-width:100%;filter:grayscale(1);transition:filter .25s}.dark .vp-sponsor-grid-image{filter:grayscale(1) invert(1)}.VPBadge{display:inline-block;margin-left:2px;border:1px solid transparent;border-radius:12px;padding:0 10px;line-height:22px;font-size:12px;font-weight:500;transform:translateY(-2px)}.VPBadge.small{padding:0 6px;line-height:18px;font-size:10px;transform:translateY(-8px)}.VPDocFooter .VPBadge{display:none}.vp-doc h1>.VPBadge{margin-top:4px;vertical-align:top}.vp-doc h2>.VPBadge{margin-top:3px;padding:0 8px;vertical-align:top}.vp-doc h3>.VPBadge{vertical-align:middle}.vp-doc h4>.VPBadge,.vp-doc h5>.VPBadge,.vp-doc h6>.VPBadge{vertical-align:middle;line-height:18px}.VPBadge.info{border-color:var(--vp-badge-info-border);color:var(--vp-badge-info-text);background-color:var(--vp-badge-info-bg)}.VPBadge.tip{border-color:var(--vp-badge-tip-border);color:var(--vp-badge-tip-text);background-color:var(--vp-badge-tip-bg)}.VPBadge.warning{border-color:var(--vp-badge-warning-border);color:var(--vp-badge-warning-text);background-color:var(--vp-badge-warning-bg)}.VPBadge.danger{border-color:var(--vp-badge-danger-border);color:var(--vp-badge-danger-text);background-color:var(--vp-badge-danger-bg)}.VPBackdrop[data-v-b06cdb19]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:var(--vp-z-index-backdrop);background:var(--vp-backdrop-bg-color);transition:opacity .5s}.VPBackdrop.fade-enter-from[data-v-b06cdb19],.VPBackdrop.fade-leave-to[data-v-b06cdb19]{opacity:0}.VPBackdrop.fade-leave-active[data-v-b06cdb19]{transition-duration:.25s}@media (min-width: 1280px){.VPBackdrop[data-v-b06cdb19]{display:none}}.NotFound[data-v-951cab6c]{padding:64px 24px 96px;text-align:center}@media (min-width: 768px){.NotFound[data-v-951cab6c]{padding:96px 32px 168px}}.code[data-v-951cab6c]{line-height:64px;font-size:64px;font-weight:600}.title[data-v-951cab6c]{padding-top:12px;letter-spacing:2px;line-height:20px;font-size:20px;font-weight:700}.divider[data-v-951cab6c]{margin:24px auto 18px;width:64px;height:1px;background-color:var(--vp-c-divider)}.quote[data-v-951cab6c]{margin:0 auto;max-width:256px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.action[data-v-951cab6c]{padding-top:20px}.link[data-v-951cab6c]{display:inline-block;border:1px solid var(--vp-c-brand-1);border-radius:16px;padding:3px 16px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:border-color .25s,color .25s}.link[data-v-951cab6c]:hover{border-color:var(--vp-c-brand-2);color:var(--vp-c-brand-2)}.root[data-v-3f927ebe]{position:relative;z-index:1}.nested[data-v-3f927ebe]{padding-right:16px;padding-left:16px}.outline-link[data-v-3f927ebe]{display:block;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .5s}.outline-link[data-v-3f927ebe]:hover,.outline-link.active[data-v-3f927ebe]{color:var(--vp-c-text-1);transition:color .25s}.outline-link.nested[data-v-3f927ebe]{padding-left:13px}.VPDocAsideOutline[data-v-b38bf2ff]{display:none}.VPDocAsideOutline.has-outline[data-v-b38bf2ff]{display:block}.content[data-v-b38bf2ff]{position:relative;border-left:1px solid var(--vp-c-divider);padding-left:16px;font-size:13px;font-weight:500}.outline-marker[data-v-b38bf2ff]{position:absolute;top:32px;left:-1px;z-index:0;opacity:0;width:2px;border-radius:2px;height:18px;background-color:var(--vp-c-brand-1);transition:top .25s cubic-bezier(0,1,.5,1),background-color .5s,opacity .25s}.outline-title[data-v-b38bf2ff]{line-height:32px;font-size:14px;font-weight:600}.VPDocAside[data-v-6d7b3c46]{display:flex;flex-direction:column;flex-grow:1}.spacer[data-v-6d7b3c46]{flex-grow:1}.VPDocAside[data-v-6d7b3c46] .spacer+.VPDocAsideSponsors,.VPDocAside[data-v-6d7b3c46] .spacer+.VPDocAsideCarbonAds{margin-top:24px}.VPDocAside[data-v-6d7b3c46] .VPDocAsideSponsors+.VPDocAsideCarbonAds{margin-top:16px}.VPLastUpdated[data-v-475f71b8]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 640px){.VPLastUpdated[data-v-475f71b8]{line-height:32px;font-size:14px;font-weight:500}}.VPDocFooter[data-v-4f9813fa]{margin-top:64px}.edit-info[data-v-4f9813fa]{padding-bottom:18px}@media (min-width: 640px){.edit-info[data-v-4f9813fa]{display:flex;justify-content:space-between;align-items:center;padding-bottom:14px}}.edit-link-button[data-v-4f9813fa]{display:flex;align-items:center;border:0;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.edit-link-button[data-v-4f9813fa]:hover{color:var(--vp-c-brand-2)}.edit-link-icon[data-v-4f9813fa]{margin-right:8px}.prev-next[data-v-4f9813fa]{border-top:1px solid var(--vp-c-divider);padding-top:24px;display:grid;grid-row-gap:8px}@media (min-width: 640px){.prev-next[data-v-4f9813fa]{grid-template-columns:repeat(2,1fr);grid-column-gap:16px}}.pager-link[data-v-4f9813fa]{display:block;border:1px solid var(--vp-c-divider);border-radius:8px;padding:11px 16px 13px;width:100%;height:100%;transition:border-color .25s}.pager-link[data-v-4f9813fa]:hover{border-color:var(--vp-c-brand-1)}.pager-link.next[data-v-4f9813fa]{margin-left:auto;text-align:right}.desc[data-v-4f9813fa]{display:block;line-height:20px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.title[data-v-4f9813fa]{display:block;line-height:20px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.VPDoc[data-v-83890dd9]{padding:32px 24px 96px;width:100%}@media (min-width: 768px){.VPDoc[data-v-83890dd9]{padding:48px 32px 128px}}@media (min-width: 960px){.VPDoc[data-v-83890dd9]{padding:48px 32px 0}.VPDoc:not(.has-sidebar) .container[data-v-83890dd9]{display:flex;justify-content:center;max-width:992px}.VPDoc:not(.has-sidebar) .content[data-v-83890dd9]{max-width:752px}}@media (min-width: 1280px){.VPDoc .container[data-v-83890dd9]{display:flex;justify-content:center}.VPDoc .aside[data-v-83890dd9]{display:block}}@media (min-width: 1440px){.VPDoc:not(.has-sidebar) .content[data-v-83890dd9]{max-width:784px}.VPDoc:not(.has-sidebar) .container[data-v-83890dd9]{max-width:1104px}}.container[data-v-83890dd9]{margin:0 auto;width:100%}.aside[data-v-83890dd9]{position:relative;display:none;order:2;flex-grow:1;padding-left:32px;width:100%;max-width:256px}.left-aside[data-v-83890dd9]{order:1;padding-left:unset;padding-right:32px}.aside-container[data-v-83890dd9]{position:fixed;top:0;padding-top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 48px);width:224px;height:100vh;overflow-x:hidden;overflow-y:auto;scrollbar-width:none}.aside-container[data-v-83890dd9]::-webkit-scrollbar{display:none}.aside-curtain[data-v-83890dd9]{position:fixed;bottom:0;z-index:10;width:224px;height:32px;background:linear-gradient(transparent,var(--vp-c-bg) 70%)}.aside-content[data-v-83890dd9]{display:flex;flex-direction:column;min-height:calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px));padding-bottom:32px}.content[data-v-83890dd9]{position:relative;margin:0 auto;width:100%}@media (min-width: 960px){.content[data-v-83890dd9]{padding:0 32px 128px}}@media (min-width: 1280px){.content[data-v-83890dd9]{order:1;margin:0;min-width:640px}}.content-container[data-v-83890dd9]{margin:0 auto}.VPDoc.has-aside .content-container[data-v-83890dd9]{max-width:688px}.VPButton[data-v-906d7fb4]{display:inline-block;border:1px solid transparent;text-align:center;font-weight:600;white-space:nowrap;transition:color .25s,border-color .25s,background-color .25s}.VPButton[data-v-906d7fb4]:active{transition:color .1s,border-color .1s,background-color .1s}.VPButton.medium[data-v-906d7fb4]{border-radius:20px;padding:0 20px;line-height:38px;font-size:14px}.VPButton.big[data-v-906d7fb4]{border-radius:24px;padding:0 24px;line-height:46px;font-size:16px}.VPButton.brand[data-v-906d7fb4]{border-color:var(--vp-button-brand-border);color:var(--vp-button-brand-text);background-color:var(--vp-button-brand-bg)}.VPButton.brand[data-v-906d7fb4]:hover{border-color:var(--vp-button-brand-hover-border);color:var(--vp-button-brand-hover-text);background-color:var(--vp-button-brand-hover-bg)}.VPButton.brand[data-v-906d7fb4]:active{border-color:var(--vp-button-brand-active-border);color:var(--vp-button-brand-active-text);background-color:var(--vp-button-brand-active-bg)}.VPButton.alt[data-v-906d7fb4]{border-color:var(--vp-button-alt-border);color:var(--vp-button-alt-text);background-color:var(--vp-button-alt-bg)}.VPButton.alt[data-v-906d7fb4]:hover{border-color:var(--vp-button-alt-hover-border);color:var(--vp-button-alt-hover-text);background-color:var(--vp-button-alt-hover-bg)}.VPButton.alt[data-v-906d7fb4]:active{border-color:var(--vp-button-alt-active-border);color:var(--vp-button-alt-active-text);background-color:var(--vp-button-alt-active-bg)}.VPButton.sponsor[data-v-906d7fb4]{border-color:var(--vp-button-sponsor-border);color:var(--vp-button-sponsor-text);background-color:var(--vp-button-sponsor-bg)}.VPButton.sponsor[data-v-906d7fb4]:hover{border-color:var(--vp-button-sponsor-hover-border);color:var(--vp-button-sponsor-hover-text);background-color:var(--vp-button-sponsor-hover-bg)}.VPButton.sponsor[data-v-906d7fb4]:active{border-color:var(--vp-button-sponsor-active-border);color:var(--vp-button-sponsor-active-text);background-color:var(--vp-button-sponsor-active-bg)}html:not(.dark) .VPImage.dark[data-v-35a7d0b8]{display:none}.dark .VPImage.light[data-v-35a7d0b8]{display:none}.VPHero[data-v-3d256e5e]{margin-top:calc((var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1);padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px) 24px 48px}@media (min-width: 640px){.VPHero[data-v-3d256e5e]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px}}@media (min-width: 960px){.VPHero[data-v-3d256e5e]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px}}.container[data-v-3d256e5e]{display:flex;flex-direction:column;margin:0 auto;max-width:1152px}@media (min-width: 960px){.container[data-v-3d256e5e]{flex-direction:row}}.main[data-v-3d256e5e]{position:relative;z-index:10;order:2;flex-grow:1;flex-shrink:0}.VPHero.has-image .container[data-v-3d256e5e]{text-align:center}@media (min-width: 960px){.VPHero.has-image .container[data-v-3d256e5e]{text-align:left}}@media (min-width: 960px){.main[data-v-3d256e5e]{order:1;width:calc((100% / 3) * 2)}.VPHero.has-image .main[data-v-3d256e5e]{max-width:592px}}.heading[data-v-3d256e5e]{display:flex;flex-direction:column}.name[data-v-3d256e5e],.text[data-v-3d256e5e]{width:fit-content;max-width:392px;letter-spacing:-.4px;line-height:40px;font-size:32px;font-weight:700;white-space:pre-wrap}.VPHero.has-image .name[data-v-3d256e5e],.VPHero.has-image .text[data-v-3d256e5e]{margin:0 auto}.name[data-v-3d256e5e]{color:var(--vp-home-hero-name-color)}.clip[data-v-3d256e5e]{background:var(--vp-home-hero-name-background);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:var(--vp-home-hero-name-color)}@media (min-width: 640px){.name[data-v-3d256e5e],.text[data-v-3d256e5e]{max-width:576px;line-height:56px;font-size:48px}}@media (min-width: 960px){.name[data-v-3d256e5e],.text[data-v-3d256e5e]{line-height:64px;font-size:56px}.VPHero.has-image .name[data-v-3d256e5e],.VPHero.has-image .text[data-v-3d256e5e]{margin:0}}.tagline[data-v-3d256e5e]{padding-top:8px;max-width:392px;line-height:28px;font-size:18px;font-weight:500;white-space:pre-wrap;color:var(--vp-c-text-2)}.VPHero.has-image .tagline[data-v-3d256e5e]{margin:0 auto}@media (min-width: 640px){.tagline[data-v-3d256e5e]{padding-top:12px;max-width:576px;line-height:32px;font-size:20px}}@media (min-width: 960px){.tagline[data-v-3d256e5e]{line-height:36px;font-size:24px}.VPHero.has-image .tagline[data-v-3d256e5e]{margin:0}}.actions[data-v-3d256e5e]{display:flex;flex-wrap:wrap;margin:-6px;padding-top:24px}.VPHero.has-image .actions[data-v-3d256e5e]{justify-content:center}@media (min-width: 640px){.actions[data-v-3d256e5e]{padding-top:32px}}@media (min-width: 960px){.VPHero.has-image .actions[data-v-3d256e5e]{justify-content:flex-start}}.action[data-v-3d256e5e]{flex-shrink:0;padding:6px}.image[data-v-3d256e5e]{order:1;margin:-76px -24px -48px}@media (min-width: 640px){.image[data-v-3d256e5e]{margin:-108px -24px -48px}}@media (min-width: 960px){.image[data-v-3d256e5e]{flex-grow:1;order:2;margin:0;min-height:100%}}.image-container[data-v-3d256e5e]{position:relative;margin:0 auto;width:320px;height:320px}@media (min-width: 640px){.image-container[data-v-3d256e5e]{width:392px;height:392px}}@media (min-width: 960px){.image-container[data-v-3d256e5e]{display:flex;justify-content:center;align-items:center;width:100%;height:100%;transform:translate(-32px,-32px)}}.image-bg[data-v-3d256e5e]{position:absolute;top:50%;left:50%;border-radius:50%;width:192px;height:192px;background-image:var(--vp-home-hero-image-background-image);filter:var(--vp-home-hero-image-filter);transform:translate(-50%,-50%)}@media (min-width: 640px){.image-bg[data-v-3d256e5e]{width:256px;height:256px}}@media (min-width: 960px){.image-bg[data-v-3d256e5e]{width:320px;height:320px}}[data-v-3d256e5e] .image-src{position:absolute;top:50%;left:50%;max-width:192px;max-height:192px;transform:translate(-50%,-50%)}@media (min-width: 640px){[data-v-3d256e5e] .image-src{max-width:256px;max-height:256px}}@media (min-width: 960px){[data-v-3d256e5e] .image-src{max-width:320px;max-height:320px}}.VPFeature[data-v-f5e9645b]{display:block;border:1px solid var(--vp-c-bg-soft);border-radius:12px;height:100%;background-color:var(--vp-c-bg-soft);transition:border-color .25s,background-color .25s}.VPFeature.link[data-v-f5e9645b]:hover{border-color:var(--vp-c-brand-1)}.box[data-v-f5e9645b]{display:flex;flex-direction:column;padding:24px;height:100%}.box[data-v-f5e9645b]>.VPImage{margin-bottom:20px}.icon[data-v-f5e9645b]{display:flex;justify-content:center;align-items:center;margin-bottom:20px;border-radius:6px;background-color:var(--vp-c-default-soft);width:48px;height:48px;font-size:24px;transition:background-color .25s}.title[data-v-f5e9645b]{line-height:24px;font-size:16px;font-weight:600}.details[data-v-f5e9645b]{flex-grow:1;padding-top:8px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.link-text[data-v-f5e9645b]{padding-top:8px}.link-text-value[data-v-f5e9645b]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.link-text-icon[data-v-f5e9645b]{margin-left:6px}.VPFeatures[data-v-d0a190d7]{position:relative;padding:0 24px}@media (min-width: 640px){.VPFeatures[data-v-d0a190d7]{padding:0 48px}}@media (min-width: 960px){.VPFeatures[data-v-d0a190d7]{padding:0 64px}}.container[data-v-d0a190d7]{margin:0 auto;max-width:1152px}.items[data-v-d0a190d7]{display:flex;flex-wrap:wrap;margin:-8px}.item[data-v-d0a190d7]{padding:8px;width:100%}@media (min-width: 640px){.item.grid-2[data-v-d0a190d7],.item.grid-4[data-v-d0a190d7],.item.grid-6[data-v-d0a190d7]{width:50%}}@media (min-width: 768px){.item.grid-2[data-v-d0a190d7],.item.grid-4[data-v-d0a190d7]{width:50%}.item.grid-3[data-v-d0a190d7],.item.grid-6[data-v-d0a190d7]{width:calc(100% / 3)}}@media (min-width: 960px){.item.grid-4[data-v-d0a190d7]{width:25%}}.container[data-v-7a48a447]{margin:auto;width:100%;max-width:1280px;padding:0 24px}@media (min-width: 640px){.container[data-v-7a48a447]{padding:0 48px}}@media (min-width: 960px){.container[data-v-7a48a447]{width:100%;padding:0 64px}}.vp-doc[data-v-7a48a447] .VPHomeSponsors,.vp-doc[data-v-7a48a447] .VPTeamPage{margin-left:var(--vp-offset, calc(50% - 50vw) );margin-right:var(--vp-offset, calc(50% - 50vw) )}.vp-doc[data-v-7a48a447] .VPHomeSponsors h2{border-top:none;letter-spacing:normal}.vp-doc[data-v-7a48a447] .VPHomeSponsors a,.vp-doc[data-v-7a48a447] .VPTeamPage a{text-decoration:none}.VPHome[data-v-e40e30de]{margin-bottom:96px}@media (min-width: 768px){.VPHome[data-v-e40e30de]{margin-bottom:128px}}.VPContent[data-v-91765379]{flex-grow:1;flex-shrink:0;margin:var(--vp-layout-top-height, 0px) auto 0;width:100%}.VPContent.is-home[data-v-91765379]{width:100%;max-width:100%}.VPContent.has-sidebar[data-v-91765379]{margin:0}@media (min-width: 960px){.VPContent[data-v-91765379]{padding-top:var(--vp-nav-height)}.VPContent.has-sidebar[data-v-91765379]{margin:var(--vp-layout-top-height, 0px) 0 0;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPContent.has-sidebar[data-v-91765379]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.VPFooter[data-v-c970a860]{position:relative;z-index:var(--vp-z-index-footer);border-top:1px solid var(--vp-c-gutter);padding:32px 24px;background-color:var(--vp-c-bg)}.VPFooter.has-sidebar[data-v-c970a860]{display:none}.VPFooter[data-v-c970a860] a{text-decoration-line:underline;text-underline-offset:2px;transition:color .25s}.VPFooter[data-v-c970a860] a:hover{color:var(--vp-c-text-1)}@media (min-width: 768px){.VPFooter[data-v-c970a860]{padding:32px}}.container[data-v-c970a860]{margin:0 auto;max-width:var(--vp-layout-max-width);text-align:center}.message[data-v-c970a860],.copyright[data-v-c970a860]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.VPLocalNavOutlineDropdown[data-v-168ddf5d]{padding:12px 20px 11px}@media (min-width: 960px){.VPLocalNavOutlineDropdown[data-v-168ddf5d]{padding:12px 36px 11px}}.VPLocalNavOutlineDropdown button[data-v-168ddf5d]{display:block;font-size:12px;font-weight:500;line-height:24px;color:var(--vp-c-text-2);transition:color .5s;position:relative}.VPLocalNavOutlineDropdown button[data-v-168ddf5d]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPLocalNavOutlineDropdown button.open[data-v-168ddf5d]{color:var(--vp-c-text-1)}.icon[data-v-168ddf5d]{display:inline-block;vertical-align:middle;margin-left:2px;font-size:14px;transform:rotate(0);transition:transform .25s}@media (min-width: 960px){.VPLocalNavOutlineDropdown button[data-v-168ddf5d]{font-size:14px}.icon[data-v-168ddf5d]{font-size:16px}}.open>.icon[data-v-168ddf5d]{transform:rotate(90deg)}.items[data-v-168ddf5d]{position:absolute;top:40px;right:16px;left:16px;display:grid;gap:1px;border:1px solid var(--vp-c-border);border-radius:8px;background-color:var(--vp-c-gutter);max-height:calc(var(--vp-vh, 100vh) - 86px);overflow:hidden auto;box-shadow:var(--vp-shadow-3)}@media (min-width: 960px){.items[data-v-168ddf5d]{right:auto;left:calc(var(--vp-sidebar-width) + 32px);width:320px}}.header[data-v-168ddf5d]{background-color:var(--vp-c-bg-soft)}.top-link[data-v-168ddf5d]{display:block;padding:0 16px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.outline[data-v-168ddf5d]{padding:8px 0;background-color:var(--vp-c-bg-soft)}.flyout-enter-active[data-v-168ddf5d]{transition:all .2s ease-out}.flyout-leave-active[data-v-168ddf5d]{transition:all .15s ease-in}.flyout-enter-from[data-v-168ddf5d],.flyout-leave-to[data-v-168ddf5d]{opacity:0;transform:translateY(-16px)}.VPLocalNav[data-v-070ab83d]{position:sticky;top:0;left:0;z-index:var(--vp-z-index-local-nav);border-bottom:1px solid var(--vp-c-gutter);padding-top:var(--vp-layout-top-height, 0px);width:100%;background-color:var(--vp-local-nav-bg-color)}.VPLocalNav.fixed[data-v-070ab83d]{position:fixed}@media (min-width: 960px){.VPLocalNav[data-v-070ab83d]{top:var(--vp-nav-height)}.VPLocalNav.has-sidebar[data-v-070ab83d]{padding-left:var(--vp-sidebar-width)}.VPLocalNav.empty[data-v-070ab83d]{display:none}}@media (min-width: 1280px){.VPLocalNav[data-v-070ab83d]{display:none}}@media (min-width: 1440px){.VPLocalNav.has-sidebar[data-v-070ab83d]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.container[data-v-070ab83d]{display:flex;justify-content:space-between;align-items:center}.menu[data-v-070ab83d]{display:flex;align-items:center;padding:12px 24px 11px;line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.menu[data-v-070ab83d]:hover{color:var(--vp-c-text-1);transition:color .25s}@media (min-width: 768px){.menu[data-v-070ab83d]{padding:0 32px}}@media (min-width: 960px){.menu[data-v-070ab83d]{display:none}}.menu-icon[data-v-070ab83d]{margin-right:8px;font-size:14px}.VPOutlineDropdown[data-v-070ab83d]{padding:12px 24px 11px}@media (min-width: 768px){.VPOutlineDropdown[data-v-070ab83d]{padding:12px 32px 11px}}.VPSwitch[data-v-4a1c76db]{position:relative;border-radius:11px;display:block;width:40px;height:22px;flex-shrink:0;border:1px solid var(--vp-input-border-color);background-color:var(--vp-input-switch-bg-color);transition:border-color .25s!important}.VPSwitch[data-v-4a1c76db]:hover{border-color:var(--vp-c-brand-1)}.check[data-v-4a1c76db]{position:absolute;top:1px;left:1px;width:18px;height:18px;border-radius:50%;background-color:var(--vp-c-neutral-inverse);box-shadow:var(--vp-shadow-1);transition:transform .25s!important}.icon[data-v-4a1c76db]{position:relative;display:block;width:18px;height:18px;border-radius:50%;overflow:hidden}.icon[data-v-4a1c76db] [class^=vpi-]{position:absolute;top:3px;left:3px;width:12px;height:12px;color:var(--vp-c-text-2)}.dark .icon[data-v-4a1c76db] [class^=vpi-]{color:var(--vp-c-text-1);transition:opacity .25s!important}.sun[data-v-e40a8bb6]{opacity:1}.moon[data-v-e40a8bb6],.dark .sun[data-v-e40a8bb6]{opacity:0}.dark .moon[data-v-e40a8bb6]{opacity:1}.dark .VPSwitchAppearance[data-v-e40a8bb6] .check{transform:translate(18px)}.VPNavBarAppearance[data-v-af096f4a]{display:none}@media (min-width: 1280px){.VPNavBarAppearance[data-v-af096f4a]{display:flex;align-items:center}}.VPMenuGroup+.VPMenuLink[data-v-acbfed09]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.link[data-v-acbfed09]{display:block;border-radius:6px;padding:0 12px;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);white-space:nowrap;transition:background-color .25s,color .25s}.link[data-v-acbfed09]:hover{color:var(--vp-c-brand-1);background-color:var(--vp-c-default-soft)}.link.active[data-v-acbfed09]{color:var(--vp-c-brand-1)}.VPMenuGroup[data-v-48c802d0]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.VPMenuGroup[data-v-48c802d0]:first-child{margin-top:0;border-top:0;padding-top:0}.VPMenuGroup+.VPMenuGroup[data-v-48c802d0]{margin-top:12px;border-top:1px solid var(--vp-c-divider)}.title[data-v-48c802d0]{padding:0 12px;line-height:32px;font-size:14px;font-weight:600;color:var(--vp-c-text-2);white-space:nowrap;transition:color .25s}.VPMenu[data-v-7dd3104a]{border-radius:12px;padding:12px;min-width:128px;border:1px solid var(--vp-c-divider);background-color:var(--vp-c-bg-elv);box-shadow:var(--vp-shadow-3);transition:background-color .5s;max-height:calc(100vh - var(--vp-nav-height));overflow-y:auto}.VPMenu[data-v-7dd3104a] .group{margin:0 -12px;padding:0 12px 12px}.VPMenu[data-v-7dd3104a] .group+.group{border-top:1px solid var(--vp-c-divider);padding:11px 12px 12px}.VPMenu[data-v-7dd3104a] .group:last-child{padding-bottom:0}.VPMenu[data-v-7dd3104a] .group+.item{border-top:1px solid var(--vp-c-divider);padding:11px 16px 0}.VPMenu[data-v-7dd3104a] .item{padding:0 16px;white-space:nowrap}.VPMenu[data-v-7dd3104a] .label{flex-grow:1;line-height:28px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.VPMenu[data-v-7dd3104a] .action{padding-left:24px}.VPFlyout[data-v-04f5c5e9]{position:relative}.VPFlyout[data-v-04f5c5e9]:hover{color:var(--vp-c-brand-1);transition:color .25s}.VPFlyout:hover .text[data-v-04f5c5e9]{color:var(--vp-c-text-2)}.VPFlyout:hover .icon[data-v-04f5c5e9]{fill:var(--vp-c-text-2)}.VPFlyout.active .text[data-v-04f5c5e9]{color:var(--vp-c-brand-1)}.VPFlyout.active:hover .text[data-v-04f5c5e9]{color:var(--vp-c-brand-2)}.button[aria-expanded=false]+.menu[data-v-04f5c5e9]{opacity:0;visibility:hidden;transform:translateY(0)}.VPFlyout:hover .menu[data-v-04f5c5e9],.button[aria-expanded=true]+.menu[data-v-04f5c5e9]{opacity:1;visibility:visible;transform:translateY(0)}.button[data-v-04f5c5e9]{display:flex;align-items:center;padding:0 12px;height:var(--vp-nav-height);color:var(--vp-c-text-1);transition:color .5s}.text[data-v-04f5c5e9]{display:flex;align-items:center;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.option-icon[data-v-04f5c5e9]{margin-right:0;font-size:16px}.text-icon[data-v-04f5c5e9]{margin-left:4px;font-size:14px}.icon[data-v-04f5c5e9]{font-size:20px;transition:fill .25s}.menu[data-v-04f5c5e9]{position:absolute;top:calc(var(--vp-nav-height) / 2 + 20px);right:0;opacity:0;visibility:hidden;transition:opacity .25s,visibility .25s,transform .25s}.VPSocialLink[data-v-d26d30cb]{display:flex;justify-content:center;align-items:center;width:36px;height:36px;color:var(--vp-c-text-2);transition:color .5s}.VPSocialLink[data-v-d26d30cb]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPSocialLink[data-v-d26d30cb]>svg,.VPSocialLink[data-v-d26d30cb]>[class^=vpi-social-]{width:20px;height:20px;fill:currentColor}.VPSocialLinks[data-v-ee7a9424]{display:flex;justify-content:center}.VPNavBarExtra[data-v-925effce]{display:none;margin-right:-12px}@media (min-width: 768px){.VPNavBarExtra[data-v-925effce]{display:block}}@media (min-width: 1280px){.VPNavBarExtra[data-v-925effce]{display:none}}.trans-title[data-v-925effce]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.item.appearance[data-v-925effce],.item.social-links[data-v-925effce]{display:flex;align-items:center;padding:0 12px}.item.appearance[data-v-925effce]{min-width:176px}.appearance-action[data-v-925effce]{margin-right:-2px}.social-links-list[data-v-925effce]{margin:-4px -8px}.VPNavBarHamburger[data-v-5dea55bf]{display:flex;justify-content:center;align-items:center;width:48px;height:var(--vp-nav-height)}@media (min-width: 768px){.VPNavBarHamburger[data-v-5dea55bf]{display:none}}.container[data-v-5dea55bf]{position:relative;width:16px;height:14px;overflow:hidden}.VPNavBarHamburger:hover .top[data-v-5dea55bf]{top:0;left:0;transform:translate(4px)}.VPNavBarHamburger:hover .middle[data-v-5dea55bf]{top:6px;left:0;transform:translate(0)}.VPNavBarHamburger:hover .bottom[data-v-5dea55bf]{top:12px;left:0;transform:translate(8px)}.VPNavBarHamburger.active .top[data-v-5dea55bf]{top:6px;transform:translate(0) rotate(225deg)}.VPNavBarHamburger.active .middle[data-v-5dea55bf]{top:6px;transform:translate(16px)}.VPNavBarHamburger.active .bottom[data-v-5dea55bf]{top:6px;transform:translate(0) rotate(135deg)}.VPNavBarHamburger.active:hover .top[data-v-5dea55bf],.VPNavBarHamburger.active:hover .middle[data-v-5dea55bf],.VPNavBarHamburger.active:hover .bottom[data-v-5dea55bf]{background-color:var(--vp-c-text-2);transition:top .25s,background-color .25s,transform .25s}.top[data-v-5dea55bf],.middle[data-v-5dea55bf],.bottom[data-v-5dea55bf]{position:absolute;width:16px;height:2px;background-color:var(--vp-c-text-1);transition:top .25s,background-color .5s,transform .25s}.top[data-v-5dea55bf]{top:0;left:0;transform:translate(0)}.middle[data-v-5dea55bf]{top:6px;left:0;transform:translate(8px)}.bottom[data-v-5dea55bf]{top:12px;left:0;transform:translate(4px)}.VPNavBarMenuLink[data-v-956ec74c]{display:flex;align-items:center;padding:0 12px;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.VPNavBarMenuLink.active[data-v-956ec74c],.VPNavBarMenuLink[data-v-956ec74c]:hover{color:var(--vp-c-brand-1)}.VPNavBarMenu[data-v-e6d46098]{display:none}@media (min-width: 768px){.VPNavBarMenu[data-v-e6d46098]{display:flex}}/*! @docsearch/css 3.8.2 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */:root{--docsearch-primary-color:#5468ff;--docsearch-text-color:#1c1e21;--docsearch-spacing:12px;--docsearch-icon-stroke-width:1.4;--docsearch-highlight-color:var(--docsearch-primary-color);--docsearch-muted-color:#969faf;--docsearch-container-background:rgba(101,108,133,.8);--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 hsla(0,0%,100%,.5),0 3px 8px 0 #555a64;--docsearch-searchbox-height:56px;--docsearch-searchbox-background:#ebedf0;--docsearch-searchbox-focus-background:#fff;--docsearch-searchbox-shadow:inset 0 0 0 2px var(--docsearch-primary-color);--docsearch-hit-height:56px;--docsearch-hit-color:#444950;--docsearch-hit-active-color:#fff;--docsearch-hit-background:#fff;--docsearch-hit-shadow:0 1px 3px 0 #d4d9e1;--docsearch-key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8);--docsearch-key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba(30,35,90,.4);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 1px 0 rgba(30,35,90,.4);--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69,98,155,.12)}html[data-theme=dark]{--docsearch-text-color:#f5f6f7;--docsearch-container-background:rgba(9,10,17,.8);--docsearch-modal-background:#15172a;--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:#090a11;--docsearch-searchbox-focus-background:#000;--docsearch-hit-color:#bec3c9;--docsearch-hit-shadow:none;--docsearch-hit-background:#090a11;--docsearch-key-gradient:linear-gradient(-26.5deg,#565872,#31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3,4,9,.3);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 1px 1px 0 #0304094d;--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73,76,106,.5),0 -4px 8px 0 rgba(0,0,0,.2);--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}.DocSearch-Button{align-items:center;background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;display:flex;font-weight:500;height:36px;justify-content:space-between;margin:0 0 0 16px;padding:0 8px;-webkit-user-select:none;user-select:none}.DocSearch-Button:active,.DocSearch-Button:focus,.DocSearch-Button:hover{background:var(--docsearch-searchbox-focus-background);box-shadow:var(--docsearch-searchbox-shadow);color:var(--docsearch-text-color);outline:none}.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 2px;position:relative;top:-1px;width:20px}.DocSearch-Button-Key--pressed{box-shadow:var(--docsearch-key-pressed-shadow);transform:translate3d(0,1px,0)}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder{display:none}}.DocSearch--active{overflow:hidden!important}.DocSearch-Container,.DocSearch-Container *{box-sizing:border-box}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Container a{text-decoration:none}.DocSearch-Link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;font:inherit;margin:0;padding:0}.DocSearch-Modal{background:var(--docsearch-modal-background);border-radius:6px;box-shadow:var(--docsearch-modal-shadow);flex-direction:column;margin:60px auto auto;max-width:var(--docsearch-modal-width);position:relative}.DocSearch-SearchBar{display:flex;padding:var(--docsearch-spacing) var(--docsearch-spacing) 0}.DocSearch-Form{align-items:center;background:var(--docsearch-searchbox-focus-background);border-radius:4px;box-shadow:var(--docsearch-searchbox-shadow);display:flex;height:var(--docsearch-searchbox-height);margin:0;padding:0 var(--docsearch-spacing);position:relative;width:100%}.DocSearch-Input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:var(--docsearch-text-color);flex:1;font:inherit;font-size:1.2em;height:100%;outline:none;padding:0 0 0 8px;width:80%}.DocSearch-Input::placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::-webkit-search-cancel-button,.DocSearch-Input::-webkit-search-decoration,.DocSearch-Input::-webkit-search-results-button,.DocSearch-Input::-webkit-search-results-decoration{display:none}.DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{margin:0;padding:0}.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator{display:none}.DocSearch-Container--Stalled .DocSearch-LoadingIndicator{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{animation:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0;stroke-width:var(--docsearch-icon-stroke-width)}}.DocSearch-Reset{animation:fade-in .1s ease-in forwards;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;padding:2px;right:0;stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.DocSearch-Cancel{display:none}.DocSearch-Dropdown{max-height:calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height));min-height:var(--docsearch-spacing);overflow-y:auto;overflow-y:overlay;padding:0 var(--docsearch-spacing);scrollbar-color:var(--docsearch-muted-color) var(--docsearch-modal-background);scrollbar-width:thin}.DocSearch-Dropdown::-webkit-scrollbar{width:12px}.DocSearch-Dropdown::-webkit-scrollbar-track{background:transparent}.DocSearch-Dropdown::-webkit-scrollbar-thumb{background-color:var(--docsearch-muted-color);border:3px solid var(--docsearch-modal-background);border-radius:20px}.DocSearch-Dropdown ul{list-style:none;margin:0;padding:0}.DocSearch-Label{font-size:.75em;line-height:1.6em}.DocSearch-Help,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Help{font-size:.9em;margin:0;-webkit-user-select:none;user-select:none}.DocSearch-Title{font-size:1.2em}.DocSearch-Logo a{display:flex}.DocSearch-Logo svg{color:var(--docsearch-logo-color);margin-left:8px}.DocSearch-Hits:last-of-type{margin-bottom:24px}.DocSearch-Hits mark{background:none;color:var(--docsearch-highlight-color)}.DocSearch-HitsFooter{color:var(--docsearch-muted-color);display:flex;font-size:.85em;justify-content:center;margin-bottom:var(--docsearch-spacing);padding:var(--docsearch-spacing)}.DocSearch-HitsFooter a{border-bottom:1px solid;color:inherit}.DocSearch-Hit{border-radius:4px;display:flex;padding-bottom:4px;position:relative}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--deleting{transition:none}}.DocSearch-Hit--deleting{opacity:0;transition:all .25s linear}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--favoriting{transition:none}}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:all .25s linear;transition-delay:.25s}.DocSearch-Hit a{background:var(--docsearch-hit-background);border-radius:4px;box-shadow:var(--docsearch-hit-shadow);display:block;padding-left:var(--docsearch-spacing);width:100%}.DocSearch-Hit-source{background:var(--docsearch-modal-background);color:var(--docsearch-highlight-color);font-size:.85em;font-weight:600;line-height:32px;margin:0 -4px;padding:8px 4px 0;position:sticky;top:0;z-index:10}.DocSearch-Hit-Tree{color:var(--docsearch-muted-color);height:var(--docsearch-hit-height);opacity:.5;stroke-width:var(--docsearch-icon-stroke-width);width:24px}.DocSearch-Hit[aria-selected=true] a{background-color:var(--docsearch-highlight-color)}.DocSearch-Hit[aria-selected=true] mark{text-decoration:underline}.DocSearch-Hit-Container{align-items:center;color:var(--docsearch-hit-color);display:flex;flex-direction:row;height:var(--docsearch-hit-height);padding:0 var(--docsearch-spacing) 0 0}.DocSearch-Hit-icon{height:20px;width:20px}.DocSearch-Hit-action,.DocSearch-Hit-icon{color:var(--docsearch-muted-color);stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Hit-action{align-items:center;display:flex;height:22px;width:22px}.DocSearch-Hit-action svg{display:block;height:18px;width:18px}.DocSearch-Hit-action+.DocSearch-Hit-action{margin-left:6px}.DocSearch-Hit-action-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:inherit;cursor:pointer;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:background-color .1s ease-in}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{transition:none}}.DocSearch-Hit-action-button:focus path,.DocSearch-Hit-action-button:hover path{fill:#fff}.DocSearch-Hit-content-wrapper{display:flex;flex:1 1 auto;flex-direction:column;font-weight:500;justify-content:center;line-height:1.2em;margin:0 8px;overflow-x:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:80%}.DocSearch-Hit-title{font-size:.9em}.DocSearch-Hit-path{color:var(--docsearch-muted-color);font-size:.75em}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-action,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-icon,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-text,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-title,.DocSearch-Hit[aria-selected=true] mark{color:var(--docsearch-hit-active-color)!important}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:none}}.DocSearch-ErrorScreen,.DocSearch-NoResults,.DocSearch-StartScreen{font-size:.9em;margin:0 auto;padding:36px 0;text-align:center;width:80%}.DocSearch-Screen-Icon{color:var(--docsearch-muted-color);padding-bottom:12px}.DocSearch-NoResults-Prefill-List{display:inline-block;padding-bottom:24px;text-align:left}.DocSearch-NoResults-Prefill-List ul{display:inline-block;padding:8px 0 0}.DocSearch-NoResults-Prefill-List li{list-style-position:inside;list-style-type:"» "}.DocSearch-Prefill{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:1em;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;font-size:1em;font-weight:700;padding:0}.DocSearch-Prefill:focus,.DocSearch-Prefill:hover{outline:none;text-decoration:underline}.DocSearch-Footer{align-items:center;background:var(--docsearch-footer-background);border-radius:0 0 8px 8px;box-shadow:var(--docsearch-footer-shadow);display:flex;flex-direction:row-reverse;flex-shrink:0;height:var(--docsearch-footer-height);justify-content:space-between;padding:0 var(--docsearch-spacing);position:relative;-webkit-user-select:none;user-select:none;width:100%;z-index:300}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li{align-items:center;display:flex}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:2px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;width:20px}.DocSearch-VisuallyHiddenForAccessibility{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}@media (max-width:768px){:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%}.DocSearch-Container{height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);position:absolute}.DocSearch-Footer{border-radius:0;bottom:0;position:absolute}.DocSearch-Hit-content-wrapper{display:flex;position:relative;width:80%}.DocSearch-Modal{border-radius:0;box-shadow:none;height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);margin:0;max-width:100%;width:100%}.DocSearch-Dropdown{max-height:calc(var(--docsearch-vh, 1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.DocSearch-Cancel{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;flex:none;font:inherit;font-size:1em;font-weight:500;margin-left:var(--docsearch-spacing);outline:none;overflow:hidden;padding:0;-webkit-user-select:none;user-select:none;white-space:nowrap}.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}[class*=DocSearch]{--docsearch-primary-color: var(--vp-c-brand-1);--docsearch-highlight-color: var(--docsearch-primary-color);--docsearch-text-color: var(--vp-c-text-1);--docsearch-muted-color: var(--vp-c-text-2);--docsearch-searchbox-shadow: none;--docsearch-searchbox-background: transparent;--docsearch-searchbox-focus-background: transparent;--docsearch-key-gradient: transparent;--docsearch-key-shadow: none;--docsearch-modal-background: var(--vp-c-bg-soft);--docsearch-footer-background: var(--vp-c-bg)}.dark [class*=DocSearch]{--docsearch-modal-shadow: none;--docsearch-footer-shadow: none;--docsearch-logo-color: var(--vp-c-text-2);--docsearch-hit-background: var(--vp-c-default-soft);--docsearch-hit-color: var(--vp-c-text-2);--docsearch-hit-shadow: none}.DocSearch-Button{display:flex;justify-content:center;align-items:center;margin:0;padding:0;width:48px;height:55px;background:transparent;transition:border-color .25s}.DocSearch-Button:hover{background:transparent}.DocSearch-Button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}.DocSearch-Button-Key--pressed{transform:none;box-shadow:none}.DocSearch-Button:focus:not(:focus-visible){outline:none!important}@media (min-width: 768px){.DocSearch-Button{justify-content:flex-start;border:1px solid transparent;border-radius:8px;padding:0 10px 0 12px;width:100%;height:40px;background-color:var(--vp-c-bg-alt)}.DocSearch-Button:hover{border-color:var(--vp-c-brand-1);background:var(--vp-c-bg-alt)}}.DocSearch-Button .DocSearch-Button-Container{display:flex;align-items:center}.DocSearch-Button .DocSearch-Search-Icon{position:relative;width:16px;height:16px;color:var(--vp-c-text-1);fill:currentColor;transition:color .5s}.DocSearch-Button:hover .DocSearch-Search-Icon{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Search-Icon{top:1px;margin-right:8px;width:14px;height:14px;color:var(--vp-c-text-2)}}.DocSearch-Button .DocSearch-Button-Placeholder{display:none;margin-top:2px;padding:0 16px 0 0;font-size:13px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.DocSearch-Button:hover .DocSearch-Button-Placeholder{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Placeholder{display:inline-block}}.DocSearch-Button .DocSearch-Button-Keys{direction:ltr;display:none;min-width:auto}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Keys{display:flex;align-items:center}}.DocSearch-Button .DocSearch-Button-Key{display:block;margin:2px 0 0;border:1px solid var(--vp-c-divider);border-right:none;border-radius:4px 0 0 4px;padding-left:6px;min-width:0;width:auto;height:22px;line-height:22px;font-family:var(--vp-font-family-base);font-size:12px;font-weight:500;transition:color .5s,border-color .5s}.DocSearch-Button .DocSearch-Button-Key+.DocSearch-Button-Key{border-right:1px solid var(--vp-c-divider);border-left:none;border-radius:0 4px 4px 0;padding-left:2px;padding-right:6px}.DocSearch-Button .DocSearch-Button-Key:first-child{font-size:0!important}.DocSearch-Button .DocSearch-Button-Key:first-child:after{content:"Ctrl";font-size:12px;letter-spacing:normal;color:var(--docsearch-muted-color)}.mac .DocSearch-Button .DocSearch-Button-Key:first-child:after{content:"⌘"}.DocSearch-Button .DocSearch-Button-Key:first-child>*{display:none}.DocSearch-Search-Icon{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' stroke-width='1.6' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='m14.386 14.386 4.088 4.088-4.088-4.088A7.533 7.533 0 1 1 3.733 3.733a7.533 7.533 0 0 1 10.653 10.653z'/%3E%3C/svg%3E")}.VPNavBarSearch{display:flex;align-items:center}@media (min-width: 768px){.VPNavBarSearch{flex-grow:1;padding-left:24px}}@media (min-width: 960px){.VPNavBarSearch{padding-left:32px}}.dark .DocSearch-Footer{border-top:1px solid var(--vp-c-divider)}.DocSearch-Form{border:1px solid var(--vp-c-brand-1);background-color:var(--vp-c-white)}.dark .DocSearch-Form{background-color:var(--vp-c-default-soft)}.DocSearch-Screen-Icon>svg{margin:auto}.VPNavBarSocialLinks[data-v-164c457f]{display:none}@media (min-width: 1280px){.VPNavBarSocialLinks[data-v-164c457f]{display:flex;align-items:center}}.title[data-v-0f4f798b]{display:flex;align-items:center;border-bottom:1px solid transparent;width:100%;height:var(--vp-nav-height);font-size:16px;font-weight:600;color:var(--vp-c-text-1);transition:opacity .25s}@media (min-width: 960px){.title[data-v-0f4f798b]{flex-shrink:0}.VPNavBarTitle.has-sidebar .title[data-v-0f4f798b]{border-bottom-color:var(--vp-c-divider)}}[data-v-0f4f798b] .logo{margin-right:8px;height:var(--vp-nav-logo-height)}.VPNavBarTranslations[data-v-c80d9ad0]{display:none}@media (min-width: 1280px){.VPNavBarTranslations[data-v-c80d9ad0]{display:flex;align-items:center}}.title[data-v-c80d9ad0]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.VPNavBar[data-v-822684d1]{position:relative;height:var(--vp-nav-height);pointer-events:none;white-space:nowrap;transition:background-color .25s}.VPNavBar.screen-open[data-v-822684d1]{transition:none;background-color:var(--vp-nav-bg-color);border-bottom:1px solid var(--vp-c-divider)}.VPNavBar[data-v-822684d1]:not(.home){background-color:var(--vp-nav-bg-color)}@media (min-width: 960px){.VPNavBar[data-v-822684d1]:not(.home){background-color:transparent}.VPNavBar[data-v-822684d1]:not(.has-sidebar):not(.home.top){background-color:var(--vp-nav-bg-color)}}.wrapper[data-v-822684d1]{padding:0 8px 0 24px}@media (min-width: 768px){.wrapper[data-v-822684d1]{padding:0 32px}}@media (min-width: 960px){.VPNavBar.has-sidebar .wrapper[data-v-822684d1]{padding:0}}.container[data-v-822684d1]{display:flex;justify-content:space-between;margin:0 auto;max-width:calc(var(--vp-layout-max-width) - 64px);height:var(--vp-nav-height);pointer-events:none}.container>.title[data-v-822684d1],.container>.content[data-v-822684d1]{pointer-events:none}.container[data-v-822684d1] *{pointer-events:auto}@media (min-width: 960px){.VPNavBar.has-sidebar .container[data-v-822684d1]{max-width:100%}}.title[data-v-822684d1]{flex-shrink:0;height:calc(var(--vp-nav-height) - 1px);transition:background-color .5s}@media (min-width: 960px){.VPNavBar.has-sidebar .title[data-v-822684d1]{position:absolute;top:0;left:0;z-index:2;padding:0 32px;width:var(--vp-sidebar-width);height:var(--vp-nav-height);background-color:transparent}}@media (min-width: 1440px){.VPNavBar.has-sidebar .title[data-v-822684d1]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}.content[data-v-822684d1]{flex-grow:1}@media (min-width: 960px){.VPNavBar.has-sidebar .content[data-v-822684d1]{position:relative;z-index:1;padding-right:32px;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .content[data-v-822684d1]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.content-body[data-v-822684d1]{display:flex;justify-content:flex-end;align-items:center;height:var(--vp-nav-height);transition:background-color .5s}@media (min-width: 960px){.VPNavBar:not(.home.top) .content-body[data-v-822684d1]{position:relative;background-color:var(--vp-nav-bg-color)}.VPNavBar:not(.has-sidebar):not(.home.top) .content-body[data-v-822684d1]{background-color:transparent}}@media (max-width: 767px){.content-body[data-v-822684d1]{column-gap:.5rem}}.menu+.translations[data-v-822684d1]:before,.menu+.appearance[data-v-822684d1]:before,.menu+.social-links[data-v-822684d1]:before,.translations+.appearance[data-v-822684d1]:before,.appearance+.social-links[data-v-822684d1]:before{margin-right:8px;margin-left:8px;width:1px;height:24px;background-color:var(--vp-c-divider);content:""}.menu+.appearance[data-v-822684d1]:before,.translations+.appearance[data-v-822684d1]:before{margin-right:16px}.appearance+.social-links[data-v-822684d1]:before{margin-left:16px}.social-links[data-v-822684d1]{margin-right:-8px}.divider[data-v-822684d1]{width:100%;height:1px}@media (min-width: 960px){.VPNavBar.has-sidebar .divider[data-v-822684d1]{padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .divider[data-v-822684d1]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.divider-line[data-v-822684d1]{width:100%;height:1px;transition:background-color .5s}.VPNavBar:not(.home) .divider-line[data-v-822684d1]{background-color:var(--vp-c-gutter)}@media (min-width: 960px){.VPNavBar:not(.home.top) .divider-line[data-v-822684d1]{background-color:var(--vp-c-gutter)}.VPNavBar:not(.has-sidebar):not(.home.top) .divider[data-v-822684d1]{background-color:var(--vp-c-gutter)}}.VPNavScreenAppearance[data-v-ffb44008]{display:flex;justify-content:space-between;align-items:center;border-radius:8px;padding:12px 14px 12px 16px;background-color:var(--vp-c-bg-soft)}.text[data-v-ffb44008]{line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.VPNavScreenMenuLink[data-v-735512b8]{display:block;border-bottom:1px solid var(--vp-c-divider);padding:12px 0 11px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:border-color .25s,color .25s}.VPNavScreenMenuLink[data-v-735512b8]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupLink[data-v-372ae7c0]{display:block;margin-left:12px;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-1);transition:color .25s}.VPNavScreenMenuGroupLink[data-v-372ae7c0]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupSection[data-v-4b8941ac]{display:block}.title[data-v-4b8941ac]{line-height:32px;font-size:13px;font-weight:700;color:var(--vp-c-text-2);transition:color .25s}.VPNavScreenMenuGroup[data-v-875057a5]{border-bottom:1px solid var(--vp-c-divider);height:48px;overflow:hidden;transition:border-color .5s}.VPNavScreenMenuGroup .items[data-v-875057a5]{visibility:hidden}.VPNavScreenMenuGroup.open .items[data-v-875057a5]{visibility:visible}.VPNavScreenMenuGroup.open[data-v-875057a5]{padding-bottom:10px;height:auto}.VPNavScreenMenuGroup.open .button[data-v-875057a5]{padding-bottom:6px;color:var(--vp-c-brand-1)}.VPNavScreenMenuGroup.open .button-icon[data-v-875057a5]{transform:rotate(45deg)}.button[data-v-875057a5]{display:flex;justify-content:space-between;align-items:center;padding:12px 4px 11px 0;width:100%;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.button[data-v-875057a5]:hover{color:var(--vp-c-brand-1)}.button-icon[data-v-875057a5]{transition:transform .25s}.group[data-v-875057a5]:first-child{padding-top:0}.group+.group[data-v-875057a5],.group+.item[data-v-875057a5]{padding-top:4px}.VPNavScreenTranslations[data-v-362991c2]{height:24px;overflow:hidden}.VPNavScreenTranslations.open[data-v-362991c2]{height:auto}.title[data-v-362991c2]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-text-1)}.icon[data-v-362991c2]{font-size:16px}.icon.lang[data-v-362991c2]{margin-right:8px}.icon.chevron[data-v-362991c2]{margin-left:4px}.list[data-v-362991c2]{padding:4px 0 0 24px}.link[data-v-362991c2]{line-height:32px;font-size:13px;color:var(--vp-c-text-1)}.VPNavScreen[data-v-833aabba]{position:fixed;top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px));right:0;bottom:0;left:0;padding:0 32px;width:100%;background-color:var(--vp-nav-screen-bg-color);overflow-y:auto;transition:background-color .25s;pointer-events:auto}.VPNavScreen.fade-enter-active[data-v-833aabba],.VPNavScreen.fade-leave-active[data-v-833aabba]{transition:opacity .25s}.VPNavScreen.fade-enter-active .container[data-v-833aabba],.VPNavScreen.fade-leave-active .container[data-v-833aabba]{transition:transform .25s ease}.VPNavScreen.fade-enter-from[data-v-833aabba],.VPNavScreen.fade-leave-to[data-v-833aabba]{opacity:0}.VPNavScreen.fade-enter-from .container[data-v-833aabba],.VPNavScreen.fade-leave-to .container[data-v-833aabba]{transform:translateY(-8px)}@media (min-width: 768px){.VPNavScreen[data-v-833aabba]{display:none}}.container[data-v-833aabba]{margin:0 auto;padding:24px 0 96px;max-width:288px}.menu+.translations[data-v-833aabba],.menu+.appearance[data-v-833aabba],.translations+.appearance[data-v-833aabba]{margin-top:24px}.menu+.social-links[data-v-833aabba]{margin-top:16px}.appearance+.social-links[data-v-833aabba]{margin-top:16px}.VPNav[data-v-f1e365da]{position:relative;top:var(--vp-layout-top-height, 0px);left:0;z-index:var(--vp-z-index-nav);width:100%;pointer-events:none;transition:background-color .5s}@media (min-width: 960px){.VPNav[data-v-f1e365da]{position:fixed}}.VPSidebarItem.level-0[data-v-a4b0d9bf]{padding-bottom:24px}.VPSidebarItem.collapsed.level-0[data-v-a4b0d9bf]{padding-bottom:10px}.item[data-v-a4b0d9bf]{position:relative;display:flex;width:100%}.VPSidebarItem.collapsible>.item[data-v-a4b0d9bf]{cursor:pointer}.indicator[data-v-a4b0d9bf]{position:absolute;top:6px;bottom:6px;left:-17px;width:2px;border-radius:2px;transition:background-color .25s}.VPSidebarItem.level-2.is-active>.item>.indicator[data-v-a4b0d9bf],.VPSidebarItem.level-3.is-active>.item>.indicator[data-v-a4b0d9bf],.VPSidebarItem.level-4.is-active>.item>.indicator[data-v-a4b0d9bf],.VPSidebarItem.level-5.is-active>.item>.indicator[data-v-a4b0d9bf]{background-color:var(--vp-c-brand-1)}.link[data-v-a4b0d9bf]{display:flex;align-items:center;flex-grow:1}.text[data-v-a4b0d9bf]{flex-grow:1;padding:4px 0;line-height:24px;font-size:14px;transition:color .25s}.VPSidebarItem.level-0 .text[data-v-a4b0d9bf]{font-weight:700;color:var(--vp-c-text-1)}.VPSidebarItem.level-1 .text[data-v-a4b0d9bf],.VPSidebarItem.level-2 .text[data-v-a4b0d9bf],.VPSidebarItem.level-3 .text[data-v-a4b0d9bf],.VPSidebarItem.level-4 .text[data-v-a4b0d9bf],.VPSidebarItem.level-5 .text[data-v-a4b0d9bf]{font-weight:500;color:var(--vp-c-text-2)}.VPSidebarItem.level-0.is-link>.item>.link:hover .text[data-v-a4b0d9bf],.VPSidebarItem.level-1.is-link>.item>.link:hover .text[data-v-a4b0d9bf],.VPSidebarItem.level-2.is-link>.item>.link:hover .text[data-v-a4b0d9bf],.VPSidebarItem.level-3.is-link>.item>.link:hover .text[data-v-a4b0d9bf],.VPSidebarItem.level-4.is-link>.item>.link:hover .text[data-v-a4b0d9bf],.VPSidebarItem.level-5.is-link>.item>.link:hover .text[data-v-a4b0d9bf]{color:var(--vp-c-brand-1)}.VPSidebarItem.level-0.has-active>.item>.text[data-v-a4b0d9bf],.VPSidebarItem.level-1.has-active>.item>.text[data-v-a4b0d9bf],.VPSidebarItem.level-2.has-active>.item>.text[data-v-a4b0d9bf],.VPSidebarItem.level-3.has-active>.item>.text[data-v-a4b0d9bf],.VPSidebarItem.level-4.has-active>.item>.text[data-v-a4b0d9bf],.VPSidebarItem.level-5.has-active>.item>.text[data-v-a4b0d9bf],.VPSidebarItem.level-0.has-active>.item>.link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-1.has-active>.item>.link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-2.has-active>.item>.link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-3.has-active>.item>.link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-4.has-active>.item>.link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-5.has-active>.item>.link>.text[data-v-a4b0d9bf]{color:var(--vp-c-text-1)}.VPSidebarItem.level-0.is-active>.item .link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-1.is-active>.item .link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-2.is-active>.item .link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-3.is-active>.item .link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-4.is-active>.item .link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-5.is-active>.item .link>.text[data-v-a4b0d9bf]{color:var(--vp-c-brand-1)}.caret[data-v-a4b0d9bf]{display:flex;justify-content:center;align-items:center;margin-right:-7px;width:32px;height:32px;color:var(--vp-c-text-3);cursor:pointer;transition:color .25s;flex-shrink:0}.item:hover .caret[data-v-a4b0d9bf]{color:var(--vp-c-text-2)}.item:hover .caret[data-v-a4b0d9bf]:hover{color:var(--vp-c-text-1)}.caret-icon[data-v-a4b0d9bf]{font-size:18px;transform:rotate(90deg);transition:transform .25s}.VPSidebarItem.collapsed .caret-icon[data-v-a4b0d9bf]{transform:rotate(0)}.VPSidebarItem.level-1 .items[data-v-a4b0d9bf],.VPSidebarItem.level-2 .items[data-v-a4b0d9bf],.VPSidebarItem.level-3 .items[data-v-a4b0d9bf],.VPSidebarItem.level-4 .items[data-v-a4b0d9bf],.VPSidebarItem.level-5 .items[data-v-a4b0d9bf]{border-left:1px solid var(--vp-c-divider);padding-left:16px}.VPSidebarItem.collapsed .items[data-v-a4b0d9bf]{display:none}.no-transition[data-v-9e426adc] .caret-icon{transition:none}.group+.group[data-v-9e426adc]{border-top:1px solid var(--vp-c-divider);padding-top:10px}@media (min-width: 960px){.group[data-v-9e426adc]{padding-top:10px;width:calc(var(--vp-sidebar-width) - 64px)}}.VPSidebar[data-v-18756405]{position:fixed;top:var(--vp-layout-top-height, 0px);bottom:0;left:0;z-index:var(--vp-z-index-sidebar);padding:32px 32px 96px;width:calc(100vw - 64px);max-width:320px;background-color:var(--vp-sidebar-bg-color);opacity:0;box-shadow:var(--vp-c-shadow-3);overflow-x:hidden;overflow-y:auto;transform:translate(-100%);transition:opacity .5s,transform .25s ease;overscroll-behavior:contain}.VPSidebar.open[data-v-18756405]{opacity:1;visibility:visible;transform:translate(0);transition:opacity .25s,transform .5s cubic-bezier(.19,1,.22,1)}.dark .VPSidebar[data-v-18756405]{box-shadow:var(--vp-shadow-1)}@media (min-width: 960px){.VPSidebar[data-v-18756405]{padding-top:var(--vp-nav-height);width:var(--vp-sidebar-width);max-width:100%;background-color:var(--vp-sidebar-bg-color);opacity:1;visibility:visible;box-shadow:none;transform:translate(0)}}@media (min-width: 1440px){.VPSidebar[data-v-18756405]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}@media (min-width: 960px){.curtain[data-v-18756405]{position:sticky;top:-64px;left:0;z-index:1;margin-top:calc(var(--vp-nav-height) * -1);margin-right:-32px;margin-left:-32px;height:var(--vp-nav-height);background-color:var(--vp-sidebar-bg-color)}}.nav[data-v-18756405]{outline:0}.VPSkipLink[data-v-492508fc]{top:8px;left:8px;padding:8px 16px;z-index:999;border-radius:8px;font-size:12px;font-weight:700;text-decoration:none;color:var(--vp-c-brand-1);box-shadow:var(--vp-shadow-3);background-color:var(--vp-c-bg)}.VPSkipLink[data-v-492508fc]:focus{height:auto;width:auto;clip:auto;clip-path:none}@media (min-width: 1280px){.VPSkipLink[data-v-492508fc]{top:14px;left:16px}}.Layout[data-v-a9a9e638]{display:flex;flex-direction:column;min-height:100vh}.VPHomeSponsors[data-v-db81191c]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPHomeSponsors[data-v-db81191c]{margin:96px 0}@media (min-width: 768px){.VPHomeSponsors[data-v-db81191c]{margin:128px 0}}.VPHomeSponsors[data-v-db81191c]{padding:0 24px}@media (min-width: 768px){.VPHomeSponsors[data-v-db81191c]{padding:0 48px}}@media (min-width: 960px){.VPHomeSponsors[data-v-db81191c]{padding:0 64px}}.container[data-v-db81191c]{margin:0 auto;max-width:1152px}.love[data-v-db81191c]{margin:0 auto;width:fit-content;font-size:28px;color:var(--vp-c-text-3)}.icon[data-v-db81191c]{display:inline-block}.message[data-v-db81191c]{margin:0 auto;padding-top:10px;max-width:320px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.sponsors[data-v-db81191c]{padding-top:32px}.action[data-v-db81191c]{padding-top:40px;text-align:center}.VPTeamMembersItem[data-v-f9987cb6]{display:flex;flex-direction:column;gap:2px;border-radius:12px;width:100%;height:100%;overflow:hidden}.VPTeamMembersItem.small .profile[data-v-f9987cb6]{padding:32px}.VPTeamMembersItem.small .data[data-v-f9987cb6]{padding-top:20px}.VPTeamMembersItem.small .avatar[data-v-f9987cb6]{width:64px;height:64px}.VPTeamMembersItem.small .name[data-v-f9987cb6]{line-height:24px;font-size:16px}.VPTeamMembersItem.small .affiliation[data-v-f9987cb6]{padding-top:4px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .desc[data-v-f9987cb6]{padding-top:12px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .links[data-v-f9987cb6]{margin:0 -16px -20px;padding:10px 0 0}.VPTeamMembersItem.medium .profile[data-v-f9987cb6]{padding:48px 32px}.VPTeamMembersItem.medium .data[data-v-f9987cb6]{padding-top:24px;text-align:center}.VPTeamMembersItem.medium .avatar[data-v-f9987cb6]{width:96px;height:96px}.VPTeamMembersItem.medium .name[data-v-f9987cb6]{letter-spacing:.15px;line-height:28px;font-size:20px}.VPTeamMembersItem.medium .affiliation[data-v-f9987cb6]{padding-top:4px;font-size:16px}.VPTeamMembersItem.medium .desc[data-v-f9987cb6]{padding-top:16px;max-width:288px;font-size:16px}.VPTeamMembersItem.medium .links[data-v-f9987cb6]{margin:0 -16px -12px;padding:16px 12px 0}.profile[data-v-f9987cb6]{flex-grow:1;background-color:var(--vp-c-bg-soft)}.data[data-v-f9987cb6]{text-align:center}.avatar[data-v-f9987cb6]{position:relative;flex-shrink:0;margin:0 auto;border-radius:50%;box-shadow:var(--vp-shadow-3)}.avatar-img[data-v-f9987cb6]{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;object-fit:cover}.name[data-v-f9987cb6]{margin:0;font-weight:600}.affiliation[data-v-f9987cb6]{margin:0;font-weight:500;color:var(--vp-c-text-2)}.org.link[data-v-f9987cb6]{color:var(--vp-c-text-2);transition:color .25s}.org.link[data-v-f9987cb6]:hover{color:var(--vp-c-brand-1)}.desc[data-v-f9987cb6]{margin:0 auto}.desc[data-v-f9987cb6] a{font-weight:500;color:var(--vp-c-brand-1);text-decoration-style:dotted;transition:color .25s}.links[data-v-f9987cb6]{display:flex;justify-content:center;height:56px}.sp-link[data-v-f9987cb6]{display:flex;justify-content:center;align-items:center;text-align:center;padding:16px;font-size:14px;font-weight:500;color:var(--vp-c-sponsor);background-color:var(--vp-c-bg-soft);transition:color .25s,background-color .25s}.sp .sp-link.link[data-v-f9987cb6]:hover,.sp .sp-link.link[data-v-f9987cb6]:focus{outline:none;color:var(--vp-c-white);background-color:var(--vp-c-sponsor)}.sp-icon[data-v-f9987cb6]{margin-right:8px;font-size:16px}.VPTeamMembers.small .container[data-v-fba19bad]{grid-template-columns:repeat(auto-fit,minmax(224px,1fr))}.VPTeamMembers.small.count-1 .container[data-v-fba19bad]{max-width:276px}.VPTeamMembers.small.count-2 .container[data-v-fba19bad]{max-width:576px}.VPTeamMembers.small.count-3 .container[data-v-fba19bad]{max-width:876px}.VPTeamMembers.medium .container[data-v-fba19bad]{grid-template-columns:repeat(auto-fit,minmax(256px,1fr))}@media (min-width: 375px){.VPTeamMembers.medium .container[data-v-fba19bad]{grid-template-columns:repeat(auto-fit,minmax(288px,1fr))}}.VPTeamMembers.medium.count-1 .container[data-v-fba19bad]{max-width:368px}.VPTeamMembers.medium.count-2 .container[data-v-fba19bad]{max-width:760px}.container[data-v-fba19bad]{display:grid;gap:24px;margin:0 auto;max-width:1152px}.VPTeamPage[data-v-c2f8e101]{margin:96px 0}@media (min-width: 768px){.VPTeamPage[data-v-c2f8e101]{margin:128px 0}}.VPHome .VPTeamPageTitle[data-v-c2f8e101-s]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPTeamPageSection+.VPTeamPageSection[data-v-c2f8e101-s],.VPTeamMembers+.VPTeamPageSection[data-v-c2f8e101-s]{margin-top:64px}.VPTeamMembers+.VPTeamMembers[data-v-c2f8e101-s]{margin-top:24px}@media (min-width: 768px){.VPTeamPageTitle+.VPTeamPageSection[data-v-c2f8e101-s]{margin-top:16px}.VPTeamPageSection+.VPTeamPageSection[data-v-c2f8e101-s],.VPTeamMembers+.VPTeamPageSection[data-v-c2f8e101-s]{margin-top:96px}}.VPTeamMembers[data-v-c2f8e101-s]{padding:0 24px}@media (min-width: 768px){.VPTeamMembers[data-v-c2f8e101-s]{padding:0 48px}}@media (min-width: 960px){.VPTeamMembers[data-v-c2f8e101-s]{padding:0 64px}}.VPTeamPageSection[data-v-d43bc49d]{padding:0 32px}@media (min-width: 768px){.VPTeamPageSection[data-v-d43bc49d]{padding:0 48px}}@media (min-width: 960px){.VPTeamPageSection[data-v-d43bc49d]{padding:0 64px}}.title[data-v-d43bc49d]{position:relative;margin:0 auto;max-width:1152px;text-align:center;color:var(--vp-c-text-2)}.title-line[data-v-d43bc49d]{position:absolute;top:16px;left:0;width:100%;height:1px;background-color:var(--vp-c-divider)}.title-text[data-v-d43bc49d]{position:relative;display:inline-block;padding:0 24px;letter-spacing:0;line-height:32px;font-size:20px;font-weight:500;background-color:var(--vp-c-bg)}.lead[data-v-d43bc49d]{margin:0 auto;max-width:480px;padding-top:12px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.members[data-v-d43bc49d]{padding-top:40px}.VPTeamPageTitle[data-v-e277e15c]{padding:48px 32px;text-align:center}@media (min-width: 768px){.VPTeamPageTitle[data-v-e277e15c]{padding:64px 48px 48px}}@media (min-width: 960px){.VPTeamPageTitle[data-v-e277e15c]{padding:80px 64px 48px}}.title[data-v-e277e15c]{letter-spacing:0;line-height:44px;font-size:36px;font-weight:500}@media (min-width: 768px){.title[data-v-e277e15c]{letter-spacing:-.5px;line-height:56px;font-size:48px}}.lead[data-v-e277e15c]{margin:0 auto;max-width:512px;padding-top:12px;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 768px){.lead[data-v-e277e15c]{max-width:592px;letter-spacing:.15px;line-height:28px;font-size:20px}}th{background-color:var(--vp-c-bg-soft);padding:.75rem;text-align:left;font-weight:600}code{font-size:.9em;padding:.2em .4em;border-radius:4px;background-color:var(--vp-c-bg-mute)}table{width:100%;border-collapse:collapse;margin:1.5rem 0}code{font-size:.9em;padding:.2em .4em;border-radius:4px}blockquote{margin:2rem 0;padding:1.5rem;background:linear-gradient(135deg,var(--vp-c-brand-soft) 0%,transparent 100%);border-radius:12px;font-size:1.2rem;font-style:italic}.npm-stats{text-align:center;margin:2rem 0}.npm-stats img{margin:0 .5rem;display:inline-block}.custom-container{max-width:1152px;margin:0 auto;padding:0 24px}.verdict-quote{text-align:center;font-size:1.5rem;margin:3rem 0;padding:2rem;background:linear-gradient(135deg,var(--vp-c-brand-soft) 0%,transparent 100%);border-radius:12px}table{width:100%;border-collapse:collapse;margin:2rem 0}th{background-color:var(--vp-c-bg-soft);padding:.75rem;text-align:left}td{padding:.75rem;border-bottom:1px solid var(--vp-c-divider)}tr:hover{background-color:var(--vp-c-bg-soft)} +@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-roman-cyrillic.C5lxZ8CY.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-roman-greek-ext.CqjqNYQ-.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-roman-greek.BBVDIX6e.woff2) format("woff2");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-roman-vietnamese.BjW4sHH5.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-roman-latin-ext.4ZJIpNVo.woff2) format("woff2");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-roman-latin.Di8DUHzh.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-italic-cyrillic-ext.r48I6akx.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-italic-cyrillic.By2_1cv3.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-italic-greek-ext.1u6EdAuj.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-italic-greek.DJ8dCoTZ.woff2) format("woff2");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-italic-vietnamese.BSbpV94h.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-italic-latin-ext.CN1xVJS-.woff2) format("woff2");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/sigpro/assets/inter-italic-latin.C2AdPX0b.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Punctuation SC;font-weight:400;src:local("PingFang SC Regular"),local("Noto Sans CJK SC"),local("Microsoft YaHei");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:500;src:local("PingFang SC Medium"),local("Noto Sans CJK SC"),local("Microsoft YaHei");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:600;src:local("PingFang SC Semibold"),local("Noto Sans CJK SC Bold"),local("Microsoft YaHei Bold");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:700;src:local("PingFang SC Semibold"),local("Noto Sans CJK SC Bold"),local("Microsoft YaHei Bold");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}:root{--vp-c-white: #ffffff;--vp-c-black: #000000;--vp-c-neutral: var(--vp-c-black);--vp-c-neutral-inverse: var(--vp-c-white)}.dark{--vp-c-neutral: var(--vp-c-white);--vp-c-neutral-inverse: var(--vp-c-black)}:root{--vp-c-gray-1: #dddde3;--vp-c-gray-2: #e4e4e9;--vp-c-gray-3: #ebebef;--vp-c-gray-soft: rgba(142, 150, 170, .14);--vp-c-indigo-1: #3451b2;--vp-c-indigo-2: #3a5ccc;--vp-c-indigo-3: #5672cd;--vp-c-indigo-soft: rgba(100, 108, 255, .14);--vp-c-purple-1: #6f42c1;--vp-c-purple-2: #7e4cc9;--vp-c-purple-3: #8e5cd9;--vp-c-purple-soft: rgba(159, 122, 234, .14);--vp-c-green-1: #18794e;--vp-c-green-2: #299764;--vp-c-green-3: #30a46c;--vp-c-green-soft: rgba(16, 185, 129, .14);--vp-c-yellow-1: #915930;--vp-c-yellow-2: #946300;--vp-c-yellow-3: #9f6a00;--vp-c-yellow-soft: rgba(234, 179, 8, .14);--vp-c-red-1: #b8272c;--vp-c-red-2: #d5393e;--vp-c-red-3: #e0575b;--vp-c-red-soft: rgba(244, 63, 94, .14);--vp-c-sponsor: #db2777}.dark{--vp-c-gray-1: #515c67;--vp-c-gray-2: #414853;--vp-c-gray-3: #32363f;--vp-c-gray-soft: rgba(101, 117, 133, .16);--vp-c-indigo-1: #a8b1ff;--vp-c-indigo-2: #5c73e7;--vp-c-indigo-3: #3e63dd;--vp-c-indigo-soft: rgba(100, 108, 255, .16);--vp-c-purple-1: #c8abfa;--vp-c-purple-2: #a879e6;--vp-c-purple-3: #8e5cd9;--vp-c-purple-soft: rgba(159, 122, 234, .16);--vp-c-green-1: #3dd68c;--vp-c-green-2: #30a46c;--vp-c-green-3: #298459;--vp-c-green-soft: rgba(16, 185, 129, .16);--vp-c-yellow-1: #f9b44e;--vp-c-yellow-2: #da8b17;--vp-c-yellow-3: #a46a0a;--vp-c-yellow-soft: rgba(234, 179, 8, .16);--vp-c-red-1: #f66f81;--vp-c-red-2: #f14158;--vp-c-red-3: #b62a3c;--vp-c-red-soft: rgba(244, 63, 94, .16)}:root{--vp-c-bg: #ffffff;--vp-c-bg-alt: #f6f6f7;--vp-c-bg-elv: #ffffff;--vp-c-bg-soft: #f6f6f7}.dark{--vp-c-bg: #1b1b1f;--vp-c-bg-alt: #161618;--vp-c-bg-elv: #202127;--vp-c-bg-soft: #202127}:root{--vp-c-border: #c2c2c4;--vp-c-divider: #e2e2e3;--vp-c-gutter: #e2e2e3}.dark{--vp-c-border: #3c3f44;--vp-c-divider: #2e2e32;--vp-c-gutter: #000000}:root{--vp-c-text-1: #3c3c43;--vp-c-text-2: #67676c;--vp-c-text-3: #929295}.dark{--vp-c-text-1: #dfdfd6;--vp-c-text-2: #98989f;--vp-c-text-3: #6a6a71}:root{--vp-c-default-1: var(--vp-c-gray-1);--vp-c-default-2: var(--vp-c-gray-2);--vp-c-default-3: var(--vp-c-gray-3);--vp-c-default-soft: var(--vp-c-gray-soft);--vp-c-brand-1: var(--vp-c-indigo-1);--vp-c-brand-2: var(--vp-c-indigo-2);--vp-c-brand-3: var(--vp-c-indigo-3);--vp-c-brand-soft: var(--vp-c-indigo-soft);--vp-c-brand: var(--vp-c-brand-1);--vp-c-tip-1: var(--vp-c-brand-1);--vp-c-tip-2: var(--vp-c-brand-2);--vp-c-tip-3: var(--vp-c-brand-3);--vp-c-tip-soft: var(--vp-c-brand-soft);--vp-c-note-1: var(--vp-c-brand-1);--vp-c-note-2: var(--vp-c-brand-2);--vp-c-note-3: var(--vp-c-brand-3);--vp-c-note-soft: var(--vp-c-brand-soft);--vp-c-success-1: var(--vp-c-green-1);--vp-c-success-2: var(--vp-c-green-2);--vp-c-success-3: var(--vp-c-green-3);--vp-c-success-soft: var(--vp-c-green-soft);--vp-c-important-1: var(--vp-c-purple-1);--vp-c-important-2: var(--vp-c-purple-2);--vp-c-important-3: var(--vp-c-purple-3);--vp-c-important-soft: var(--vp-c-purple-soft);--vp-c-warning-1: var(--vp-c-yellow-1);--vp-c-warning-2: var(--vp-c-yellow-2);--vp-c-warning-3: var(--vp-c-yellow-3);--vp-c-warning-soft: var(--vp-c-yellow-soft);--vp-c-danger-1: var(--vp-c-red-1);--vp-c-danger-2: var(--vp-c-red-2);--vp-c-danger-3: var(--vp-c-red-3);--vp-c-danger-soft: var(--vp-c-red-soft);--vp-c-caution-1: var(--vp-c-red-1);--vp-c-caution-2: var(--vp-c-red-2);--vp-c-caution-3: var(--vp-c-red-3);--vp-c-caution-soft: var(--vp-c-red-soft)}:root{--vp-font-family-base: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--vp-font-family-mono: ui-monospace, "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace;font-optical-sizing:auto}:root:where(:lang(zh)){--vp-font-family-base: "Punctuation SC", "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"}:root{--vp-shadow-1: 0 1px 2px rgba(0, 0, 0, .04), 0 1px 2px rgba(0, 0, 0, .06);--vp-shadow-2: 0 3px 12px rgba(0, 0, 0, .07), 0 1px 4px rgba(0, 0, 0, .07);--vp-shadow-3: 0 12px 32px rgba(0, 0, 0, .1), 0 2px 6px rgba(0, 0, 0, .08);--vp-shadow-4: 0 14px 44px rgba(0, 0, 0, .12), 0 3px 9px rgba(0, 0, 0, .12);--vp-shadow-5: 0 18px 56px rgba(0, 0, 0, .16), 0 4px 12px rgba(0, 0, 0, .16)}:root{--vp-z-index-footer: 10;--vp-z-index-local-nav: 20;--vp-z-index-nav: 30;--vp-z-index-layout-top: 40;--vp-z-index-backdrop: 50;--vp-z-index-sidebar: 60}@media (min-width: 960px){:root{--vp-z-index-sidebar: 25}}:root{--vp-layout-max-width: 1440px}:root{--vp-header-anchor-symbol: "#"}:root{--vp-code-line-height: 1.7;--vp-code-font-size: .875em;--vp-code-color: var(--vp-c-brand-1);--vp-code-link-color: var(--vp-c-brand-1);--vp-code-link-hover-color: var(--vp-c-brand-2);--vp-code-bg: var(--vp-c-default-soft);--vp-code-block-color: var(--vp-c-text-2);--vp-code-block-bg: var(--vp-c-bg-alt);--vp-code-block-divider-color: var(--vp-c-gutter);--vp-code-lang-color: var(--vp-c-text-3);--vp-code-line-highlight-color: var(--vp-c-default-soft);--vp-code-line-number-color: var(--vp-c-text-3);--vp-code-line-diff-add-color: var(--vp-c-success-soft);--vp-code-line-diff-add-symbol-color: var(--vp-c-success-1);--vp-code-line-diff-remove-color: var(--vp-c-danger-soft);--vp-code-line-diff-remove-symbol-color: var(--vp-c-danger-1);--vp-code-line-warning-color: var(--vp-c-warning-soft);--vp-code-line-error-color: var(--vp-c-danger-soft);--vp-code-copy-code-border-color: var(--vp-c-divider);--vp-code-copy-code-bg: var(--vp-c-bg-soft);--vp-code-copy-code-hover-border-color: var(--vp-c-divider);--vp-code-copy-code-hover-bg: var(--vp-c-bg);--vp-code-copy-code-active-text: var(--vp-c-text-2);--vp-code-copy-copied-text-content: "Copied";--vp-code-tab-divider: var(--vp-code-block-divider-color);--vp-code-tab-text-color: var(--vp-c-text-2);--vp-code-tab-bg: var(--vp-code-block-bg);--vp-code-tab-hover-text-color: var(--vp-c-text-1);--vp-code-tab-active-text-color: var(--vp-c-text-1);--vp-code-tab-active-bar-color: var(--vp-c-brand-1)}:lang(es),:lang(pt){--vp-code-copy-copied-text-content: "Copiado"}:lang(fa){--vp-code-copy-copied-text-content: "کپی شد"}:lang(ko){--vp-code-copy-copied-text-content: "복사됨"}:lang(ru){--vp-code-copy-copied-text-content: "Скопировано"}:lang(zh){--vp-code-copy-copied-text-content: "已复制"}:root{--vp-button-brand-border: transparent;--vp-button-brand-text: var(--vp-c-white);--vp-button-brand-bg: var(--vp-c-brand-3);--vp-button-brand-hover-border: transparent;--vp-button-brand-hover-text: var(--vp-c-white);--vp-button-brand-hover-bg: var(--vp-c-brand-2);--vp-button-brand-active-border: transparent;--vp-button-brand-active-text: var(--vp-c-white);--vp-button-brand-active-bg: var(--vp-c-brand-1);--vp-button-alt-border: transparent;--vp-button-alt-text: var(--vp-c-text-1);--vp-button-alt-bg: var(--vp-c-default-3);--vp-button-alt-hover-border: transparent;--vp-button-alt-hover-text: var(--vp-c-text-1);--vp-button-alt-hover-bg: var(--vp-c-default-2);--vp-button-alt-active-border: transparent;--vp-button-alt-active-text: var(--vp-c-text-1);--vp-button-alt-active-bg: var(--vp-c-default-1);--vp-button-sponsor-border: var(--vp-c-text-2);--vp-button-sponsor-text: var(--vp-c-text-2);--vp-button-sponsor-bg: transparent;--vp-button-sponsor-hover-border: var(--vp-c-sponsor);--vp-button-sponsor-hover-text: var(--vp-c-sponsor);--vp-button-sponsor-hover-bg: transparent;--vp-button-sponsor-active-border: var(--vp-c-sponsor);--vp-button-sponsor-active-text: var(--vp-c-sponsor);--vp-button-sponsor-active-bg: transparent}:root{--vp-custom-block-font-size: 14px;--vp-custom-block-code-font-size: 13px;--vp-custom-block-info-border: transparent;--vp-custom-block-info-text: var(--vp-c-text-1);--vp-custom-block-info-bg: var(--vp-c-default-soft);--vp-custom-block-info-code-bg: var(--vp-c-default-soft);--vp-custom-block-note-border: transparent;--vp-custom-block-note-text: var(--vp-c-text-1);--vp-custom-block-note-bg: var(--vp-c-default-soft);--vp-custom-block-note-code-bg: var(--vp-c-default-soft);--vp-custom-block-tip-border: transparent;--vp-custom-block-tip-text: var(--vp-c-text-1);--vp-custom-block-tip-bg: var(--vp-c-tip-soft);--vp-custom-block-tip-code-bg: var(--vp-c-tip-soft);--vp-custom-block-important-border: transparent;--vp-custom-block-important-text: var(--vp-c-text-1);--vp-custom-block-important-bg: var(--vp-c-important-soft);--vp-custom-block-important-code-bg: var(--vp-c-important-soft);--vp-custom-block-warning-border: transparent;--vp-custom-block-warning-text: var(--vp-c-text-1);--vp-custom-block-warning-bg: var(--vp-c-warning-soft);--vp-custom-block-warning-code-bg: var(--vp-c-warning-soft);--vp-custom-block-danger-border: transparent;--vp-custom-block-danger-text: var(--vp-c-text-1);--vp-custom-block-danger-bg: var(--vp-c-danger-soft);--vp-custom-block-danger-code-bg: var(--vp-c-danger-soft);--vp-custom-block-caution-border: transparent;--vp-custom-block-caution-text: var(--vp-c-text-1);--vp-custom-block-caution-bg: var(--vp-c-caution-soft);--vp-custom-block-caution-code-bg: var(--vp-c-caution-soft);--vp-custom-block-details-border: var(--vp-custom-block-info-border);--vp-custom-block-details-text: var(--vp-custom-block-info-text);--vp-custom-block-details-bg: var(--vp-custom-block-info-bg);--vp-custom-block-details-code-bg: var(--vp-custom-block-info-code-bg)}:root{--vp-input-border-color: var(--vp-c-border);--vp-input-bg-color: var(--vp-c-bg-alt);--vp-input-switch-bg-color: var(--vp-c-default-soft)}:root{--vp-nav-height: 64px;--vp-nav-bg-color: var(--vp-c-bg);--vp-nav-screen-bg-color: var(--vp-c-bg);--vp-nav-logo-height: 24px}.hide-nav{--vp-nav-height: 0px}.hide-nav .VPSidebar{--vp-nav-height: 22px}:root{--vp-local-nav-bg-color: var(--vp-c-bg)}:root{--vp-sidebar-width: 272px;--vp-sidebar-bg-color: var(--vp-c-bg-alt)}:root{--vp-backdrop-bg-color: rgba(0, 0, 0, .6)}:root{--vp-home-hero-name-color: var(--vp-c-brand-1);--vp-home-hero-name-background: transparent;--vp-home-hero-image-background-image: none;--vp-home-hero-image-filter: none}:root{--vp-badge-info-border: transparent;--vp-badge-info-text: var(--vp-c-text-2);--vp-badge-info-bg: var(--vp-c-default-soft);--vp-badge-tip-border: transparent;--vp-badge-tip-text: var(--vp-c-tip-1);--vp-badge-tip-bg: var(--vp-c-tip-soft);--vp-badge-warning-border: transparent;--vp-badge-warning-text: var(--vp-c-warning-1);--vp-badge-warning-bg: var(--vp-c-warning-soft);--vp-badge-danger-border: transparent;--vp-badge-danger-text: var(--vp-c-danger-1);--vp-badge-danger-bg: var(--vp-c-danger-soft)}:root{--vp-carbon-ads-text-color: var(--vp-c-text-1);--vp-carbon-ads-poweredby-color: var(--vp-c-text-2);--vp-carbon-ads-bg-color: var(--vp-c-bg-soft);--vp-carbon-ads-hover-text-color: var(--vp-c-brand-1);--vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1)}:root{--vp-local-search-bg: var(--vp-c-bg);--vp-local-search-result-bg: var(--vp-c-bg);--vp-local-search-result-border: var(--vp-c-divider);--vp-local-search-result-selected-bg: var(--vp-c-bg);--vp-local-search-result-selected-border: var(--vp-c-brand-1);--vp-local-search-highlight-bg: var(--vp-c-brand-1);--vp-local-search-highlight-text: var(--vp-c-neutral-inverse)}@media (prefers-reduced-motion: reduce){*,:before,:after{animation-delay:-1ms!important;animation-duration:1ms!important;animation-iteration-count:1!important;background-attachment:initial!important;scroll-behavior:auto!important;transition-duration:0s!important;transition-delay:0s!important}}*,:before,:after{box-sizing:border-box}html{line-height:1.4;font-size:16px;-webkit-text-size-adjust:100%}html.dark{color-scheme:dark}body{margin:0;width:100%;min-width:320px;min-height:100vh;line-height:24px;font-family:var(--vp-font-family-base);font-size:16px;font-weight:400;color:var(--vp-c-text-1);background-color:var(--vp-c-bg);font-synthesis:style;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}main{display:block}h1,h2,h3,h4,h5,h6{margin:0;line-height:24px;font-size:16px;font-weight:400}p{margin:0}strong,b{font-weight:600}a,area,button,[role=button],input,label,select,summary,textarea{touch-action:manipulation}a{color:inherit;text-decoration:inherit}ol,ul{list-style:none;margin:0;padding:0}blockquote{margin:0}pre,code,kbd,samp{font-family:var(--vp-font-family-mono)}img,svg,video,canvas,audio,iframe,embed,object{display:block}figure{margin:0}img,video{max-width:100%;height:auto}button,input,optgroup,select,textarea{border:0;padding:0;line-height:inherit;color:inherit}button{padding:0;font-family:inherit;background-color:transparent;background-image:none}button:enabled,[role=button]:enabled{cursor:pointer}button:focus,button:focus-visible{outline:1px dotted;outline:4px auto -webkit-focus-ring-color}button:focus:not(:focus-visible){outline:none!important}input:focus,textarea:focus,select:focus{outline:none}table{border-collapse:collapse}input{background-color:transparent}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:var(--vp-c-text-3)}input::-ms-input-placeholder,textarea::-ms-input-placeholder{color:var(--vp-c-text-3)}input::placeholder,textarea::placeholder{color:var(--vp-c-text-3)}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=number]{-moz-appearance:textfield}textarea{resize:vertical}select{-webkit-appearance:none}fieldset{margin:0;padding:0}h1,h2,h3,h4,h5,h6,li,p{overflow-wrap:break-word}vite-error-overlay{z-index:9999}mjx-container{overflow-x:auto}mjx-container>svg{display:inline-block;margin:auto}[class^=vpi-],[class*=" vpi-"],.vp-icon{width:1em;height:1em}[class^=vpi-].bg,[class*=" vpi-"].bg,.vp-icon.bg{background-size:100% 100%;background-color:transparent}[class^=vpi-]:not(.bg),[class*=" vpi-"]:not(.bg),.vp-icon:not(.bg){-webkit-mask:var(--icon) no-repeat;mask:var(--icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit}.vpi-align-left{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M21 6H3M15 12H3M17 18H3'/%3E%3C/svg%3E")}.vpi-arrow-right,.vpi-arrow-down,.vpi-arrow-left,.vpi-arrow-up{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5l7 7-7 7'/%3E%3C/svg%3E")}.vpi-chevron-right,.vpi-chevron-down,.vpi-chevron-left,.vpi-chevron-up{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E")}.vpi-chevron-down,.vpi-arrow-down{transform:rotate(90deg)}.vpi-chevron-left,.vpi-arrow-left{transform:rotate(180deg)}.vpi-chevron-up,.vpi-arrow-up{transform:rotate(-90deg)}.vpi-square-pen{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'/%3E%3Cpath d='M18.375 2.625a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4Z'/%3E%3C/svg%3E")}.vpi-plus{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5v14'/%3E%3C/svg%3E")}.vpi-sun{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41'/%3E%3C/svg%3E")}.vpi-moon{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z'/%3E%3C/svg%3E")}.vpi-more-horizontal{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='1'/%3E%3Ccircle cx='19' cy='12' r='1'/%3E%3Ccircle cx='5' cy='12' r='1'/%3E%3C/svg%3E")}.vpi-languages{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m5 8 6 6M4 14l6-6 2-3M2 5h12M7 2h1M22 22l-5-10-5 10M14 18h6'/%3E%3C/svg%3E")}.vpi-heart{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z'/%3E%3C/svg%3E")}.vpi-search{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E")}.vpi-layout-list{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='7' height='7' x='3' y='3' rx='1'/%3E%3Crect width='7' height='7' x='3' y='14' rx='1'/%3E%3Cpath d='M14 4h7M14 9h7M14 15h7M14 20h7'/%3E%3C/svg%3E")}.vpi-delete{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M20 5H9l-7 7 7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2ZM18 9l-6 6M12 9l6 6'/%3E%3C/svg%3E")}.vpi-corner-down-left{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 10-5 5 5 5'/%3E%3Cpath d='M20 4v7a4 4 0 0 1-4 4H4'/%3E%3C/svg%3E")}:root{--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E");--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E")}.visually-hidden{position:absolute;width:1px;height:1px;white-space:nowrap;clip:rect(0 0 0 0);clip-path:inset(50%);overflow:hidden}.custom-block{border:1px solid transparent;border-radius:8px;padding:16px 16px 8px;line-height:24px;font-size:var(--vp-custom-block-font-size);color:var(--vp-c-text-2)}.custom-block.info{border-color:var(--vp-custom-block-info-border);color:var(--vp-custom-block-info-text);background-color:var(--vp-custom-block-info-bg)}.custom-block.info a,.custom-block.info code{color:var(--vp-c-brand-1)}.custom-block.info a:hover,.custom-block.info a:hover>code{color:var(--vp-c-brand-2)}.custom-block.info code{background-color:var(--vp-custom-block-info-code-bg)}.custom-block.note{border-color:var(--vp-custom-block-note-border);color:var(--vp-custom-block-note-text);background-color:var(--vp-custom-block-note-bg)}.custom-block.note a,.custom-block.note code{color:var(--vp-c-brand-1)}.custom-block.note a:hover,.custom-block.note a:hover>code{color:var(--vp-c-brand-2)}.custom-block.note code{background-color:var(--vp-custom-block-note-code-bg)}.custom-block.tip{border-color:var(--vp-custom-block-tip-border);color:var(--vp-custom-block-tip-text);background-color:var(--vp-custom-block-tip-bg)}.custom-block.tip a,.custom-block.tip code{color:var(--vp-c-tip-1)}.custom-block.tip a:hover,.custom-block.tip a:hover>code{color:var(--vp-c-tip-2)}.custom-block.tip code{background-color:var(--vp-custom-block-tip-code-bg)}.custom-block.important{border-color:var(--vp-custom-block-important-border);color:var(--vp-custom-block-important-text);background-color:var(--vp-custom-block-important-bg)}.custom-block.important a,.custom-block.important code{color:var(--vp-c-important-1)}.custom-block.important a:hover,.custom-block.important a:hover>code{color:var(--vp-c-important-2)}.custom-block.important code{background-color:var(--vp-custom-block-important-code-bg)}.custom-block.warning{border-color:var(--vp-custom-block-warning-border);color:var(--vp-custom-block-warning-text);background-color:var(--vp-custom-block-warning-bg)}.custom-block.warning a,.custom-block.warning code{color:var(--vp-c-warning-1)}.custom-block.warning a:hover,.custom-block.warning a:hover>code{color:var(--vp-c-warning-2)}.custom-block.warning code{background-color:var(--vp-custom-block-warning-code-bg)}.custom-block.danger{border-color:var(--vp-custom-block-danger-border);color:var(--vp-custom-block-danger-text);background-color:var(--vp-custom-block-danger-bg)}.custom-block.danger a,.custom-block.danger code{color:var(--vp-c-danger-1)}.custom-block.danger a:hover,.custom-block.danger a:hover>code{color:var(--vp-c-danger-2)}.custom-block.danger code{background-color:var(--vp-custom-block-danger-code-bg)}.custom-block.caution{border-color:var(--vp-custom-block-caution-border);color:var(--vp-custom-block-caution-text);background-color:var(--vp-custom-block-caution-bg)}.custom-block.caution a,.custom-block.caution code{color:var(--vp-c-caution-1)}.custom-block.caution a:hover,.custom-block.caution a:hover>code{color:var(--vp-c-caution-2)}.custom-block.caution code{background-color:var(--vp-custom-block-caution-code-bg)}.custom-block.details{border-color:var(--vp-custom-block-details-border);color:var(--vp-custom-block-details-text);background-color:var(--vp-custom-block-details-bg)}.custom-block.details a{color:var(--vp-c-brand-1)}.custom-block.details a:hover,.custom-block.details a:hover>code{color:var(--vp-c-brand-2)}.custom-block.details code{background-color:var(--vp-custom-block-details-code-bg)}.custom-block-title{font-weight:600}.custom-block p+p{margin:8px 0}.custom-block.details summary{margin:0 0 8px;font-weight:700;cursor:pointer;-webkit-user-select:none;user-select:none}.custom-block.details summary+p{margin:8px 0}.custom-block a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px;transition:opacity .25s}.custom-block a:hover{opacity:.75}.custom-block code{font-size:var(--vp-custom-block-code-font-size)}.custom-block.custom-block th,.custom-block.custom-block blockquote>p{font-size:var(--vp-custom-block-font-size);color:inherit}.dark .vp-code span{color:var(--shiki-dark, inherit)}html:not(.dark) .vp-code span{color:var(--shiki-light, inherit)}.vp-code-group{margin-top:16px}.vp-code-group .tabs{position:relative;display:flex;margin-right:-24px;margin-left:-24px;padding:0 12px;background-color:var(--vp-code-tab-bg);overflow-x:auto;overflow-y:hidden;box-shadow:inset 0 -1px var(--vp-code-tab-divider)}@media (min-width: 640px){.vp-code-group .tabs{margin-right:0;margin-left:0;border-radius:8px 8px 0 0}}.vp-code-group .tabs input{position:fixed;opacity:0;pointer-events:none}.vp-code-group .tabs label{position:relative;display:inline-block;border-bottom:1px solid transparent;padding:0 12px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-code-tab-text-color);white-space:nowrap;cursor:pointer;transition:color .25s}.vp-code-group .tabs label:after{position:absolute;right:8px;bottom:-1px;left:8px;z-index:1;height:2px;border-radius:2px;content:"";background-color:transparent;transition:background-color .25s}.vp-code-group label:hover{color:var(--vp-code-tab-hover-text-color)}.vp-code-group input:checked+label{color:var(--vp-code-tab-active-text-color)}.vp-code-group input:checked+label:after{background-color:var(--vp-code-tab-active-bar-color)}.vp-code-group div[class*=language-],.vp-block{display:none;margin-top:0!important;border-top-left-radius:0!important;border-top-right-radius:0!important}.vp-code-group div[class*=language-].active,.vp-block.active{display:block}.vp-block{padding:20px 24px}.vp-doc h1,.vp-doc h2,.vp-doc h3,.vp-doc h4,.vp-doc h5,.vp-doc h6{position:relative;font-weight:600;outline:none}.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:28px}.vp-doc h2{margin:48px 0 16px;border-top:1px solid var(--vp-c-divider);padding-top:24px;letter-spacing:-.02em;line-height:32px;font-size:24px}.vp-doc h3{margin:32px 0 0;letter-spacing:-.01em;line-height:28px;font-size:20px}.vp-doc h4{margin:24px 0 0;letter-spacing:-.01em;line-height:24px;font-size:18px}.vp-doc .header-anchor{position:absolute;top:0;left:0;margin-left:-.87em;font-weight:500;-webkit-user-select:none;user-select:none;opacity:0;text-decoration:none;transition:color .25s,opacity .25s}.vp-doc .header-anchor:before{content:var(--vp-header-anchor-symbol)}.vp-doc h1:hover .header-anchor,.vp-doc h1 .header-anchor:focus,.vp-doc h2:hover .header-anchor,.vp-doc h2 .header-anchor:focus,.vp-doc h3:hover .header-anchor,.vp-doc h3 .header-anchor:focus,.vp-doc h4:hover .header-anchor,.vp-doc h4 .header-anchor:focus,.vp-doc h5:hover .header-anchor,.vp-doc h5 .header-anchor:focus,.vp-doc h6:hover .header-anchor,.vp-doc h6 .header-anchor:focus{opacity:1}@media (min-width: 768px){.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:32px}}.vp-doc h2 .header-anchor{top:24px}.vp-doc p,.vp-doc summary{margin:16px 0}.vp-doc p{line-height:28px}.vp-doc blockquote{margin:16px 0;border-left:2px solid var(--vp-c-divider);padding-left:16px;transition:border-color .5s;color:var(--vp-c-text-2)}.vp-doc blockquote>p{margin:0;font-size:16px;transition:color .5s}.vp-doc a{font-weight:500;color:var(--vp-c-brand-1);text-decoration:underline;text-underline-offset:2px;transition:color .25s,opacity .25s}.vp-doc a:hover{color:var(--vp-c-brand-2)}.vp-doc strong{font-weight:600}.vp-doc ul,.vp-doc ol{padding-left:1.25rem;margin:16px 0}.vp-doc ul{list-style:disc}.vp-doc ol{list-style:decimal}.vp-doc li+li{margin-top:8px}.vp-doc li>ol,.vp-doc li>ul{margin:8px 0 0}.vp-doc table{display:block;border-collapse:collapse;margin:20px 0;overflow-x:auto}.vp-doc tr{background-color:var(--vp-c-bg);border-top:1px solid var(--vp-c-divider);transition:background-color .5s}.vp-doc tr:nth-child(2n){background-color:var(--vp-c-bg-soft)}.vp-doc th,.vp-doc td{border:1px solid var(--vp-c-divider);padding:8px 16px}.vp-doc th{text-align:left;font-size:14px;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-doc td{font-size:14px}.vp-doc hr{margin:16px 0;border:none;border-top:1px solid var(--vp-c-divider)}.vp-doc .custom-block{margin:16px 0}.vp-doc .custom-block p{margin:8px 0;line-height:24px}.vp-doc .custom-block p:first-child{margin:0}.vp-doc .custom-block div[class*=language-]{margin:8px 0;border-radius:8px}.vp-doc .custom-block div[class*=language-] code{font-weight:400;background-color:transparent}.vp-doc .custom-block .vp-code-group .tabs{margin:0;border-radius:8px 8px 0 0}.vp-doc :not(pre,h1,h2,h3,h4,h5,h6)>code{font-size:var(--vp-code-font-size);color:var(--vp-code-color)}.vp-doc :not(pre)>code{border-radius:4px;padding:3px 6px;background-color:var(--vp-code-bg);transition:color .25s,background-color .5s}.vp-doc a>code{color:var(--vp-code-link-color)}.vp-doc a:hover>code{color:var(--vp-code-link-hover-color)}.vp-doc h1>code,.vp-doc h2>code,.vp-doc h3>code,.vp-doc h4>code{font-size:.9em}.vp-doc div[class*=language-],.vp-block{position:relative;margin:16px -24px;background-color:var(--vp-code-block-bg);overflow-x:auto;transition:background-color .5s}@media (min-width: 640px){.vp-doc div[class*=language-],.vp-block{border-radius:8px;margin:16px 0}}@media (max-width: 639px){.vp-doc li div[class*=language-]{border-radius:8px 0 0 8px}}.vp-doc div[class*=language-]+div[class*=language-],.vp-doc div[class$=-api]+div[class*=language-],.vp-doc div[class*=language-]+div[class$=-api]>div[class*=language-]{margin-top:-8px}.vp-doc [class*=language-] pre,.vp-doc [class*=language-] code{direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}.vp-doc [class*=language-] pre{position:relative;z-index:1;margin:0;padding:20px 0;background:transparent;overflow-x:auto}.vp-doc [class*=language-] code{display:block;padding:0 24px;width:fit-content;min-width:100%;line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-block-color);transition:color .5s}.vp-doc [class*=language-] code .highlighted{background-color:var(--vp-code-line-highlight-color);transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .highlighted.error{background-color:var(--vp-code-line-error-color)}.vp-doc [class*=language-] code .highlighted.warning{background-color:var(--vp-code-line-warning-color)}.vp-doc [class*=language-] code .diff{transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .diff:before{position:absolute;left:10px}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){filter:blur(.095rem);opacity:.4;transition:filter .35s,opacity .35s}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){opacity:.7;transition:filter .35s,opacity .35s}.vp-doc [class*=language-]:hover .has-focused-lines .line:not(.has-focus){filter:blur(0);opacity:1}.vp-doc [class*=language-] code .diff.remove{background-color:var(--vp-code-line-diff-remove-color);opacity:.7}.vp-doc [class*=language-] code .diff.remove:before{content:"-";color:var(--vp-code-line-diff-remove-symbol-color)}.vp-doc [class*=language-] code .diff.add{background-color:var(--vp-code-line-diff-add-color)}.vp-doc [class*=language-] code .diff.add:before{content:"+";color:var(--vp-code-line-diff-add-symbol-color)}.vp-doc div[class*=language-].line-numbers-mode{padding-left:32px}.vp-doc .line-numbers-wrapper{position:absolute;top:0;bottom:0;left:0;z-index:3;border-right:1px solid var(--vp-code-block-divider-color);padding-top:20px;width:32px;text-align:center;font-family:var(--vp-font-family-mono);line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-line-number-color);transition:border-color .5s,color .5s}.vp-doc [class*=language-]>button.copy{direction:ltr;position:absolute;top:12px;right:12px;z-index:3;border:1px solid var(--vp-code-copy-code-border-color);border-radius:4px;width:40px;height:40px;background-color:var(--vp-code-copy-code-bg);opacity:0;cursor:pointer;background-image:var(--vp-icon-copy);background-position:50%;background-size:20px;background-repeat:no-repeat;transition:border-color .25s,background-color .25s,opacity .25s}.vp-doc [class*=language-]:hover>button.copy,.vp-doc [class*=language-]>button.copy:focus{opacity:1}.vp-doc [class*=language-]>button.copy:hover,.vp-doc [class*=language-]>button.copy.copied{border-color:var(--vp-code-copy-code-hover-border-color);background-color:var(--vp-code-copy-code-hover-bg)}.vp-doc [class*=language-]>button.copy.copied,.vp-doc [class*=language-]>button.copy:hover.copied{border-radius:0 4px 4px 0;background-color:var(--vp-code-copy-code-hover-bg);background-image:var(--vp-icon-copied)}.vp-doc [class*=language-]>button.copy.copied:before,.vp-doc [class*=language-]>button.copy:hover.copied:before{position:relative;top:-1px;transform:translate(calc(-100% - 1px));display:flex;justify-content:center;align-items:center;border:1px solid var(--vp-code-copy-code-hover-border-color);border-right:0;border-radius:4px 0 0 4px;padding:0 10px;width:fit-content;height:40px;text-align:center;font-size:12px;font-weight:500;color:var(--vp-code-copy-code-active-text);background-color:var(--vp-code-copy-code-hover-bg);white-space:nowrap;content:var(--vp-code-copy-copied-text-content)}.vp-doc [class*=language-]>span.lang{position:absolute;top:2px;right:8px;z-index:2;font-size:12px;font-weight:500;-webkit-user-select:none;user-select:none;color:var(--vp-code-lang-color);transition:color .4s,opacity .4s}.vp-doc [class*=language-]:hover>button.copy+span.lang,.vp-doc [class*=language-]>button.copy:focus+span.lang{opacity:0}.vp-doc .VPTeamMembers{margin-top:24px}.vp-doc .VPTeamMembers.small.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}.vp-doc .VPTeamMembers.small.count-2 .container,.vp-doc .VPTeamMembers.small.count-3 .container{max-width:100%!important}.vp-doc .VPTeamMembers.medium.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}:is(.vp-external-link-icon,.vp-doc a[href*="://"],.vp-doc a[target=_blank]):not(:is(.no-icon,svg a,:has(img,svg))):after{display:inline-block;margin-top:-1px;margin-left:4px;width:11px;height:11px;background:currentColor;color:var(--vp-c-text-3);flex-shrink:0;--icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E");-webkit-mask-image:var(--icon);mask-image:var(--icon)}.vp-external-link-icon:after{content:""}.external-link-icon-enabled :is(.vp-doc a[href*="://"],.vp-doc a[target=_blank]):not(:is(.no-icon,svg a,:has(img,svg))):after{content:"";color:currentColor}.vp-sponsor{border-radius:16px;overflow:hidden}.vp-sponsor.aside{border-radius:12px}.vp-sponsor-section+.vp-sponsor-section{margin-top:4px}.vp-sponsor-tier{margin:0 0 4px!important;text-align:center;letter-spacing:1px!important;line-height:24px;width:100%;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-sponsor.normal .vp-sponsor-tier{padding:13px 0 11px;font-size:14px}.vp-sponsor.aside .vp-sponsor-tier{padding:9px 0 7px;font-size:12px}.vp-sponsor-grid+.vp-sponsor-tier{margin-top:4px}.vp-sponsor-grid{display:flex;flex-wrap:wrap;gap:4px}.vp-sponsor-grid.xmini .vp-sponsor-grid-link{height:64px}.vp-sponsor-grid.xmini .vp-sponsor-grid-image{max-width:64px;max-height:22px}.vp-sponsor-grid.mini .vp-sponsor-grid-link{height:72px}.vp-sponsor-grid.mini .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.small .vp-sponsor-grid-link{height:96px}.vp-sponsor-grid.small .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.medium .vp-sponsor-grid-link{height:112px}.vp-sponsor-grid.medium .vp-sponsor-grid-image{max-width:120px;max-height:36px}.vp-sponsor-grid.big .vp-sponsor-grid-link{height:184px}.vp-sponsor-grid.big .vp-sponsor-grid-image{max-width:192px;max-height:56px}.vp-sponsor-grid[data-vp-grid="2"] .vp-sponsor-grid-item{width:calc((100% - 4px)/2)}.vp-sponsor-grid[data-vp-grid="3"] .vp-sponsor-grid-item{width:calc((100% - 4px * 2) / 3)}.vp-sponsor-grid[data-vp-grid="4"] .vp-sponsor-grid-item{width:calc((100% - 12px)/4)}.vp-sponsor-grid[data-vp-grid="5"] .vp-sponsor-grid-item{width:calc((100% - 16px)/5)}.vp-sponsor-grid[data-vp-grid="6"] .vp-sponsor-grid-item{width:calc((100% - 4px * 5) / 6)}.vp-sponsor-grid-item{flex-shrink:0;width:100%;background-color:var(--vp-c-bg-soft);transition:background-color .25s}.vp-sponsor-grid-item:hover{background-color:var(--vp-c-default-soft)}.vp-sponsor-grid-item:hover .vp-sponsor-grid-image{filter:grayscale(0) invert(0)}.vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.dark .vp-sponsor-grid-item:hover{background-color:var(--vp-c-white)}.dark .vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.vp-sponsor-grid-link{display:flex}.vp-sponsor-grid-box{display:flex;justify-content:center;align-items:center;width:100%}.vp-sponsor-grid-image{max-width:100%;filter:grayscale(1);transition:filter .25s}.dark .vp-sponsor-grid-image{filter:grayscale(1) invert(1)}.VPBadge{display:inline-block;margin-left:2px;border:1px solid transparent;border-radius:12px;padding:0 10px;line-height:22px;font-size:12px;font-weight:500;transform:translateY(-2px)}.VPBadge.small{padding:0 6px;line-height:18px;font-size:10px;transform:translateY(-8px)}.VPDocFooter .VPBadge{display:none}.vp-doc h1>.VPBadge{margin-top:4px;vertical-align:top}.vp-doc h2>.VPBadge{margin-top:3px;padding:0 8px;vertical-align:top}.vp-doc h3>.VPBadge{vertical-align:middle}.vp-doc h4>.VPBadge,.vp-doc h5>.VPBadge,.vp-doc h6>.VPBadge{vertical-align:middle;line-height:18px}.VPBadge.info{border-color:var(--vp-badge-info-border);color:var(--vp-badge-info-text);background-color:var(--vp-badge-info-bg)}.VPBadge.tip{border-color:var(--vp-badge-tip-border);color:var(--vp-badge-tip-text);background-color:var(--vp-badge-tip-bg)}.VPBadge.warning{border-color:var(--vp-badge-warning-border);color:var(--vp-badge-warning-text);background-color:var(--vp-badge-warning-bg)}.VPBadge.danger{border-color:var(--vp-badge-danger-border);color:var(--vp-badge-danger-text);background-color:var(--vp-badge-danger-bg)}.VPBackdrop[data-v-b06cdb19]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:var(--vp-z-index-backdrop);background:var(--vp-backdrop-bg-color);transition:opacity .5s}.VPBackdrop.fade-enter-from[data-v-b06cdb19],.VPBackdrop.fade-leave-to[data-v-b06cdb19]{opacity:0}.VPBackdrop.fade-leave-active[data-v-b06cdb19]{transition-duration:.25s}@media (min-width: 1280px){.VPBackdrop[data-v-b06cdb19]{display:none}}.NotFound[data-v-951cab6c]{padding:64px 24px 96px;text-align:center}@media (min-width: 768px){.NotFound[data-v-951cab6c]{padding:96px 32px 168px}}.code[data-v-951cab6c]{line-height:64px;font-size:64px;font-weight:600}.title[data-v-951cab6c]{padding-top:12px;letter-spacing:2px;line-height:20px;font-size:20px;font-weight:700}.divider[data-v-951cab6c]{margin:24px auto 18px;width:64px;height:1px;background-color:var(--vp-c-divider)}.quote[data-v-951cab6c]{margin:0 auto;max-width:256px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.action[data-v-951cab6c]{padding-top:20px}.link[data-v-951cab6c]{display:inline-block;border:1px solid var(--vp-c-brand-1);border-radius:16px;padding:3px 16px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:border-color .25s,color .25s}.link[data-v-951cab6c]:hover{border-color:var(--vp-c-brand-2);color:var(--vp-c-brand-2)}.root[data-v-3f927ebe]{position:relative;z-index:1}.nested[data-v-3f927ebe]{padding-right:16px;padding-left:16px}.outline-link[data-v-3f927ebe]{display:block;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .5s}.outline-link[data-v-3f927ebe]:hover,.outline-link.active[data-v-3f927ebe]{color:var(--vp-c-text-1);transition:color .25s}.outline-link.nested[data-v-3f927ebe]{padding-left:13px}.VPDocAsideOutline[data-v-b38bf2ff]{display:none}.VPDocAsideOutline.has-outline[data-v-b38bf2ff]{display:block}.content[data-v-b38bf2ff]{position:relative;border-left:1px solid var(--vp-c-divider);padding-left:16px;font-size:13px;font-weight:500}.outline-marker[data-v-b38bf2ff]{position:absolute;top:32px;left:-1px;z-index:0;opacity:0;width:2px;border-radius:2px;height:18px;background-color:var(--vp-c-brand-1);transition:top .25s cubic-bezier(0,1,.5,1),background-color .5s,opacity .25s}.outline-title[data-v-b38bf2ff]{line-height:32px;font-size:14px;font-weight:600}.VPDocAside[data-v-6d7b3c46]{display:flex;flex-direction:column;flex-grow:1}.spacer[data-v-6d7b3c46]{flex-grow:1}.VPDocAside[data-v-6d7b3c46] .spacer+.VPDocAsideSponsors,.VPDocAside[data-v-6d7b3c46] .spacer+.VPDocAsideCarbonAds{margin-top:24px}.VPDocAside[data-v-6d7b3c46] .VPDocAsideSponsors+.VPDocAsideCarbonAds{margin-top:16px}.VPLastUpdated[data-v-475f71b8]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 640px){.VPLastUpdated[data-v-475f71b8]{line-height:32px;font-size:14px;font-weight:500}}.VPDocFooter[data-v-4f9813fa]{margin-top:64px}.edit-info[data-v-4f9813fa]{padding-bottom:18px}@media (min-width: 640px){.edit-info[data-v-4f9813fa]{display:flex;justify-content:space-between;align-items:center;padding-bottom:14px}}.edit-link-button[data-v-4f9813fa]{display:flex;align-items:center;border:0;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.edit-link-button[data-v-4f9813fa]:hover{color:var(--vp-c-brand-2)}.edit-link-icon[data-v-4f9813fa]{margin-right:8px}.prev-next[data-v-4f9813fa]{border-top:1px solid var(--vp-c-divider);padding-top:24px;display:grid;grid-row-gap:8px}@media (min-width: 640px){.prev-next[data-v-4f9813fa]{grid-template-columns:repeat(2,1fr);grid-column-gap:16px}}.pager-link[data-v-4f9813fa]{display:block;border:1px solid var(--vp-c-divider);border-radius:8px;padding:11px 16px 13px;width:100%;height:100%;transition:border-color .25s}.pager-link[data-v-4f9813fa]:hover{border-color:var(--vp-c-brand-1)}.pager-link.next[data-v-4f9813fa]{margin-left:auto;text-align:right}.desc[data-v-4f9813fa]{display:block;line-height:20px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.title[data-v-4f9813fa]{display:block;line-height:20px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.VPDoc[data-v-83890dd9]{padding:32px 24px 96px;width:100%}@media (min-width: 768px){.VPDoc[data-v-83890dd9]{padding:48px 32px 128px}}@media (min-width: 960px){.VPDoc[data-v-83890dd9]{padding:48px 32px 0}.VPDoc:not(.has-sidebar) .container[data-v-83890dd9]{display:flex;justify-content:center;max-width:992px}.VPDoc:not(.has-sidebar) .content[data-v-83890dd9]{max-width:752px}}@media (min-width: 1280px){.VPDoc .container[data-v-83890dd9]{display:flex;justify-content:center}.VPDoc .aside[data-v-83890dd9]{display:block}}@media (min-width: 1440px){.VPDoc:not(.has-sidebar) .content[data-v-83890dd9]{max-width:784px}.VPDoc:not(.has-sidebar) .container[data-v-83890dd9]{max-width:1104px}}.container[data-v-83890dd9]{margin:0 auto;width:100%}.aside[data-v-83890dd9]{position:relative;display:none;order:2;flex-grow:1;padding-left:32px;width:100%;max-width:256px}.left-aside[data-v-83890dd9]{order:1;padding-left:unset;padding-right:32px}.aside-container[data-v-83890dd9]{position:fixed;top:0;padding-top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 48px);width:224px;height:100vh;overflow-x:hidden;overflow-y:auto;scrollbar-width:none}.aside-container[data-v-83890dd9]::-webkit-scrollbar{display:none}.aside-curtain[data-v-83890dd9]{position:fixed;bottom:0;z-index:10;width:224px;height:32px;background:linear-gradient(transparent,var(--vp-c-bg) 70%)}.aside-content[data-v-83890dd9]{display:flex;flex-direction:column;min-height:calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px));padding-bottom:32px}.content[data-v-83890dd9]{position:relative;margin:0 auto;width:100%}@media (min-width: 960px){.content[data-v-83890dd9]{padding:0 32px 128px}}@media (min-width: 1280px){.content[data-v-83890dd9]{order:1;margin:0;min-width:640px}}.content-container[data-v-83890dd9]{margin:0 auto}.VPDoc.has-aside .content-container[data-v-83890dd9]{max-width:688px}.VPButton[data-v-906d7fb4]{display:inline-block;border:1px solid transparent;text-align:center;font-weight:600;white-space:nowrap;transition:color .25s,border-color .25s,background-color .25s}.VPButton[data-v-906d7fb4]:active{transition:color .1s,border-color .1s,background-color .1s}.VPButton.medium[data-v-906d7fb4]{border-radius:20px;padding:0 20px;line-height:38px;font-size:14px}.VPButton.big[data-v-906d7fb4]{border-radius:24px;padding:0 24px;line-height:46px;font-size:16px}.VPButton.brand[data-v-906d7fb4]{border-color:var(--vp-button-brand-border);color:var(--vp-button-brand-text);background-color:var(--vp-button-brand-bg)}.VPButton.brand[data-v-906d7fb4]:hover{border-color:var(--vp-button-brand-hover-border);color:var(--vp-button-brand-hover-text);background-color:var(--vp-button-brand-hover-bg)}.VPButton.brand[data-v-906d7fb4]:active{border-color:var(--vp-button-brand-active-border);color:var(--vp-button-brand-active-text);background-color:var(--vp-button-brand-active-bg)}.VPButton.alt[data-v-906d7fb4]{border-color:var(--vp-button-alt-border);color:var(--vp-button-alt-text);background-color:var(--vp-button-alt-bg)}.VPButton.alt[data-v-906d7fb4]:hover{border-color:var(--vp-button-alt-hover-border);color:var(--vp-button-alt-hover-text);background-color:var(--vp-button-alt-hover-bg)}.VPButton.alt[data-v-906d7fb4]:active{border-color:var(--vp-button-alt-active-border);color:var(--vp-button-alt-active-text);background-color:var(--vp-button-alt-active-bg)}.VPButton.sponsor[data-v-906d7fb4]{border-color:var(--vp-button-sponsor-border);color:var(--vp-button-sponsor-text);background-color:var(--vp-button-sponsor-bg)}.VPButton.sponsor[data-v-906d7fb4]:hover{border-color:var(--vp-button-sponsor-hover-border);color:var(--vp-button-sponsor-hover-text);background-color:var(--vp-button-sponsor-hover-bg)}.VPButton.sponsor[data-v-906d7fb4]:active{border-color:var(--vp-button-sponsor-active-border);color:var(--vp-button-sponsor-active-text);background-color:var(--vp-button-sponsor-active-bg)}html:not(.dark) .VPImage.dark[data-v-35a7d0b8]{display:none}.dark .VPImage.light[data-v-35a7d0b8]{display:none}.VPHero[data-v-3d256e5e]{margin-top:calc((var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1);padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px) 24px 48px}@media (min-width: 640px){.VPHero[data-v-3d256e5e]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px}}@media (min-width: 960px){.VPHero[data-v-3d256e5e]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px}}.container[data-v-3d256e5e]{display:flex;flex-direction:column;margin:0 auto;max-width:1152px}@media (min-width: 960px){.container[data-v-3d256e5e]{flex-direction:row}}.main[data-v-3d256e5e]{position:relative;z-index:10;order:2;flex-grow:1;flex-shrink:0}.VPHero.has-image .container[data-v-3d256e5e]{text-align:center}@media (min-width: 960px){.VPHero.has-image .container[data-v-3d256e5e]{text-align:left}}@media (min-width: 960px){.main[data-v-3d256e5e]{order:1;width:calc((100% / 3) * 2)}.VPHero.has-image .main[data-v-3d256e5e]{max-width:592px}}.heading[data-v-3d256e5e]{display:flex;flex-direction:column}.name[data-v-3d256e5e],.text[data-v-3d256e5e]{width:fit-content;max-width:392px;letter-spacing:-.4px;line-height:40px;font-size:32px;font-weight:700;white-space:pre-wrap}.VPHero.has-image .name[data-v-3d256e5e],.VPHero.has-image .text[data-v-3d256e5e]{margin:0 auto}.name[data-v-3d256e5e]{color:var(--vp-home-hero-name-color)}.clip[data-v-3d256e5e]{background:var(--vp-home-hero-name-background);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:var(--vp-home-hero-name-color)}@media (min-width: 640px){.name[data-v-3d256e5e],.text[data-v-3d256e5e]{max-width:576px;line-height:56px;font-size:48px}}@media (min-width: 960px){.name[data-v-3d256e5e],.text[data-v-3d256e5e]{line-height:64px;font-size:56px}.VPHero.has-image .name[data-v-3d256e5e],.VPHero.has-image .text[data-v-3d256e5e]{margin:0}}.tagline[data-v-3d256e5e]{padding-top:8px;max-width:392px;line-height:28px;font-size:18px;font-weight:500;white-space:pre-wrap;color:var(--vp-c-text-2)}.VPHero.has-image .tagline[data-v-3d256e5e]{margin:0 auto}@media (min-width: 640px){.tagline[data-v-3d256e5e]{padding-top:12px;max-width:576px;line-height:32px;font-size:20px}}@media (min-width: 960px){.tagline[data-v-3d256e5e]{line-height:36px;font-size:24px}.VPHero.has-image .tagline[data-v-3d256e5e]{margin:0}}.actions[data-v-3d256e5e]{display:flex;flex-wrap:wrap;margin:-6px;padding-top:24px}.VPHero.has-image .actions[data-v-3d256e5e]{justify-content:center}@media (min-width: 640px){.actions[data-v-3d256e5e]{padding-top:32px}}@media (min-width: 960px){.VPHero.has-image .actions[data-v-3d256e5e]{justify-content:flex-start}}.action[data-v-3d256e5e]{flex-shrink:0;padding:6px}.image[data-v-3d256e5e]{order:1;margin:-76px -24px -48px}@media (min-width: 640px){.image[data-v-3d256e5e]{margin:-108px -24px -48px}}@media (min-width: 960px){.image[data-v-3d256e5e]{flex-grow:1;order:2;margin:0;min-height:100%}}.image-container[data-v-3d256e5e]{position:relative;margin:0 auto;width:320px;height:320px}@media (min-width: 640px){.image-container[data-v-3d256e5e]{width:392px;height:392px}}@media (min-width: 960px){.image-container[data-v-3d256e5e]{display:flex;justify-content:center;align-items:center;width:100%;height:100%;transform:translate(-32px,-32px)}}.image-bg[data-v-3d256e5e]{position:absolute;top:50%;left:50%;border-radius:50%;width:192px;height:192px;background-image:var(--vp-home-hero-image-background-image);filter:var(--vp-home-hero-image-filter);transform:translate(-50%,-50%)}@media (min-width: 640px){.image-bg[data-v-3d256e5e]{width:256px;height:256px}}@media (min-width: 960px){.image-bg[data-v-3d256e5e]{width:320px;height:320px}}[data-v-3d256e5e] .image-src{position:absolute;top:50%;left:50%;max-width:192px;max-height:192px;transform:translate(-50%,-50%)}@media (min-width: 640px){[data-v-3d256e5e] .image-src{max-width:256px;max-height:256px}}@media (min-width: 960px){[data-v-3d256e5e] .image-src{max-width:320px;max-height:320px}}.VPFeature[data-v-f5e9645b]{display:block;border:1px solid var(--vp-c-bg-soft);border-radius:12px;height:100%;background-color:var(--vp-c-bg-soft);transition:border-color .25s,background-color .25s}.VPFeature.link[data-v-f5e9645b]:hover{border-color:var(--vp-c-brand-1)}.box[data-v-f5e9645b]{display:flex;flex-direction:column;padding:24px;height:100%}.box[data-v-f5e9645b]>.VPImage{margin-bottom:20px}.icon[data-v-f5e9645b]{display:flex;justify-content:center;align-items:center;margin-bottom:20px;border-radius:6px;background-color:var(--vp-c-default-soft);width:48px;height:48px;font-size:24px;transition:background-color .25s}.title[data-v-f5e9645b]{line-height:24px;font-size:16px;font-weight:600}.details[data-v-f5e9645b]{flex-grow:1;padding-top:8px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.link-text[data-v-f5e9645b]{padding-top:8px}.link-text-value[data-v-f5e9645b]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.link-text-icon[data-v-f5e9645b]{margin-left:6px}.VPFeatures[data-v-d0a190d7]{position:relative;padding:0 24px}@media (min-width: 640px){.VPFeatures[data-v-d0a190d7]{padding:0 48px}}@media (min-width: 960px){.VPFeatures[data-v-d0a190d7]{padding:0 64px}}.container[data-v-d0a190d7]{margin:0 auto;max-width:1152px}.items[data-v-d0a190d7]{display:flex;flex-wrap:wrap;margin:-8px}.item[data-v-d0a190d7]{padding:8px;width:100%}@media (min-width: 640px){.item.grid-2[data-v-d0a190d7],.item.grid-4[data-v-d0a190d7],.item.grid-6[data-v-d0a190d7]{width:50%}}@media (min-width: 768px){.item.grid-2[data-v-d0a190d7],.item.grid-4[data-v-d0a190d7]{width:50%}.item.grid-3[data-v-d0a190d7],.item.grid-6[data-v-d0a190d7]{width:calc(100% / 3)}}@media (min-width: 960px){.item.grid-4[data-v-d0a190d7]{width:25%}}.container[data-v-7a48a447]{margin:auto;width:100%;max-width:1280px;padding:0 24px}@media (min-width: 640px){.container[data-v-7a48a447]{padding:0 48px}}@media (min-width: 960px){.container[data-v-7a48a447]{width:100%;padding:0 64px}}.vp-doc[data-v-7a48a447] .VPHomeSponsors,.vp-doc[data-v-7a48a447] .VPTeamPage{margin-left:var(--vp-offset, calc(50% - 50vw) );margin-right:var(--vp-offset, calc(50% - 50vw) )}.vp-doc[data-v-7a48a447] .VPHomeSponsors h2{border-top:none;letter-spacing:normal}.vp-doc[data-v-7a48a447] .VPHomeSponsors a,.vp-doc[data-v-7a48a447] .VPTeamPage a{text-decoration:none}.VPHome[data-v-e40e30de]{margin-bottom:96px}@media (min-width: 768px){.VPHome[data-v-e40e30de]{margin-bottom:128px}}.VPContent[data-v-91765379]{flex-grow:1;flex-shrink:0;margin:var(--vp-layout-top-height, 0px) auto 0;width:100%}.VPContent.is-home[data-v-91765379]{width:100%;max-width:100%}.VPContent.has-sidebar[data-v-91765379]{margin:0}@media (min-width: 960px){.VPContent[data-v-91765379]{padding-top:var(--vp-nav-height)}.VPContent.has-sidebar[data-v-91765379]{margin:var(--vp-layout-top-height, 0px) 0 0;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPContent.has-sidebar[data-v-91765379]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.VPFooter[data-v-c970a860]{position:relative;z-index:var(--vp-z-index-footer);border-top:1px solid var(--vp-c-gutter);padding:32px 24px;background-color:var(--vp-c-bg)}.VPFooter.has-sidebar[data-v-c970a860]{display:none}.VPFooter[data-v-c970a860] a{text-decoration-line:underline;text-underline-offset:2px;transition:color .25s}.VPFooter[data-v-c970a860] a:hover{color:var(--vp-c-text-1)}@media (min-width: 768px){.VPFooter[data-v-c970a860]{padding:32px}}.container[data-v-c970a860]{margin:0 auto;max-width:var(--vp-layout-max-width);text-align:center}.message[data-v-c970a860],.copyright[data-v-c970a860]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.VPLocalNavOutlineDropdown[data-v-168ddf5d]{padding:12px 20px 11px}@media (min-width: 960px){.VPLocalNavOutlineDropdown[data-v-168ddf5d]{padding:12px 36px 11px}}.VPLocalNavOutlineDropdown button[data-v-168ddf5d]{display:block;font-size:12px;font-weight:500;line-height:24px;color:var(--vp-c-text-2);transition:color .5s;position:relative}.VPLocalNavOutlineDropdown button[data-v-168ddf5d]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPLocalNavOutlineDropdown button.open[data-v-168ddf5d]{color:var(--vp-c-text-1)}.icon[data-v-168ddf5d]{display:inline-block;vertical-align:middle;margin-left:2px;font-size:14px;transform:rotate(0);transition:transform .25s}@media (min-width: 960px){.VPLocalNavOutlineDropdown button[data-v-168ddf5d]{font-size:14px}.icon[data-v-168ddf5d]{font-size:16px}}.open>.icon[data-v-168ddf5d]{transform:rotate(90deg)}.items[data-v-168ddf5d]{position:absolute;top:40px;right:16px;left:16px;display:grid;gap:1px;border:1px solid var(--vp-c-border);border-radius:8px;background-color:var(--vp-c-gutter);max-height:calc(var(--vp-vh, 100vh) - 86px);overflow:hidden auto;box-shadow:var(--vp-shadow-3)}@media (min-width: 960px){.items[data-v-168ddf5d]{right:auto;left:calc(var(--vp-sidebar-width) + 32px);width:320px}}.header[data-v-168ddf5d]{background-color:var(--vp-c-bg-soft)}.top-link[data-v-168ddf5d]{display:block;padding:0 16px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.outline[data-v-168ddf5d]{padding:8px 0;background-color:var(--vp-c-bg-soft)}.flyout-enter-active[data-v-168ddf5d]{transition:all .2s ease-out}.flyout-leave-active[data-v-168ddf5d]{transition:all .15s ease-in}.flyout-enter-from[data-v-168ddf5d],.flyout-leave-to[data-v-168ddf5d]{opacity:0;transform:translateY(-16px)}.VPLocalNav[data-v-070ab83d]{position:sticky;top:0;left:0;z-index:var(--vp-z-index-local-nav);border-bottom:1px solid var(--vp-c-gutter);padding-top:var(--vp-layout-top-height, 0px);width:100%;background-color:var(--vp-local-nav-bg-color)}.VPLocalNav.fixed[data-v-070ab83d]{position:fixed}@media (min-width: 960px){.VPLocalNav[data-v-070ab83d]{top:var(--vp-nav-height)}.VPLocalNav.has-sidebar[data-v-070ab83d]{padding-left:var(--vp-sidebar-width)}.VPLocalNav.empty[data-v-070ab83d]{display:none}}@media (min-width: 1280px){.VPLocalNav[data-v-070ab83d]{display:none}}@media (min-width: 1440px){.VPLocalNav.has-sidebar[data-v-070ab83d]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.container[data-v-070ab83d]{display:flex;justify-content:space-between;align-items:center}.menu[data-v-070ab83d]{display:flex;align-items:center;padding:12px 24px 11px;line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.menu[data-v-070ab83d]:hover{color:var(--vp-c-text-1);transition:color .25s}@media (min-width: 768px){.menu[data-v-070ab83d]{padding:0 32px}}@media (min-width: 960px){.menu[data-v-070ab83d]{display:none}}.menu-icon[data-v-070ab83d]{margin-right:8px;font-size:14px}.VPOutlineDropdown[data-v-070ab83d]{padding:12px 24px 11px}@media (min-width: 768px){.VPOutlineDropdown[data-v-070ab83d]{padding:12px 32px 11px}}.VPSwitch[data-v-4a1c76db]{position:relative;border-radius:11px;display:block;width:40px;height:22px;flex-shrink:0;border:1px solid var(--vp-input-border-color);background-color:var(--vp-input-switch-bg-color);transition:border-color .25s!important}.VPSwitch[data-v-4a1c76db]:hover{border-color:var(--vp-c-brand-1)}.check[data-v-4a1c76db]{position:absolute;top:1px;left:1px;width:18px;height:18px;border-radius:50%;background-color:var(--vp-c-neutral-inverse);box-shadow:var(--vp-shadow-1);transition:transform .25s!important}.icon[data-v-4a1c76db]{position:relative;display:block;width:18px;height:18px;border-radius:50%;overflow:hidden}.icon[data-v-4a1c76db] [class^=vpi-]{position:absolute;top:3px;left:3px;width:12px;height:12px;color:var(--vp-c-text-2)}.dark .icon[data-v-4a1c76db] [class^=vpi-]{color:var(--vp-c-text-1);transition:opacity .25s!important}.sun[data-v-e40a8bb6]{opacity:1}.moon[data-v-e40a8bb6],.dark .sun[data-v-e40a8bb6]{opacity:0}.dark .moon[data-v-e40a8bb6]{opacity:1}.dark .VPSwitchAppearance[data-v-e40a8bb6] .check{transform:translate(18px)}.VPNavBarAppearance[data-v-af096f4a]{display:none}@media (min-width: 1280px){.VPNavBarAppearance[data-v-af096f4a]{display:flex;align-items:center}}.VPMenuGroup+.VPMenuLink[data-v-acbfed09]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.link[data-v-acbfed09]{display:block;border-radius:6px;padding:0 12px;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);white-space:nowrap;transition:background-color .25s,color .25s}.link[data-v-acbfed09]:hover{color:var(--vp-c-brand-1);background-color:var(--vp-c-default-soft)}.link.active[data-v-acbfed09]{color:var(--vp-c-brand-1)}.VPMenuGroup[data-v-48c802d0]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.VPMenuGroup[data-v-48c802d0]:first-child{margin-top:0;border-top:0;padding-top:0}.VPMenuGroup+.VPMenuGroup[data-v-48c802d0]{margin-top:12px;border-top:1px solid var(--vp-c-divider)}.title[data-v-48c802d0]{padding:0 12px;line-height:32px;font-size:14px;font-weight:600;color:var(--vp-c-text-2);white-space:nowrap;transition:color .25s}.VPMenu[data-v-7dd3104a]{border-radius:12px;padding:12px;min-width:128px;border:1px solid var(--vp-c-divider);background-color:var(--vp-c-bg-elv);box-shadow:var(--vp-shadow-3);transition:background-color .5s;max-height:calc(100vh - var(--vp-nav-height));overflow-y:auto}.VPMenu[data-v-7dd3104a] .group{margin:0 -12px;padding:0 12px 12px}.VPMenu[data-v-7dd3104a] .group+.group{border-top:1px solid var(--vp-c-divider);padding:11px 12px 12px}.VPMenu[data-v-7dd3104a] .group:last-child{padding-bottom:0}.VPMenu[data-v-7dd3104a] .group+.item{border-top:1px solid var(--vp-c-divider);padding:11px 16px 0}.VPMenu[data-v-7dd3104a] .item{padding:0 16px;white-space:nowrap}.VPMenu[data-v-7dd3104a] .label{flex-grow:1;line-height:28px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.VPMenu[data-v-7dd3104a] .action{padding-left:24px}.VPFlyout[data-v-04f5c5e9]{position:relative}.VPFlyout[data-v-04f5c5e9]:hover{color:var(--vp-c-brand-1);transition:color .25s}.VPFlyout:hover .text[data-v-04f5c5e9]{color:var(--vp-c-text-2)}.VPFlyout:hover .icon[data-v-04f5c5e9]{fill:var(--vp-c-text-2)}.VPFlyout.active .text[data-v-04f5c5e9]{color:var(--vp-c-brand-1)}.VPFlyout.active:hover .text[data-v-04f5c5e9]{color:var(--vp-c-brand-2)}.button[aria-expanded=false]+.menu[data-v-04f5c5e9]{opacity:0;visibility:hidden;transform:translateY(0)}.VPFlyout:hover .menu[data-v-04f5c5e9],.button[aria-expanded=true]+.menu[data-v-04f5c5e9]{opacity:1;visibility:visible;transform:translateY(0)}.button[data-v-04f5c5e9]{display:flex;align-items:center;padding:0 12px;height:var(--vp-nav-height);color:var(--vp-c-text-1);transition:color .5s}.text[data-v-04f5c5e9]{display:flex;align-items:center;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.option-icon[data-v-04f5c5e9]{margin-right:0;font-size:16px}.text-icon[data-v-04f5c5e9]{margin-left:4px;font-size:14px}.icon[data-v-04f5c5e9]{font-size:20px;transition:fill .25s}.menu[data-v-04f5c5e9]{position:absolute;top:calc(var(--vp-nav-height) / 2 + 20px);right:0;opacity:0;visibility:hidden;transition:opacity .25s,visibility .25s,transform .25s}.VPSocialLink[data-v-d26d30cb]{display:flex;justify-content:center;align-items:center;width:36px;height:36px;color:var(--vp-c-text-2);transition:color .5s}.VPSocialLink[data-v-d26d30cb]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPSocialLink[data-v-d26d30cb]>svg,.VPSocialLink[data-v-d26d30cb]>[class^=vpi-social-]{width:20px;height:20px;fill:currentColor}.VPSocialLinks[data-v-ee7a9424]{display:flex;justify-content:center}.VPNavBarExtra[data-v-925effce]{display:none;margin-right:-12px}@media (min-width: 768px){.VPNavBarExtra[data-v-925effce]{display:block}}@media (min-width: 1280px){.VPNavBarExtra[data-v-925effce]{display:none}}.trans-title[data-v-925effce]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.item.appearance[data-v-925effce],.item.social-links[data-v-925effce]{display:flex;align-items:center;padding:0 12px}.item.appearance[data-v-925effce]{min-width:176px}.appearance-action[data-v-925effce]{margin-right:-2px}.social-links-list[data-v-925effce]{margin:-4px -8px}.VPNavBarHamburger[data-v-5dea55bf]{display:flex;justify-content:center;align-items:center;width:48px;height:var(--vp-nav-height)}@media (min-width: 768px){.VPNavBarHamburger[data-v-5dea55bf]{display:none}}.container[data-v-5dea55bf]{position:relative;width:16px;height:14px;overflow:hidden}.VPNavBarHamburger:hover .top[data-v-5dea55bf]{top:0;left:0;transform:translate(4px)}.VPNavBarHamburger:hover .middle[data-v-5dea55bf]{top:6px;left:0;transform:translate(0)}.VPNavBarHamburger:hover .bottom[data-v-5dea55bf]{top:12px;left:0;transform:translate(8px)}.VPNavBarHamburger.active .top[data-v-5dea55bf]{top:6px;transform:translate(0) rotate(225deg)}.VPNavBarHamburger.active .middle[data-v-5dea55bf]{top:6px;transform:translate(16px)}.VPNavBarHamburger.active .bottom[data-v-5dea55bf]{top:6px;transform:translate(0) rotate(135deg)}.VPNavBarHamburger.active:hover .top[data-v-5dea55bf],.VPNavBarHamburger.active:hover .middle[data-v-5dea55bf],.VPNavBarHamburger.active:hover .bottom[data-v-5dea55bf]{background-color:var(--vp-c-text-2);transition:top .25s,background-color .25s,transform .25s}.top[data-v-5dea55bf],.middle[data-v-5dea55bf],.bottom[data-v-5dea55bf]{position:absolute;width:16px;height:2px;background-color:var(--vp-c-text-1);transition:top .25s,background-color .5s,transform .25s}.top[data-v-5dea55bf]{top:0;left:0;transform:translate(0)}.middle[data-v-5dea55bf]{top:6px;left:0;transform:translate(8px)}.bottom[data-v-5dea55bf]{top:12px;left:0;transform:translate(4px)}.VPNavBarMenuLink[data-v-956ec74c]{display:flex;align-items:center;padding:0 12px;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.VPNavBarMenuLink.active[data-v-956ec74c],.VPNavBarMenuLink[data-v-956ec74c]:hover{color:var(--vp-c-brand-1)}.VPNavBarMenu[data-v-e6d46098]{display:none}@media (min-width: 768px){.VPNavBarMenu[data-v-e6d46098]{display:flex}}/*! @docsearch/css 3.8.2 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */:root{--docsearch-primary-color:#5468ff;--docsearch-text-color:#1c1e21;--docsearch-spacing:12px;--docsearch-icon-stroke-width:1.4;--docsearch-highlight-color:var(--docsearch-primary-color);--docsearch-muted-color:#969faf;--docsearch-container-background:rgba(101,108,133,.8);--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 hsla(0,0%,100%,.5),0 3px 8px 0 #555a64;--docsearch-searchbox-height:56px;--docsearch-searchbox-background:#ebedf0;--docsearch-searchbox-focus-background:#fff;--docsearch-searchbox-shadow:inset 0 0 0 2px var(--docsearch-primary-color);--docsearch-hit-height:56px;--docsearch-hit-color:#444950;--docsearch-hit-active-color:#fff;--docsearch-hit-background:#fff;--docsearch-hit-shadow:0 1px 3px 0 #d4d9e1;--docsearch-key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8);--docsearch-key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba(30,35,90,.4);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 1px 0 rgba(30,35,90,.4);--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69,98,155,.12)}html[data-theme=dark]{--docsearch-text-color:#f5f6f7;--docsearch-container-background:rgba(9,10,17,.8);--docsearch-modal-background:#15172a;--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:#090a11;--docsearch-searchbox-focus-background:#000;--docsearch-hit-color:#bec3c9;--docsearch-hit-shadow:none;--docsearch-hit-background:#090a11;--docsearch-key-gradient:linear-gradient(-26.5deg,#565872,#31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3,4,9,.3);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 1px 1px 0 #0304094d;--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73,76,106,.5),0 -4px 8px 0 rgba(0,0,0,.2);--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}.DocSearch-Button{align-items:center;background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;display:flex;font-weight:500;height:36px;justify-content:space-between;margin:0 0 0 16px;padding:0 8px;-webkit-user-select:none;user-select:none}.DocSearch-Button:active,.DocSearch-Button:focus,.DocSearch-Button:hover{background:var(--docsearch-searchbox-focus-background);box-shadow:var(--docsearch-searchbox-shadow);color:var(--docsearch-text-color);outline:none}.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 2px;position:relative;top:-1px;width:20px}.DocSearch-Button-Key--pressed{box-shadow:var(--docsearch-key-pressed-shadow);transform:translate3d(0,1px,0)}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder{display:none}}.DocSearch--active{overflow:hidden!important}.DocSearch-Container,.DocSearch-Container *{box-sizing:border-box}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Container a{text-decoration:none}.DocSearch-Link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;font:inherit;margin:0;padding:0}.DocSearch-Modal{background:var(--docsearch-modal-background);border-radius:6px;box-shadow:var(--docsearch-modal-shadow);flex-direction:column;margin:60px auto auto;max-width:var(--docsearch-modal-width);position:relative}.DocSearch-SearchBar{display:flex;padding:var(--docsearch-spacing) var(--docsearch-spacing) 0}.DocSearch-Form{align-items:center;background:var(--docsearch-searchbox-focus-background);border-radius:4px;box-shadow:var(--docsearch-searchbox-shadow);display:flex;height:var(--docsearch-searchbox-height);margin:0;padding:0 var(--docsearch-spacing);position:relative;width:100%}.DocSearch-Input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:var(--docsearch-text-color);flex:1;font:inherit;font-size:1.2em;height:100%;outline:none;padding:0 0 0 8px;width:80%}.DocSearch-Input::placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::-webkit-search-cancel-button,.DocSearch-Input::-webkit-search-decoration,.DocSearch-Input::-webkit-search-results-button,.DocSearch-Input::-webkit-search-results-decoration{display:none}.DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{margin:0;padding:0}.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator{display:none}.DocSearch-Container--Stalled .DocSearch-LoadingIndicator{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{animation:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0;stroke-width:var(--docsearch-icon-stroke-width)}}.DocSearch-Reset{animation:fade-in .1s ease-in forwards;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;padding:2px;right:0;stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.DocSearch-Cancel{display:none}.DocSearch-Dropdown{max-height:calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height));min-height:var(--docsearch-spacing);overflow-y:auto;overflow-y:overlay;padding:0 var(--docsearch-spacing);scrollbar-color:var(--docsearch-muted-color) var(--docsearch-modal-background);scrollbar-width:thin}.DocSearch-Dropdown::-webkit-scrollbar{width:12px}.DocSearch-Dropdown::-webkit-scrollbar-track{background:transparent}.DocSearch-Dropdown::-webkit-scrollbar-thumb{background-color:var(--docsearch-muted-color);border:3px solid var(--docsearch-modal-background);border-radius:20px}.DocSearch-Dropdown ul{list-style:none;margin:0;padding:0}.DocSearch-Label{font-size:.75em;line-height:1.6em}.DocSearch-Help,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Help{font-size:.9em;margin:0;-webkit-user-select:none;user-select:none}.DocSearch-Title{font-size:1.2em}.DocSearch-Logo a{display:flex}.DocSearch-Logo svg{color:var(--docsearch-logo-color);margin-left:8px}.DocSearch-Hits:last-of-type{margin-bottom:24px}.DocSearch-Hits mark{background:none;color:var(--docsearch-highlight-color)}.DocSearch-HitsFooter{color:var(--docsearch-muted-color);display:flex;font-size:.85em;justify-content:center;margin-bottom:var(--docsearch-spacing);padding:var(--docsearch-spacing)}.DocSearch-HitsFooter a{border-bottom:1px solid;color:inherit}.DocSearch-Hit{border-radius:4px;display:flex;padding-bottom:4px;position:relative}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--deleting{transition:none}}.DocSearch-Hit--deleting{opacity:0;transition:all .25s linear}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--favoriting{transition:none}}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:all .25s linear;transition-delay:.25s}.DocSearch-Hit a{background:var(--docsearch-hit-background);border-radius:4px;box-shadow:var(--docsearch-hit-shadow);display:block;padding-left:var(--docsearch-spacing);width:100%}.DocSearch-Hit-source{background:var(--docsearch-modal-background);color:var(--docsearch-highlight-color);font-size:.85em;font-weight:600;line-height:32px;margin:0 -4px;padding:8px 4px 0;position:sticky;top:0;z-index:10}.DocSearch-Hit-Tree{color:var(--docsearch-muted-color);height:var(--docsearch-hit-height);opacity:.5;stroke-width:var(--docsearch-icon-stroke-width);width:24px}.DocSearch-Hit[aria-selected=true] a{background-color:var(--docsearch-highlight-color)}.DocSearch-Hit[aria-selected=true] mark{text-decoration:underline}.DocSearch-Hit-Container{align-items:center;color:var(--docsearch-hit-color);display:flex;flex-direction:row;height:var(--docsearch-hit-height);padding:0 var(--docsearch-spacing) 0 0}.DocSearch-Hit-icon{height:20px;width:20px}.DocSearch-Hit-action,.DocSearch-Hit-icon{color:var(--docsearch-muted-color);stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Hit-action{align-items:center;display:flex;height:22px;width:22px}.DocSearch-Hit-action svg{display:block;height:18px;width:18px}.DocSearch-Hit-action+.DocSearch-Hit-action{margin-left:6px}.DocSearch-Hit-action-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:inherit;cursor:pointer;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:background-color .1s ease-in}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{transition:none}}.DocSearch-Hit-action-button:focus path,.DocSearch-Hit-action-button:hover path{fill:#fff}.DocSearch-Hit-content-wrapper{display:flex;flex:1 1 auto;flex-direction:column;font-weight:500;justify-content:center;line-height:1.2em;margin:0 8px;overflow-x:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:80%}.DocSearch-Hit-title{font-size:.9em}.DocSearch-Hit-path{color:var(--docsearch-muted-color);font-size:.75em}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-action,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-icon,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-text,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-title,.DocSearch-Hit[aria-selected=true] mark{color:var(--docsearch-hit-active-color)!important}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:none}}.DocSearch-ErrorScreen,.DocSearch-NoResults,.DocSearch-StartScreen{font-size:.9em;margin:0 auto;padding:36px 0;text-align:center;width:80%}.DocSearch-Screen-Icon{color:var(--docsearch-muted-color);padding-bottom:12px}.DocSearch-NoResults-Prefill-List{display:inline-block;padding-bottom:24px;text-align:left}.DocSearch-NoResults-Prefill-List ul{display:inline-block;padding:8px 0 0}.DocSearch-NoResults-Prefill-List li{list-style-position:inside;list-style-type:"» "}.DocSearch-Prefill{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:1em;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;font-size:1em;font-weight:700;padding:0}.DocSearch-Prefill:focus,.DocSearch-Prefill:hover{outline:none;text-decoration:underline}.DocSearch-Footer{align-items:center;background:var(--docsearch-footer-background);border-radius:0 0 8px 8px;box-shadow:var(--docsearch-footer-shadow);display:flex;flex-direction:row-reverse;flex-shrink:0;height:var(--docsearch-footer-height);justify-content:space-between;padding:0 var(--docsearch-spacing);position:relative;-webkit-user-select:none;user-select:none;width:100%;z-index:300}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li{align-items:center;display:flex}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:2px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;width:20px}.DocSearch-VisuallyHiddenForAccessibility{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}@media (max-width:768px){:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%}.DocSearch-Container{height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);position:absolute}.DocSearch-Footer{border-radius:0;bottom:0;position:absolute}.DocSearch-Hit-content-wrapper{display:flex;position:relative;width:80%}.DocSearch-Modal{border-radius:0;box-shadow:none;height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);margin:0;max-width:100%;width:100%}.DocSearch-Dropdown{max-height:calc(var(--docsearch-vh, 1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.DocSearch-Cancel{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;flex:none;font:inherit;font-size:1em;font-weight:500;margin-left:var(--docsearch-spacing);outline:none;overflow:hidden;padding:0;-webkit-user-select:none;user-select:none;white-space:nowrap}.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}[class*=DocSearch]{--docsearch-primary-color: var(--vp-c-brand-1);--docsearch-highlight-color: var(--docsearch-primary-color);--docsearch-text-color: var(--vp-c-text-1);--docsearch-muted-color: var(--vp-c-text-2);--docsearch-searchbox-shadow: none;--docsearch-searchbox-background: transparent;--docsearch-searchbox-focus-background: transparent;--docsearch-key-gradient: transparent;--docsearch-key-shadow: none;--docsearch-modal-background: var(--vp-c-bg-soft);--docsearch-footer-background: var(--vp-c-bg)}.dark [class*=DocSearch]{--docsearch-modal-shadow: none;--docsearch-footer-shadow: none;--docsearch-logo-color: var(--vp-c-text-2);--docsearch-hit-background: var(--vp-c-default-soft);--docsearch-hit-color: var(--vp-c-text-2);--docsearch-hit-shadow: none}.DocSearch-Button{display:flex;justify-content:center;align-items:center;margin:0;padding:0;width:48px;height:55px;background:transparent;transition:border-color .25s}.DocSearch-Button:hover{background:transparent}.DocSearch-Button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}.DocSearch-Button-Key--pressed{transform:none;box-shadow:none}.DocSearch-Button:focus:not(:focus-visible){outline:none!important}@media (min-width: 768px){.DocSearch-Button{justify-content:flex-start;border:1px solid transparent;border-radius:8px;padding:0 10px 0 12px;width:100%;height:40px;background-color:var(--vp-c-bg-alt)}.DocSearch-Button:hover{border-color:var(--vp-c-brand-1);background:var(--vp-c-bg-alt)}}.DocSearch-Button .DocSearch-Button-Container{display:flex;align-items:center}.DocSearch-Button .DocSearch-Search-Icon{position:relative;width:16px;height:16px;color:var(--vp-c-text-1);fill:currentColor;transition:color .5s}.DocSearch-Button:hover .DocSearch-Search-Icon{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Search-Icon{top:1px;margin-right:8px;width:14px;height:14px;color:var(--vp-c-text-2)}}.DocSearch-Button .DocSearch-Button-Placeholder{display:none;margin-top:2px;padding:0 16px 0 0;font-size:13px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.DocSearch-Button:hover .DocSearch-Button-Placeholder{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Placeholder{display:inline-block}}.DocSearch-Button .DocSearch-Button-Keys{direction:ltr;display:none;min-width:auto}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Keys{display:flex;align-items:center}}.DocSearch-Button .DocSearch-Button-Key{display:block;margin:2px 0 0;border:1px solid var(--vp-c-divider);border-right:none;border-radius:4px 0 0 4px;padding-left:6px;min-width:0;width:auto;height:22px;line-height:22px;font-family:var(--vp-font-family-base);font-size:12px;font-weight:500;transition:color .5s,border-color .5s}.DocSearch-Button .DocSearch-Button-Key+.DocSearch-Button-Key{border-right:1px solid var(--vp-c-divider);border-left:none;border-radius:0 4px 4px 0;padding-left:2px;padding-right:6px}.DocSearch-Button .DocSearch-Button-Key:first-child{font-size:0!important}.DocSearch-Button .DocSearch-Button-Key:first-child:after{content:"Ctrl";font-size:12px;letter-spacing:normal;color:var(--docsearch-muted-color)}.mac .DocSearch-Button .DocSearch-Button-Key:first-child:after{content:"⌘"}.DocSearch-Button .DocSearch-Button-Key:first-child>*{display:none}.DocSearch-Search-Icon{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' stroke-width='1.6' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='m14.386 14.386 4.088 4.088-4.088-4.088A7.533 7.533 0 1 1 3.733 3.733a7.533 7.533 0 0 1 10.653 10.653z'/%3E%3C/svg%3E")}.VPNavBarSearch{display:flex;align-items:center}@media (min-width: 768px){.VPNavBarSearch{flex-grow:1;padding-left:24px}}@media (min-width: 960px){.VPNavBarSearch{padding-left:32px}}.dark .DocSearch-Footer{border-top:1px solid var(--vp-c-divider)}.DocSearch-Form{border:1px solid var(--vp-c-brand-1);background-color:var(--vp-c-white)}.dark .DocSearch-Form{background-color:var(--vp-c-default-soft)}.DocSearch-Screen-Icon>svg{margin:auto}.VPNavBarSocialLinks[data-v-164c457f]{display:none}@media (min-width: 1280px){.VPNavBarSocialLinks[data-v-164c457f]{display:flex;align-items:center}}.title[data-v-0f4f798b]{display:flex;align-items:center;border-bottom:1px solid transparent;width:100%;height:var(--vp-nav-height);font-size:16px;font-weight:600;color:var(--vp-c-text-1);transition:opacity .25s}@media (min-width: 960px){.title[data-v-0f4f798b]{flex-shrink:0}.VPNavBarTitle.has-sidebar .title[data-v-0f4f798b]{border-bottom-color:var(--vp-c-divider)}}[data-v-0f4f798b] .logo{margin-right:8px;height:var(--vp-nav-logo-height)}.VPNavBarTranslations[data-v-c80d9ad0]{display:none}@media (min-width: 1280px){.VPNavBarTranslations[data-v-c80d9ad0]{display:flex;align-items:center}}.title[data-v-c80d9ad0]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.VPNavBar[data-v-822684d1]{position:relative;height:var(--vp-nav-height);pointer-events:none;white-space:nowrap;transition:background-color .25s}.VPNavBar.screen-open[data-v-822684d1]{transition:none;background-color:var(--vp-nav-bg-color);border-bottom:1px solid var(--vp-c-divider)}.VPNavBar[data-v-822684d1]:not(.home){background-color:var(--vp-nav-bg-color)}@media (min-width: 960px){.VPNavBar[data-v-822684d1]:not(.home){background-color:transparent}.VPNavBar[data-v-822684d1]:not(.has-sidebar):not(.home.top){background-color:var(--vp-nav-bg-color)}}.wrapper[data-v-822684d1]{padding:0 8px 0 24px}@media (min-width: 768px){.wrapper[data-v-822684d1]{padding:0 32px}}@media (min-width: 960px){.VPNavBar.has-sidebar .wrapper[data-v-822684d1]{padding:0}}.container[data-v-822684d1]{display:flex;justify-content:space-between;margin:0 auto;max-width:calc(var(--vp-layout-max-width) - 64px);height:var(--vp-nav-height);pointer-events:none}.container>.title[data-v-822684d1],.container>.content[data-v-822684d1]{pointer-events:none}.container[data-v-822684d1] *{pointer-events:auto}@media (min-width: 960px){.VPNavBar.has-sidebar .container[data-v-822684d1]{max-width:100%}}.title[data-v-822684d1]{flex-shrink:0;height:calc(var(--vp-nav-height) - 1px);transition:background-color .5s}@media (min-width: 960px){.VPNavBar.has-sidebar .title[data-v-822684d1]{position:absolute;top:0;left:0;z-index:2;padding:0 32px;width:var(--vp-sidebar-width);height:var(--vp-nav-height);background-color:transparent}}@media (min-width: 1440px){.VPNavBar.has-sidebar .title[data-v-822684d1]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}.content[data-v-822684d1]{flex-grow:1}@media (min-width: 960px){.VPNavBar.has-sidebar .content[data-v-822684d1]{position:relative;z-index:1;padding-right:32px;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .content[data-v-822684d1]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.content-body[data-v-822684d1]{display:flex;justify-content:flex-end;align-items:center;height:var(--vp-nav-height);transition:background-color .5s}@media (min-width: 960px){.VPNavBar:not(.home.top) .content-body[data-v-822684d1]{position:relative;background-color:var(--vp-nav-bg-color)}.VPNavBar:not(.has-sidebar):not(.home.top) .content-body[data-v-822684d1]{background-color:transparent}}@media (max-width: 767px){.content-body[data-v-822684d1]{column-gap:.5rem}}.menu+.translations[data-v-822684d1]:before,.menu+.appearance[data-v-822684d1]:before,.menu+.social-links[data-v-822684d1]:before,.translations+.appearance[data-v-822684d1]:before,.appearance+.social-links[data-v-822684d1]:before{margin-right:8px;margin-left:8px;width:1px;height:24px;background-color:var(--vp-c-divider);content:""}.menu+.appearance[data-v-822684d1]:before,.translations+.appearance[data-v-822684d1]:before{margin-right:16px}.appearance+.social-links[data-v-822684d1]:before{margin-left:16px}.social-links[data-v-822684d1]{margin-right:-8px}.divider[data-v-822684d1]{width:100%;height:1px}@media (min-width: 960px){.VPNavBar.has-sidebar .divider[data-v-822684d1]{padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .divider[data-v-822684d1]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.divider-line[data-v-822684d1]{width:100%;height:1px;transition:background-color .5s}.VPNavBar:not(.home) .divider-line[data-v-822684d1]{background-color:var(--vp-c-gutter)}@media (min-width: 960px){.VPNavBar:not(.home.top) .divider-line[data-v-822684d1]{background-color:var(--vp-c-gutter)}.VPNavBar:not(.has-sidebar):not(.home.top) .divider[data-v-822684d1]{background-color:var(--vp-c-gutter)}}.VPNavScreenAppearance[data-v-ffb44008]{display:flex;justify-content:space-between;align-items:center;border-radius:8px;padding:12px 14px 12px 16px;background-color:var(--vp-c-bg-soft)}.text[data-v-ffb44008]{line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.VPNavScreenMenuLink[data-v-735512b8]{display:block;border-bottom:1px solid var(--vp-c-divider);padding:12px 0 11px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:border-color .25s,color .25s}.VPNavScreenMenuLink[data-v-735512b8]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupLink[data-v-372ae7c0]{display:block;margin-left:12px;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-1);transition:color .25s}.VPNavScreenMenuGroupLink[data-v-372ae7c0]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupSection[data-v-4b8941ac]{display:block}.title[data-v-4b8941ac]{line-height:32px;font-size:13px;font-weight:700;color:var(--vp-c-text-2);transition:color .25s}.VPNavScreenMenuGroup[data-v-875057a5]{border-bottom:1px solid var(--vp-c-divider);height:48px;overflow:hidden;transition:border-color .5s}.VPNavScreenMenuGroup .items[data-v-875057a5]{visibility:hidden}.VPNavScreenMenuGroup.open .items[data-v-875057a5]{visibility:visible}.VPNavScreenMenuGroup.open[data-v-875057a5]{padding-bottom:10px;height:auto}.VPNavScreenMenuGroup.open .button[data-v-875057a5]{padding-bottom:6px;color:var(--vp-c-brand-1)}.VPNavScreenMenuGroup.open .button-icon[data-v-875057a5]{transform:rotate(45deg)}.button[data-v-875057a5]{display:flex;justify-content:space-between;align-items:center;padding:12px 4px 11px 0;width:100%;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.button[data-v-875057a5]:hover{color:var(--vp-c-brand-1)}.button-icon[data-v-875057a5]{transition:transform .25s}.group[data-v-875057a5]:first-child{padding-top:0}.group+.group[data-v-875057a5],.group+.item[data-v-875057a5]{padding-top:4px}.VPNavScreenTranslations[data-v-362991c2]{height:24px;overflow:hidden}.VPNavScreenTranslations.open[data-v-362991c2]{height:auto}.title[data-v-362991c2]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-text-1)}.icon[data-v-362991c2]{font-size:16px}.icon.lang[data-v-362991c2]{margin-right:8px}.icon.chevron[data-v-362991c2]{margin-left:4px}.list[data-v-362991c2]{padding:4px 0 0 24px}.link[data-v-362991c2]{line-height:32px;font-size:13px;color:var(--vp-c-text-1)}.VPNavScreen[data-v-833aabba]{position:fixed;top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px));right:0;bottom:0;left:0;padding:0 32px;width:100%;background-color:var(--vp-nav-screen-bg-color);overflow-y:auto;transition:background-color .25s;pointer-events:auto}.VPNavScreen.fade-enter-active[data-v-833aabba],.VPNavScreen.fade-leave-active[data-v-833aabba]{transition:opacity .25s}.VPNavScreen.fade-enter-active .container[data-v-833aabba],.VPNavScreen.fade-leave-active .container[data-v-833aabba]{transition:transform .25s ease}.VPNavScreen.fade-enter-from[data-v-833aabba],.VPNavScreen.fade-leave-to[data-v-833aabba]{opacity:0}.VPNavScreen.fade-enter-from .container[data-v-833aabba],.VPNavScreen.fade-leave-to .container[data-v-833aabba]{transform:translateY(-8px)}@media (min-width: 768px){.VPNavScreen[data-v-833aabba]{display:none}}.container[data-v-833aabba]{margin:0 auto;padding:24px 0 96px;max-width:288px}.menu+.translations[data-v-833aabba],.menu+.appearance[data-v-833aabba],.translations+.appearance[data-v-833aabba]{margin-top:24px}.menu+.social-links[data-v-833aabba]{margin-top:16px}.appearance+.social-links[data-v-833aabba]{margin-top:16px}.VPNav[data-v-f1e365da]{position:relative;top:var(--vp-layout-top-height, 0px);left:0;z-index:var(--vp-z-index-nav);width:100%;pointer-events:none;transition:background-color .5s}@media (min-width: 960px){.VPNav[data-v-f1e365da]{position:fixed}}.VPSidebarItem.level-0[data-v-a4b0d9bf]{padding-bottom:24px}.VPSidebarItem.collapsed.level-0[data-v-a4b0d9bf]{padding-bottom:10px}.item[data-v-a4b0d9bf]{position:relative;display:flex;width:100%}.VPSidebarItem.collapsible>.item[data-v-a4b0d9bf]{cursor:pointer}.indicator[data-v-a4b0d9bf]{position:absolute;top:6px;bottom:6px;left:-17px;width:2px;border-radius:2px;transition:background-color .25s}.VPSidebarItem.level-2.is-active>.item>.indicator[data-v-a4b0d9bf],.VPSidebarItem.level-3.is-active>.item>.indicator[data-v-a4b0d9bf],.VPSidebarItem.level-4.is-active>.item>.indicator[data-v-a4b0d9bf],.VPSidebarItem.level-5.is-active>.item>.indicator[data-v-a4b0d9bf]{background-color:var(--vp-c-brand-1)}.link[data-v-a4b0d9bf]{display:flex;align-items:center;flex-grow:1}.text[data-v-a4b0d9bf]{flex-grow:1;padding:4px 0;line-height:24px;font-size:14px;transition:color .25s}.VPSidebarItem.level-0 .text[data-v-a4b0d9bf]{font-weight:700;color:var(--vp-c-text-1)}.VPSidebarItem.level-1 .text[data-v-a4b0d9bf],.VPSidebarItem.level-2 .text[data-v-a4b0d9bf],.VPSidebarItem.level-3 .text[data-v-a4b0d9bf],.VPSidebarItem.level-4 .text[data-v-a4b0d9bf],.VPSidebarItem.level-5 .text[data-v-a4b0d9bf]{font-weight:500;color:var(--vp-c-text-2)}.VPSidebarItem.level-0.is-link>.item>.link:hover .text[data-v-a4b0d9bf],.VPSidebarItem.level-1.is-link>.item>.link:hover .text[data-v-a4b0d9bf],.VPSidebarItem.level-2.is-link>.item>.link:hover .text[data-v-a4b0d9bf],.VPSidebarItem.level-3.is-link>.item>.link:hover .text[data-v-a4b0d9bf],.VPSidebarItem.level-4.is-link>.item>.link:hover .text[data-v-a4b0d9bf],.VPSidebarItem.level-5.is-link>.item>.link:hover .text[data-v-a4b0d9bf]{color:var(--vp-c-brand-1)}.VPSidebarItem.level-0.has-active>.item>.text[data-v-a4b0d9bf],.VPSidebarItem.level-1.has-active>.item>.text[data-v-a4b0d9bf],.VPSidebarItem.level-2.has-active>.item>.text[data-v-a4b0d9bf],.VPSidebarItem.level-3.has-active>.item>.text[data-v-a4b0d9bf],.VPSidebarItem.level-4.has-active>.item>.text[data-v-a4b0d9bf],.VPSidebarItem.level-5.has-active>.item>.text[data-v-a4b0d9bf],.VPSidebarItem.level-0.has-active>.item>.link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-1.has-active>.item>.link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-2.has-active>.item>.link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-3.has-active>.item>.link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-4.has-active>.item>.link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-5.has-active>.item>.link>.text[data-v-a4b0d9bf]{color:var(--vp-c-text-1)}.VPSidebarItem.level-0.is-active>.item .link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-1.is-active>.item .link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-2.is-active>.item .link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-3.is-active>.item .link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-4.is-active>.item .link>.text[data-v-a4b0d9bf],.VPSidebarItem.level-5.is-active>.item .link>.text[data-v-a4b0d9bf]{color:var(--vp-c-brand-1)}.caret[data-v-a4b0d9bf]{display:flex;justify-content:center;align-items:center;margin-right:-7px;width:32px;height:32px;color:var(--vp-c-text-3);cursor:pointer;transition:color .25s;flex-shrink:0}.item:hover .caret[data-v-a4b0d9bf]{color:var(--vp-c-text-2)}.item:hover .caret[data-v-a4b0d9bf]:hover{color:var(--vp-c-text-1)}.caret-icon[data-v-a4b0d9bf]{font-size:18px;transform:rotate(90deg);transition:transform .25s}.VPSidebarItem.collapsed .caret-icon[data-v-a4b0d9bf]{transform:rotate(0)}.VPSidebarItem.level-1 .items[data-v-a4b0d9bf],.VPSidebarItem.level-2 .items[data-v-a4b0d9bf],.VPSidebarItem.level-3 .items[data-v-a4b0d9bf],.VPSidebarItem.level-4 .items[data-v-a4b0d9bf],.VPSidebarItem.level-5 .items[data-v-a4b0d9bf]{border-left:1px solid var(--vp-c-divider);padding-left:16px}.VPSidebarItem.collapsed .items[data-v-a4b0d9bf]{display:none}.no-transition[data-v-9e426adc] .caret-icon{transition:none}.group+.group[data-v-9e426adc]{border-top:1px solid var(--vp-c-divider);padding-top:10px}@media (min-width: 960px){.group[data-v-9e426adc]{padding-top:10px;width:calc(var(--vp-sidebar-width) - 64px)}}.VPSidebar[data-v-18756405]{position:fixed;top:var(--vp-layout-top-height, 0px);bottom:0;left:0;z-index:var(--vp-z-index-sidebar);padding:32px 32px 96px;width:calc(100vw - 64px);max-width:320px;background-color:var(--vp-sidebar-bg-color);opacity:0;box-shadow:var(--vp-c-shadow-3);overflow-x:hidden;overflow-y:auto;transform:translate(-100%);transition:opacity .5s,transform .25s ease;overscroll-behavior:contain}.VPSidebar.open[data-v-18756405]{opacity:1;visibility:visible;transform:translate(0);transition:opacity .25s,transform .5s cubic-bezier(.19,1,.22,1)}.dark .VPSidebar[data-v-18756405]{box-shadow:var(--vp-shadow-1)}@media (min-width: 960px){.VPSidebar[data-v-18756405]{padding-top:var(--vp-nav-height);width:var(--vp-sidebar-width);max-width:100%;background-color:var(--vp-sidebar-bg-color);opacity:1;visibility:visible;box-shadow:none;transform:translate(0)}}@media (min-width: 1440px){.VPSidebar[data-v-18756405]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}@media (min-width: 960px){.curtain[data-v-18756405]{position:sticky;top:-64px;left:0;z-index:1;margin-top:calc(var(--vp-nav-height) * -1);margin-right:-32px;margin-left:-32px;height:var(--vp-nav-height);background-color:var(--vp-sidebar-bg-color)}}.nav[data-v-18756405]{outline:0}.VPSkipLink[data-v-492508fc]{top:8px;left:8px;padding:8px 16px;z-index:999;border-radius:8px;font-size:12px;font-weight:700;text-decoration:none;color:var(--vp-c-brand-1);box-shadow:var(--vp-shadow-3);background-color:var(--vp-c-bg)}.VPSkipLink[data-v-492508fc]:focus{height:auto;width:auto;clip:auto;clip-path:none}@media (min-width: 1280px){.VPSkipLink[data-v-492508fc]{top:14px;left:16px}}.Layout[data-v-a9a9e638]{display:flex;flex-direction:column;min-height:100vh}.VPHomeSponsors[data-v-db81191c]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPHomeSponsors[data-v-db81191c]{margin:96px 0}@media (min-width: 768px){.VPHomeSponsors[data-v-db81191c]{margin:128px 0}}.VPHomeSponsors[data-v-db81191c]{padding:0 24px}@media (min-width: 768px){.VPHomeSponsors[data-v-db81191c]{padding:0 48px}}@media (min-width: 960px){.VPHomeSponsors[data-v-db81191c]{padding:0 64px}}.container[data-v-db81191c]{margin:0 auto;max-width:1152px}.love[data-v-db81191c]{margin:0 auto;width:fit-content;font-size:28px;color:var(--vp-c-text-3)}.icon[data-v-db81191c]{display:inline-block}.message[data-v-db81191c]{margin:0 auto;padding-top:10px;max-width:320px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.sponsors[data-v-db81191c]{padding-top:32px}.action[data-v-db81191c]{padding-top:40px;text-align:center}.VPTeamMembersItem[data-v-f9987cb6]{display:flex;flex-direction:column;gap:2px;border-radius:12px;width:100%;height:100%;overflow:hidden}.VPTeamMembersItem.small .profile[data-v-f9987cb6]{padding:32px}.VPTeamMembersItem.small .data[data-v-f9987cb6]{padding-top:20px}.VPTeamMembersItem.small .avatar[data-v-f9987cb6]{width:64px;height:64px}.VPTeamMembersItem.small .name[data-v-f9987cb6]{line-height:24px;font-size:16px}.VPTeamMembersItem.small .affiliation[data-v-f9987cb6]{padding-top:4px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .desc[data-v-f9987cb6]{padding-top:12px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .links[data-v-f9987cb6]{margin:0 -16px -20px;padding:10px 0 0}.VPTeamMembersItem.medium .profile[data-v-f9987cb6]{padding:48px 32px}.VPTeamMembersItem.medium .data[data-v-f9987cb6]{padding-top:24px;text-align:center}.VPTeamMembersItem.medium .avatar[data-v-f9987cb6]{width:96px;height:96px}.VPTeamMembersItem.medium .name[data-v-f9987cb6]{letter-spacing:.15px;line-height:28px;font-size:20px}.VPTeamMembersItem.medium .affiliation[data-v-f9987cb6]{padding-top:4px;font-size:16px}.VPTeamMembersItem.medium .desc[data-v-f9987cb6]{padding-top:16px;max-width:288px;font-size:16px}.VPTeamMembersItem.medium .links[data-v-f9987cb6]{margin:0 -16px -12px;padding:16px 12px 0}.profile[data-v-f9987cb6]{flex-grow:1;background-color:var(--vp-c-bg-soft)}.data[data-v-f9987cb6]{text-align:center}.avatar[data-v-f9987cb6]{position:relative;flex-shrink:0;margin:0 auto;border-radius:50%;box-shadow:var(--vp-shadow-3)}.avatar-img[data-v-f9987cb6]{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;object-fit:cover}.name[data-v-f9987cb6]{margin:0;font-weight:600}.affiliation[data-v-f9987cb6]{margin:0;font-weight:500;color:var(--vp-c-text-2)}.org.link[data-v-f9987cb6]{color:var(--vp-c-text-2);transition:color .25s}.org.link[data-v-f9987cb6]:hover{color:var(--vp-c-brand-1)}.desc[data-v-f9987cb6]{margin:0 auto}.desc[data-v-f9987cb6] a{font-weight:500;color:var(--vp-c-brand-1);text-decoration-style:dotted;transition:color .25s}.links[data-v-f9987cb6]{display:flex;justify-content:center;height:56px}.sp-link[data-v-f9987cb6]{display:flex;justify-content:center;align-items:center;text-align:center;padding:16px;font-size:14px;font-weight:500;color:var(--vp-c-sponsor);background-color:var(--vp-c-bg-soft);transition:color .25s,background-color .25s}.sp .sp-link.link[data-v-f9987cb6]:hover,.sp .sp-link.link[data-v-f9987cb6]:focus{outline:none;color:var(--vp-c-white);background-color:var(--vp-c-sponsor)}.sp-icon[data-v-f9987cb6]{margin-right:8px;font-size:16px}.VPTeamMembers.small .container[data-v-fba19bad]{grid-template-columns:repeat(auto-fit,minmax(224px,1fr))}.VPTeamMembers.small.count-1 .container[data-v-fba19bad]{max-width:276px}.VPTeamMembers.small.count-2 .container[data-v-fba19bad]{max-width:576px}.VPTeamMembers.small.count-3 .container[data-v-fba19bad]{max-width:876px}.VPTeamMembers.medium .container[data-v-fba19bad]{grid-template-columns:repeat(auto-fit,minmax(256px,1fr))}@media (min-width: 375px){.VPTeamMembers.medium .container[data-v-fba19bad]{grid-template-columns:repeat(auto-fit,minmax(288px,1fr))}}.VPTeamMembers.medium.count-1 .container[data-v-fba19bad]{max-width:368px}.VPTeamMembers.medium.count-2 .container[data-v-fba19bad]{max-width:760px}.container[data-v-fba19bad]{display:grid;gap:24px;margin:0 auto;max-width:1152px}.VPTeamPage[data-v-c2f8e101]{margin:96px 0}@media (min-width: 768px){.VPTeamPage[data-v-c2f8e101]{margin:128px 0}}.VPHome .VPTeamPageTitle[data-v-c2f8e101-s]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPTeamPageSection+.VPTeamPageSection[data-v-c2f8e101-s],.VPTeamMembers+.VPTeamPageSection[data-v-c2f8e101-s]{margin-top:64px}.VPTeamMembers+.VPTeamMembers[data-v-c2f8e101-s]{margin-top:24px}@media (min-width: 768px){.VPTeamPageTitle+.VPTeamPageSection[data-v-c2f8e101-s]{margin-top:16px}.VPTeamPageSection+.VPTeamPageSection[data-v-c2f8e101-s],.VPTeamMembers+.VPTeamPageSection[data-v-c2f8e101-s]{margin-top:96px}}.VPTeamMembers[data-v-c2f8e101-s]{padding:0 24px}@media (min-width: 768px){.VPTeamMembers[data-v-c2f8e101-s]{padding:0 48px}}@media (min-width: 960px){.VPTeamMembers[data-v-c2f8e101-s]{padding:0 64px}}.VPTeamPageSection[data-v-d43bc49d]{padding:0 32px}@media (min-width: 768px){.VPTeamPageSection[data-v-d43bc49d]{padding:0 48px}}@media (min-width: 960px){.VPTeamPageSection[data-v-d43bc49d]{padding:0 64px}}.title[data-v-d43bc49d]{position:relative;margin:0 auto;max-width:1152px;text-align:center;color:var(--vp-c-text-2)}.title-line[data-v-d43bc49d]{position:absolute;top:16px;left:0;width:100%;height:1px;background-color:var(--vp-c-divider)}.title-text[data-v-d43bc49d]{position:relative;display:inline-block;padding:0 24px;letter-spacing:0;line-height:32px;font-size:20px;font-weight:500;background-color:var(--vp-c-bg)}.lead[data-v-d43bc49d]{margin:0 auto;max-width:480px;padding-top:12px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.members[data-v-d43bc49d]{padding-top:40px}.VPTeamPageTitle[data-v-e277e15c]{padding:48px 32px;text-align:center}@media (min-width: 768px){.VPTeamPageTitle[data-v-e277e15c]{padding:64px 48px 48px}}@media (min-width: 960px){.VPTeamPageTitle[data-v-e277e15c]{padding:80px 64px 48px}}.title[data-v-e277e15c]{letter-spacing:0;line-height:44px;font-size:36px;font-weight:500}@media (min-width: 768px){.title[data-v-e277e15c]{letter-spacing:-.5px;line-height:56px;font-size:48px}}.lead[data-v-e277e15c]{margin:0 auto;max-width:512px;padding-top:12px;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 768px){.lead[data-v-e277e15c]{max-width:592px;letter-spacing:.15px;line-height:28px;font-size:20px}} diff --git a/docs/assets/vite_plugin.md.4TJA8cv0.js b/docs/assets/vite_plugin.md.4TJA8cv0.js new file mode 100644 index 0000000..823f111 --- /dev/null +++ b/docs/assets/vite_plugin.md.4TJA8cv0.js @@ -0,0 +1,50 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Vite Plugin: File-based Routing","description":"","frontmatter":{},"headers":[],"relativePath":"vite/plugin.md","filePath":"vite/plugin.md"}'),e={name:"vite/plugin.md"};function p(l,s,h,k,r,d){return a(),t("div",null,[...s[0]||(s[0]=[n(`

Vite Plugin: File-based Routing

The sigproRouter plugin for Vite automates route generation by scanning your pages directory. It creates a virtual module that you can import directly into your code, eliminating the need to maintain a manual routes array.

1. Project Structure

To use the plugin, organize your files within the src/pages directory. The folder hierarchy directly determines your application's URL structure.

text
my-sigpro-app/
+├── src/
+│   ├── pages/
+│   │   ├── index.js          →  #/
+│   │   ├── about.js          →  #/about
+│   │   ├── users/
+│   │   │   └── [id].js       →  #/users/:id
+│   │   └── blog/
+│   │       ├── index.js      →  #/blog
+│   │       └── [slug].js     →  #/blog/:slug
+│   ├── App.js                (Optional App Shell)
+│   └── main.js               (Entry Point)
+├── vite.config.js
+└── package.json

2. Setup & Configuration

Add the plugin to your vite.config.js.

javascript
// vite.config.js
+import { defineConfig } from 'vite';
+import { sigproRouter } from 'sigpro/vite';
+
+export default defineConfig({
+  plugins: [sigproRouter()]
+});

3. Implementation

You can implement the router either directly in your entry point or inside an App component to support persistent layouts (like a navbar that doesn't re-render).

Option A: Direct in main.js

Best for simple apps where the router occupies the entire viewport.

javascript
// src/main.js
+import { $ } from 'sigpro';
+import { Router } from 'sigpro/plugins';
+import { routes } from 'virtual:sigpro-routes';
+
+$.plugin(Router).then(() => {
+  $.mount(_router(routes), '#app');
+});

Option B: Inside App.js (With Layout)

Recommended for apps with a fixed Sidebar or Navbar.

javascript
// src/main.js
+import { $ } from 'sigpro';
+import { Router } from 'sigpro/plugins';
+
+$.plugin(Router).then(() => {
+  import('./App.js').then(app => $.mount(app.default, '#app'));
+});
+
+// src/App.js
+import { routes } from 'virtual:sigpro-routes';
+
+export default () => {
+  return div({ class: 'layout' }, [
+    header([
+      h1("SigPro App"),
+      nav([
+        a({ href: '#/' }, "Home"),
+        a({ href: '#/blog' }, "Blog")
+      ])
+    ]),
+    // The router only swaps the content inside this <main> tag
+    main(_router(routes))
+  ]);
+};

4. Route Mapping Reference

File PathGenerated RouteLogic
index.js/Home page
about.js/aboutStatic path
[id].js/:idDynamic parameter
blog/index.js/blogFolder index
_utils.jsIgnoredFiles starting with _ are skipped

5. Installation

bash
npm install sigpro
bash
pnpm add sigpro
bash
yarn add sigpro
bash
bun add sigpro
`,24)])])}const E=i(e,[["render",p]]);export{g as __pageData,E as default}; diff --git a/docs/assets/vite_plugin.md.4TJA8cv0.lean.js b/docs/assets/vite_plugin.md.4TJA8cv0.lean.js new file mode 100644 index 0000000..8544b5e --- /dev/null +++ b/docs/assets/vite_plugin.md.4TJA8cv0.lean.js @@ -0,0 +1 @@ +import{_ as i,o as a,c as t,ae as n}from"./chunks/framework.C8AWLET_.js";const g=JSON.parse('{"title":"Vite Plugin: File-based Routing","description":"","frontmatter":{},"headers":[],"relativePath":"vite/plugin.md","filePath":"vite/plugin.md"}'),e={name:"vite/plugin.md"};function p(l,s,h,k,r,d){return a(),t("div",null,[...s[0]||(s[0]=[n("",24)])])}const E=i(e,[["render",p]]);export{g as __pageData,E as default}; diff --git a/docs/assets/vite_plugin.md.gDWEi8f0.js b/docs/assets/vite_plugin.md.gDWEi8f0.js deleted file mode 100644 index 1892e00..0000000 --- a/docs/assets/vite_plugin.md.gDWEi8f0.js +++ /dev/null @@ -1,225 +0,0 @@ -import{_ as i,o as a,c as n,ae as t}from"./chunks/framework.C8AWLET_.js";const E=JSON.parse('{"title":"Vite Plugin: Automatic File-based Routing 🚦","description":"","frontmatter":{},"headers":[],"relativePath":"vite/plugin.md","filePath":"vite/plugin.md"}'),l={name:"vite/plugin.md"};function p(h,s,e,k,r,d){return a(),n("div",null,[...s[0]||(s[0]=[t(`

Vite Plugin: Automatic File-based Routing 🚦

SigPro provides an optional Vite plugin that automatically generates routes based on your file structure. No configuration needed - just create pages and they're instantly available with the correct paths.

Why Use This Plugin?

While SigPro's router works perfectly with manually defined routes, this plugin:

  • Eliminates boilerplate - No need to write route configurations
  • Enforces conventions - Consistent URL structure across your app
  • Supports dynamic routes - Use [param] syntax for parameters
  • Automatic code-splitting - Each page becomes a separate chunk
  • Type-safe (with JSDoc) - Routes follow your file structure

Installation

The plugin is included with SigPro, but you need to add it to your Vite config:

javascript
// vite.config.js
-import { defineConfig } from 'vite';
-import { sigproRouter } from 'sigpro';
-
-export default defineConfig({
-  plugins: [sigproRouter()]
-});

How It Works

The plugin scans your src/pages directory and automatically generates routes based on the file structure:

src/pages/
-├── index.js              →  '/'
-├── about.js              →  '/about'
-├── blog/
-│   ├── index.js          →  '/blog'
-│   └── [slug].js         →  '/blog/:slug'
-└── users/
-    ├── [id].js           →  '/users/:id'
-    └── [id]/edit.js      →  '/users/:id/edit'

Usage

1. Enable the Plugin

Add the plugin to your Vite config as shown above.

2. Import the Generated Routes

Once you have the generated routes, using them with the router is straightforward:

javascript
// main.js
-import { $, html } from 'sigpro';
-import { routes } from 'virtual:sigpro-routes';
-
-// Simple usage
-const router = $.router(routes);
-document.body.appendChild(router);

Or directly in your template:

javascript
// app.js
-import { $, html } from 'sigpro';
-import { routes } from 'virtual:sigpro-routes';
-
-const App = () => html\`
-  <div class="app">
-    <header>
-      <h1>My Application</h1>
-    </header>
-    
-    <main class="p-4 flex flex-col gap-4 mx-auto w-full">
-      <div class="p-4 bg-base-100 rounded-box shadow-sm">
-        \${$.router(routes)}
-      </div>
-    </main>
-  </div>
-\`;
-
-document.body.appendChild(App());

This approach keeps your template clean and lets the router handle all the page rendering automatically.

3. Create Pages

javascript
// src/pages/index.js
-import { $, html } from 'sigpro';
-
-export default () => {
-  return html\`
-    <div>
-      <h1>Home Page</h1>
-      <a href="#/about">About</a>
-    </div>
-  \`;
-};
javascript
// src/pages/users/[id].js
-import { $, html } from 'sigpro';
-
-export default (params) => {
-  const userId = params.id;
-  
-  return html\`
-    <div>
-      <h1>User Profile: \${userId}</h1>
-      <a href="#/users/\${userId}/edit">Edit</a>
-    </div>
-  \`;
-};

📋 File-to-Route Mapping

Static Routes

File PathGenerated Route
src/pages/index.js/
src/pages/about.js/about
src/pages/contact/index.js/contact
src/pages/blog/post.js/blog/post

Dynamic Routes

File PathGenerated RouteExample URL
src/pages/users/[id].js/users/:id/users/42
src/pages/blog/[slug].js/blog/:slug/blog/hello-world
src/pages/users/[id]/posts/[pid].js/users/:id/posts/:pid/users/42/posts/123

Nested Routes

File PathGenerated RouteNotes
src/pages/settings/index.js/settingsIndex page
src/pages/settings/profile.js/settings/profileSub-page
src/pages/settings/security.js/settings/securitySub-page
src/pages/settings/[section].js/settings/:sectionDynamic section

🎯 Advanced Examples

Blog with Posts

javascript
// src/pages/blog/index.js - Lists all posts
-export default () => {
-  const posts = $([]);
-  
-  $.effect(() => {
-    fetch('/api/posts')
-      .then(res => res.json())
-      .then(data => posts(data));
-  });
-  
-  return html\`
-    <div>
-      <h1>Blog</h1>
-      \${posts().map(post => html\`
-        <article>
-          <h2><a href="#/blog/\${post.slug}">\${post.title}</a></h2>
-          <p>\${post.excerpt}</p>
-        </article>
-      \`)}
-    </div>
-  \`;
-};
javascript
// src/pages/blog/[slug].js - Single post
-export default (params) => {
-  const post = $(null);
-  const slug = params.slug;
-  
-  $.effect(() => {
-    fetch(\`/api/posts/\${slug}\`)
-      .then(res => res.json())
-      .then(data => post(data));
-  });
-  
-  return html\`
-    <div>
-      <a href="#/blog">← Back to blog</a>
-      \${() => post() ? html\`
-        <article>
-          <h1>\${post().title}</h1>
-          <div>\${post().content}</div>
-        </article>
-      \` : html\`<div>Loading...</div>\`}
-    </div>
-  \`;
-};

Dashboard with Nested Sections

javascript
// src/pages/dashboard/index.js
-export default () => {
-  return html\`
-    <div class="dashboard">
-      <nav>
-        <a href="#/dashboard">Overview</a>
-        <a href="#/dashboard/analytics">Analytics</a>
-        <a href="#/dashboard/settings">Settings</a>
-      </nav>
-      <main>
-        <h1>Dashboard Overview</h1>
-        <!-- Overview content -->
-      </main>
-    </div>
-  \`;
-};
javascript
// src/pages/dashboard/analytics.js
-export default () => {
-  return html\`
-    <div class="dashboard">
-      <nav>
-        <a href="#/dashboard">Overview</a>
-        <a href="#/dashboard/analytics">Analytics</a>
-        <a href="#/dashboard/settings">Settings</a>
-      </nav>
-      <main>
-        <h1>Analytics</h1>
-        <!-- Analytics content -->
-      </main>
-    </div>
-  \`;
-};

E-commerce Product Routes

javascript
// src/pages/products/[category]/[id].js
-export default (params) => {
-  const { category, id } = params;
-  const product = $(null);
-  
-  $.effect(() => {
-    fetch(\`/api/products/\${category}/\${id}\`)
-      .then(res => res.json())
-      .then(data => product(data));
-  });
-  
-  return html\`
-    <div class="product-page">
-      <nav class="breadcrumbs">
-        <a href="#/products">Products</a> &gt;
-        <a href="#/products/\${category}">\${category}</a> &gt;
-        <span>\${id}</span>
-      </nav>
-      
-      \${() => product() ? html\`
-        <div class="product">
-          <h1>\${product().name}</h1>
-          <p class="price">$\${product().price}</p>
-          <p>\${product().description}</p>
-          <button @click=\${() => addToCart(product())}>
-            Add to Cart
-          </button>
-        </div>
-      \` : html\`<div>Loading...</div>\`}
-    </div>
-  \`;
-};

🔧 Configuration Options

The plugin accepts an optional configuration object:

javascript
// vite.config.js
-import { defineConfig } from 'vite';
-import { sigproRouter } from 'sigpro/vite';
-
-export default defineConfig({
-  plugins: [
-    sigproRouter({
-      pagesDir: 'src/pages',      // Default: 'src/pages'
-      extensions: ['.js', '.jsx'], // Default: ['.js', '.jsx']
-      exclude: ['**/_*', '**/components/**'] // Glob patterns to exclude
-    })
-  ]
-});

Options

OptionTypeDefaultDescription
pagesDirstring'src/pages'Directory containing your pages
extensionsstring[]['.js', '.jsx']File extensions to include
excludestring[][]Glob patterns to exclude

🎯 Route Priority

The plugin automatically sorts routes to ensure correct matching:

  1. Static routes take precedence over dynamic ones
  2. More specific routes (deeper paths) come first
  3. Alphabetical order for routes at the same level

Example sorting:

/users/new           (static, specific)
-/users/[id]/edit     (dynamic, deeper)
-/users/[id]          (dynamic, shallower)
-/users/profile       (static, shallower)

📦 Output Example

When you import virtual:sigpro-routes, you get:

javascript
// Generated module
-import Page_0 from '/src/pages/index.js';
-import Page_1 from '/src/pages/about.js';
-import Page_2 from '/src/pages/blog/index.js';
-import Page_3 from '/src/pages/blog/[slug].js';
-import Page_4 from '/src/pages/users/[id].js';
-import Page_5 from '/src/pages/users/[id]/edit.js';
-
-export const routes = [
-  { path: '/', component: Page_0 },
-  { path: '/about', component: Page_1 },
-  { path: '/blog', component: Page_2 },
-  { path: '/blog/:slug', component: Page_3 },
-  { path: '/users/:id', component: Page_4 },
-  { path: '/users/:id/edit', component: Page_5 },
-];

🚀 Performance Benefits

  • Automatic code splitting - Each page becomes a separate chunk
  • Lazy loading ready - Import pages dynamically
  • Tree shaking - Only used routes are included
javascript
// With dynamic imports (automatic with Vite)
-const routes = [
-  { path: '/', component: () => import('./pages/index.js') },
-  { path: '/about', component: () => import('./pages/about.js') },
-  // ...
-];

💡 Pro Tips

src/pages/
-├── dashboard/
-│   ├── index.js
-│   ├── analytics.js
-│   └── settings.js
-└── dashboard.js   # ❌ Don't mix with folder

2. Use Index Files for Clean URLs

✅ Good:
-pages/blog/index.js      → /blog
-pages/blog/post.js       → /blog/post
-
-❌ Avoid:
-pages/blog.js            → /blog (conflicts with folder)

3. Private Components

Prefix with underscore to exclude from routing:

src/pages/
-├── index.js
-├── about.js
-└── _components/         # ❌ Not scanned
-    └── Header.js

4. Layout Components

Create a layout wrapper in your main entry:

javascript
// main.js
-import { $, html } from 'sigpro';
-import { routes } from 'virtual:sigpro-routes';
-
-// Wrap all routes with layout
-const routesWithLayout = routes.map(route => ({
-  ...route,
-  component: (params) => Layout(route.component(params))
-}));
-
-const router = $.router(routesWithLayout);
-document.body.appendChild(router);

Note: This plugin is completely optional. You can always define routes manually if you prefer. The plugin just saves you from writing boilerplate route configurations.

Pro Tip: The plugin works great with hot module replacement (HMR) - add a new page and it's instantly available in your dev server without restarting!

`,69)])])}const o=i(l,[["render",p]]);export{E as __pageData,o as default}; diff --git a/docs/assets/vite_plugin.md.gDWEi8f0.lean.js b/docs/assets/vite_plugin.md.gDWEi8f0.lean.js deleted file mode 100644 index 59b7c1e..0000000 --- a/docs/assets/vite_plugin.md.gDWEi8f0.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as i,o as a,c as n,ae as t}from"./chunks/framework.C8AWLET_.js";const E=JSON.parse('{"title":"Vite Plugin: Automatic File-based Routing 🚦","description":"","frontmatter":{},"headers":[],"relativePath":"vite/plugin.md","filePath":"vite/plugin.md"}'),l={name:"vite/plugin.md"};function p(h,s,e,k,r,d){return a(),n("div",null,[...s[0]||(s[0]=[t("",69)])])}const o=i(l,[["render",p]]);export{E as __pageData,o as default}; diff --git a/docs/guide/getting-started.html b/docs/guide/getting-started.html index dae75ec..7d4f7e6 100644 --- a/docs/guide/getting-started.html +++ b/docs/guide/getting-started.html @@ -3,194 +3,48 @@ - Getting Started with SigPro 🚀 | SigPro + Getting Started | SigPro - + - + -
Skip to content

Getting Started with SigPro 🚀

Welcome to SigPro! This guide will help you get up and running with the library in minutes. SigPro is a minimalist reactive library that embraces the web platform - no compilation, no virtual DOM, just pure JavaScript and intelligent reactivity.

📦 Installation

Choose your preferred installation method:

bash
# Using npm
-npm install sigpro
+    
Skip to content

Getting Started

SigPro is a lightweight, atomic reactive engine designed to build modern web interfaces with zero overhead. It focuses on high performance through fine-grained reactivity.

1. Installation

You can install SigPro via your favorite package manager:

bash
npm install SigPro
bash
pnpm add SigPro
bash
yarn add SigPro
bash
bun add SigPro

2. Basic Usage

The core of SigPro is the $ function, which creates reactive state (Signals) and computed effects.

Create a main.js file and try this:

javascript
import { $ } from 'SigPro';
 
-# Using bun
-bun add sigpro
+// 1. Create a reactive signal
+const $name = $("World");
 
-# Or simply copy sigpro.js to your project
-# (yes, it's that simple!)

🎯 Core Imports

javascript
import { $, html } from 'sigpro';

That's it! Just two imports to unlock the entire reactive system:

  • $ - Creates reactive signals (the heart of reactivity)
  • html - Template literal tag for reactive DOM rendering

🧠 Understanding the Basics

Signals - The Reactive Heart

Signals are reactive values that automatically track dependencies and update when changed:

javascript
// Create a signal with initial value
-const count = $(0);
-
-// Read value (with auto dependency tracking)
-console.log(count()); // 0
-
-// Set new value
-count(5);
-
-// Update using previous value
-count(prev => prev + 1); // 6
-
-// Create computed signals (auto-updating)
-const firstName = $('John');
-const lastName = $('Doe');
-const fullName = $(() => `${firstName()} ${lastName()}`);
-console.log(fullName()); // "John Doe"
-firstName('Jane'); // fullName() now returns "Jane Doe"

Effects - Automatic Reactions

Effects automatically run and re-run when their signal dependencies change:

javascript
const count = $(0);
-
-$.effect(() => {
-  console.log(`Count is: ${count()}`);
-});
-// Logs: "Count is: 0"
-
-count(1);
-// Logs: "Count is: 1"
-
-// Effects can return cleanup functions
-$.effect(() => {
-  const id = count();
-  const timer = setInterval(() => {
-    console.log(`Polling with count: ${id}`);
-  }, 1000);
+// 2. Define a reactive component
+const App = () => div({ class: 'container' }, [
+  h1(["Hello, ", $name, "!"]),
   
-  // Cleanup runs before next effect execution
-  return () => clearInterval(timer);
-});

Rendering with html

The html tag creates reactive DOM fragments:

javascript
const count = $(0);
-const isActive = $(true);
-
-const fragment = html`
-  <div class="counter">
-    <h2>Count: ${count}</h2>
-    
-    <!-- Event binding -->
-    <button @click=${() => count(c => c + 1)}>
-      Increment
-    </button>
-    
-    <!-- Boolean attributes -->
-    <button ?disabled=${() => !isActive()}>
-      Submit
-    </button>
-  </div>
-`;
-
-document.body.appendChild(fragment);

🎨 Your First Reactive App

Let's build a simple todo app to see SigPro in action:

javascript
import { $, html } from 'sigpro';
-
-// Create a simple todo app
-function TodoApp() {
-  // Reactive state
-  const todos = $(['Learn SigPro', 'Build something awesome']);
-  const newTodo = $('');
+  input({ 
+    type: 'text', 
+    $value: $name, // Two-way binding
+    placeholder: 'Enter your name...' 
+  }),
   
-  // Computed value
-  const todoCount = $(() => todos().length);
-  
-  // Add todo function
-  const addTodo = () => {
-    if (newTodo().trim()) {
-      todos([...todos(), newTodo()]);
-      newTodo('');
-    }
-  };
-  
-  // Remove todo function
-  const removeTodo = (index) => {
-    todos(todos().filter((_, i) => i !== index));
-  };
-  
-  // Return reactive template
-  return html`
-    <div style="max-width: 400px; margin: 2rem auto; font-family: system-ui;">
-      <h1>📝 Todo App</h1>
-      
-      <!-- Input form -->
-      <div style="display: flex; gap: 8px; margin-bottom: 16px;">
-        <input 
-          type="text" 
-          :value=${newTodo}
-          placeholder="Add a new todo..."
-          style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"
-          @keydown.enter=${addTodo}
-        />
-        <button 
-          @click=${addTodo}
-          style="padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;"
-        >
-          Add
-        </button>
-      </div>
-      
-      <!-- Todo count -->
-      <p>Total todos: ${todoCount}</p>
-      
-      <!-- Todo list -->
-      <ul style="list-style: none; padding: 0;">
-        ${() => todos().map((todo, index) => html`
-          <li style="display: flex; justify-content: space-between; align-items: center; padding: 8px; margin: 4px 0; background: #f5f5f5; border-radius: 4px;">
-            <span>${todo}</span>
-            <button 
-              @click=${() => removeTodo(index)}
-              style="padding: 4px 8px; background: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer;"
-            >
-
-            </button>
-          </li>
-        `)}
-      </ul>
-    </div>
-  `;
-}
+  button({ 
+    onclick: () => $name("SigPro") 
+  }, "Set to SigPro")
+]);
 
-// Mount the app
-document.body.appendChild(TodoApp());

🎯 Key Concepts

1. Signal Patterns

PatternExampleUse Case
Basic signalconst count = $(0)Simple values
Computed$( () => first() + last() )Derived values
Signal updatecount(5)Direct set
Functional updatecount(prev => prev + 1)Based on previous

2. Effect Patterns

javascript
// Basic effect
-$.effect(() => console.log(count()));
-
-// Effect with cleanup
-$.effect(() => {
-  const timer = setInterval(() => {}, 1000);
-  return () => clearInterval(timer);
-});
-
-// Stopping an effect
-const stop = $.effect(() => {});
-stop(); // Effect won't run again

3. HTML Directives

DirectiveExampleDescription
@event@click=${handler}Event listeners
:property:value=${signal}Two-way binding
?attribute?disabled=${signal}Boolean attributes
.property.scrollTop=${value}DOM properties

💡 Pro Tips for Beginners

1. Start Simple

javascript
// Begin with basic signals
-const name = $('World');
-html`<h1>Hello, ${name}!</h1>`;

2. Use Computed Signals for Derived State

javascript
// ❌ Don't compute in template
-html`<p>Total: ${items().length * price()}</p>`;
-
-// ✅ Compute with signals
-const total = $(() => items().length * price());
-html`<p>Total: ${total}</p>`;

3. Leverage Effects for Side Effects

javascript
// Auto-save to localStorage
-$.effect(() => {
-  localStorage.setItem('draft', JSON.stringify(draft()));
-});

🔧 VS Code Setup

For the best development experience, install these VS Code extensions:

  • lit-html - Adds syntax highlighting for html tagged templates
  • Prettier - Automatically formats your template literals
javascript
// With lit-html extension, you get full syntax highlighting!
-html`
-  <div style="color: #ff4444; background: linear-gradient(45deg, blue, green)">
-    <h1>Beautiful highlighted template</h1>
-  </div>
-`

📁 Project Structure

Here's a recommended structure for larger apps:

my-sigpro-app/
-├── index.html
-├── main.js
-├── components/
-│   ├── Button.js
-│   ├── TodoList.js
-│   └── TodoItem.js
-├── pages/
-│   ├── HomePage.js
-│   └── AboutPage.js
-└── utils/
-    └── helpers.js

Example main.js:

javascript
import { $, html } from 'sigpro';
-import HomePage from './pages/HomePage.js';
-
-// Mount your app
-document.body.appendChild(HomePage());

🎓 Summary

You've learned:

  • ✅ How to install SigPro
  • ✅ Core concepts: signals, effects, and reactive rendering
  • ✅ Built a complete todo app
  • ✅ Key patterns and best practices
  • ✅ How to structure larger applications

Remember: SigPro embraces the web platform. You're writing vanilla JavaScript with superpowers—no compilation, no lock-in, just clean, maintainable code that will work for years to come.

"Stop fighting the platform. Start building with it."

Happy coding! 🎉

- +// 3. Mount the application +$.mount(App, '#app');

3. How it Works

SigPro doesn't use a Virtual DOM. Instead, it creates real DOM nodes and binds them directly to your data:

  1. Signals: $(value) creates a getter/setter function.
  2. Reactivity: When you pass a signal or a function to a DOM element, SigPro automatically creates a subscription.
  3. Fine-Grained Updates: Only the specific text node or attribute linked to the signal updates when the value changes.

4. Global Tags

By default, SigPro exports common HTML tags to the global scope (window) when initialized. This allows you to write clean, declarative UI without importing every single tag:

javascript
// Instead of $.html('div', ...), just use:
+div([
+  h1("Clean Syntax"),
+  p("No more boilerplate.")
+]);
+ \ No newline at end of file diff --git a/docs/guide/why.html b/docs/guide/why.html index ce4e0bf..c1a45cb 100644 --- a/docs/guide/why.html +++ b/docs/guide/why.html @@ -3,45 +3,29 @@ - Why SigPro? ❓ | SigPro + Why SigPro? | SigPro - + - + -
Skip to content

Why SigPro? ❓

After years of building applications with React, Vue, and Svelte—investing countless hours mastering their unique mental models, build tools, and update cycles—I kept circling back to the same realization: no matter how sophisticated the framework, it all eventually compiles down to HTML, CSS, and vanilla JavaScript. The web platform has evolved tremendously, yet many libraries continue to reinvent the wheel, creating parallel universes with their own rules, their own syntaxes, and their own steep learning curves.

SigPro is my answer to a simple question: Why fight the platform when we can embrace it?

🌐 The Web Platform Is Finally Ready

Modern browsers now offer powerful primitives that make true reactivity possible without virtual DOM diffing, without compilers, and without lock-in:

Browser PrimitiveWhat It Enables
Custom ElementsCreate reusable components with native browser APIs
Shadow DOMEncapsulate styles and markup without preprocessors
CSS Custom PropertiesDynamic theming without CSS-in-JS
Microtask QueuesEfficient update batching without complex scheduling

🎯 The SigPro Philosophy

SigPro strips away the complexity, delivering a reactive programming model that feels familiar but stays remarkably close to vanilla JS:

  • No JSX transformations - Just template literals
  • No template compilers - The browser parses your HTML
  • No proprietary syntax to learn - Just functions and signals
  • No build step required - Works directly in the browser
javascript
// Just vanilla JavaScript with signals
-import { $, html } from 'sigpro';
+    
Skip to content

Why SigPro?

After years of building applications with React, Vue, and Svelte—investing countless hours mastering unique mental models, proprietary syntaxes, and complex build tools—we reached a realization: the web platform has evolved, but frameworks have become layers of abstraction that often move us further away from the browser.

SigPro is the answer to a simple question: Why fight the platform when we can embrace it?

The Modern Web is Ready

SigPro bypasses the overhead of the Virtual DOM and heavy compilers by using modern browser primitives. It treats the DOM as a first-class citizen, not as a side effect of a state change.

Browser PrimitiveWhat It Enables
Closures & ProxiesAutomatic dependency tracking without heavy overhead.
ES ModulesNative modularity and lazy loading without complex bundlers.
Direct DOM APIsSurgical updates that are faster than any reconciliation algorithm.
Microtask QueuesBatching updates efficiently to ensure 60fps performance.

The SigPro Philosophy

SigPro strips away the complexity, delivering a reactive programming model that feels like a framework but stays remarkably close to Vanilla JS:

  • No JSX transformations – Pure JavaScript functions.
  • No Virtual DOM – Direct, fine-grained DOM manipulation.
  • No proprietary syntax – If you know JS, you know SigPro.
  • Zero Build Step Required – It can run directly in the browser via ESM.
javascript
// Pure, Atomic, Reactive.
+const $count = $(0);
 
-const count = $(0);
-
-document.body.appendChild(html`
-  <div>
-    <p>Count: ${count}</p>
-    <button @click=${() => count(c => c + 1)}>
-      Increment
-    </button>
-  </div>
-`);

📊 Comparative

MetricSigProSolidSvelteVueReact
Bundle Size (gzip)🥇 5.2KB🥈 15KB🥉 16.6KB20.4KB43.9KB
Time to Interactive🥇 0.8s🥈 1.3s🥉 1.4s1.6s2.3s
Initial Render (ms)🥇 124ms🥈 198ms🥉 287ms298ms452ms
Update Performance (ms)🥇 4ms🥈 5ms🥈 5ms🥉 7ms18ms
Dependencies🥇 0🥇 0🥇 0🥈 2🥉 5
Compilation Required🥇 No🥇 No🥈 Yes🥇 No🥇 No
Browser Native🥇 Yes🥈 Partial🥉 Partial🥉 PartialNo
Framework Lock-in🥇 None🥈 Medium🥉 High🥈 Medium🥉 High
Longevity (standards-based)🥇 10+ years🥈 5 years🥉 3 years🥈 5 years🥈 5 years

🔑 Core Principles

SigPro is built on four fundamental principles:

📡 True Reactivity

Automatic dependency tracking with no manual subscriptions. When a signal changes, only the exact DOM nodes that depend on it update—surgically, efficiently, instantly.

Surgical Updates

No virtual DOM diffing. No tree reconciliation. Just direct DOM updates where and when needed. The result is predictable performance that scales with your content, not your component count.

🧩 Web Standards

Built on Custom Elements, not a custom rendering engine. Your components are real web components that work in any framework—or none at all.

🔬 Predictable

No magic, just signals and effects. What you see is what you get. The debugging experience is straightforward because there's no framework layer between your code and the browser.

🎨 The Development Experience

javascript
// With VS Code + lit-html extension, you get:
-// ✅ Syntax highlighting
-// ✅ Color previews
-// ✅ Auto-formatting
-// ✅ IntelliSense
-
-html`
-  <div style="color: #ff4444; background: linear-gradient(45deg, blue, green)">
-    <h1>Beautiful highlighted template</h1>
-  </div>
-`

⏱️ Built for the Long Term

What emerged is a library that proves we've reached a turning point: the web is finally mature enough that we don't need to abstract it anymore. We can build reactive, component-based applications using virtually pure JavaScript, leveraging the platform's latest advances instead of working against them.

The result isn't just smaller bundles or faster rendering—it's code that will still run 10 years from now, in any browser, without maintenance.

📈 The Verdict

While other frameworks build parallel universes with proprietary syntax and compilation steps, SigPro embraces the web platform. SigPro isn't just another framework—it's a return to fundamentals, showing that the dream of simple, powerful reactivity is now achievable with the tools browsers give us out of the box.

"Stop fighting the platform. Start building with it."

🚀 Ready to Start?

Get Started with SigProView on GitHubnpm Package

- +const Counter = () => div([ + p(["Count: ", $count]), + button({ onclick: () => $count(c => c + 1) }, "Increment") +]);

Performance Comparison

SigPro isn't just lighter; it's architecturally faster because it skips the "diffing" phase entirely.

MetricSigProSolidJSSvelteVueReact
Bundle Size (gzip)🥇 < 2KB🥈 7KB🥉 16KB20KB45KB
ArchitectureAtomicAtomicCompiledV-DOMV-DOM
Initial Render🥇 Fastest🥈 Fast🥉 FastAverageSlow
Update Perf🥇 Surgical🥇 Surgical🥈 Fast🥉 AverageSlow
Dependencies🥇 0🥇 0🥇 0🥈 2🥉 5+
Build Step🥇 Optional🥈 Required🥈 Required🥇 Optional🥈 Required

🔑 Core Principles

SigPro is built on four fundamental pillars:

📡 Atomic Reactivity

Automatic dependency tracking with no manual subscriptions. When a signal changes, only the exact text nodes or attributes that depend on it update—instantly and surgically.

⚡ Surgical DOM Updates

No Virtual DOM diffing. No tree reconciliation. We don't guess what changed; we know exactly where the update needs to happen. Performance scales with your data, not the size of your component tree.

🧩 Plugin-First Architecture

The core is a tiny, powerful engine. Need Routing? Fetching? Global UI? Just plug it in. This keeps your production bundles "pay-only-for-what-you-use."

🔬 Predictable & Transparent

There is no "magic" hidden in a black-box compiler. What you write is what the browser executes. Debugging is straightforward because there is no framework layer between your code and the DevTools.


"SigPro returns the joy of web development by making the browser the hero again."

+ \ No newline at end of file diff --git a/docs/hashmap.json b/docs/hashmap.json index ce62046..4c13e72 100644 --- a/docs/hashmap.json +++ b/docs/hashmap.json @@ -1 +1 @@ -{"api_components.md":"BlFwj17l","api_effects.md":"Br_yStBS","api_fetch.md":"DQLBJSoq","api_pages.md":"BP19nHXw","api_quick.md":"BDS3ttnt","api_routing.md":"7SNAZXtp","api_signals.md":"CrW68-BA","api_storage.md":"COEWBXHk","guide_getting-started.md":"BeQpK3vd","guide_why.md":"DXchYMN-","index.md":"uvMJmU4o","vite_plugin.md":"gDWEi8f0"} +{"api__.md":"BVVMY-2O","api_html.md":"-lEpgX-Z","api_mount.md":"eGRwkZvh","api_quick.md":"Cy_XozKR","api_tags.md":"33XeBTH-","guide_getting-started.md":"D_gqopPp","guide_why.md":"lyU7T5_c","index.md":"BWH7zN4c","plugins_core.debug.md":"CVHw_PN0","plugins_core.fetch.md":"BIc8aMQh","plugins_core.router.md":"bGFltJyy","plugins_core.storage.md":"Bgu1q6YH","plugins_core.ui.md":"DDLum7rv","plugins_custom.md":"D2KGTblR","plugins_quick.md":"ODjl7edh","vite_plugin.md":"4TJA8cv0"} diff --git a/docs/index.html b/docs/index.html index 75cf30e..c7e25c4 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,20 +6,36 @@ SigPro - + - + -
Skip to content

SigProReactivity for the Web Platform

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.

SigPro

npm versionbundle sizelicense

"Stop fighting the platform. Start building with it."

- +
Skip to content

SigProAtomic Unified Reactive Engine

Fine-grained reactivity, built-in routing, and modular plugins. All under 2KB.

SigPro Logo

Why SigPro?

SigPro isn't just another framework; it's a high-performance engine. It strips away the complexity of massive bundles and returns to the essence of the web, enhanced with reactive superpowers.

The Core in Action

javascript
import { $ } from 'sigpro2';
+
+// A reactive state Signal
+const $count = $(0);
+
+// A Computed signal that updates automatically
+const $double = $(() => $count() * 2);
+
+// UI that breathes with your data
+const Counter = () => div([
+  h1(["Count: ", $count]),
+  p(["Double: ", $double]),
+  button({ onclick: () => $count(c => c + 1) }, "Increment")
+]);
+
+$.mount(Counter);

Key Features

⚡️ Fine-Grained Reactivity

Unlike frameworks that diff complex trees (V-DOM), SigPro binds your signals directly to real DOM text nodes and attributes. If the data changes, the node changes. Period.

🔌 Polymorphic Plugin System

Extend core capabilities in a single line. Add global UI helpers, routing, or state persistence seamlessly.

javascript
import { UI, Router } from 'sigpro/plugins';
+$.plugin([UI, Router]);

📂 File-Based Routing

With our dedicated Vite plugin, manage your routes simply by creating files in src/pages/. It supports native Lazy Loading out of the box for lightning-fast initial loads.


Quick Install

bash
npm install sigpro
bash
pnpm add sigpro
bash
yarn add sigpro
bash
bun add sigpro

Community & Support

SigPro is an open-source project. Whether you want to contribute, report a bug, or just talk about reactivity, join us on our official repository.

Built with ❤️ by NatxoCC
+ \ No newline at end of file diff --git a/docs/plugins/core.debug.html b/docs/plugins/core.debug.html new file mode 100644 index 0000000..8453264 --- /dev/null +++ b/docs/plugins/core.debug.html @@ -0,0 +1,51 @@ + + + + + + Development Tool: _debug | SigPro + + + + + + + + + + + + + + +
Skip to content

Development Tool: _debug

The Debug Plugin is a lightweight reactive listener. Once attached to a signal or a computed function, it automatically monitors changes, compares values, and formats the output in the browser console.

1. Core Features

  • Reactive Tracking: Automatically logs whenever the tracked signal updates.
  • Visual Grouping: Uses styled console groups to keep your dev tools organized.
  • Object Inspection: Automatically uses console.table() when the signal contains an object or array.
  • Efficient Comparison: Uses Object.is to prevent redundant logging if the value hasn't actually changed.

2. Installation

To use _debug, you only need the SigPro core. Register the plugin in your main.js. You can conditionally load it so it only runs during development.

javascript
import { $ } from 'sigpro';
+import { Debug } from 'sigpro/plugins';
+
+// Only load Debug in development mode
+const plugins = [];
+if (import.meta.env.DEV) plugins.push(Debug);
+
+$.plugin(plugins).then(() => {
+  import('./App.js').then(app => $.mount(app.default));
+});
bash
npm install sigpro
bash
pnpm add sigpro
bash
yarn add sigpro
bash
bun add sigpro

3. Basic Usage

Call _debug anywhere in your component. It stays active in the background, watching the signal's lifecycle.

javascript
export default () => {
+  const $count = $(0);
+  const $user = $({ name: "Guest", role: "Viewer" });
+
+  // Start tracking
+  _debug($count, "Main Counter");
+  _debug($user, "User Session");
+
+  return div([
+    button({ onclick: () => $count(c => c + 1) }, "Increment"),
+    button({ onclick: () => $user({ name: "Admin", role: "Super" }) }, "Promote")
+  ]);
+};

4. Console Output Breakdown

When a signal changes, the console displays a structured block:

  1. Header: A styled badge with the name (e.g., SigPro Debug: Main Counter).
  2. Previous Value: The value before the update (in red).
  3. Current Value: The new value (in green).
  4. Table View: If the value is an object, a formatted table appears automatically.

5. Debugging Computed Values

You can also debug computed functions to see exactly when derived state is recalculated.

javascript
const $price = $(100);
+const $tax = $(0.21);
+const $total = $(() => $price() * (1 + $tax()));
+
+// Monitor the result of the calculation
+_debug($total, "Final Invoice Total");

6. Why use _debug?

  1. Clean Logic: No need to scatter console.log inside your reactive functions.
  2. State History: Instantly see the "Before" and "After" of any user action.
  3. No-Noise: It only logs when a real change occurs, keeping the console clean.
  4. Deep Inspection: The automatic console.table makes debugging large API responses much faster.
+ + + + \ No newline at end of file diff --git a/docs/plugins/core.fetch.html b/docs/plugins/core.fetch.html new file mode 100644 index 0000000..00dc2bb --- /dev/null +++ b/docs/plugins/core.fetch.html @@ -0,0 +1,54 @@ + + + + + + Data Fetching: _fetch | SigPro + + + + + + + + + + + + + + +
Skip to content

Data Fetching: _fetch

The Fetch Plugin provides a reactive wrapper around the native browser Fetch API. Instead of managing complex async/await flows within your UI, _fetch returns a "Reactive Tripod" (Data, Loading, and Error) that your components can listen to automatically.

1. Core Concept

When you call _fetch, it returns three signals immediately. Your UI declares how to react to these signals as they change from their initial state to the final response.

  • $data: Initialized as null. Automatically holds the JSON response on success.
  • $loading: Initialized as true. Flips to false once the request settles.
  • $error: Initialized as null. Holds the error message if the request fails.

2. Installation

Register the Fetch plugin in your main.js. By convention, we load it alongside the UI and Router to have the full SigPro ecosystem ready.

javascript
import { $ } from 'sigpro';
+import { Fetch } from 'sigpro/plugins';
+
+$.plugin([Fetch]).then(() => {
+  // Now _fetch() is available globally
+  import('./App.js').then(app => $.mount(app.default));
+});

3. Basic Usage

Use _fetch inside your component to get live updates. The UI updates surgically whenever a signal changes.

javascript
export default () => {
+  const { $data, $loading, $error } = _fetch('https://api.github.com/users/octocat');
+
+  return div({ class: 'p-6 flex flex-col gap-4' }, [
+    h1("Profile Details"),
+    
+    // 1. Loading State (using SigPro UI button)
+    () => $loading() && _button({ $loading: true }, "Fetching..."),
+
+    // 2. Error State
+    () => $error() && div({ class: 'alert alert-error' }, $error()),
+
+    // 3. Success State
+    () => $data() && div({ class: 'card bg-base-200 p-4' }, [
+      img({ src: $data().avatar_url, class: 'w-16 rounded-full' }),
+      h2($data().name),
+      p($data().bio)
+    ])
+  ]);
+};

4. Advanced Configuration

_fetch accepts the same RequestInit options as the standard fetch() (methods, headers, body, etc.).

javascript
const { $data, $loading } = _fetch('/api/v1/update', {
+  method: 'PATCH',
+  headers: { 'Content-Type': 'application/json' },
+  body: JSON.stringify({ status: 'active' })
+});

5. Why use _fetch instead of native Fetch?

  1. Declarative UI: You define the "Loading", "Error", and "Success" templates once, and they swap automatically.
  2. No useEffect required: Since SigPro is natively reactive, you don't need lifecycle hooks to trigger re-renders; the signals handle it.
  3. Consistency: It follows the same _prefix pattern as the rest of the official plugin ecosystem.
  4. Automatic JSON Parsing: It assumes JSON by default and handles 404/500 errors by populating the $error signal.
+ + + + \ No newline at end of file diff --git a/docs/plugins/core.router.html b/docs/plugins/core.router.html new file mode 100644 index 0000000..2205b1a --- /dev/null +++ b/docs/plugins/core.router.html @@ -0,0 +1,55 @@ + + + + + + Navigation Plugin: Router | SigPro + + + + + + + + + + + + + + +
Skip to content

Navigation Plugin: Router

The SigPro Router handles URL changes via hashes (#) and maps them to components. It supports dynamic parameters (like :id) and asynchronous loading for heavy pages.

1. Core Features

  • Hash-based: Works everywhere without special server configuration.
  • Lazy Loading: Pages are only downloaded when the user visits the route.
  • Reactive: The view updates automatically when the hash changes.
  • Dynamic Routes: Supports paths like /user/:id.

2. Installation

The Router is usually included in the official plugins package.

bash
npm install -D tailwindcss @tailwindcss/vite daisyui@next
bash
pnpm add -D tailwindcss @tailwindcss/vite daisyui@next
bash
yarn add -D tailwindcss @tailwindcss/vite daisyui@next
bash
bun add -d tailwindcss @tailwindcss/vite daisyui@next

3. Setting Up Routes

In your App.js (or a dedicated routes file), define your navigation map.

javascript
const routes = [
+  { path: '/', component: () => h1("Home Page") },
+  { 
+    path: '/admin', 
+    // Lazy Loading: This file is only fetched when needed
+    component: () => import('./pages/Admin.js') 
+  },
+  { path: '/user/:id', component: (params) => h2(`User ID: ${params.id}`) },
+  { path: '*', component: () => div("404 - Page Not Found") }
+];
+
+export default () => div([
+  _navbar({ title: "My App" }),
+  _router(routes) // The router is now a global tag
+]);

4. Navigation (_router.go)

To move between pages programmatically (e.g., inside an onclick event), use the global _router.go helper.

javascript
_button({ 
+  onclick: () => _router.go('/admin') 
+}, "Go to Admin")

5. How it Works (Under the Hood)

The router tracks the window.location.hash and uses a reactive signal to trigger a re-render of the specific area where _router(routes) is placed.

  1. Match: It filters your route array to find the best fit.
  2. Resolve: * If it's a standard function, it executes it immediately.
    • If it's a Promise (via import()), it shows a loading state and swaps the content once the module arrives.
  3. Inject: It replaces the previous DOM node with the new page content surgically.

6. Integration with UI Components

Since you are using the UI Plugin, you can easily create active states in your navigation menus by checking the current hash.

javascript
// Example of a reactive sidebar menu
+_menu({
+  items: [
+    { 
+      label: 'Dashboard', 
+      active: () => window.location.hash === '#/', 
+      onclick: () => _router.go('/') 
+    },
+    { 
+      label: 'Settings', 
+      active: () => window.location.hash === '#/settings', 
+      onclick: () => _router.go('/settings') 
+    }
+  ]
+})
+ + + + \ No newline at end of file diff --git a/docs/plugins/core.storage.html b/docs/plugins/core.storage.html new file mode 100644 index 0000000..81f37c3 --- /dev/null +++ b/docs/plugins/core.storage.html @@ -0,0 +1,53 @@ + + + + + + Persistence Tool: _storage | SigPro + + + + + + + + + + + + + + +
Skip to content

Persistence Tool: _storage

The Storage plugin synchronizes a signal with a specific key in your browser's localStorage. It handles both the initial hydration (loading data when the app starts) and automatic saving whenever the signal's value changes.

1. Core Concept

When you "attach" a signal to _storage, two things happen:

  1. Hydration: The plugin checks if the key already exists in localStorage. If it does, it parses the JSON and updates the signal immediately.
  2. Reactive Sync: It creates a reactive watcher that stringifies and saves the signal's value to the disk every time it is updated.

2. Installation

Register the Storage plugin in your main.js. Since this is a logic-only plugin, it doesn't require any CSS or UI dependencies.

javascript
import { $ } from 'sigpro';
+import { Storage } from 'sigpro/plugins';
+
+$.plugin(Storage).then(() => {
+  import('./App.js').then(app => $.mount(app.default));
+});
bash
npm install sigpro
bash
pnpm add sigpro
bash
yarn add sigpro
bash
bun add sigpro

3. Basic Usage

You can wrap any signal with _storage. It is common practice to do this right after creating the signal.

javascript
export default () => {
+  // 1. Create a signal with a default value
+  const $theme = $( 'light' );
+
+  // 2. Persist it. If 'user_theme' exists in localStorage, 
+  // $theme will be updated to that value instantly.
+  _storage($theme, 'user_theme');
+
+  return div({ class: () => `app-${$theme()}` }, [
+    h1(`Current Theme: ${$theme()}`),
+    button({ 
+      onclick: () => $theme(t => t === 'light' ? 'dark' : 'light') 
+    }, "Toggle Theme")
+  ]);
+};

4. Complex Data (Objects & Arrays)

Since the plugin uses JSON.parse and JSON.stringify internally, it works perfectly with complex state structures.

javascript
const $settings = $({ 
+  notifications: true, 
+  fontSize: 16 
+});
+
+// Automatically saves the whole object whenever any property changes
+_storage($settings, 'app_settings');

5. Why use _storage?

  1. Zero Boilerplate: You don't need to manually write localStorage.getItem or setItem logic inside your components.
  2. Chaining: Because _storage returns the signal, you can persist it inline.
  3. Error Resilience: It includes a built-in try/catch block to prevent your app from crashing if the stored JSON is corrupted.
  4. Surgical Persistence: Only the signals you explicitly mark for storage are saved, keeping your localStorage clean.

6. Pro Tip: Combining with Debug

You can chain plugins to create a fully monitored and persistent state:

javascript
const $score = _storage($(0), 'high_score');
+
+// Now it's saved to disk AND logged to console on every change
+_debug($score, "Game Score");
+ + + + \ No newline at end of file diff --git a/docs/plugins/core.ui.html b/docs/plugins/core.ui.html new file mode 100644 index 0000000..05d9503 --- /dev/null +++ b/docs/plugins/core.ui.html @@ -0,0 +1,54 @@ + + + + + + Official UI Plugin: UI | SigPro + + + + + + + + + + + + + + +
Skip to content

Official UI Plugin: UI

The SigPro UI plugin is a high-level component library built on top of the reactive core. It leverages Tailwind CSS v4 for utility styling and daisyUI v5 for semantic components.

1. Prerequisites & Installation

To use these components, you must install the styling engine. SigPro UI provides the logic, but Tailwind and daisyUI provide the visuals.

bash
npm install -D tailwindcss @tailwindcss/vite daisyui@next
bash
pnpm add -D tailwindcss @tailwindcss/vite daisyui@next
bash
yarn add -D tailwindcss @tailwindcss/vite daisyui@next
bash
bun add -d tailwindcss @tailwindcss/vite daisyui@next

Would you like to continue with the Router.md documentation now?

CSS Configuration (app.css)

In Tailwind v4, configuration is handled directly in your CSS. Create a src/app.css file:

css
/* src/app.css */
+@import "tailwindcss";
+
+/* Import daisyUI v5 as a Tailwind v4 plugin */
+@plugin "daisyui";
+
+/* Optional: Configure themes */
+@custom-variant dark (&:where(.dark, [data-theme="dark"], [data-theme="dark"] *)));

2. Initialization

You must import your CSS and register the UI plugin in your entry point. This populates the global scope with reactive component helpers (prefixed with _).

javascript
// main.js
+import './app.css';
+import { $ } from 'sigpro';
+import { UI } from 'sigpro/plugins';
+
+$.plugin(UI).then(() => {
+  // Global components like _button and _input are now ready
+  import('./App.js').then(app => $.mount(app.default));
+});

3. Core Component Tags (_tags)

SigPro UI components are more than just HTML; they are Reactive Functional Components that manage complex states (loading, errors, accessibility) automatically.

A. Action Components (_button)

The _button automatically handles spinners and disabled states based on signals.

PropertyTypeDescription
$loadingsignalIf true, shows a spinner and disables the button.
$disabledsignalManually disables the button (logic-bound).
iconnode/strPrepends an icon to the text.
badgestringAppends a small badge to the button.
javascript
_button({ 
+  $loading: $isSaving, 
+  icon: '💾', 
+  class: 'btn-primary' 
+}, "Save Data")

B. High-Density Forms (_input, _select, _checkbox)

These components wrap the raw input in a fieldset with integrated labels and tooltips.

  • label: Field title displayed above the input.
  • tip: Displays a ? badge that shows a tooltip on hover.
  • $error: A signal that, when populated, turns the input red and displays the message.
  • $value: Two-way binding. Updates the signal on input and the input on signal change.
javascript
_input({
+  label: "Username",
+  tip: "Choose a unique name",
+  $value: $name,
+  $error: $nameError
+})

4. Complex UI Patterns

Reactive Modals (_modal)

The _modal is surgically mounted. If the $open signal is false, the component is completely removed from the DOM, optimizing performance.

javascript
const $showModal = $(false);
+
+_modal({ $open: $showModal, title: "Alert" }, [
+  p("Are you sure you want to proceed?"),
+  _button({ onclick: () => doAction() }, "Confirm")
+])

Designed to work seamlessly with the Router.

ComponentKey Logic
_tabsAccepts an active property (signal or function) to highlight the current tab.
_drawerA responsive sidebar that toggles via an ID or an $open signal.
_navbarStandard top bar with shadow and glass effect support.
_menuVertical navigation list with active state support.

5. Summary Table: UI Globals

Once $.plugin(UI) is active, these tags are available project-wide:

TagCategoryUse Case
_fieldsetLayoutGrouping related inputs with a legend.
_accordionContentCollapsible sections (FAQs).
_badgeFeedbackStatus indicators (Success, Warning).
_tooltipFeedbackDescriptive text on hover.
_rangeInputReactive slider for numerical values.

What's next?

With the UI ready and styled via Tailwind v4, we can move to the Router.md. We will explain how to link _tabs and _menu to different URL paths for a full SPA experience.

Would you like to start with the Router configuration?

+ + + + \ No newline at end of file diff --git a/docs/plugins/custom.html b/docs/plugins/custom.html new file mode 100644 index 0000000..5d7e61c --- /dev/null +++ b/docs/plugins/custom.html @@ -0,0 +1,72 @@ + + + + + + Creating Custom Plugins | SigPro + + + + + + + + + + + + + + +
Skip to content

Creating Custom Plugins

There are two main ways to expose a plugin's functionality: Static/Manual Imports (cleaner for large projects) or Global/Automatic Window Injection (easier for quick scripts and global helpers).

1. The Anatomy of a Plugin

A plugin is a standard JavaScript function. By convention, if a plugin adds a global helper or component, it should be prefixed with an underscore (_).

javascript
// plugins/my-utils.js
+export const MyUtils = ($) => {
+  
+  // 1. Attach to the SigPro instance
+  $.capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
+
+  // 2. Attach to the Window (Global access)
+  window._hello = (name) => div(`Hello, ${$.capitalize(name)}!`);
+  
+  // 3. You can also return values if needed
+  return { version: '1.0.0' };
+};

2. Integration Strategies

This approach keeps your global namespace clean. You import the logic only where you need it, but the plugin still initializes the core $ extensions.

javascript
// main.js
+import { $ } from 'sigpro';
+import { MyUtils } from './plugins/my-utils.js';
+
+$.plugin(MyUtils);
+
+// App.js
+export default () => {
+  const name = "sigpro";
+  // $.capitalize was added by the plugin
+  return h1($.capitalize(name)); 
+};

Option B: Automatic Window Injection

If your plugin defines global tags (like _button or _hello), you should attach them to the window object inside the plugin function. This makes them available everywhere without imports.

javascript
// plugins/theme.js
+export const Theme = ($) => {
+  const $dark = $(false);
+
+  window._themeToggle = () => button({
+    onclick: () => $dark(v => !v),
+    class: () => $dark() ? 'bg-black text-white' : 'bg-white text-black'
+  }, "Toggle Mode");
+};
+
+// main.js
+$.plugin(Theme).then(() => {
+   // _themeToggle is now a global function
+   $.mount(App);
+});

3. Asynchronous Plugins

If your plugin needs to load external data or scripts before the app starts, make it async. SigPro will wait for it.

javascript
export const ConfigLoader = async ($) => {
+  const res = await fetch('/config.json');
+  const config = await res.json();
+  
+  $.config = config; // Attach loaded config to SigPro
+};
+
+// Usage
+$.plugin(ConfigLoader).then(() => {
+  console.log("Config loaded:", $.config);
+  $.mount(App);
+});

4. Best Practices for Plugin Authors

RuleDescription
PrefixingUse _ for UI components (_modal) and $. for logic ($.fetch).
IdempotencyEnsure calling $.plugin(MyPlugin) twice doesn't break the app.
EncapsulationUse the $ instance passed as an argument rather than importing it again inside the plugin.
ReactivityAlways use $(...) for internal state so the app stays reactive.

5. Installation

Custom plugins don't require extra packages, but ensure your build tool (Vite/Bun) is configured to handle the module imports.

bash
npm install sigpro
bash
pnpm add sigpro
bash
yarn add sigpro
bash
bun add sigpro
+ + + + \ No newline at end of file diff --git a/docs/plugins/quick.html b/docs/plugins/quick.html new file mode 100644 index 0000000..31eff69 --- /dev/null +++ b/docs/plugins/quick.html @@ -0,0 +1,60 @@ + + + + + + Extending SigPro: $.plugin | SigPro + + + + + + + + + + + + + + +
Skip to content

Extending SigPro: $.plugin

The plugin system is the engine's way of growing. It allows you to inject new functionality directly into the $ object or load external resources.

1. How Plugins Work

A plugin in SigPro is simply a function that receives the core instance. When you run $.plugin(MyPlugin), the engine hands over the $ object so the plugin can attach new methods or register global tags (like div(), span(), etc.).

Functional Plugin Example

javascript
// A plugin that adds a simple logger to any signal
+const Logger = ($) => {
+  $.watch = (target, label = "Log") => {
+    $(() => console.log(`[${label}]:`, target()));
+  };
+};
+
+// Activation
+$.plugin(Logger);
+const $count = $(0);
+$.watch($count, "Counter"); // Now available globally via $

2. Initialization Patterns

Since plugins often set up global variables (like the HTML tags), the order of initialization is critical. Here are the two ways to start your app:

This is the most robust way. It ensures all global tags (div, button, etc.) are created before your App code is even read by the browser.

javascript
// main.js
+import { $ } from 'sigpro';
+import { UI, Router } from 'sigpro/plugins';
+
+// 1. Load plugins first
+$.plugin([UI, Router]).then(() => {
+  
+  // 2. Import your app only after the environment is ready
+  import('./App.js').then(appFile => {
+    const MyApp = appFile.default;
+    $.mount(MyApp, '#app');
+  });
+
+});

Option B: Static Start (No Global Tags)

Use this only if you prefer not to use global tags and want to use $.html directly in your components. This allows for standard static imports.

javascript
// main.js
+import { $ } from 'sigpro';
+import { UI } from 'sigpro/plugins';
+import MyApp from './App.js'; // Static import works here
+
+$.plugin(UI);
+$.mount(MyApp, '#app');

Warning: In this mode, if App.js uses div() instead of $.html('div'), it will throw a ReferenceError.


3. Resource Plugins (External Scripts)

You can pass a URL or an Array of URLs. SigPro will inject them as <script> tags and return a Promise that resolves when the scripts are fully loaded and executed.

javascript
// Loading external libraries as plugins
+await $.plugin([
+  'https://cdn.jsdelivr.net/npm/chart.js',
+  'https://cdn.example.com/custom-ui-lib.js'
+]);
+
+console.log("External resources are ready to use!");

4. Polymorphic Loading Reference

The $.plugin method adapts to whatever you throw at it:

Input TypeActionBehavior
FunctionExecutes fn($)Synchronous / Immediate
String (URL)Injects <script src="...">Asynchronous (Returns Promise)
ArrayProcesses each item in the listReturns Promise if any item is Async

💡 Pro Tip: Why the .then()?

Using $.plugin([...]).then(...) is like giving your app a "Pre-flight Check". It guarantees that:

  1. All reactive methods are attached.
  2. Global HTML tags are defined.
  3. External libraries (like Chart.js) are loaded.
  4. The result: Your components are cleaner, smaller, and error-free.
+ + + + \ No newline at end of file diff --git a/docs/vite/plugin.html b/docs/vite/plugin.html index e3e9c4e..db6b4a9 100644 --- a/docs/vite/plugin.html +++ b/docs/vite/plugin.html @@ -3,247 +3,72 @@ - Vite Plugin: Automatic File-based Routing 🚦 | SigPro + Vite Plugin: File-based Routing | SigPro - + - + -
Skip to content

Vite Plugin: Automatic File-based Routing 🚦

SigPro provides an optional Vite plugin that automatically generates routes based on your file structure. No configuration needed - just create pages and they're instantly available with the correct paths.

Why Use This Plugin?

While SigPro's router works perfectly with manually defined routes, this plugin:

  • Eliminates boilerplate - No need to write route configurations
  • Enforces conventions - Consistent URL structure across your app
  • Supports dynamic routes - Use [param] syntax for parameters
  • Automatic code-splitting - Each page becomes a separate chunk
  • Type-safe (with JSDoc) - Routes follow your file structure

Installation

The plugin is included with SigPro, but you need to add it to your Vite config:

javascript
// vite.config.js
-import { defineConfig } from 'vite';
-import { sigproRouter } from 'sigpro';
-
-export default defineConfig({
-  plugins: [sigproRouter()]
-});

How It Works

The plugin scans your src/pages directory and automatically generates routes based on the file structure:

src/pages/
-├── index.js              →  '/'
-├── about.js              →  '/about'
-├── blog/
-│   ├── index.js          →  '/blog'
-│   └── [slug].js         →  '/blog/:slug'
-└── users/
-    ├── [id].js           →  '/users/:id'
-    └── [id]/edit.js      →  '/users/:id/edit'

Usage

1. Enable the Plugin

Add the plugin to your Vite config as shown above.

2. Import the Generated Routes

Once you have the generated routes, using them with the router is straightforward:

javascript
// main.js
-import { $, html } from 'sigpro';
-import { routes } from 'virtual:sigpro-routes';
-
-// Simple usage
-const router = $.router(routes);
-document.body.appendChild(router);

Or directly in your template:

javascript
// app.js
-import { $, html } from 'sigpro';
-import { routes } from 'virtual:sigpro-routes';
-
-const App = () => html`
-  <div class="app">
-    <header>
-      <h1>My Application</h1>
-    </header>
-    
-    <main class="p-4 flex flex-col gap-4 mx-auto w-full">
-      <div class="p-4 bg-base-100 rounded-box shadow-sm">
-        ${$.router(routes)}
-      </div>
-    </main>
-  </div>
-`;
-
-document.body.appendChild(App());

This approach keeps your template clean and lets the router handle all the page rendering automatically.

3. Create Pages

javascript
// src/pages/index.js
-import { $, html } from 'sigpro';
-
-export default () => {
-  return html`
-    <div>
-      <h1>Home Page</h1>
-      <a href="#/about">About</a>
-    </div>
-  `;
-};
javascript
// src/pages/users/[id].js
-import { $, html } from 'sigpro';
-
-export default (params) => {
-  const userId = params.id;
-  
-  return html`
-    <div>
-      <h1>User Profile: ${userId}</h1>
-      <a href="#/users/${userId}/edit">Edit</a>
-    </div>
-  `;
-};

📋 File-to-Route Mapping

Static Routes

File PathGenerated Route
src/pages/index.js/
src/pages/about.js/about
src/pages/contact/index.js/contact
src/pages/blog/post.js/blog/post

Dynamic Routes

File PathGenerated RouteExample URL
src/pages/users/[id].js/users/:id/users/42
src/pages/blog/[slug].js/blog/:slug/blog/hello-world
src/pages/users/[id]/posts/[pid].js/users/:id/posts/:pid/users/42/posts/123

Nested Routes

File PathGenerated RouteNotes
src/pages/settings/index.js/settingsIndex page
src/pages/settings/profile.js/settings/profileSub-page
src/pages/settings/security.js/settings/securitySub-page
src/pages/settings/[section].js/settings/:sectionDynamic section

🎯 Advanced Examples

Blog with Posts

javascript
// src/pages/blog/index.js - Lists all posts
-export default () => {
-  const posts = $([]);
-  
-  $.effect(() => {
-    fetch('/api/posts')
-      .then(res => res.json())
-      .then(data => posts(data));
-  });
-  
-  return html`
-    <div>
-      <h1>Blog</h1>
-      ${posts().map(post => html`
-        <article>
-          <h2><a href="#/blog/${post.slug}">${post.title}</a></h2>
-          <p>${post.excerpt}</p>
-        </article>
-      `)}
-    </div>
-  `;
-};
javascript
// src/pages/blog/[slug].js - Single post
-export default (params) => {
-  const post = $(null);
-  const slug = params.slug;
-  
-  $.effect(() => {
-    fetch(`/api/posts/${slug}`)
-      .then(res => res.json())
-      .then(data => post(data));
-  });
-  
-  return html`
-    <div>
-      <a href="#/blog">← Back to blog</a>
-      ${() => post() ? html`
-        <article>
-          <h1>${post().title}</h1>
-          <div>${post().content}</div>
-        </article>
-      ` : html`<div>Loading...</div>`}
-    </div>
-  `;
-};

Dashboard with Nested Sections

javascript
// src/pages/dashboard/index.js
-export default () => {
-  return html`
-    <div class="dashboard">
-      <nav>
-        <a href="#/dashboard">Overview</a>
-        <a href="#/dashboard/analytics">Analytics</a>
-        <a href="#/dashboard/settings">Settings</a>
-      </nav>
-      <main>
-        <h1>Dashboard Overview</h1>
-        <!-- Overview content -->
-      </main>
-    </div>
-  `;
-};
javascript
// src/pages/dashboard/analytics.js
-export default () => {
-  return html`
-    <div class="dashboard">
-      <nav>
-        <a href="#/dashboard">Overview</a>
-        <a href="#/dashboard/analytics">Analytics</a>
-        <a href="#/dashboard/settings">Settings</a>
-      </nav>
-      <main>
-        <h1>Analytics</h1>
-        <!-- Analytics content -->
-      </main>
-    </div>
-  `;
-};

E-commerce Product Routes

javascript
// src/pages/products/[category]/[id].js
-export default (params) => {
-  const { category, id } = params;
-  const product = $(null);
-  
-  $.effect(() => {
-    fetch(`/api/products/${category}/${id}`)
-      .then(res => res.json())
-      .then(data => product(data));
-  });
-  
-  return html`
-    <div class="product-page">
-      <nav class="breadcrumbs">
-        <a href="#/products">Products</a> &gt;
-        <a href="#/products/${category}">${category}</a> &gt;
-        <span>${id}</span>
-      </nav>
-      
-      ${() => product() ? html`
-        <div class="product">
-          <h1>${product().name}</h1>
-          <p class="price">$${product().price}</p>
-          <p>${product().description}</p>
-          <button @click=${() => addToCart(product())}>
-            Add to Cart
-          </button>
-        </div>
-      ` : html`<div>Loading...</div>`}
-    </div>
-  `;
-};

🔧 Configuration Options

The plugin accepts an optional configuration object:

javascript
// vite.config.js
+    
Skip to content

Vite Plugin: File-based Routing

The sigproRouter plugin for Vite automates route generation by scanning your pages directory. It creates a virtual module that you can import directly into your code, eliminating the need to maintain a manual routes array.

1. Project Structure

To use the plugin, organize your files within the src/pages directory. The folder hierarchy directly determines your application's URL structure.

text
my-sigpro-app/
+├── src/
+│   ├── pages/
+│   │   ├── index.js          →  #/
+│   │   ├── about.js          →  #/about
+│   │   ├── users/
+│   │   │   └── [id].js       →  #/users/:id
+│   │   └── blog/
+│   │       ├── index.js      →  #/blog
+│   │       └── [slug].js     →  #/blog/:slug
+│   ├── App.js                (Optional App Shell)
+│   └── main.js               (Entry Point)
+├── vite.config.js
+└── package.json

2. Setup & Configuration

Add the plugin to your vite.config.js.

javascript
// vite.config.js
 import { defineConfig } from 'vite';
 import { sigproRouter } from 'sigpro/vite';
 
 export default defineConfig({
-  plugins: [
-    sigproRouter({
-      pagesDir: 'src/pages',      // Default: 'src/pages'
-      extensions: ['.js', '.jsx'], // Default: ['.js', '.jsx']
-      exclude: ['**/_*', '**/components/**'] // Glob patterns to exclude
-    })
-  ]
-});

Options

OptionTypeDefaultDescription
pagesDirstring'src/pages'Directory containing your pages
extensionsstring[]['.js', '.jsx']File extensions to include
excludestring[][]Glob patterns to exclude

🎯 Route Priority

The plugin automatically sorts routes to ensure correct matching:

  1. Static routes take precedence over dynamic ones
  2. More specific routes (deeper paths) come first
  3. Alphabetical order for routes at the same level

Example sorting:

/users/new           (static, specific)
-/users/[id]/edit     (dynamic, deeper)
-/users/[id]          (dynamic, shallower)
-/users/profile       (static, shallower)

📦 Output Example

When you import virtual:sigpro-routes, you get:

javascript
// Generated module
-import Page_0 from '/src/pages/index.js';
-import Page_1 from '/src/pages/about.js';
-import Page_2 from '/src/pages/blog/index.js';
-import Page_3 from '/src/pages/blog/[slug].js';
-import Page_4 from '/src/pages/users/[id].js';
-import Page_5 from '/src/pages/users/[id]/edit.js';
-
-export const routes = [
-  { path: '/', component: Page_0 },
-  { path: '/about', component: Page_1 },
-  { path: '/blog', component: Page_2 },
-  { path: '/blog/:slug', component: Page_3 },
-  { path: '/users/:id', component: Page_4 },
-  { path: '/users/:id/edit', component: Page_5 },
-];

🚀 Performance Benefits

  • Automatic code splitting - Each page becomes a separate chunk
  • Lazy loading ready - Import pages dynamically
  • Tree shaking - Only used routes are included
javascript
// With dynamic imports (automatic with Vite)
-const routes = [
-  { path: '/', component: () => import('./pages/index.js') },
-  { path: '/about', component: () => import('./pages/about.js') },
-  // ...
-];

💡 Pro Tips

src/pages/
-├── dashboard/
-│   ├── index.js
-│   ├── analytics.js
-│   └── settings.js
-└── dashboard.js   # ❌ Don't mix with folder

2. Use Index Files for Clean URLs

✅ Good:
-pages/blog/index.js      → /blog
-pages/blog/post.js       → /blog/post
-
-❌ Avoid:
-pages/blog.js            → /blog (conflicts with folder)

3. Private Components

Prefix with underscore to exclude from routing:

src/pages/
-├── index.js
-├── about.js
-└── _components/         # ❌ Not scanned
-    └── Header.js

4. Layout Components

Create a layout wrapper in your main entry:

javascript
// main.js
-import { $, html } from 'sigpro';
+  plugins: [sigproRouter()]
+});

3. Implementation

You can implement the router either directly in your entry point or inside an App component to support persistent layouts (like a navbar that doesn't re-render).

Option A: Direct in main.js

Best for simple apps where the router occupies the entire viewport.

javascript
// src/main.js
+import { $ } from 'sigpro';
+import { Router } from 'sigpro/plugins';
 import { routes } from 'virtual:sigpro-routes';
 
-// Wrap all routes with layout
-const routesWithLayout = routes.map(route => ({
-  ...route,
-  component: (params) => Layout(route.component(params))
-}));
+$.plugin(Router).then(() => {
+  $.mount(_router(routes), '#app');
+});

Option B: Inside App.js (With Layout)

Recommended for apps with a fixed Sidebar or Navbar.

javascript
// src/main.js
+import { $ } from 'sigpro';
+import { Router } from 'sigpro/plugins';
 
-const router = $.router(routesWithLayout);
-document.body.appendChild(router);

Note: This plugin is completely optional. You can always define routes manually if you prefer. The plugin just saves you from writing boilerplate route configurations.

Pro Tip: The plugin works great with hot module replacement (HMR) - add a new page and it's instantly available in your dev server without restarting!

- +$.plugin(Router).then(() => { + import('./App.js').then(app => $.mount(app.default, '#app')); +}); + +// src/App.js +import { routes } from 'virtual:sigpro-routes'; + +export default () => { + return div({ class: 'layout' }, [ + header([ + h1("SigPro App"), + nav([ + a({ href: '#/' }, "Home"), + a({ href: '#/blog' }, "Blog") + ]) + ]), + // The router only swaps the content inside this <main> tag + main(_router(routes)) + ]); +};

4. Route Mapping Reference

File PathGenerated RouteLogic
index.js/Home page
about.js/aboutStatic path
[id].js/:idDynamic parameter
blog/index.js/blogFolder index
_utils.jsIgnoredFiles starting with _ are skipped

5. Installation

bash
npm install sigpro
bash
pnpm add sigpro
bash
yarn add sigpro
bash
bun add sigpro
+ \ No newline at end of file diff --git a/index.js b/index.js index 62237f3..53dd9f3 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,2 @@ // index.js -export * from './packages/sigpro/sigpro.js'; -export { default as sigproRouter } from './packages/sigpro/plugin.js'; +export * from './sigpro/sigpro.js'; diff --git a/package.json b/package.json index 4f0f00a..ed6bfd1 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,25 @@ { "name": "sigpro", - "version": "1.0.14", + "version": "1.1.0", "type": "module", "license": "MIT", + "exports": { + ".": "./index.js", + "./plugins": "./plugins/index.js", + "./vite/*": "./vite/*.js" + }, "homepage": "https://natxocc.github.io/sigpro/", "repository": { "type": "git", - "url": "git+https://github.com/natxocc/sigpro.git" + "url": "https://github.com/natxocc/sigpro.git" }, "bugs": { "url": "https://github.com/natxocc/sigpro/issues" }, "scripts": { - "docs:dev": "vitepress dev packages/docs", - "docs:build": "vitepress build packages/docs", - "docs:preview": "vitepress preview packages/docs" + "docs:dev": "vitepress dev src/docs", + "docs:build": "vitepress build src/docs", + "docs:preview": "vitepress preview src/docs" }, "devDependencies": { "vitepress": "^1.6.4" diff --git a/packages/docs/api/components.md b/packages/docs/api/components.md deleted file mode 100644 index f4b69ae..0000000 --- a/packages/docs/api/components.md +++ /dev/null @@ -1,760 +0,0 @@ -# Components API 🧩 - -Components in SigPro are native Web Components built on the Custom Elements standard. They provide a way to create reusable, encapsulated pieces of UI with reactive properties and automatic cleanup. - -## `$.component(tagName, setupFunction, observedAttributes, useShadowDOM)` - -Creates a custom element with reactive properties and automatic dependency tracking. - -```javascript -import { $, html } from 'sigpro'; - -$.component('my-button', (props, { slot, emit }) => { - return html` - - `; -}, ['variant']); // Observe the 'variant' attribute -``` - -## 📋 API Reference - -### Parameters - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `tagName` | `string` | required | Custom element tag name (must include a hyphen, e.g., `my-button`) | -| `setupFunction` | `Function` | required | Function that returns the component's template | -| `observedAttributes` | `string[]` | `[]` | Attributes to observe for changes (become reactive props) | -| `useShadowDOM` | `boolean` | `false` | `true` = Shadow DOM (encapsulated), `false` = Light DOM (inherits styles) | - -### Setup Function Parameters - -The setup function receives two arguments: - -1. **`props`** - Object containing reactive signals for each observed attribute -2. **`context`** - Object with helper methods and properties - -#### Context Object Properties - -| Property | Type | Description | -|----------|------|-------------| -| `slot(name)` | `Function` | Returns array of child nodes for the specified slot | -| `emit(name, detail)` | `Function` | Dispatches a custom event | -| `select(selector)` | `Function` | Query selector within component's root | -| `selectAll(selector)` | `Function` | Query selector all within component's root | -| `host` | `HTMLElement` | Reference to the custom element instance | -| `root` | `Node` | Component's root (shadow root or element itself) | -| `onUnmount(callback)` | `Function` | Register cleanup function | - -## 🏠 Light DOM vs Shadow DOM - -### Light DOM (`useShadowDOM = false`) - Default - -The component **inherits global styles** from the application. Perfect for components that should integrate with your site's design system. - -```javascript -// Button that uses global Tailwind CSS -$.component('tw-button', (props, { slot, emit }) => { - const variant = props.variant() || 'primary'; - - const variants = { - primary: 'bg-blue-500 hover:bg-blue-600 text-white', - secondary: 'bg-gray-500 hover:bg-gray-600 text-white', - outline: 'border border-blue-500 text-blue-500 hover:bg-blue-50' - }; - - return html` - - `; -}, ['variant']); -``` - -### Shadow DOM (`useShadowDOM = true`) - Encapsulated - -The component **encapsulates its styles** completely. External styles don't affect it, and its styles don't leak out. - -```javascript -// Calendar with encapsulated styles -$.component('ui-calendar', (props) => { - return html` - - -
- ${renderCalendar(props.date())} -
- `; -}, ['date'], true); // true = use Shadow DOM -``` - -## 🎯 Basic Examples - -### Simple Counter Component - -```javascript -// counter.js -$.component('my-counter', (props) => { - const count = $(0); - - return html` -
-

Count: ${count}

- - - -
- `; -}); -``` - -**Usage:** -```html - -``` - -### Component with Props - -```javascript -// greeting.js -$.component('my-greeting', (props) => { - const name = props.name() || 'World'; - const greeting = $(() => `Hello, ${name}!`); - - return html` -
-

${greeting}

-

This is a greeting component.

-
- `; -}, ['name']); // Observe the 'name' attribute -``` - -**Usage:** -```html - - -``` - -### Component with Events - -```javascript -// toggle.js -$.component('my-toggle', (props, { emit }) => { - const isOn = $(props.initial() === 'on'); - - const toggle = () => { - isOn(!isOn()); - emit('toggle', { isOn: isOn() }); - emit(isOn() ? 'on' : 'off'); - }; - - return html` - - `; -}, ['initial']); -``` - -**Usage:** -```html - console.log('Toggled:', e.detail)} - @on=${() => console.log('Turned on')} - @off=${() => console.log('Turned off')} -> -``` - -## 🎨 Advanced Examples - -### Form Input Component - -```javascript -// form-input.js -$.component('form-input', (props, { emit }) => { - const value = $(props.value() || ''); - const error = $(null); - const touched = $(false); - - // Validation effect - $.effect(() => { - if (props.pattern() && touched()) { - const regex = new RegExp(props.pattern()); - const isValid = regex.test(value()); - error(isValid ? null : props.errorMessage() || 'Invalid input'); - emit('validate', { isValid, value: value() }); - } - }); - - const handleInput = (e) => { - value(e.target.value); - emit('update', e.target.value); - }; - - const handleBlur = () => { - touched(true); - }; - - return html` -
- ${props.label() ? html` - - ` : ''} - - - - ${() => error() ? html` -
${error()}
- ` : ''} - - ${props.helpText() ? html` - ${props.helpText()} - ` : ''} -
- `; -}, ['label', 'type', 'value', 'placeholder', 'disabled', 'required', 'pattern', 'errorMessage', 'helpText']); -``` - -**Usage:** -```html - formData.email = e.detail} - @validate=${(e) => setEmailValid(e.detail.isValid)} -> - -``` - -### Modal/Dialog Component - -```javascript -// modal.js -$.component('my-modal', (props, { slot, emit, onUnmount }) => { - const isOpen = $(false); - - // Handle escape key - const handleKeydown = (e) => { - if (e.key === 'Escape' && isOpen()) { - close(); - } - }; - - $.effect(() => { - if (isOpen()) { - document.addEventListener('keydown', handleKeydown); - document.body.style.overflow = 'hidden'; - } else { - document.removeEventListener('keydown', handleKeydown); - document.body.style.overflow = ''; - } - }); - - // Cleanup on unmount - onUnmount(() => { - document.removeEventListener('keydown', handleKeydown); - document.body.style.overflow = ''; - }); - - const open = () => { - isOpen(true); - emit('open'); - }; - - const close = () => { - isOpen(false); - emit('close'); - }; - - // Expose methods to parent - props.open = open; - props.close = close; - - return html` -
- - - - - ${() => isOpen() ? html` - - ` : ''} -
- `; -}, ['title'], false); -``` - -**Usage:** -```html - - - -
-

Are you sure you want to delete this item?

-

This action cannot be undone.

-
- -
- - -
-
-``` - -### Data Table Component - -```javascript -// data-table.js -$.component('data-table', (props, { emit }) => { - const data = $(props.data() || []); - const columns = $(props.columns() || []); - const sortColumn = $(null); - const sortDirection = $('asc'); - const filterText = $(''); - - // Computed: filtered and sorted data - const processedData = $(() => { - let result = [...data()]; - - // Filter - if (filterText()) { - const search = filterText().toLowerCase(); - result = result.filter(row => - Object.values(row).some(val => - String(val).toLowerCase().includes(search) - ) - ); - } - - // Sort - if (sortColumn()) { - const col = sortColumn(); - const direction = sortDirection() === 'asc' ? 1 : -1; - - result.sort((a, b) => { - if (a[col] < b[col]) return -direction; - if (a[col] > b[col]) return direction; - return 0; - }); - } - - return result; - }); - - const handleSort = (col) => { - if (sortColumn() === col) { - sortDirection(sortDirection() === 'asc' ? 'desc' : 'asc'); - } else { - sortColumn(col); - sortDirection('asc'); - } - emit('sort', { column: col, direction: sortDirection() }); - }; - - return html` -
- -
- - - ${() => `${processedData().length} of ${data().length} records`} - -
- - - - - - ${columns().map(col => html` - - `)} - - - - ${() => processedData().map(row => html` - emit('row-click', row)}> - ${columns().map(col => html` - - `)} - - `)} - -
handleSort(col.field)} - class:sortable=${true} - class:sorted=${() => sortColumn() === col.field} - > - ${col.label} - ${() => sortColumn() === col.field ? html` - - ${sortDirection() === 'asc' ? '↑' : '↓'} - - ` : ''} -
${row[col.field]}
- - - ${() => processedData().length === 0 ? html` -
- No data found -
- ` : ''} -
- `; -}, ['data', 'columns']); -``` - -**Usage:** -```javascript -const userColumns = [ - { field: 'id', label: 'ID' }, - { field: 'name', label: 'Name' }, - { field: 'email', label: 'Email' }, - { field: 'role', label: 'Role' } -]; - -const userData = [ - { id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' }, - { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' } -]; -``` - -```html - console.log('Row clicked:', e.detail)} -> - -``` - -### Tabs Component - -```javascript -// tabs.js -$.component('my-tabs', (props, { slot, emit }) => { - const activeTab = $(props.active() || 0); - - // Get all tab headers from slots - const tabs = $(() => { - const headers = slot('tab'); - return headers.map((node, index) => ({ - index, - title: node.textContent, - content: slot(`panel-${index}`)[0] - })); - }); - - $.effect(() => { - emit('change', { index: activeTab(), tab: tabs()[activeTab()] }); - }); - - return html` -
-
- ${tabs().map(tab => html` - - `)} -
- -
- ${tabs().map(tab => html` -
- ${tab.content} -
- `)} -
-
- `; -}, ['active']); -``` - -**Usage:** -```html - console.log('Tab changed:', e.detail)}> -
Profile
-
-

Profile Settings

-
...
-
- -
Security
-
-

Security Settings

-
...
-
- -
Notifications
-
-

Notification Preferences

-
...
-
-
-``` - -### Component with External Data - -```javascript -// user-profile.js -$.component('user-profile', (props, { emit, onUnmount }) => { - const user = $(null); - const loading = $(false); - const error = $(null); - - // Fetch user data when userId changes - $.effect(() => { - const userId = props.userId(); - if (!userId) return; - - loading(true); - error(null); - - const controller = new AbortController(); - - fetch(`/api/users/${userId}`, { signal: controller.signal }) - .then(res => res.json()) - .then(data => { - user(data); - emit('loaded', data); - }) - .catch(err => { - if (err.name !== 'AbortError') { - error(err.message); - emit('error', err); - } - }) - .finally(() => loading(false)); - - // Cleanup: abort fetch if component unmounts or userId changes - onUnmount(() => controller.abort()); - }); - - return html` - - `; -}, ['user-id']); -``` - -## 📦 Component Libraries - -### Building a Reusable Component Library - -```javascript -// components/index.js -import { $, html } from 'sigpro'; - -// Button component -export const Button = $.component('ui-button', (props, { slot, emit }) => { - const variant = props.variant() || 'primary'; - const size = props.size() || 'md'; - - const sizes = { - sm: 'px-2 py-1 text-sm', - md: 'px-4 py-2', - lg: 'px-6 py-3 text-lg' - }; - - const variants = { - primary: 'bg-blue-500 hover:bg-blue-600 text-white', - secondary: 'bg-gray-500 hover:bg-gray-600 text-white', - danger: 'bg-red-500 hover:bg-red-600 text-white' - }; - - return html` - - `; -}, ['variant', 'size', 'disabled']); - -// Card component -export const Card = $.component('ui-card', (props, { slot }) => { - return html` -
- ${props.title() ? html` -
-

${props.title()}

-
- ` : ''} - -
- ${slot()} -
- - ${props.footer() ? html` - - ` : ''} -
- `; -}, ['title']); - -// Badge component -export const Badge = $.component('ui-badge', (props, { slot }) => { - const type = props.type() || 'default'; - - const types = { - default: 'bg-gray-100 text-gray-800', - success: 'bg-green-100 text-green-800', - warning: 'bg-yellow-100 text-yellow-800', - error: 'bg-red-100 text-red-800', - info: 'bg-blue-100 text-blue-800' - }; - - return html` - - ${slot()} - - `; -}, ['type']); - -export { $, html }; -``` - -**Usage:** -```javascript -import { Button, Card, Badge } from './components/index.js'; - -// Use components anywhere -const app = html` -
- -

This is a card component

-
- - New -
-
-
-`; -``` - -## 🎯 Decision Guide: Light DOM vs Shadow DOM - -| Use Light DOM (`false`) when... | Use Shadow DOM (`true`) when... | -|--------------------------------|-------------------------------| -| Component is part of your main app | Building a UI library for others | -| Using global CSS (Tailwind, Bootstrap) | Creating embeddable widgets | -| Need to inherit theme variables | Styles must be pixel-perfect everywhere | -| Working with existing design system | Component has complex, specific styles | -| Quick prototyping | Distributing to different projects | -| Form elements that should match site | Need style isolation/encapsulation | - -## 📊 Summary - -| Feature | Description | -|---------|-------------| -| **Native Web Components** | Built on Custom Elements standard | -| **Reactive Props** | Observed attributes become signals | -| **Two Rendering Modes** | Light DOM (default) or Shadow DOM | -| **Automatic Cleanup** | Effects and listeners cleaned up on disconnect | -| **Event System** | Custom events with `emit()` | -| **Slot Support** | Full slot API for content projection | -| **Zero Dependencies** | Pure vanilla JavaScript | - ---- - -> **Pro Tip:** Start with Light DOM components for app-specific UI, and use Shadow DOM when building components that need to work identically across different projects or websites. \ No newline at end of file diff --git a/packages/docs/api/effects.md b/packages/docs/api/effects.md deleted file mode 100644 index 7e7c2fb..0000000 --- a/packages/docs/api/effects.md +++ /dev/null @@ -1,1039 +0,0 @@ -# Effects API 🔄 - -Effects are the bridge between reactive signals and side effects in your application. They automatically track signal dependencies and re-run whenever those signals change, enabling everything from DOM updates to data fetching and localStorage synchronization. - -## Core Concepts - -### What is an Effect? - -An effect is a function that: -- **Runs immediately** when created -- **Tracks all signals** read during its execution -- **Re-runs automatically** when any tracked signal changes -- **Can return a cleanup function** that runs before the next execution or when the effect is stopped - -### How Effects Work - -1. When an effect runs, it sets itself as the `activeEffect` -2. Any signal read during execution adds the effect to its subscribers -3. When a signal changes, it queues all its subscribers -4. Effects are batched and run in the next microtask -5. If an effect returns a function, it's stored as a cleanup handler - -## `$.effect(effectFn)` - -Creates a reactive effect that automatically tracks dependencies and re-runs when they change. - -```javascript -import { $ } from 'sigpro'; - -const count = $(0); - -$.effect(() => { - console.log(`Count is: ${count()}`); -}); -// Logs: "Count is: 0" - -count(1); -// Logs: "Count is: 1" -``` - -## 📋 API Reference - -| Pattern | Example | Description | -|---------|---------|-------------| -| Basic Effect | `$.effect(() => console.log(count()))` | Run on dependency changes | -| With Cleanup | `$.effect(() => { timer = setInterval(...); return () => clearInterval(timer) })` | Return cleanup function | -| Stop Effect | `const stop = $.effect(...); stop()` | Manually stop an effect | - -### Effect Object (Internal) - -| Property/Method | Description | -|-----------------|-------------| -| `dependencies` | Set of signal subscriber sets this effect belongs to | -| `cleanupHandlers` | Set of cleanup functions to run before next execution | -| `run()` | Executes the effect and tracks dependencies | -| `stop()` | Stops the effect and runs all cleanup handlers | - -## 🎯 Basic Examples - -### Console Logging - -```javascript -import { $ } from 'sigpro'; - -const name = $('World'); -const count = $(0); - -$.effect(() => { - console.log(`Hello ${name()}! Count is ${count()}`); -}); -// Logs: "Hello World! Count is 0" - -name('John'); -// Logs: "Hello John! Count is 0" - -count(5); -// Logs: "Hello John! Count is 5" -``` - -### DOM Updates - -```javascript -import { $ } from 'sigpro'; - -const count = $(0); -const element = document.getElementById('counter'); - -$.effect(() => { - element.textContent = `Count: ${count()}`; -}); - -// Updates DOM automatically when count changes -count(10); // Element text becomes "Count: 10" -``` - -### Document Title - -```javascript -import { $ } from 'sigpro'; - -const page = $('home'); -const unreadCount = $(0); - -$.effect(() => { - const base = page() === 'home' ? 'Home' : 'Dashboard'; - const unread = unreadCount() > 0 ? ` (${unreadCount()})` : ''; - document.title = `${base}${unread} - My App`; -}); - -page('dashboard'); // Title: "Dashboard - My App" -unreadCount(3); // Title: "Dashboard (3) - My App" -``` - -## 🧹 Effects with Cleanup - -Cleanup functions are essential for managing resources like intervals, event listeners, and subscriptions. - -### Basic Cleanup - -```javascript -import { $ } from 'sigpro'; - -const userId = $(1); - -$.effect(() => { - const id = userId(); - console.log(`Setting up timer for user ${id}`); - - const timer = setInterval(() => { - console.log(`Polling user ${id}...`); - }, 1000); - - // Cleanup runs before next effect execution - return () => { - console.log(`Cleaning up timer for user ${id}`); - clearInterval(timer); - }; -}); -// Sets up timer for user 1 - -userId(2); -// Cleans up timer for user 1 -// Sets up timer for user 2 -``` - -### Event Listener Cleanup - -```javascript -import { $ } from 'sigpro'; - -const isListening = $(false); - -$.effect(() => { - if (!isListening()) return; - - const handleClick = (e) => { - console.log('Window clicked:', e.clientX, e.clientY); - }; - - window.addEventListener('click', handleClick); - console.log('Click listener added'); - - return () => { - window.removeEventListener('click', handleClick); - console.log('Click listener removed'); - }; -}); - -isListening(true); // Adds listener -isListening(false); // Removes listener -isListening(true); // Adds listener again -``` - -### WebSocket Connection - -```javascript -import { $ } from 'sigpro'; - -const room = $('general'); -const messages = $([]); - -$.effect(() => { - const currentRoom = room(); - console.log(`Connecting to room: ${currentRoom}`); - - const ws = new WebSocket(`wss://chat.example.com/${currentRoom}`); - - ws.onmessage = (event) => { - messages([...messages(), JSON.parse(event.data)]); - }; - - ws.onerror = (error) => { - console.error('WebSocket error:', error); - }; - - // Cleanup: close connection when room changes - return () => { - console.log(`Disconnecting from room: ${currentRoom}`); - ws.close(); - }; -}); - -room('random'); // Closes 'general' connection, opens 'random' -``` - -## ⏱️ Effect Timing and Batching - -### Microtask Batching - -Effects are batched using `queueMicrotask` for optimal performance: - -```javascript -import { $ } from 'sigpro'; - -const a = $(1); -const b = $(2); -const c = $(3); - -$.effect(() => { - console.log('Effect ran with:', a(), b(), c()); -}); -// Logs immediately: "Effect ran with: 1 2 3" - -// Multiple updates in same tick - only one effect run! -a(10); -b(20); -c(30); -// Only logs once: "Effect ran with: 10 20 30" -``` - -### Async Effects - -Effects can be asynchronous, but be careful with dependency tracking: - -```javascript -import { $ } from 'sigpro'; - -const userId = $(1); -const userData = $(null); - -$.effect(() => { - const id = userId(); - console.log(`Fetching user ${id}...`); - - // Only id() is tracked (synchronous part) - fetch(`/api/users/${id}`) - .then(res => res.json()) - .then(data => { - // This runs later - no dependency tracking here! - userData(data); - }); -}); - -userId(2); // Triggers effect again, cancels previous fetch -``` - -### Effect with AbortController - -For proper async cleanup with fetch: - -```javascript -import { $ } from 'sigpro'; - -const userId = $(1); -const userData = $(null); -const loading = $(false); - -$.effect(() => { - const id = userId(); - const controller = new AbortController(); - - loading(true); - - fetch(`/api/users/${id}`, { signal: controller.signal }) - .then(res => res.json()) - .then(data => { - userData(data); - loading(false); - }) - .catch(err => { - if (err.name !== 'AbortError') { - console.error('Fetch error:', err); - loading(false); - } - }); - - // Cleanup: abort fetch if userId changes before completion - return () => { - controller.abort(); - }; -}); -``` - -## 🎨 Advanced Effect Patterns - -### Debounced Effects - -```javascript -import { $ } from 'sigpro'; - -const searchTerm = $(''); -const results = $([]); -let debounceTimeout; - -$.effect(() => { - const term = searchTerm(); - - // Clear previous timeout - clearTimeout(debounceTimeout); - - // Don't search if term is too short - if (term.length < 3) { - results([]); - return; - } - - // Debounce search - debounceTimeout = setTimeout(async () => { - console.log('Searching for:', term); - const data = await fetch(`/api/search?q=${term}`).then(r => r.json()); - results(data); - }, 300); - - // Cleanup on effect re-run - return () => clearTimeout(debounceTimeout); -}); -``` - -### Throttled Effects - -```javascript -import { $ } from 'sigpro'; - -const scrollPosition = $(0); -let lastRun = 0; -let rafId = null; - -$.effect(() => { - const pos = scrollPosition(); - - // Throttle with requestAnimationFrame - if (rafId) cancelAnimationFrame(rafId); - - rafId = requestAnimationFrame(() => { - console.log('Scroll position:', pos); - updateScrollUI(pos); - lastRun = Date.now(); - rafId = null; - }); - - return () => { - if (rafId) { - cancelAnimationFrame(rafId); - rafId = null; - } - }; -}); - -// Even with many updates, effect runs at most once per frame -for (let i = 0; i < 100; i++) { - scrollPosition(i); -} -``` - -### Conditional Effects - -```javascript -import { $ } from 'sigpro'; - -const isEnabled = $(false); -const value = $(0); -const threshold = $(10); - -$.effect(() => { - // Effect only runs when isEnabled is true - if (!isEnabled()) return; - - console.log(`Monitoring value: ${value()}, threshold: ${threshold()}`); - - if (value() > threshold()) { - alert(`Value ${value()} exceeded threshold ${threshold()}!`); - } -}); - -isEnabled(true); // Effect starts monitoring -value(15); // Triggers alert -isEnabled(false); // Effect stops (still runs, but condition prevents logic) -``` - -### Effect with Multiple Cleanups - -```javascript -import { $ } from 'sigpro'; - -const config = $({ theme: 'light', notifications: true }); - -$.effect(() => { - const { theme, notifications } = config(); - const cleanups = []; - - // Setup theme - document.body.className = `theme-${theme}`; - cleanups.push(() => { - document.body.classList.remove(`theme-${theme}`); - }); - - // Setup notifications - if (notifications) { - const handler = (e) => console.log('Notification:', e.detail); - window.addEventListener('notification', handler); - cleanups.push(() => { - window.removeEventListener('notification', handler); - }); - } - - // Return combined cleanup - return () => { - cleanups.forEach(cleanup => cleanup()); - }; -}); -``` - -## 🎯 Effects in Components - -### Component Lifecycle - -```javascript -import { $, html } from 'sigpro'; - -$.component('timer-display', () => { - const seconds = $(0); - - // Effect for timer - automatically cleaned up when component unmounts - $.effect(() => { - const interval = setInterval(() => { - seconds(s => s + 1); - }, 1000); - - return () => clearInterval(interval); - }); - - return html` -
-

Timer: ${seconds}s

-
- `; -}); -``` - -### Effects with Props - -```javascript -import { $, html } from 'sigpro'; - -$.component('data-viewer', (props) => { - const data = $(null); - const error = $(null); - - // Effect reacts to prop changes - $.effect(() => { - const url = props.url(); - if (!url) return; - - const controller = new AbortController(); - - fetch(url, { signal: controller.signal }) - .then(res => res.json()) - .then(data) - .catch(err => { - if (err.name !== 'AbortError') { - error(err.message); - } - }); - - return () => controller.abort(); - }); - - return html` -
- ${() => { - if (error()) return html`
${error()}
`; - if (!data()) return html`
Loading...
`; - return html`
${JSON.stringify(data(), null, 2)}
`; - }} -
- `; -}, ['url']); -``` - -## 🔧 Effect Management - -### Stopping Effects - -```javascript -import { $ } from 'sigpro'; - -const count = $(0); - -// Start effect -const stopEffect = $.effect(() => { - console.log('Count:', count()); -}); - -count(1); // Logs: "Count: 1" -count(2); // Logs: "Count: 2" - -// Stop the effect -stopEffect(); - -count(3); // No logging - effect is stopped -``` - -### Conditional Effect Stopping - -```javascript -import { $ } from 'sigpro'; - -const isActive = $(true); -const count = $(0); - -let currentEffect = null; - -$.effect(() => { - if (isActive()) { - // Start or restart the monitoring effect - if (currentEffect) currentEffect(); - - currentEffect = $.effect(() => { - console.log('Monitoring count:', count()); - }); - } else { - // Stop monitoring - if (currentEffect) { - currentEffect(); - currentEffect = null; - } - } -}); -``` - -### Nested Effects - -```javascript -import { $ } from 'sigpro'; - -const user = $({ id: 1, name: 'John' }); -const settings = $({ theme: 'dark' }); - -$.effect(() => { - console.log('User changed:', user().name); - - // Nested effect - tracks settings independently - $.effect(() => { - console.log('Settings changed:', settings().theme); - }); - - // When user changes, the nested effect is recreated -}); -``` - -## 🚀 Real-World Examples - -### Auto-saving Form - -```javascript -import { $ } from 'sigpro'; - -const formData = $({ - title: '', - content: '', - tags: [] -}); - -const lastSaved = $(null); -const saveStatus = $('idle'); // 'idle', 'saving', 'saved', 'error' -let saveTimeout; - -$.effect(() => { - const data = formData(); - - // Clear previous timeout - clearTimeout(saveTimeout); - - // Don't save empty form - if (!data.title && !data.content) { - saveStatus('idle'); - return; - } - - saveStatus('saving'); - - // Debounce save - saveTimeout = setTimeout(async () => { - try { - await fetch('/api/posts', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }); - saveStatus('saved'); - lastSaved(new Date()); - } catch (error) { - saveStatus('error'); - console.error('Auto-save failed:', error); - } - }, 1000); - - return () => clearTimeout(saveTimeout); -}); - -// UI feedback -const statusMessage = $(() => { - const status = saveStatus(); - const saved = lastSaved(); - - if (status === 'saving') return 'Saving...'; - if (status === 'error') return 'Save failed'; - if (status === 'saved' && saved) { - return `Last saved: ${saved().toLocaleTimeString()}`; - } - return ''; -}); -``` - -### Real-time Search with Debounce - -```javascript -import { $ } from 'sigpro'; - -const searchInput = $(''); -const searchResults = $([]); -const searchStatus = $('idle'); // 'idle', 'searching', 'results', 'no-results', 'error' -let searchTimeout; -let abortController = null; - -$.effect(() => { - const query = searchInput().trim(); - - // Clear previous timeout - clearTimeout(searchTimeout); - - // Cancel previous request - if (abortController) { - abortController.abort(); - abortController = null; - } - - // Don't search for short queries - if (query.length < 2) { - searchResults([]); - searchStatus('idle'); - return; - } - - searchStatus('searching'); - - // Debounce search - searchTimeout = setTimeout(async () => { - abortController = new AbortController(); - - try { - const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { - signal: abortController.signal - }); - - const data = await response.json(); - - if (!abortController.signal.aborted) { - searchResults(data); - searchStatus(data.length ? 'results' : 'no-results'); - abortController = null; - } - } catch (error) { - if (error.name !== 'AbortError') { - console.error('Search failed:', error); - searchStatus('error'); - } - } - }, 300); - - return () => { - clearTimeout(searchTimeout); - if (abortController) { - abortController.abort(); - abortController = null; - } - }; -}); -``` - -### Analytics Tracking - -```javascript -import { $ } from 'sigpro'; - -// Analytics configuration -const analyticsEnabled = $(true); -const currentPage = $('/'); -const userProperties = $({}); - -// Track page views -$.effect(() => { - if (!analyticsEnabled()) return; - - const page = currentPage(); - const properties = userProperties(); - - console.log('Track page view:', page, properties); - - // Send to analytics - gtag('config', 'GA-MEASUREMENT-ID', { - page_path: page, - ...properties - }); -}); - -// Track user interactions -const trackEvent = (eventName, properties = {}) => { - $.effect(() => { - if (!analyticsEnabled()) return; - - console.log('Track event:', eventName, properties); - gtag('event', eventName, properties); - }); -}; - -// Usage -currentPage('/dashboard'); -userProperties({ userId: 123, plan: 'premium' }); -trackEvent('button_click', { buttonId: 'signup' }); -``` - -### Keyboard Shortcuts - -```javascript -import { $ } from 'sigpro'; - -const shortcuts = $({ - 'ctrl+s': { handler: null, description: 'Save' }, - 'ctrl+z': { handler: null, description: 'Undo' }, - 'ctrl+shift+z': { handler: null, description: 'Redo' }, - 'escape': { handler: null, description: 'Close modal' } -}); - -const pressedKeys = new Set(); - -$.effect(() => { - const handleKeyDown = (e) => { - const key = e.key.toLowerCase(); - const ctrl = e.ctrlKey ? 'ctrl+' : ''; - const shift = e.shiftKey ? 'shift+' : ''; - const alt = e.altKey ? 'alt+' : ''; - const meta = e.metaKey ? 'meta+' : ''; - - const combo = `${ctrl}${shift}${alt}${meta}${key}`.replace(/\+$/, ''); - - const shortcut = shortcuts()[combo]; - if (shortcut?.handler) { - e.preventDefault(); - shortcut.handler(); - } - }; - - window.addEventListener('keydown', handleKeyDown); - - return () => window.removeEventListener('keydown', handleKeyDown); -}); - -// Register shortcuts -shortcuts({ - ...shortcuts(), - 'ctrl+s': { - handler: () => saveDocument(), - description: 'Save document' - }, - 'ctrl+z': { - handler: () => undo(), - description: 'Undo' - } -}); -``` - -### Infinite Scroll - -```javascript -import { $ } from 'sigpro'; - -const posts = $([]); -const page = $(1); -const hasMore = $(true); -const loading = $(false); -let observer = null; - -// Load more posts -const loadMore = async () => { - if (loading() || !hasMore()) return; - - loading(true); - try { - const response = await fetch(`/api/posts?page=${page()}`); - const newPosts = await response.json(); - - if (newPosts.length === 0) { - hasMore(false); - } else { - posts([...posts(), ...newPosts]); - page(p => p + 1); - } - } finally { - loading(false); - } -}; - -// Setup intersection observer for infinite scroll -$.effect(() => { - const sentinel = document.getElementById('sentinel'); - if (!sentinel) return; - - observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && !loading() && hasMore()) { - loadMore(); - } - }, - { threshold: 0.1 } - ); - - observer.observe(sentinel); - - return () => { - if (observer) { - observer.disconnect(); - observer = null; - } - }; -}); - -// Initial load -loadMore(); -``` - -## 📊 Performance Considerations - -| Pattern | Performance Impact | Best Practice | -|---------|-------------------|---------------| -| Multiple signal reads | O(n) per effect | Group related signals | -| Deep object access | Minimal | Use computed signals | -| Large arrays | O(n) for iteration | Memoize with computed | -| Frequent updates | Batched | Let batching work | -| Heavy computations | Blocking | Use Web Workers | - -## 🎯 Best Practices - -### 1. Keep Effects Focused - -```javascript -// ❌ Avoid doing too much in one effect -$.effect(() => { - updateUI(count()); // UI update - saveToStorage(count()); // Storage - sendAnalytics(count()); // Analytics - validate(count()); // Validation -}); - -// ✅ Split into focused effects -$.effect(() => updateUI(count())); -$.effect(() => saveToStorage(count())); -$.effect(() => sendAnalytics(count())); -$.effect(() => validate(count())); -``` - -### 2. Always Clean Up - -```javascript -// ❌ Missing cleanup -$.effect(() => { - const timer = setInterval(() => {}, 1000); - // Memory leak! -}); - -// ✅ Proper cleanup -$.effect(() => { - const timer = setInterval(() => {}, 1000); - return () => clearInterval(timer); -}); -``` - -### 3. Avoid Writing to Signals in Effects - -```javascript -import { $ } from 'sigpro'; - -const a = $(1); -const b = $(2); - -// ❌ Avoid - can cause loops -$.effect(() => { - a(b()); // Writing to a while reading b -}); - -// ✅ Use computed signals instead -const sum = $(() => a() + b()); -``` - -### 4. Use Conditional Logic Carefully - -```javascript -// ❌ Condition affects dependency tracking -$.effect(() => { - if (condition()) { - console.log(a()); // Only tracks a when condition is true - } -}); - -// ✅ Track all dependencies explicitly -$.effect(() => { - const cond = condition(); // Track condition - if (cond) { - console.log(a()); // Track a - } -}); -``` - -### 5. Memoize Expensive Computations - -```javascript -import { $ } from 'sigpro'; - -const items = $([]); - -// ❌ Expensive computation runs on every effect -$.effect(() => { - const total = items().reduce((sum, i) => sum + i.price, 0); - updateTotal(total); -}); - -// ✅ Memoize with computed signal -const total = $(() => items().reduce((sum, i) => sum + i.price, 0)); -$.effect(() => updateTotal(total())); -``` - -## 🔍 Debugging Effects - -### Logging Effect Runs - -```javascript -import { $ } from 'sigpro'; - -const withLogging = (effectFn, name) => { - return $.effect(() => { - console.log(`[${name}] Running...`); - const start = performance.now(); - - const result = effectFn(); - - const duration = performance.now() - start; - console.log(`[${name}] Completed in ${duration.toFixed(2)}ms`); - - return result; - }); -}; - -// Usage -withLogging(() => { - console.log('Count:', count()); -}, 'count-effect'); -``` - -### Effect Inspector - -```javascript -import { $ } from 'sigpro'; - -const createEffectInspector = () => { - const effects = new Map(); - let id = 0; - - const trackedEffect = (fn, name = `effect-${++id}`) => { - const info = { - name, - runs: 0, - lastRun: null, - duration: 0, - dependencies: new Set() - }; - - const wrapped = () => { - info.runs++; - info.lastRun = new Date(); - const start = performance.now(); - - const result = fn(); - - info.duration = performance.now() - start; - return result; - }; - - const stop = $.effect(wrapped); - effects.set(stop, info); - - return stop; - }; - - const getReport = () => { - const report = {}; - effects.forEach((info, stop) => { - report[info.name] = { - runs: info.runs, - lastRun: info.lastRun, - avgDuration: info.duration / info.runs - }; - }); - return report; - }; - - return { trackedEffect, getReport }; -}; - -// Usage -const inspector = createEffectInspector(); -inspector.trackedEffect(() => { - console.log('Count:', count()); -}, 'counter-effect'); -``` - -## 📊 Summary - -| Feature | Description | -|---------|-------------| -| **Automatic Tracking** | Dependencies tracked automatically | -| **Cleanup Functions** | Return function to clean up resources | -| **Batch Updates** | Multiple changes batched in microtask | -| **Manual Stop** | Can stop effects with returned function | -| **Nested Effects** | Effects can contain other effects | -| **Auto-cleanup** | Effects in pages/components auto-cleaned | - ---- - -> **Pro Tip:** Effects are the perfect place for side effects like DOM updates, data fetching, and subscriptions. Keep them focused and always clean up resources! diff --git a/packages/docs/api/fetch.md b/packages/docs/api/fetch.md deleted file mode 100644 index 27c08bc..0000000 --- a/packages/docs/api/fetch.md +++ /dev/null @@ -1,998 +0,0 @@ -# Fetch API 🌐 - -SigPro provides a simple, lightweight wrapper around the native Fetch API that integrates seamlessly with signals for loading state management. It's designed for common use cases with sensible defaults. - -## Core Concepts - -### What is `$.fetch`? - -A ultra-simple fetch wrapper that: -- **Automatically handles JSON** serialization and parsing -- **Integrates with signals** for loading state -- **Returns `null` on error** (no try/catch needed for basic usage) -- **Works great with effects** for reactive data fetching - -## `$.fetch(url, data, [loading])` - -Makes a POST request with JSON data and optional loading signal. - -```javascript -import { $ } from 'sigpro'; - -const loading = $(false); - -async function loadUser() { - const user = await $.fetch('/api/user', { id: 123 }, loading); - if (user) { - console.log('User loaded:', user); - } -} -``` - -## 📋 API Reference - -### Parameters - -| Parameter | Type | Description | -|-----------|------|-------------| -| `url` | `string` | Endpoint URL | -| `data` | `Object` | Data to send (automatically JSON.stringify'd) | -| `loading` | `Function` (optional) | Signal function to track loading state | - -### Returns - -| Return | Description | -|--------|-------------| -| `Promise` | Parsed JSON response or `null` on error | - -## 🎯 Basic Examples - -### Simple Data Fetching - -```javascript -import { $ } from 'sigpro'; - -const userData = $(null); - -async function fetchUser(id) { - const data = await $.fetch('/api/user', { id }); - if (data) { - userData(data); - } -} - -fetchUser(123); -``` - -### With Loading State - -```javascript -import { $, html } from 'sigpro'; - -const user = $(null); -const loading = $(false); - -async function loadUser(id) { - const data = await $.fetch('/api/user', { id }, loading); - if (data) user(data); -} - -// In your template -html` -
- ${() => loading() ? html` -
Loading...
- ` : user() ? html` -
-

${user().name}

-

Email: ${user().email}

-
- ` : html` -

No user found

- `} -
-`; -``` - -### In an Effect - -```javascript -import { $ } from 'sigpro'; - -const userId = $(1); -const user = $(null); -const loading = $(false); - -$.effect(() => { - const id = userId(); - if (id) { - $.fetch(`/api/users/${id}`, null, loading).then(data => { - if (data) user(data); - }); - } -}); - -userId(2); // Automatically fetches new user -``` - -## 🚀 Advanced Examples - -### User Profile with Loading States - -```javascript -import { $, html } from 'sigpro'; - -const Profile = () => { - const userId = $(1); - const user = $(null); - const loading = $(false); - const error = $(null); - - const fetchUser = async (id) => { - error(null); - const data = await $.fetch('/api/user', { id }, loading); - if (data) { - user(data); - } else { - error('Failed to load user'); - } - }; - - // Fetch when userId changes - $.effect(() => { - fetchUser(userId()); - }); - - return html` -
-
- - - -
- - ${() => { - if (loading()) { - return html`
Loading profile...
`; - } - - if (error()) { - return html`
${error()}
`; - } - - if (user()) { - return html` - - `; - } - - return html`

Select a user

`; - }} -
- `; -}; -``` - -### Todo List with API - -```javascript -import { $, html } from 'sigpro'; - -const TodoApp = () => { - const todos = $([]); - const loading = $(false); - const newTodo = $(''); - const filter = $('all'); // 'all', 'active', 'completed' - - // Load todos - const loadTodos = async () => { - const data = await $.fetch('/api/todos', {}, loading); - if (data) todos(data); - }; - - // Add todo - const addTodo = async () => { - if (!newTodo().trim()) return; - - const todo = await $.fetch('/api/todos', { - text: newTodo(), - completed: false - }); - - if (todo) { - todos([...todos(), todo]); - newTodo(''); - } - }; - - // Toggle todo - const toggleTodo = async (id, completed) => { - const updated = await $.fetch(`/api/todos/${id}`, { - completed: !completed - }); - - if (updated) { - todos(todos().map(t => - t.id === id ? updated : t - )); - } - }; - - // Delete todo - const deleteTodo = async (id) => { - const result = await $.fetch(`/api/todos/${id}/delete`, {}); - if (result) { - todos(todos().filter(t => t.id !== id)); - } - }; - - // Filtered todos - const filteredTodos = $(() => { - const currentFilter = filter(); - if (currentFilter === 'all') return todos(); - if (currentFilter === 'active') { - return todos().filter(t => !t.completed); - } - return todos().filter(t => t.completed); - }); - - // Load on mount - loadTodos(); - - return html` -
-

Todo List

- -
- - -
- -
- - - -
- - ${() => loading() ? html` -
Loading todos...
- ) : html` -
    - ${filteredTodos().map(todo => html` -
  • - toggleTodo(todo.id, todo.completed)} - /> - ${todo.text} - -
  • - `)} -
- `} -
- `; -}; -``` - -### Infinite Scroll with Pagination - -```javascript -import { $, html } from 'sigpro'; - -const InfiniteScroll = () => { - const posts = $([]); - const page = $(1); - const loading = $(false); - const hasMore = $(true); - const error = $(null); - - const loadMore = async () => { - if (loading() || !hasMore()) return; - - const data = await $.fetch('/api/posts', { - page: page(), - limit: 10 - }, loading); - - if (data) { - if (data.posts.length === 0) { - hasMore(false); - } else { - posts([...posts(), ...data.posts]); - page(p => p + 1); - } - } else { - error('Failed to load posts'); - } - }; - - // Intersection Observer for infinite scroll - $.effect(() => { - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) { - loadMore(); - } - }, - { threshold: 0.1 } - ); - - const sentinel = document.getElementById('sentinel'); - if (sentinel) observer.observe(sentinel); - - return () => observer.disconnect(); - }); - - // Initial load - loadMore(); - - return html` -
-

Posts

- -
- ${posts().map(post => html` -
-

${post.title}

-

${post.body}

- By ${post.author} -
- `)} -
- -
- ${() => { - if (loading()) { - return html`
Loading more...
`; - } - if (error()) { - return html`
${error()}
`; - } - if (!hasMore()) { - return html`
No more posts
`; - } - return ''; - }} -
-
- `; -}; -``` - -### Search with Debounce - -```javascript -import { $, html } from 'sigpro'; - -const SearchComponent = () => { - const query = $(''); - const results = $([]); - const loading = $(false); - const error = $(null); - let searchTimeout; - - const performSearch = async (searchQuery) => { - if (!searchQuery.trim()) { - results([]); - return; - } - - const data = await $.fetch('/api/search', { - q: searchQuery - }, loading); - - if (data) { - results(data); - } else { - error('Search failed'); - } - }; - - // Debounced search - $.effect(() => { - const searchQuery = query(); - - clearTimeout(searchTimeout); - - if (searchQuery.length < 2) { - results([]); - return; - } - - searchTimeout = setTimeout(() => { - performSearch(searchQuery); - }, 300); - - return () => clearTimeout(searchTimeout); - }); - - return html` - - `; -}; -``` - -### Form Submission - -```javascript -import { $, html } from 'sigpro'; - -const ContactForm = () => { - const formData = $({ - name: '', - email: '', - message: '' - }); - - const submitting = $(false); - const submitError = $(null); - const submitSuccess = $(false); - - const handleSubmit = async (e) => { - e.preventDefault(); - - submitError(null); - submitSuccess(false); - - const result = await $.fetch('/api/contact', formData(), submitting); - - if (result) { - submitSuccess(true); - formData({ name: '', email: '', message: '' }); - } else { - submitError('Failed to send message. Please try again.'); - } - }; - - const updateField = (field, value) => { - formData({ - ...formData(), - [field]: value - }); - }; - - return html` -
-

Contact Us

- -
- - formData().name} - @input=${(e) => updateField('name', e.target.value)} - required - ?disabled=${submitting} - /> -
- -
- - formData().email} - @input=${(e) => updateField('email', e.target.value)} - required - ?disabled=${submitting} - /> -
- -
- - -
- - ${() => { - if (submitting()) { - return html`
Sending...
`; - } - - if (submitError()) { - return html`
${submitError()}
`; - } - - if (submitSuccess()) { - return html`
Message sent successfully!
`; - } - - return ''; - }} - - -
- `; -}; -``` - -### Real-time Dashboard with Multiple Endpoints - -```javascript -import { $, html } from 'sigpro'; - -const Dashboard = () => { - // Multiple data streams - const metrics = $({}); - const alerts = $([]); - const logs = $([]); - - const loading = $({ - metrics: false, - alerts: false, - logs: false - }); - - const refreshInterval = $(5000); // 5 seconds - - const fetchMetrics = async () => { - const data = await $.fetch('/api/metrics', {}, loading().metrics); - if (data) metrics(data); - }; - - const fetchAlerts = async () => { - const data = await $.fetch('/api/alerts', {}, loading().alerts); - if (data) alerts(data); - }; - - const fetchLogs = async () => { - const data = await $.fetch('/api/logs', { - limit: 50 - }, loading().logs); - if (data) logs(data); - }; - - // Auto-refresh all data - $.effect(() => { - fetchMetrics(); - fetchAlerts(); - fetchLogs(); - - const interval = setInterval(() => { - fetchMetrics(); - fetchAlerts(); - }, refreshInterval()); - - return () => clearInterval(interval); - }); - - return html` -
-
-

System Dashboard

-
- -
-
- -
- -
-

System Metrics

- ${() => loading().metrics ? html` -
Loading metrics...
- ) : html` -
-
- - ${metrics().cpu || 0}% -
-
- - ${metrics().memory || 0}% -
-
- - ${metrics().requests || 0}/s -
-
- `} -
- - -
-

Active Alerts

- ${() => loading().alerts ? html` -
Loading alerts...
- ) : alerts().length > 0 ? html` -
    - ${alerts().map(alert => html` -
  • - ${alert.type} -

    ${alert.message}

    - ${new Date(alert.timestamp).toLocaleTimeString()} -
  • - `)} -
- ) : html` -

No active alerts

- `} -
- - -
-

Recent Logs

- ${() => loading().logs ? html` -
Loading logs...
- ) : html` -
    - ${logs().map(log => html` -
  • - ${new Date(log.timestamp).toLocaleTimeString()} - ${log.message} -
  • - `)} -
- `} -
-
-
- `; -}; -``` - -### File Upload - -```javascript -import { $, html } from 'sigpro'; - -const FileUploader = () => { - const files = $([]); - const uploading = $(false); - const uploadProgress = $({}); - const uploadResults = $([]); - - const handleFileSelect = (e) => { - files([...e.target.files]); - }; - - const uploadFiles = async () => { - if (files().length === 0) return; - - uploading(true); - uploadResults([]); - - for (const file of files()) { - const formData = new FormData(); - formData.append('file', file); - - // Track progress for this file - uploadProgress({ - ...uploadProgress(), - [file.name]: 0 - }); - - try { - // Custom fetch for FormData - const response = await fetch('/api/upload', { - method: 'POST', - body: formData - }); - - const result = await response.json(); - - uploadResults([ - ...uploadResults(), - { file: file.name, success: true, result } - ]); - } catch (error) { - uploadResults([ - ...uploadResults(), - { file: file.name, success: false, error: error.message } - ]); - } - - uploadProgress({ - ...uploadProgress(), - [file.name]: 100 - }); - } - - uploading(false); - }; - - return html` -
-

Upload Files

- - - - ${() => files().length > 0 ? html` -
-

Selected Files:

-
    - ${files().map(file => html` -
  • - ${file.name} (${(file.size / 1024).toFixed(2)} KB) - ${() => uploadProgress()[file.name] ? html` - - ) : ''} -
  • - `)} -
- - -
- ` : ''} - - ${() => uploadResults().length > 0 ? html` -
-

Upload Results:

-
    - ${uploadResults().map(result => html` -
  • - ${result.file}: - ${result.success ? 'Uploaded successfully' : `Failed: ${result.error}`} -
  • - `)} -
-
- ` : ''} -
- `; -}; -``` - -### Retry Logic - -```javascript -import { $ } from 'sigpro'; - -// Enhanced fetch with retry -const fetchWithRetry = async (url, data, loading, maxRetries = 3) => { - let lastError; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - if (loading) loading(true); - - const result = await $.fetch(url, data); - if (result !== null) { - return result; - } - - // If we get null but no error, wait and retry - if (attempt < maxRetries) { - await new Promise(resolve => - setTimeout(resolve, Math.pow(2, attempt) * 1000) // Exponential backoff - ); - } - } catch (error) { - lastError = error; - console.warn(`Attempt ${attempt} failed:`, error); - - if (attempt < maxRetries) { - await new Promise(resolve => - setTimeout(resolve, Math.pow(2, attempt) * 1000) - ); - } - } finally { - if (attempt === maxRetries && loading) { - loading(false); - } - } - } - - console.error('All retry attempts failed:', lastError); - return null; -}; - -// Usage -const loading = $(false); -const data = await fetchWithRetry('/api/unreliable-endpoint', {}, loading, 5); -``` - -## 🎯 Best Practices - -### 1. Always Handle Null Responses - -```javascript -// ❌ Don't assume success -const data = await $.fetch('/api/data'); -console.log(data.property); // Might throw if data is null - -// ✅ Check for null -const data = await $.fetch('/api/data'); -if (data) { - console.log(data.property); -} else { - showError('Failed to load data'); -} -``` - -### 2. Use with Effects for Reactivity - -```javascript -// ❌ Manual fetching -button.addEventListener('click', async () => { - const data = await $.fetch('/api/data'); - updateUI(data); -}); - -// ✅ Reactive fetching -const trigger = $(false); - -$.effect(() => { - if (trigger()) { - $.fetch('/api/data').then(data => { - if (data) updateUI(data); - }); - } -}); - -trigger(true); // Triggers fetch -``` - -### 3. Combine with Loading Signals - -```javascript -// ✅ Always show loading state -const loading = $(false); -const data = $(null); - -async function load() { - const result = await $.fetch('/api/data', {}, loading); - if (result) data(result); -} - -// In template -html` -
- ${() => loading() ? '' : - data() ? '' : - ''} -
-`; -``` - -### 4. Cancel In-flight Requests - -```javascript -// ✅ Use AbortController with effects -let controller; - -$.effect(() => { - if (controller) { - controller.abort(); - } - - controller = new AbortController(); - - fetch(url, { signal: controller.signal }) - .then(res => res.json()) - .then(data => { - if (!controller.signal.aborted) { - updateData(data); - } - }); - - return () => controller.abort(); -}); -``` - -## 📊 Error Handling - -### Basic Error Handling - -```javascript -const data = await $.fetch('/api/data'); -if (!data) { - // Handle error (show message, retry, etc.) -} -``` - -### With Error Signal - -```javascript -const data = $(null); -const error = $(null); -const loading = $(false); - -async function loadData() { - error(null); - const result = await $.fetch('/api/data', {}, loading); - - if (result) { - data(result); - } else { - error('Failed to load data'); - } -} -``` ---- - -> **Pro Tip:** Combine `$.fetch` with `$.effect` and loading signals for a complete reactive data fetching solution. The loading signal integration makes it trivial to show loading states in your UI. diff --git a/packages/docs/api/pages.md b/packages/docs/api/pages.md deleted file mode 100644 index 1c8a19b..0000000 --- a/packages/docs/api/pages.md +++ /dev/null @@ -1,497 +0,0 @@ -# Pages API 📄 - -Pages in SigPro are special components designed for route-based navigation with **automatic cleanup**. When you navigate away from a page, all signals, effects, and event listeners created within that page are automatically cleaned up - no memory leaks, no manual cleanup needed. - -## `$.page(setupFunction)` - -Creates a page with automatic cleanup of all signals and effects when navigated away. - -```javascript -import { $, html } from 'sigpro'; - -export default $.page(() => { - // All signals and effects created here - // will be automatically cleaned up on navigation - const count = $(0); - - $.effect(() => { - console.log(`Count: ${count()}`); - }); - - return html` -
-

My Page

-

Count: ${count}

- -
- `; -}); -``` - -## 📋 API Reference - -| Parameter | Type | Description | -|-----------|------|-------------| -| `setupFunction` | `Function` | Function that returns the page content. Receives context object with `params` and `onUnmount` | - -### Context Object Properties - -| Property | Type | Description | -|----------|------|-------------| -| `params` | `Object` | Route parameters passed to the page | -| `onUnmount` | `Function` | Register cleanup callbacks (alternative to automatic cleanup) | - -## 🎯 Basic Usage - -### Simple Page - -```javascript -// pages/home.js -import { $, html } from 'sigpro'; - -export default $.page(() => { - const title = $('Welcome to SigPro'); - - return html` -
-

${title}

-

This page will clean itself up when you navigate away.

-
- `; -}); -``` - -### Page with Route Parameters - -```javascript -// pages/user.js -import { $, html } from 'sigpro'; - -export default $.page(({ params }) => { - // Access route parameters - const userId = params.id; - const userData = $(null); - const loading = $(false); - - // Auto-cleaned effect - $.effect(() => { - loading(true); - $.fetch(`/api/users/${userId}`, null, loading) - .then(data => userData(data)); - }); - - return html` -
- ${() => loading() ? html` -
Loading...
- ` : html` -

User Profile: ${userData()?.name}

-

Email: ${userData()?.email}

- `} -
- `; -}); -``` - -## 🧹 Automatic Cleanup - -The magic of `$.page` is automatic cleanup. Everything created inside the page is tracked and cleaned up: - -```javascript -export default $.page(() => { - // ✅ Signals are auto-cleaned - const count = $(0); - const user = $(null); - - // ✅ Effects are auto-cleaned - $.effect(() => { - document.title = `Count: ${count()}`; - }); - - // ✅ Event listeners are auto-cleaned - window.addEventListener('resize', handleResize); - - // ✅ Intervals and timeouts are auto-cleaned - const interval = setInterval(() => { - refreshData(); - }, 5000); - - return html`
Page content
`; -}); -// When navigating away: all signals, effects, listeners, intervals STOP -``` - -## 📝 Manual Cleanup with `onUnmount` - -Sometimes you need custom cleanup logic. Use `onUnmount` for that: - -```javascript -export default $.page(({ onUnmount }) => { - // WebSocket connection - const socket = new WebSocket('wss://api.example.com'); - - socket.onmessage = (event) => { - updateData(JSON.parse(event.data)); - }; - - // Manual cleanup - onUnmount(() => { - socket.close(); - console.log('WebSocket closed'); - }); - - return html`
Real-time updates
`; -}); -``` - -## 🔄 Integration with Router - -Pages are designed to work seamlessly with `$.router`: - -```javascript -import { $, html } from 'sigpro'; -import HomePage from './pages/Home.js'; -import UserPage from './pages/User.js'; -import SettingsPage from './pages/Settings.js'; - -const routes = [ - { path: '/', component: HomePage }, - { path: '/user/:id', component: UserPage }, - { path: '/settings', component: SettingsPage }, -]; - -// Mount router -document.body.appendChild($.router(routes)); -``` - -## 💡 Practical Examples - -### Example 1: Data Fetching Page - -```javascript -// pages/posts.js -export default $.page(({ params }) => { - const posts = $([]); - const loading = $(true); - const error = $(null); - - $.effect(() => { - fetch('/api/posts') - .then(res => res.json()) - .then(data => { - posts(data); - loading(false); - }) - .catch(err => { - error(err.message); - loading(false); - }); - }); - - return html` -
-

Blog Posts

- - ${() => loading() ? html` -
Loading posts...
- ` : error() ? html` -
Error: ${error()}
- ` : html` -
- ${posts().map(post => html` -
-

${post.title}

-

${post.excerpt}

- Read more -
- `)} -
- `} -
- `; -}); -``` - -### Example 2: Real-time Dashboard - -```javascript -// pages/dashboard.js -export default $.page(({ onUnmount }) => { - const metrics = $({ - cpu: 0, - memory: 0, - requests: 0 - }); - - // Auto-refresh data - const refreshInterval = setInterval(async () => { - const data = await $.fetch('/api/metrics'); - if (data) metrics(data); - }, 5000); - - // Manual cleanup for interval - onUnmount(() => clearInterval(refreshInterval)); - - // Live clock - const currentTime = $(new Date()); - const clockInterval = setInterval(() => { - currentTime(new Date()); - }, 1000); - - onUnmount(() => clearInterval(clockInterval)); - - return html` -
-

System Dashboard

- -
- Last updated: ${() => currentTime().toLocaleTimeString()} -
- -
-
-

CPU Usage

-

${() => metrics().cpu}%

-
-
-

Memory Usage

-

${() => metrics().memory}%

-
-
-

Requests/min

-

${() => metrics().requests}

-
-
-
- `; -}); -``` - -### Example 3: Multi-step Form - -```javascript -// pages/checkout.js -export default $.page(({ onUnmount }) => { - const step = $(1); - const formData = $({ - email: '', - address: '', - payment: '' - }); - - // Warn user before leaving - const handleBeforeUnload = (e) => { - if (step() < 3) { - e.preventDefault(); - e.returnValue = ''; - } - }; - - window.addEventListener('beforeunload', handleBeforeUnload); - onUnmount(() => { - window.removeEventListener('beforeunload', handleBeforeUnload); - }); - - const nextStep = () => step(s => Math.min(s + 1, 3)); - const prevStep = () => step(s => Math.max(s - 1, 1)); - - return html` -
-

Checkout - Step ${step} of 3

- - ${() => { - switch(step()) { - case 1: - return html` -
-

Email

- formData().email} - @input=${(e) => formData({...formData(), email: e.target.value})} - /> -
- `; - case 2: - return html` -
-

Address

- -
- `; - case 3: - return html` -
-

Payment

- formData().payment} - @input=${(e) => formData({...formData(), payment: e.target.value})} - /> -
- `; - } - }} - -
- ${() => step() > 1 ? html` - - ` : ''} - - ${() => step() < 3 ? html` - - ` : html` - - `} -
-
- `; -}); -``` - -### Example 4: Page with Tabs - -```javascript -// pages/profile.js -export default $.page(({ params }) => { - const activeTab = $('overview'); - const userData = $(null); - - // Load user data - $.effect(() => { - $.fetch(`/api/users/${params.id}`) - .then(data => userData(data)); - }); - - const tabs = { - overview: () => html` -
-

Overview

-

Username: ${userData()?.username}

-

Member since: ${userData()?.joined}

-
- `, - posts: () => html` -
-

Posts

- ${userData()?.posts.map(post => html` -
${post.title}
- `)} -
- `, - settings: () => html` -
-

Settings

- -
- ` - }; - - return html` -
-

${() => userData()?.name}

- -
- ${Object.keys(tabs).map(tab => html` - - `)} -
- -
- ${() => tabs[activeTab()]()} -
-
- `; -}); -``` - -## 🎯 Advanced Patterns - -### Page with Nested Routes - -```javascript -// pages/settings/index.js -export default $.page(({ params }) => { - const section = params.section || 'general'; - - const sections = { - general: () => import('./general.js').then(m => m.default), - security: () => import('./security.js').then(m => m.default), - notifications: () => import('./notifications.js').then(m => m.default) - }; - - const currentSection = $(null); - - $.effect(() => { - sections[section]().then(comp => currentSection(comp)); - }); - - return html` -
- - -
- ${currentSection} -
-
- `; -}); -``` - -### Page with Authentication - -```javascript -// pages/dashboard.js -export default $.page(({ onUnmount }) => { - const isAuthenticated = $(false); - const authCheck = $.effect(() => { - const token = localStorage.getItem('token'); - isAuthenticated(!!token); - }); - - // Redirect if not authenticated - $.effect(() => { - if (!isAuthenticated()) { - $.router.go('/login'); - } - }); - - return html` -
-

Protected Dashboard

- -
- `; -}); -``` - -## 📊 Summary - -| Feature | Description | -|---------|-------------| -| **Automatic Cleanup** | All signals, effects, and resources auto-cleaned on navigation | -| **Memory Safe** | No memory leaks, even with complex nested effects | -| **Router Integration** | Designed to work perfectly with `$.router` | -| **Parameters** | Access route parameters via `params` object | -| **Manual Cleanup** | `onUnmount` for custom cleanup needs | -| **Zero Configuration** | Just wrap your page in `$.page()` and it works | - ---- - -> **Pro Tip:** Always wrap route-based views in `$.page()` to ensure proper cleanup. This prevents memory leaks and ensures your app stays performant even after many navigation changes. \ No newline at end of file diff --git a/packages/docs/api/quick.md b/packages/docs/api/quick.md deleted file mode 100644 index 450b805..0000000 --- a/packages/docs/api/quick.md +++ /dev/null @@ -1,436 +0,0 @@ -# Quick API Reference ⚡ - -A comprehensive reference for all SigPro APIs. Everything you need to build reactive web applications with signals and web components. - -## 📋 API Functions Reference - -| Function | Description | Example | -|----------|-------------|---------| -| **`$(initialValue)`** | Creates a reactive signal (getter/setter) | `const count = $(0)` | -| **`$(computedFn)`** | Creates a computed signal | `const full = $(() => first() + last())` | -| **`$.effect(fn)`** | Runs effect when dependencies change | `$.effect(() => console.log(count()))` | -| **`$.page(setupFn)`** | Creates a page with automatic cleanup | `$.page(() => html`
Page
`)` | -| **`$.component(tagName, setupFn, attrs, useShadow)`** | Creates reactive Web Component | `$.component('my-menu', setup, ['items'])` | -| **`$.router(routes)`** | Creates a hash-based router | `$.router([{path:'/', component:Home}])` | -| **`$.router.go(path)`** | Navigates to a route | `$.router.go('/user/42')` | -| **`$.fetch(url, data, loadingSignal)`** | Fetch wrapper with loading state | `const data = await $.fetch('/api', data, loading)` | -| **`$.storage(key, initialValue, storageType)`** | Persistent signal (local/sessionStorage) | `const theme = $.storage('theme', 'light')` | -| **`` html`...` ``** | Template literal for reactive HTML | `` html`
${count}
` `` | - -### Signal Methods - -| Method | Description | Example | -|--------|-------------|---------| -| **`signal()`** | Gets current value | `count()` | -| **`signal(newValue)`** | Sets new value | `count(5)` | -| **`signal(prev => new)`** | Updates using previous value | `count(c => c + 1)` | - -### Component Context Properties - -| Property | Description | Example | -|----------|-------------|---------| -| **`props`** | Reactive component properties | `props.title()` | -| **`slot(name)`** | Accesses slot content | `slot()` or `slot('footer')` | -| **`emit(event, data)`** | Dispatches custom event | `emit('update', value)` | -| **`onUnmount(cb)`** | Registers cleanup callback | `onUnmount(() => clearInterval(timer))` | - -### Page Context Properties - -| Property | Description | Example | -|----------|-------------|---------| -| **`params`** | Route parameters | `params.id`, `params.slug` | -| **`onUnmount(cb)`** | Registers cleanup callback | `onUnmount(() => clearInterval(timer))` | - -### HTML Directives - -| Directive | Description | Example | -|-----------|-------------|---------| -| **`@event`** | Event listener | `` @click=${handler} `` | -| **`:property`** | Two-way binding | `` :value=${signal} `` | -| **`?attribute`** | Boolean attribute | `` ?disabled=${signal} `` | -| **`.property`** | DOM property binding | `` .scrollTop=${value} `` | -| **`class:name`** | Conditional class | `` class:active=${isActive} `` | - - - - -## 📡 Signals - `$(initialValue)` - -Creates a reactive value that notifies dependents when changed. - -| Pattern | Example | Description | -|---------|---------|-------------| -| **Basic Signal** | `const count = $(0)` | Create signal with initial value | -| **Getter** | `count()` | Read current value | -| **Setter** | `count(5)` | Set new value directly | -| **Updater** | `count(prev => prev + 1)` | Update based on previous value | -| **Computed** | `const full = $(() => first() + last())` | Auto-updating derived signal | - -### Examples - -```javascript -// Basic signal -const count = $(0); -console.log(count()); // 0 -count(5); -count(c => c + 1); // 6 - -// Computed signal -const firstName = $('John'); -const lastName = $('Doe'); -const fullName = $(() => `${firstName()} ${lastName()}`); -console.log(fullName()); // "John Doe" -firstName('Jane'); // fullName auto-updates to "Jane Doe" -``` - -## 🔄 Effects - `$.effect(fn)` - -Executes a function and automatically re-runs when its dependencies change. - -| Pattern | Example | Description | -|---------|---------|-------------| -| **Basic Effect** | `$.effect(() => console.log(count()))` | Run effect on dependency changes | -| **Cleanup** | `$.effect(() => { timer = setInterval(...); return () => clearInterval(timer) })` | Return cleanup function | -| **Stop Effect** | `const stop = $.effect(...); stop()` | Manually stop an effect | - -### Examples - -```javascript -// Auto-running effect -const count = $(0); -$.effect(() => { - console.log(`Count is: ${count()}`); -}); // Logs immediately and whenever count changes - -// Effect with cleanup -const userId = $(1); -$.effect(() => { - const id = userId(); - const timer = setInterval(() => fetchUser(id), 5000); - return () => clearInterval(timer); // Cleanup before re-run -}); -``` - -## 📄 Pages - `$.page(setupFunction)` - -Creates a page with automatic cleanup of all signals and effects when navigated away. - -```javascript -// pages/about.js -import { $, html } from 'sigpro'; - -export default $.page(() => { - const count = $(0); - - // Auto-cleaned on navigation - $.effect(() => { - document.title = `Count: ${count()}`; - }); - - return html` -
-

About Page

-

Count: ${count}

- -
- `; -}); -``` - -### With Parameters - -```javascript -export default $.page(({ params, onUnmount }) => { - const userId = params.id; - - // Manual cleanup if needed - const interval = setInterval(() => refresh(), 10000); - onUnmount(() => clearInterval(interval)); - - return html`
User: ${userId}
`; -}); -``` - -## 🧩 Components - `$.component(tagName, setup, observedAttributes, useShadowDOM)` - -Creates Custom Elements with reactive properties. - -### Parameters - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `tagName` | `string` | required | Custom element tag (must include hyphen) | -| `setupFunction` | `Function` | required | Function that renders the component | -| `observedAttributes` | `string[]` | `[]` | Attributes to observe for changes | -| `useShadowDOM` | `boolean` | `false` | `true` = Shadow DOM (encapsulated), `false` = Light DOM | - -### Light DOM Example (Default) - -```javascript -// button.js - inherits global styles -$.component('my-button', (props, { slot, emit }) => { - return html` - - `; -}, ['variant']); // Observe 'variant' attribute -``` - -### Shadow DOM Example - -```javascript -// calendar.js - encapsulated styles -$.component('my-calendar', (props) => { - return html` - -
- ${renderCalendar(props.date())} -
- `; -}, ['date'], true); // true = use Shadow DOM -``` - -## 🌐 Router - `$.router(routes)` - -Creates a hash-based router with automatic page cleanup. - -### Route Definition - -```javascript -const routes = [ - // Simple routes - { path: '/', component: HomePage }, - { path: '/about', component: AboutPage }, - - // Routes with parameters - { path: '/user/:id', component: UserPage }, - { path: '/user/:id/posts/:pid', component: PostPage }, - - // RegExp routes for advanced matching - { path: /^\/posts\/(?\d+)$/, component: PostPage }, -]; -``` - -### Usage - -```javascript -import { $, html } from 'sigpro'; -import Home from './pages/Home.js'; -import User from './pages/User.js'; - -const router = $.router([ - { path: '/', component: Home }, - { path: '/user/:id', component: User }, -]); - -// Navigation -$.router.go('/user/42'); -$.router.go('about'); // Same as '/about' - -// In templates -html` - -`; -``` - -## 📦 Storage - `$.storage(key, initialValue, [storage])` - -Persistent signal that syncs with localStorage or sessionStorage. - -```javascript -// localStorage (default) -const theme = $.storage('theme', 'light'); -const user = $.storage('user', null); -const settings = $.storage('settings', { notifications: true }); - -// sessionStorage -const tempData = $.storage('temp', {}, sessionStorage); - -// Usage like a normal signal -theme('dark'); // Auto-saves to localStorage -console.log(theme()); // 'dark' (even after page refresh) -``` - -## 🌐 Fetch - `$.fetch(url, data, [loading])` - -Simple fetch wrapper with automatic JSON handling. - -```javascript -const loading = $(false); - -async function loadUser(id) { - const user = await $.fetch(`/api/users/${id}`, null, loading); - if (user) userData(user); -} - -// In template -html` -
- ${() => loading() ? html`` : html` -

${userData()?.name}

- `} -
-`; -``` - -## 🎨 Template Literals - `` html`...` `` - -Creates reactive DOM fragments with directives. - -### Directives Reference - -| Directive | Example | Description | -|-----------|---------|-------------| -| **Event** | `@click=${handler}` | Add event listener | -| **Two-way binding** | `:value=${signal}` | Bind signal to input value | -| **Boolean attribute** | `?disabled=${signal}` | Toggle boolean attribute | -| **Property** | `.scrollTop=${value}` | Set DOM property directly | -| **Class toggle** | `class:active=${isActive}` | Toggle class conditionally | - -### Examples - -```javascript -const text = $(''); -const isDisabled = $(false); -const activeTab = $('home'); - -html` - - - - - -

You typed: ${text}

- - - - - -
- Home content -
- - -
-`; -``` - -## 🎯 Complete Component Example - -```javascript -import { $, html } from 'sigpro'; - -// Create a component -$.component('user-profile', (props, { slot, emit }) => { - // Reactive state - const user = $(null); - const loading = $(false); - - // Load user data when userId changes - $.effect(() => { - const id = props.userId(); - if (id) { - loading(true); - $.fetch(`/api/users/${id}`, null, loading) - .then(data => user(data)); - } - }); - - // Computed value - const fullName = $(() => - user() ? `${user().firstName} ${user().lastName}` : '' - ); - - // Template - return html` - - `; -}, ['user-id']); // Observe userId attribute -``` - - diff --git a/packages/docs/api/routing.md b/packages/docs/api/routing.md deleted file mode 100644 index 6be3094..0000000 --- a/packages/docs/api/routing.md +++ /dev/null @@ -1,784 +0,0 @@ -# Routing API 🌐 - -SigPro includes a simple yet powerful hash-based router designed for Single Page Applications (SPAs). It works everywhere with zero server configuration and integrates seamlessly with `$.page` for automatic cleanup. - -## Why Hash-Based Routing? - -Hash routing (`#/about`) works **everywhere** - no server configuration needed. Perfect for: -- Static sites and SPAs -- GitHub Pages, Netlify, any static hosting -- Local development without a server -- Projects that need to work immediately - -## `$.router(routes)` - -Creates a hash-based router that renders the matching component and handles navigation. - -```javascript -import { $, html } from 'sigpro'; -import HomePage from './pages/Home.js'; -import AboutPage from './pages/About.js'; -import UserPage from './pages/User.js'; - -const routes = [ - { path: '/', component: HomePage }, - { path: '/about', component: AboutPage }, - { path: '/user/:id', component: UserPage }, -]; - -// Mount the router -document.body.appendChild($.router(routes)); -``` - -## 📋 API Reference - -### `$.router(routes)` - -| Parameter | Type | Description | -|-----------|------|-------------| -| `routes` | `Array` | Array of route configurations | - -**Returns:** `HTMLDivElement` - Container that renders the current page - -### `$.router.go(path)` - -| Parameter | Type | Description | -|-----------|------|-------------| -| `path` | `string` | Route path to navigate to (automatically adds leading slash) | - -### Route Object - -| Property | Type | Description | -|----------|------|-------------| -| `path` | `string` or `RegExp` | Route pattern to match | -| `component` | `Function` | Function that returns page content (receives `params`) | - -## 🎯 Route Patterns - -### String Paths (Simple Routes) - -```javascript -const routes = [ - // Static routes - { path: '/', component: HomePage }, - { path: '/about', component: AboutPage }, - { path: '/contact', component: ContactPage }, - - // Routes with parameters - { path: '/user/:id', component: UserPage }, - { path: '/user/:id/posts', component: UserPostsPage }, - { path: '/user/:id/posts/:postId', component: PostPage }, - { path: '/search/:query/page/:num', component: SearchPage }, -]; -``` - -### RegExp Paths (Advanced Routing) - -```javascript -const routes = [ - // Match numeric IDs only - { path: /^\/users\/(?\d+)$/, component: UserPage }, - - // Match product slugs (letters, numbers, hyphens) - { path: /^\/products\/(?[a-z0-9-]+)$/, component: ProductPage }, - - // Match blog posts by year/month - { path: /^\/blog\/(?\d{4})\/(?\d{2})$/, component: BlogArchive }, - - // Match optional language prefix - { path: /^\/(?en|es|fr)?\/?about$/, component: AboutPage }, - - // Match UUID format - { path: /^\/items\/(?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/, - component: ItemPage }, -]; -``` - -## 📦 Basic Examples - -### Simple Router Setup - -```javascript -// main.js -import { $, html } from 'sigpro'; -import Home from './pages/Home.js'; -import About from './pages/About.js'; -import Contact from './pages/Contact.js'; - -const routes = [ - { path: '/', component: Home }, - { path: '/about', component: About }, - { path: '/contact', component: Contact }, -]; - -const router = $.router(routes); - -// Mount to DOM -document.body.appendChild(router); -``` - -### Page Components with Parameters - -```javascript -// pages/User.js -import { $, html } from 'sigpro'; - -export default (params) => $.page(() => { - // /user/42 → params = { id: '42' } - // /user/john/posts/123 → params = { id: 'john', postId: '123' } - const userId = params.id; - const userData = $(null); - - $.effect(() => { - fetch(`/api/users/${userId}`) - .then(res => res.json()) - .then(data => userData(data)); - }); - - return html` -
-

User Profile: ${userId}

- ${() => userData() ? html` -

Name: ${userData().name}

-

Email: ${userData().email}

- ` : html`

Loading...

`} -
- `; -}); -``` - -### Navigation - -```javascript -import { $, html } from 'sigpro'; - -// In templates -const NavBar = () => html` - -`; -``` - -## 🚀 Advanced Examples - -### Complete Application with Layout - -```javascript -// App.js -import { $, html } from 'sigpro'; -import HomePage from './pages/Home.js'; -import AboutPage from './pages/About.js'; -import UserPage from './pages/User.js'; -import SettingsPage from './pages/Settings.js'; -import NotFound from './pages/NotFound.js'; - -// Layout component with navigation -const Layout = (content) => html` -
-
-

My SigPro App

- -
- -
- ${content} -
- -
-

© 2024 SigPro App

-
-
-`; - -// Helper to check active route -const isActive = (path) => { - const current = window.location.hash.replace(/^#/, '') || '/'; - return current === path; -}; - -// Routes with layout -const routes = [ - { path: '/', component: (params) => Layout(HomePage(params)) }, - { path: '/about', component: (params) => Layout(AboutPage(params)) }, - { path: '/user/:id', component: (params) => Layout(UserPage(params)) }, - { path: '/settings', component: (params) => Layout(SettingsPage(params)) }, - { path: '/:path(.*)', component: (params) => Layout(NotFound(params)) }, // Catch-all -]; - -// Create and mount router -const router = $.router(routes); -document.body.appendChild(router); -``` - -### Nested Routes - -```javascript -// pages/Settings.js (parent route) -import { $, html } from 'sigpro'; -import SettingsGeneral from './settings/General.js'; -import SettingsSecurity from './settings/Security.js'; -import SettingsNotifications from './settings/Notifications.js'; - -export default (params) => $.page(() => { - const section = params.section || 'general'; - - const sections = { - general: SettingsGeneral, - security: SettingsSecurity, - notifications: SettingsNotifications - }; - - const CurrentSection = sections[section]; - - return html` - - `; -}); - -// pages/settings/General.js -export default (params) => $.page(() => { - return html` -
-

General Settings

-
...
-
- `; -}); - -// Main router with nested routes -const routes = [ - { path: '/', component: HomePage }, - { path: '/settings/:section?', component: SettingsPage }, // Optional section param -]; -``` - -### Protected Routes (Authentication) - -```javascript -// auth.js -import { $ } from 'sigpro'; - -const isAuthenticated = $(false); -const user = $(null); - -export const checkAuth = async () => { - const token = localStorage.getItem('token'); - if (token) { - try { - const response = await fetch('/api/verify'); - if (response.ok) { - const userData = await response.json(); - user(userData); - isAuthenticated(true); - return true; - } - } catch (e) { - // Handle error - } - } - isAuthenticated(false); - user(null); - return false; -}; - -export const requireAuth = (component) => (params) => { - if (isAuthenticated()) { - return component(params); - } - // Redirect to login - $.router.go('/login'); - return null; -}; - -export { isAuthenticated, user }; -``` - -```javascript -// pages/Dashboard.js (protected route) -import { $, html } from 'sigpro'; -import { requireAuth, user } from '../auth.js'; - -const Dashboard = (params) => $.page(() => { - return html` -
-

Welcome, ${() => user()?.name}!

-

This is your protected dashboard.

-
- `; -}); - -export default requireAuth(Dashboard); -``` - -```javascript -// main.js with protected routes -import { $, html } from 'sigpro'; -import { checkAuth } from './auth.js'; -import HomePage from './pages/Home.js'; -import LoginPage from './pages/Login.js'; -import DashboardPage from './pages/Dashboard.js'; -import AdminPage from './pages/Admin.js'; - -// Check auth on startup -checkAuth(); - -const routes = [ - { path: '/', component: HomePage }, - { path: '/login', component: LoginPage }, - { path: '/dashboard', component: DashboardPage }, // Protected - { path: '/admin', component: AdminPage }, // Protected -]; - -document.body.appendChild($.router(routes)); -``` - -### Route Transitions - -```javascript -// with-transitions.js -import { $, html } from 'sigpro'; - -export const createRouterWithTransitions = (routes) => { - const transitioning = $(false); - const currentView = $(null); - const nextView = $(null); - - const container = document.createElement('div'); - container.style.display = 'contents'; - - const renderWithTransition = async (newView) => { - if (currentView() === newView) return; - - transitioning(true); - nextView(newView); - - // Fade out - container.style.transition = 'opacity 0.2s'; - container.style.opacity = '0'; - - await new Promise(resolve => setTimeout(resolve, 200)); - - // Update content - container.replaceChildren(newView); - currentView(newView); - - // Fade in - container.style.opacity = '1'; - - await new Promise(resolve => setTimeout(resolve, 200)); - transitioning(false); - container.style.transition = ''; - }; - - const router = $.router(routes.map(route => ({ - ...route, - component: (params) => { - const view = route.component(params); - renderWithTransition(view); - return document.createComment('router-placeholder'); - } - }))); - - return router; -}; -``` - -### Breadcrumbs Navigation - -```javascript -// with-breadcrumbs.js -import { $, html } from 'sigpro'; - -export const createBreadcrumbs = (routes) => { - const breadcrumbs = $([]); - - const updateBreadcrumbs = (path) => { - const parts = path.split('/').filter(Boolean); - const crumbs = []; - let currentPath = ''; - - parts.forEach((part, index) => { - currentPath += `/${part}`; - - // Find matching route - const route = routes.find(r => { - if (r.path.includes(':')) { - const pattern = r.path.replace(/:[^/]+/g, part); - return pattern === currentPath; - } - return r.path === currentPath; - }); - - crumbs.push({ - path: currentPath, - label: route?.name || part.charAt(0).toUpperCase() + part.slice(1), - isLast: index === parts.length - 1 - }); - }); - - breadcrumbs(crumbs); - }; - - // Listen to route changes - window.addEventListener('hashchange', () => { - const path = window.location.hash.replace(/^#/, '') || '/'; - updateBreadcrumbs(path); - }); - - // Initial update - updateBreadcrumbs(window.location.hash.replace(/^#/, '') || '/'); - - return breadcrumbs; -}; -``` - -```javascript -// Usage in layout -import { createBreadcrumbs } from './with-breadcrumbs.js'; - -const breadcrumbs = createBreadcrumbs(routes); - -const Layout = (content) => html` -
- - -
- ${content} -
-
-`; -``` - -### Query Parameters - -```javascript -// with-query-params.js -export const getQueryParams = () => { - const hash = window.location.hash; - const queryStart = hash.indexOf('?'); - if (queryStart === -1) return {}; - - const queryString = hash.slice(queryStart + 1); - const params = new URLSearchParams(queryString); - const result = {}; - - for (const [key, value] of params) { - result[key] = value; - } - - return result; -}; - -export const updateQueryParams = (params) => { - const hash = window.location.hash.split('?')[0]; - const queryString = new URLSearchParams(params).toString(); - window.location.hash = queryString ? `${hash}?${queryString}` : hash; -}; -``` - -```javascript -// Search page with query params -import { $, html } from 'sigpro'; -import { getQueryParams, updateQueryParams } from './with-query-params.js'; - -export default (params) => $.page(() => { - // Get initial query from URL - const queryParams = getQueryParams(); - const searchQuery = $(queryParams.q || ''); - const page = $(parseInt(queryParams.page) || 1); - const results = $([]); - - // Update URL when search changes - $.effect(() => { - updateQueryParams({ - q: searchQuery() || undefined, - page: page() > 1 ? page() : undefined - }); - }); - - // Fetch results when search or page changes - $.effect(() => { - if (searchQuery()) { - fetch(`/api/search?q=${searchQuery()}&page=${page()}`) - .then(res => res.json()) - .then(data => results(data)); - } - }); - - return html` -
-

Search

- - { - searchQuery(e.target.value); - page(1); // Reset to first page on new search - }} - /> - -
- ${results().map(item => html` -
${item.title}
- `)} -
- - ${() => results().length ? html` - - ` : ''} -
- `; -}); -``` - -### Lazy Loading Routes - -```javascript -// lazy.js -export const lazy = (loader) => { - let component = null; - - return async (params) => { - if (!component) { - const module = await loader(); - component = module.default; - } - return component(params); - }; -}; -``` - -```javascript -// main.js with lazy loading -import { $, html } from 'sigpro'; -import { lazy } from './lazy.js'; -import Layout from './Layout.js'; - -const routes = [ - { path: '/', component: lazy(() => import('./pages/Home.js')) }, - { path: '/about', component: lazy(() => import('./pages/About.js')) }, - { path: '/dashboard', component: lazy(() => import('./pages/Dashboard.js')) }, - { - path: '/admin', - component: lazy(() => import('./pages/Admin.js')), - // Show loading state - loading: () => html`
Loading admin panel...
` - }, -]; - -// Wrap with layout -const routesWithLayout = routes.map(route => ({ - ...route, - component: (params) => Layout(route.component(params)) -})); - -document.body.appendChild($.router(routesWithLayout)); -``` - -### Route Guards / Middleware - -```javascript -// middleware.js -export const withGuard = (component, guard) => (params) => { - const result = guard(params); - if (result === true) { - return component(params); - } else if (typeof result === 'string') { - $.router.go(result); - return null; - } - return result; // Custom component (e.g., AccessDenied) -}; - -// Guards -export const roleGuard = (requiredRole) => (params) => { - const userRole = localStorage.getItem('userRole'); - if (userRole === requiredRole) return true; - if (!userRole) return '/login'; - return AccessDeniedPage(params); -}; - -export const authGuard = () => (params) => { - const token = localStorage.getItem('token'); - return token ? true : '/login'; -}; - -export const pendingChangesGuard = (hasPendingChanges) => (params) => { - if (hasPendingChanges()) { - return ConfirmLeavePage(params); - } - return true; -}; -``` - -```javascript -// Usage -import { withGuard, authGuard, roleGuard } from './middleware.js'; - -const routes = [ - { path: '/', component: HomePage }, - { path: '/profile', component: withGuard(ProfilePage, authGuard()) }, - { - path: '/admin', - component: withGuard(AdminPage, roleGuard('admin')) - }, -]; -``` - -## 📊 Route Matching Priority - -Routes are matched in the order they are defined. More specific routes should come first: - -```javascript -const routes = [ - // More specific first - { path: '/user/:id/edit', component: EditUserPage }, - { path: '/user/:id/posts', component: UserPostsPage }, - { path: '/user/:id', component: UserPage }, - - // Static routes - { path: '/about', component: AboutPage }, - { path: '/contact', component: ContactPage }, - - // Catch-all last - { path: '/:path(.*)', component: NotFoundPage }, -]; -``` - -## 🎯 Complete Example - -```javascript -// main.js - Complete application -import { $, html } from 'sigpro'; -import { lazy } from './utils/lazy.js'; -import { withGuard, authGuard } from './utils/middleware.js'; -import Layout from './components/Layout.js'; - -// Lazy load pages -const HomePage = lazy(() => import('./pages/Home.js')); -const AboutPage = lazy(() => import('./pages/About.js')); -const LoginPage = lazy(() => import('./pages/Login.js')); -const DashboardPage = lazy(() => import('./pages/Dashboard.js')); -const UserPage = lazy(() => import('./pages/User.js')); -const SettingsPage = lazy(() => import('./pages/Settings.js')); -const NotFoundPage = lazy(() => import('./pages/NotFound.js')); - -// Route configuration -const routes = [ - { path: '/', component: HomePage, name: 'Home' }, - { path: '/about', component: AboutPage, name: 'About' }, - { path: '/login', component: LoginPage, name: 'Login' }, - { - path: '/dashboard', - component: withGuard(DashboardPage, authGuard()), - name: 'Dashboard' - }, - { - path: '/user/:id', - component: UserPage, - name: 'User Profile' - }, - { - path: '/settings/:section?', - component: withGuard(SettingsPage, authGuard()), - name: 'Settings' - }, - { path: '/:path(.*)', component: NotFoundPage, name: 'Not Found' }, -]; - -// Wrap all routes with layout -const routesWithLayout = routes.map(route => ({ - ...route, - component: (params) => Layout(route.component(params)) -})); - -// Create and mount router -const router = $.router(routesWithLayout); -document.body.appendChild(router); - -// Navigation helper (available globally) -window.navigate = $.router.go; -``` - -## 📊 Summary - -| Feature | Description | -|---------|-------------| -| **Hash-based** | Works everywhere, no server config | -| **Route Parameters** | `:param` syntax for dynamic segments | -| **RegExp Support** | Advanced pattern matching | -| **Query Parameters** | Support for `?key=value` in URLs | -| **Programmatic Navigation** | `$.router.go(path)` | -| **Auto-cleanup** | Works with `$.page` for memory management | -| **Zero Dependencies** | Pure vanilla JavaScript | -| **Lazy Loading Ready** | Easy code splitting | - ---- - -> **Pro Tip:** Order matters in route definitions - put more specific routes (with parameters) before static ones, and always include a catch-all route (404) at the end. diff --git a/packages/docs/api/signals.md b/packages/docs/api/signals.md deleted file mode 100644 index 0eb22e6..0000000 --- a/packages/docs/api/signals.md +++ /dev/null @@ -1,899 +0,0 @@ -# Signals API 📡 - -Signals are the heart of SigPro's reactivity system. They are reactive values that automatically track dependencies and notify subscribers when they change. This enables fine-grained updates without virtual DOM diffing. - -## Core Concepts - -### What is a Signal? - -A signal is a function that holds a value and notifies dependents when that value changes. Signals can be: - -- **Basic signals** - Hold simple values (numbers, strings, objects) -- **Computed signals** - Derive values from other signals -- **Persistent signals** - Automatically sync with localStorage/sessionStorage - -### How Reactivity Works - -SigPro uses automatic dependency tracking: - -1. When you read a signal inside an effect, the effect becomes a subscriber -2. When the signal's value changes, all subscribers are notified -3. Updates are batched using microtasks for optimal performance -4. Only the exact nodes that depend on changed values are updated - -## `$(initialValue)` - -Creates a reactive signal. The behavior changes based on the type of `initialValue`: - -- If `initialValue` is a **function**, creates a computed signal -- Otherwise, creates a basic signal - -```javascript -import { $ } from 'sigpro'; - -// Basic signal -const count = $(0); - -// Computed signal -const firstName = $('John'); -const lastName = $('Doe'); -const fullName = $(() => `${firstName()} ${lastName()}`); -``` - -## 📋 API Reference - -### Basic Signals - -| Pattern | Example | Description | -|---------|---------|-------------| -| Create | `const count = $(0)` | Create signal with initial value | -| Get | `count()` | Read current value | -| Set | `count(5)` | Set new value directly | -| Update | `count(prev => prev + 1)` | Update based on previous value | - -### Computed Signals - -| Pattern | Example | Description | -|---------|---------|-------------| -| Create | `const total = $(() => price() * quantity())` | Derive value from other signals | -| Get | `total()` | Read computed value (auto-updates) | - -### Signal Methods - -| Method | Description | Example | -|--------|-------------|---------| -| `signal()` | Gets current value | `count()` | -| `signal(newValue)` | Sets new value | `count(5)` | -| `signal(prev => new)` | Updates using previous value | `count(c => c + 1)` | - -## 🎯 Basic Examples - -### Counter Signal - -```javascript -import { $ } from 'sigpro'; - -const count = $(0); - -console.log(count()); // 0 - -count(5); -console.log(count()); // 5 - -count(prev => prev + 1); -console.log(count()); // 6 -``` - -### Object Signal - -```javascript -import { $ } from 'sigpro'; - -const user = $({ - name: 'John', - age: 30, - email: 'john@example.com' -}); - -// Read -console.log(user().name); // 'John' - -// Update (immutable pattern) -user({ - ...user(), - age: 31 -}); - -// Partial update with function -user(prev => ({ - ...prev, - email: 'john.doe@example.com' -})); -``` - -### Array Signal - -```javascript -import { $ } from 'sigpro'; - -const todos = $(['Learn SigPro', 'Build an app']); - -// Add item -todos([...todos(), 'Deploy to production']); - -// Remove item -todos(todos().filter((_, i) => i !== 1)); - -// Update item -todos(todos().map((todo, i) => - i === 0 ? 'Master SigPro' : todo -)); -``` - -## 🔄 Computed Signals - -Computed signals automatically update when their dependencies change: - -```javascript -import { $ } from 'sigpro'; - -const price = $(10); -const quantity = $(2); -const tax = $(0.21); - -// Computed signals -const subtotal = $(() => price() * quantity()); -const taxAmount = $(() => subtotal() * tax()); -const total = $(() => subtotal() + taxAmount()); - -console.log(total()); // 24.2 - -price(15); -console.log(total()); // 36.3 (automatically updated) - -quantity(3); -console.log(total()); // 54.45 (automatically updated) -``` - -### Computed with Multiple Dependencies - -```javascript -import { $ } from 'sigpro'; - -const firstName = $('John'); -const lastName = $('Doe'); -const prefix = $('Mr.'); - -const fullName = $(() => { - // Computed signals can contain logic - const name = `${firstName()} ${lastName()}`; - return prefix() ? `${prefix()} ${name}` : name; -}); - -console.log(fullName()); // 'Mr. John Doe' - -prefix(''); -console.log(fullName()); // 'John Doe' -``` - -### Computed with Conditional Logic - -```javascript -import { $ } from 'sigpro'; - -const user = $({ role: 'admin', permissions: [] }); -const isAdmin = $(() => user().role === 'admin'); -const hasPermission = $(() => - isAdmin() || user().permissions.includes('edit') -); - -console.log(hasPermission()); // true - -user({ role: 'user', permissions: ['view'] }); -console.log(hasPermission()); // false (can't edit) - -user({ role: 'user', permissions: ['view', 'edit'] }); -console.log(hasPermission()); // true (now has permission) -``` - -## 🧮 Advanced Signal Patterns - -### Derived State Pattern - -```javascript -import { $ } from 'sigpro'; - -// Shopping cart example -const cart = $([ - { id: 1, name: 'Product 1', price: 10, quantity: 2 }, - { id: 2, name: 'Product 2', price: 15, quantity: 1 }, -]); - -// Derived values -const itemCount = $(() => - cart().reduce((sum, item) => sum + item.quantity, 0) -); - -const subtotal = $(() => - cart().reduce((sum, item) => sum + (item.price * item.quantity), 0) -); - -const tax = $(() => subtotal() * 0.21); -const total = $(() => subtotal() + tax()); - -// Update cart -cart([ - ...cart(), - { id: 3, name: 'Product 3', price: 20, quantity: 1 } -]); - -// All derived values auto-update -console.log(itemCount()); // 4 -console.log(total()); // (10*2 + 15*1 + 20*1) * 1.21 = 78.65 -``` - -### Validation Pattern - -```javascript -import { $ } from 'sigpro'; - -const email = $(''); -const password = $(''); -const confirmPassword = $(''); - -// Validation signals -const isEmailValid = $(() => { - const value = email(); - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); -}); - -const isPasswordValid = $(() => { - const value = password(); - return value.length >= 8; -}); - -const doPasswordsMatch = $(() => - password() === confirmPassword() -); - -const isFormValid = $(() => - isEmailValid() && isPasswordValid() && doPasswordsMatch() -); - -// Update form -email('user@example.com'); -password('secure123'); -confirmPassword('secure123'); - -console.log(isFormValid()); // true - -// Validation messages -const emailError = $(() => - email() && !isEmailValid() ? 'Invalid email format' : '' -); -``` - -### Filtering and Search Pattern - -```javascript -import { $ } from 'sigpro'; - -const items = $([ - { id: 1, name: 'Apple', category: 'fruit' }, - { id: 2, name: 'Banana', category: 'fruit' }, - { id: 3, name: 'Carrot', category: 'vegetable' }, - { id: 4, name: 'Date', category: 'fruit' }, -]); - -const searchTerm = $(''); -const categoryFilter = $('all'); - -// Filtered items (computed) -const filteredItems = $(() => { - let result = items(); - - // Apply search filter - if (searchTerm()) { - const term = searchTerm().toLowerCase(); - result = result.filter(item => - item.name.toLowerCase().includes(term) - ); - } - - // Apply category filter - if (categoryFilter() !== 'all') { - result = result.filter(item => - item.category === categoryFilter() - ); - } - - return result; -}); - -// Stats -const fruitCount = $(() => - items().filter(item => item.category === 'fruit').length -); - -const vegCount = $(() => - items().filter(item => item.category === 'vegetable').length -); - -// Update filters -searchTerm('a'); -console.log(filteredItems().map(i => i.name)); // ['Apple', 'Banana', 'Carrot', 'Date'] - -categoryFilter('fruit'); -console.log(filteredItems().map(i => i.name)); // ['Apple', 'Banana', 'Date'] -``` - -### Pagination Pattern - -```javascript -import { $ } from 'sigpro'; - -const allItems = $([...Array(100).keys()].map(i => `Item ${i + 1}`)); -const currentPage = $(1); -const itemsPerPage = $(10); - -// Paginated items (computed) -const paginatedItems = $(() => { - const start = (currentPage() - 1) * itemsPerPage(); - const end = start + itemsPerPage(); - return allItems().slice(start, end); -}); - -// Pagination metadata -const totalPages = $(() => - Math.ceil(allItems().length / itemsPerPage()) -); - -const hasNextPage = $(() => - currentPage() < totalPages() -); - -const hasPrevPage = $(() => - currentPage() > 1 -); - -const pageRange = $(() => { - const current = currentPage(); - const total = totalPages(); - const delta = 2; - - let range = []; - for (let i = Math.max(2, current - delta); - i <= Math.min(total - 1, current + delta); - i++) { - range.push(i); - } - - if (current - delta > 2) range = ['...', ...range]; - if (current + delta < total - 1) range = [...range, '...']; - - return [1, ...range, total]; -}); - -// Navigation -const nextPage = () => { - if (hasNextPage()) currentPage(c => c + 1); -}; - -const prevPage = () => { - if (hasPrevPage()) currentPage(c => c - 1); -}; - -const goToPage = (page) => { - if (page >= 1 && page <= totalPages()) { - currentPage(page); - } -}; -``` - -## 🔧 Advanced Signal Features - -### Signal Equality Comparison - -Signals use `Object.is` for change detection. Only notify subscribers when values are actually different: - -```javascript -import { $ } from 'sigpro'; - -const count = $(0); - -// These won't trigger updates: -count(0); // Same value -count(prev => prev); // Returns same value - -// These will trigger updates: -count(1); // Different value -count(prev => prev + 0); // Still 0? Actually returns 0? Wait... -// Be careful with functional updates! -``` - -### Batch Updates - -Multiple signal updates are batched into a single microtask: - -```javascript -import { $ } from 'sigpro'; - -const firstName = $('John'); -const lastName = $('Doe'); -const fullName = $(() => `${firstName()} ${lastName()}`); - -$.effect(() => { - console.log('Full name:', fullName()); -}); -// Logs: 'Full name: John Doe' - -// Multiple updates in same tick - only one effect run! -firstName('Jane'); -lastName('Smith'); -// Only logs once: 'Full name: Jane Smith' -``` - -### Infinite Loop Protection - -SigPro includes protection against infinite reactive loops: - -```javascript -import { $ } from 'sigpro'; - -const a = $(1); -const b = $(2); - -// This would create a loop, but SigPro prevents it -$.effect(() => { - a(b()); // Reading b - b(a()); // Reading a - loop detected! -}); -// Throws: "SigPro: Infinite reactive loop detected." -``` - -## 📊 Performance Characteristics - -| Operation | Complexity | Notes | -|-----------|------------|-------| -| Signal read | O(1) | Direct value access | -| Signal write | O(n) | n = number of subscribers | -| Computed read | O(1) or O(m) | m = computation complexity | -| Effect run | O(s) | s = number of signal reads | - -## 🎯 Best Practices - -### 1. Keep Signals Focused - -```javascript -// ❌ Avoid large monolithic signals -const state = $({ - user: null, - posts: [], - theme: 'light', - notifications: [] -}); - -// ✅ Split into focused signals -const user = $(null); -const posts = $([]); -const theme = $('light'); -const notifications = $([]); -``` - -### 2. Use Computed for Derived State - -```javascript -// ❌ Don't compute in templates/effects -$.effect(() => { - const total = items().reduce((sum, i) => sum + i.price, 0); - updateUI(total); -}); - -// ✅ Compute with signals -const total = $(() => items().reduce((sum, i) => sum + i.price, 0)); -$.effect(() => updateUI(total())); -``` - -### 3. Immutable Updates - -```javascript -// ❌ Don't mutate objects/arrays -const user = $({ name: 'John' }); -user().name = 'Jane'; // Won't trigger updates! - -// ✅ Create new objects/arrays -user({ ...user(), name: 'Jane' }); - -// ❌ Don't mutate arrays -const todos = $(['a', 'b']); -todos().push('c'); // Won't trigger updates! - -// ✅ Create new arrays -todos([...todos(), 'c']); -``` - -### 4. Functional Updates for Dependencies - -```javascript -// ❌ Avoid if new value depends on current -count(count() + 1); - -// ✅ Use functional update -count(prev => prev + 1); -``` - -### 5. Clean Up Effects - -```javascript -import { $ } from 'sigpro'; - -const userId = $(1); - -// Effects auto-clean in pages, but you can stop manually -const stop = $.effect(() => { - fetchUser(userId()); -}); - -// Later, if needed -stop(); -``` - -## 🚀 Real-World Examples - -### Form State Management - -```javascript -import { $ } from 'sigpro'; - -// Form state -const formData = $({ - username: '', - email: '', - age: '', - newsletter: false -}); - -// Touched fields (for validation UI) -const touched = $({ - username: false, - email: false, - age: false -}); - -// Validation rules -const validations = { - username: (value) => - value.length >= 3 ? null : 'Username must be at least 3 characters', - email: (value) => - /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : 'Invalid email', - age: (value) => - !value || (value >= 18 && value <= 120) ? null : 'Age must be 18-120' -}; - -// Validation signals -const errors = $(() => { - const data = formData(); - const result = {}; - - Object.keys(validations).forEach(field => { - const error = validations[field](data[field]); - if (error) result[field] = error; - }); - - return result; -}); - -const isValid = $(() => Object.keys(errors()).length === 0); - -// Field helpers -const fieldProps = (field) => ({ - value: formData()[field], - error: touched()[field] ? errors()[field] : null, - onChange: (e) => { - const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; - formData({ - ...formData(), - [field]: value - }); - }, - onBlur: () => { - touched({ - ...touched(), - [field]: true - }); - } -}); - -// Form submission -const submitAttempts = $(0); -const isSubmitting = $(false); - -const handleSubmit = async () => { - submitAttempts(s => s + 1); - - if (!isValid()) { - // Mark all fields as touched to show errors - touched(Object.keys(formData()).reduce((acc, field) => ({ - ...acc, - [field]: true - }), {})); - return; - } - - isSubmitting(true); - try { - await saveForm(formData()); - // Reset form on success - formData({ username: '', email: '', age: '', newsletter: false }); - touched({ username: false, email: false, age: false }); - } finally { - isSubmitting(false); - } -}; -``` - -### Todo App with Filters - -```javascript -import { $ } from 'sigpro'; - -// State -const todos = $([ - { id: 1, text: 'Learn SigPro', completed: true }, - { id: 2, text: 'Build an app', completed: false }, - { id: 3, text: 'Write docs', completed: false } -]); - -const filter = $('all'); // 'all', 'active', 'completed' -const newTodoText = $(''); - -// Computed values -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 activeCount = $(() => - todos().filter(t => !t.completed).length -); - -const completedCount = $(() => - todos().filter(t => t.completed).length -); - -const hasCompleted = $(() => completedCount() > 0); - -// Actions -const addTodo = () => { - const text = newTodoText().trim(); - if (text) { - todos([ - ...todos(), - { - id: Date.now(), - text, - completed: false - } - ]); - newTodoText(''); - } -}; - -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)); -}; - -const clearCompleted = () => { - todos(todos().filter(todo => !todo.completed)); -}; - -const toggleAll = () => { - const allCompleted = activeCount() === 0; - todos(todos().map(todo => ({ - ...todo, - completed: !allCompleted - }))); -}; -``` - -### Shopping Cart - -```javascript -import { $ } from 'sigpro'; - -// Products catalog -const products = $([ - { id: 1, name: 'Laptop', price: 999, stock: 5 }, - { id: 2, name: 'Mouse', price: 29, stock: 20 }, - { id: 3, name: 'Keyboard', price: 79, stock: 10 }, - { id: 4, name: 'Monitor', price: 299, stock: 3 } -]); - -// Cart state -const cart = $({}); -const selectedProduct = $(null); -const quantity = $(1); - -// Computed cart values -const cartItems = $(() => { - const items = []; - Object.entries(cart()).forEach(([productId, qty]) => { - const product = products().find(p => p.id === parseInt(productId)); - if (product) { - items.push({ - ...product, - quantity: qty, - subtotal: product.price * qty - }); - } - }); - return items; -}); - -const itemCount = $(() => - cartItems().reduce((sum, item) => sum + item.quantity, 0) -); - -const subtotal = $(() => - cartItems().reduce((sum, item) => sum + item.subtotal, 0) -); - -const tax = $(() => subtotal() * 0.10); -const shipping = $(() => subtotal() > 100 ? 0 : 10); -const total = $(() => subtotal() + tax() + shipping()); - -const isCartEmpty = $(() => itemCount() === 0); - -// Cart actions -const addToCart = (product, qty = 1) => { - const currentQty = cart()[product.id] || 0; - const newQty = currentQty + qty; - - if (newQty <= product.stock) { - cart({ - ...cart(), - [product.id]: newQty - }); - return true; - } - return false; -}; - -const updateQuantity = (productId, newQty) => { - const product = products().find(p => p.id === productId); - if (newQty <= product.stock) { - if (newQty <= 0) { - removeFromCart(productId); - } else { - cart({ - ...cart(), - [productId]: newQty - }); - } - } -}; - -const removeFromCart = (productId) => { - const newCart = { ...cart() }; - delete newCart[productId]; - cart(newCart); -}; - -const clearCart = () => cart({}); - -// Stock management -const productStock = (productId) => { - const product = products().find(p => p.id === productId); - if (!product) return 0; - const inCart = cart()[productId] || 0; - return product.stock - inCart; -}; - -const isInStock = (productId, qty = 1) => { - return productStock(productId) >= qty; -}; -``` - -## 📈 Debugging Signals - -### Logging Signal Changes - -```javascript -import { $ } from 'sigpro'; - -// Wrap a signal to log changes -const withLogging = (signal, name) => { - return (...args) => { - if (args.length) { - const oldValue = signal(); - const result = signal(...args); - console.log(`${name}:`, oldValue, '->', signal()); - return result; - } - return signal(); - }; -}; - -// Usage -const count = withLogging($(0), 'count'); -count(5); // Logs: "count: 0 -> 5" -``` - -### Signal Inspector - -```javascript -import { $ } from 'sigpro'; - -// Create an inspectable signal -const createInspector = () => { - const signals = new Map(); - - const createSignal = (initialValue, name) => { - const signal = $(initialValue); - signals.set(signal, { name, subscribers: new Set() }); - - // Wrap to track subscribers - const wrapped = (...args) => { - if (!args.length && activeEffect) { - const info = signals.get(wrapped); - info.subscribers.add(activeEffect); - } - return signal(...args); - }; - - return wrapped; - }; - - const getInfo = () => { - const info = {}; - signals.forEach((data, signal) => { - info[data.name] = { - subscribers: data.subscribers.size, - value: signal() - }; - }); - return info; - }; - - return { createSignal, getInfo }; -}; - -// Usage -const inspector = createInspector(); -const count = inspector.createSignal(0, 'count'); -const doubled = inspector.createSignal(() => count() * 2, 'doubled'); - -console.log(inspector.getInfo()); -// { count: { subscribers: 0, value: 0 }, doubled: { subscribers: 0, value: 0 } } -``` - -## 📊 Summary - -| Feature | Description | -|---------|-------------| -| **Basic Signals** | Hold values and notify on change | -| **Computed Signals** | Auto-updating derived values | -| **Automatic Tracking** | Dependencies tracked automatically | -| **Batch Updates** | Multiple updates batched in microtask | -| **Infinite Loop Protection** | Prevents reactive cycles | -| **Zero Dependencies** | Pure vanilla JavaScript | - ---- - -> **Pro Tip:** Signals are the foundation of reactivity in SigPro. Master them, and you've mastered 80% of the library! diff --git a/packages/docs/api/storage.md b/packages/docs/api/storage.md deleted file mode 100644 index d07f81e..0000000 --- a/packages/docs/api/storage.md +++ /dev/null @@ -1,952 +0,0 @@ -# Storage API 💾 - -SigPro provides persistent signals that automatically synchronize with browser storage APIs. This allows you to create reactive state that survives page reloads and browser sessions with zero additional code. - -## Core Concepts - -### What is Persistent Storage? - -Persistent signals are special signals that: -- **Initialize from storage** (localStorage/sessionStorage) if a saved value exists -- **Auto-save** whenever the signal value changes -- **Handle JSON serialization** automatically -- **Clean up** when set to `null` or `undefined` - -### Storage Types - -| Storage | Persistence | Use Case | -|---------|-------------|----------| -| `localStorage` | Forever (until cleared) | User preferences, themes, saved data | -| `sessionStorage` | Until tab/window closes | Form drafts, temporary state | - -## `$.storage(key, initialValue, [storage])` - -Creates a persistent signal that syncs with browser storage. - -```javascript -import { $ } from 'sigpro'; - -// localStorage (default) -const theme = $.storage('theme', 'light'); -const user = $.storage('user', null); -const settings = $.storage('settings', { notifications: true }); - -// sessionStorage -const draft = $.storage('draft', '', sessionStorage); -const formData = $.storage('form', {}, sessionStorage); -``` - -## 📋 API Reference - -### Parameters - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `key` | `string` | required | Storage key name | -| `initialValue` | `any` | required | Default value if none stored | -| `storage` | `Storage` | `localStorage` | Storage type (`localStorage` or `sessionStorage`) | - -### Returns - -| Return | Description | -|--------|-------------| -| `Function` | Signal function (getter/setter) with persistence | - -## 🎯 Basic Examples - -### Theme Preference - -```javascript -import { $, html } from 'sigpro'; - -// Persistent theme signal -const theme = $.storage('theme', 'light'); - -// Apply theme to document -$.effect(() => { - document.body.className = `theme-${theme()}`; -}); - -// Toggle theme -const toggleTheme = () => { - theme(t => t === 'light' ? 'dark' : 'light'); -}; - -// Template -html` -
-

Current theme: ${theme}

- -
-`; -``` - -### User Preferences - -```javascript -import { $ } from 'sigpro'; - -// Complex preferences object -const preferences = $.storage('preferences', { - language: 'en', - fontSize: 'medium', - notifications: true, - compactView: false, - sidebarOpen: true -}); - -// Update single preference -const setPreference = (key, value) => { - preferences({ - ...preferences(), - [key]: value - }); -}; - -// Usage -setPreference('language', 'es'); -setPreference('fontSize', 'large'); -console.log(preferences().language); // 'es' -``` - -### Form Draft - -```javascript -import { $, html } from 'sigpro'; - -// Session-based draft (clears when tab closes) -const draft = $.storage('contact-form', { - name: '', - email: '', - message: '' -}, sessionStorage); - -// Auto-save on input -const handleInput = (field, value) => { - draft({ - ...draft(), - [field]: value - }); -}; - -// Clear draft after submit -const handleSubmit = async () => { - await submitForm(draft()); - draft(null); // Clears from storage -}; - -// Template -html` -
- draft().name} - @input=${(e) => handleInput('name', e.target.value)} - placeholder="Name" - /> - draft().email} - @input=${(e) => handleInput('email', e.target.value)} - placeholder="Email" - /> - - -
-`; -``` - -## 🚀 Advanced Examples - -### Authentication State - -```javascript -import { $, html } from 'sigpro'; - -// Persistent auth state -const auth = $.storage('auth', { - token: null, - user: null, - expiresAt: null -}); - -// Computed helpers -const isAuthenticated = $(() => { - const { token, expiresAt } = auth(); - if (!token || !expiresAt) return false; - return new Date(expiresAt) > new Date(); -}); - -const user = $(() => auth().user); - -// Login function -const login = async (email, password) => { - const response = await fetch('/api/login', { - method: 'POST', - body: JSON.stringify({ email, password }) - }); - - if (response.ok) { - const { token, user, expiresIn } = await response.json(); - auth({ - token, - user, - expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString() - }); - return true; - } - return false; -}; - -// Logout -const logout = () => { - auth(null); // Clear from storage -}; - -// Auto-refresh token -$.effect(() => { - if (!isAuthenticated()) return; - - const { expiresAt } = auth(); - const expiresIn = new Date(expiresAt) - new Date(); - const refreshTime = expiresIn - 60000; // 1 minute before expiry - - if (refreshTime > 0) { - const timer = setTimeout(refreshToken, refreshTime); - return () => clearTimeout(timer); - } -}); - -// Navigation guard -$.effect(() => { - if (!isAuthenticated() && window.location.pathname !== '/login') { - $.router.go('/login'); - } -}); -``` - -### Multi-tab Synchronization - -```javascript -import { $ } from 'sigpro'; - -// Storage key for cross-tab communication -const STORAGE_KEY = 'app-state'; - -// Create persistent signal -const appState = $.storage(STORAGE_KEY, { - count: 0, - lastUpdated: null -}); - -// Listen for storage events (changes from other tabs) -window.addEventListener('storage', (event) => { - if (event.key === STORAGE_KEY && event.newValue) { - try { - // Update signal without triggering save loop - const newValue = JSON.parse(event.newValue); - appState(newValue); - } catch (e) { - console.error('Failed to parse storage event:', e); - } - } -}); - -// Update state (syncs across all tabs) -const increment = () => { - appState({ - count: appState().count + 1, - lastUpdated: new Date().toISOString() - }); -}; - -// Tab counter -const tabCount = $(1); - -// Track number of tabs open -window.addEventListener('storage', (event) => { - if (event.key === 'tab-heartbeat') { - tabCount(parseInt(event.newValue) || 1); - } -}); - -// Send heartbeat -setInterval(() => { - localStorage.setItem('tab-heartbeat', tabCount()); -}, 1000); -``` - -### Settings Manager - -```javascript -import { $, html } from 'sigpro'; - -// Settings schema -const settingsSchema = { - theme: { - type: 'select', - options: ['light', 'dark', 'system'], - default: 'system' - }, - fontSize: { - type: 'range', - min: 12, - max: 24, - default: 16 - }, - notifications: { - type: 'checkbox', - default: true - }, - language: { - type: 'select', - options: ['en', 'es', 'fr', 'de'], - default: 'en' - } -}; - -// Persistent settings -const settings = $.storage('app-settings', - Object.entries(settingsSchema).reduce((acc, [key, config]) => ({ - ...acc, - [key]: config.default - }), {}) -); - -// Settings component -const SettingsPanel = () => { - return html` -
-

Settings

- - ${Object.entries(settingsSchema).map(([key, config]) => { - switch(config.type) { - case 'select': - return html` -
- - -
- `; - - case 'range': - return html` -
- - settings()[key]} - @input=${(e) => updateSetting(key, parseInt(e.target.value))} - /> -
- `; - - case 'checkbox': - return html` -
- -
- `; - } - })} - - -
- `; -}; - -// Helper functions -const updateSetting = (key, value) => { - settings({ - ...settings(), - [key]: value - }); -}; - -const resetDefaults = () => { - const defaults = Object.entries(settingsSchema).reduce((acc, [key, config]) => ({ - ...acc, - [key]: config.default - }), {}); - settings(defaults); -}; - -// Apply settings globally -$.effect(() => { - const { theme, fontSize } = settings(); - - // Apply theme - document.documentElement.setAttribute('data-theme', theme); - - // Apply font size - document.documentElement.style.fontSize = `${fontSize}px`; -}); -``` - -### Shopping Cart Persistence - -```javascript -import { $, html } from 'sigpro'; - -// Persistent shopping cart -const cart = $.storage('shopping-cart', { - items: [], - lastUpdated: null -}); - -// Computed values -const cartItems = $(() => cart().items); -const itemCount = $(() => cartItems().reduce((sum, item) => sum + item.quantity, 0)); -const subtotal = $(() => cartItems().reduce((sum, item) => sum + (item.price * item.quantity), 0)); -const tax = $(() => subtotal() * 0.1); -const total = $(() => subtotal() + tax()); - -// Cart actions -const addToCart = (product, quantity = 1) => { - const existing = cartItems().findIndex(item => item.id === product.id); - - if (existing >= 0) { - // Update quantity - const newItems = [...cartItems()]; - newItems[existing] = { - ...newItems[existing], - quantity: newItems[existing].quantity + quantity - }; - - cart({ - items: newItems, - lastUpdated: new Date().toISOString() - }); - } else { - // Add new item - cart({ - items: [...cartItems(), { ...product, quantity }], - lastUpdated: new Date().toISOString() - }); - } -}; - -const removeFromCart = (productId) => { - cart({ - items: cartItems().filter(item => item.id !== productId), - lastUpdated: new Date().toISOString() - }); -}; - -const updateQuantity = (productId, quantity) => { - if (quantity <= 0) { - removeFromCart(productId); - } else { - const newItems = cartItems().map(item => - item.id === productId ? { ...item, quantity } : item - ); - - cart({ - items: newItems, - lastUpdated: new Date().toISOString() - }); - } -}; - -const clearCart = () => { - cart({ - items: [], - lastUpdated: new Date().toISOString() - }); -}; - -// Cart expiration (7 days) -const CART_EXPIRY_DAYS = 7; - -$.effect(() => { - const lastUpdated = cart().lastUpdated; - if (lastUpdated) { - const expiryDate = new Date(lastUpdated); - expiryDate.setDate(expiryDate.getDate() + CART_EXPIRY_DAYS); - - if (new Date() > expiryDate) { - clearCart(); - } - } -}); - -// Cart display component -const CartDisplay = () => html` -
-

Shopping Cart (${itemCount} items)

- - ${cartItems().map(item => html` -
- ${item.name} - $${item.price} x ${item.quantity} - $${item.price * item.quantity} - - updateQuantity(item.id, parseInt(e.target.value))} - /> -
- `)} - -
-

Subtotal: $${subtotal}

-

Tax (10%): $${tax}

-

Total: $${total}

-
- - ${() => cartItems().length > 0 ? html` - - - ` : html` -

Your cart is empty

- `} -
-`; -``` - -### Recent Searches History - -```javascript -import { $, html } from 'sigpro'; - -// Persistent search history (max 10 items) -const searchHistory = $.storage('search-history', []); - -// Add search to history -const addSearch = (query) => { - if (!query.trim()) return; - - const current = searchHistory(); - const newHistory = [ - { query, timestamp: new Date().toISOString() }, - ...current.filter(item => item.query !== query) - ].slice(0, 10); // Keep only last 10 - - searchHistory(newHistory); -}; - -// Clear history -const clearHistory = () => { - searchHistory([]); -}; - -// Remove specific item -const removeFromHistory = (query) => { - searchHistory(searchHistory().filter(item => item.query !== query)); -}; - -// Search component -const SearchWithHistory = () => { - const searchInput = $(''); - - const handleSearch = () => { - const query = searchInput(); - if (query) { - addSearch(query); - performSearch(query); - searchInput(''); - } - }; - - return html` -
- - - ${() => searchHistory().length > 0 ? html` -
-

Recent Searches

- ${searchHistory().map(item => html` -
- - ${new Date(item.timestamp).toLocaleString()} - -
- `)} - -
- ` : ''} -
- `; -}; -``` - -### Multiple Profiles / Accounts - -```javascript -import { $, html } from 'sigpro'; - -// Profile manager -const profiles = $.storage('user-profiles', { - current: 'default', - list: { - default: { - name: 'Default', - theme: 'light', - preferences: {} - } - } -}); - -// Switch profile -const switchProfile = (profileId) => { - profiles({ - ...profiles(), - current: profileId - }); -}; - -// Create profile -const createProfile = (name) => { - const id = `profile-${Date.now()}`; - profiles({ - current: id, - list: { - ...profiles().list, - [id]: { - name, - theme: 'light', - preferences: {}, - createdAt: new Date().toISOString() - } - } - }); - return id; -}; - -// Delete profile -const deleteProfile = (profileId) => { - if (profileId === 'default') return; // Can't delete default - - const newList = { ...profiles().list }; - delete newList[profileId]; - - profiles({ - current: 'default', - list: newList - }); -}; - -// Get current profile data -const currentProfile = $(() => { - const { current, list } = profiles(); - return list[current] || list.default; -}); - -// Profile-aware settings -const profileTheme = $(() => currentProfile().theme); -const profilePreferences = $(() => currentProfile().preferences); - -// Update profile data -const updateCurrentProfile = (updates) => { - const { current, list } = profiles(); - profiles({ - current, - list: { - ...list, - [current]: { - ...list[current], - ...updates - } - } - }); -}; - -// Profile selector component -const ProfileSelector = () => html` -
- - - -
-`; -``` - -## 🛡️ Error Handling - -### Storage Errors - -```javascript -import { $ } from 'sigpro'; - -// Safe storage wrapper -const safeStorage = (key, initialValue, storage = localStorage) => { - try { - return $.storage(key, initialValue, storage); - } catch (error) { - console.warn(`Storage failed for ${key}, using in-memory fallback:`, error); - return $(initialValue); - } -}; - -// Usage with fallback -const theme = safeStorage('theme', 'light'); -const user = safeStorage('user', null); -``` - -### Quota Exceeded Handling - -```javascript -import { $ } from 'sigpro'; - -const createManagedStorage = (key, initialValue, maxSize = 1024 * 100) => { // 100KB limit - const signal = $.storage(key, initialValue); - - // Monitor size - const size = $(0); - - $.effect(() => { - try { - const value = signal(); - const json = JSON.stringify(value); - const bytes = new Blob([json]).size; - - size(bytes); - - if (bytes > maxSize) { - console.warn(`Storage for ${key} exceeded ${maxSize} bytes`); - // Could implement cleanup strategy here - } - } catch (e) { - console.error('Size check failed:', e); - } - }); - - return { signal, size }; -}; - -// Usage -const { signal: largeData, size } = createManagedStorage('app-data', {}, 50000); -``` - -## 📊 Storage Limits - -| Storage Type | Typical Limit | Notes | -|--------------|---------------|-------| -| `localStorage` | 5-10MB | Varies by browser | -| `sessionStorage` | 5-10MB | Cleared when tab closes | -| `cookies` | 4KB | Not recommended for SigPro | - -## 🎯 Best Practices - -### 1. Validate Stored Data - -```javascript -import { $ } from 'sigpro'; - -// Schema validation -const createValidatedStorage = (key, schema, defaultValue, storage) => { - const signal = $.storage(key, defaultValue, storage); - - // Wrap to validate on read/write - const validated = (...args) => { - if (args.length) { - // Validate before writing - const value = args[0]; - if (typeof value === 'function') { - // Handle functional updates - return validated(validated()); - } - - // Basic validation - const isValid = Object.keys(schema).every(key => { - const validator = schema[key]; - return !validator || validator(value[key]); - }); - - if (!isValid) { - console.warn('Invalid data, skipping storage write'); - return signal(); - } - } - - return signal(...args); - }; - - return validated; -}; - -// Usage -const userSchema = { - name: v => v && v.length > 0, - age: v => v >= 18 && v <= 120, - email: v => /@/.test(v) -}; - -const user = createValidatedStorage('user', userSchema, { - name: '', - age: 25, - email: '' -}); -``` - -### 2. Handle Versioning - -```javascript -import { $ } from 'sigpro'; - -const VERSION = 2; - -const createVersionedStorage = (key, migrations, storage) => { - const raw = $.storage(key, { version: VERSION, data: {} }, storage); - - const migrate = (data) => { - let current = data; - const currentVersion = current.version || 1; - - for (let v = currentVersion; v < VERSION; v++) { - const migrator = migrations[v]; - if (migrator) { - current = migrator(current); - } - } - - return current; - }; - - // Migrate if needed - const stored = raw(); - if (stored.version !== VERSION) { - const migrated = migrate(stored); - raw(migrated); - } - - return raw; -}; - -// Usage -const migrations = { - 1: (old) => ({ - version: 2, - data: { - ...old.data, - preferences: old.preferences || {} - } - }) -}; - -const settings = createVersionedStorage('app-settings', migrations); -``` - -### 3. Encrypt Sensitive Data - -```javascript -import { $ } from 'sigpro'; - -// Simple encryption (use proper crypto in production) -const encrypt = (text) => { - return btoa(text); // Base64 - NOT secure, just example -}; - -const decrypt = (text) => { - try { - return atob(text); - } catch { - return null; - } -}; - -const createSecureStorage = (key, initialValue, storage) => { - const encryptedKey = `enc_${key}`; - const signal = $.storage(encryptedKey, null, storage); - - const secure = (...args) => { - if (args.length) { - // Encrypt before storing - const value = args[0]; - const encrypted = encrypt(JSON.stringify(value)); - return signal(encrypted); - } - - // Decrypt when reading - const encrypted = signal(); - if (!encrypted) return initialValue; - - try { - const decrypted = decrypt(encrypted); - return decrypted ? JSON.parse(decrypted) : initialValue; - } catch { - return initialValue; - } - }; - - return secure; -}; - -// Usage -const secureToken = createSecureStorage('auth-token', null); -secureToken('sensitive-data-123'); // Stored encrypted -``` - -## 📈 Performance Considerations - -| Operation | Cost | Notes | -|-----------|------|-------| -| Initial read | O(1) | Single storage read | -| Write | O(1) + JSON.stringify | Auto-save on change | -| Large objects | O(n) | Stringify/parse overhead | -| Multiple keys | O(k) | k = number of keys | - ---- - -> **Pro Tip:** Use `sessionStorage` for temporary data like form drafts, and `localStorage` for persistent user preferences. Always validate data when reading from storage to handle corrupted values gracefully. diff --git a/packages/docs/guide/getting-started.md b/packages/docs/guide/getting-started.md deleted file mode 100644 index b19c4e1..0000000 --- a/packages/docs/guide/getting-started.md +++ /dev/null @@ -1,308 +0,0 @@ -# Getting Started with SigPro 🚀 - -Welcome to SigPro! This guide will help you get up and running with the library in minutes. SigPro is a minimalist reactive library that embraces the web platform - no compilation, no virtual DOM, just pure JavaScript and intelligent reactivity. - -## 📦 Installation - -Choose your preferred installation method: - -```bash -# Using npm -npm install sigpro - -# Using bun -bun add sigpro - -# Or simply copy sigpro.js to your project -# (yes, it's that simple!) -``` - -## 🎯 Core Imports - -```javascript -import { $, html } from 'sigpro'; -``` - -That's it! Just two imports to unlock the entire reactive system: -- **`$`** - Creates reactive signals (the heart of reactivity) -- **`html`** - Template literal tag for reactive DOM rendering - -## 🧠 Understanding the Basics - -### Signals - The Reactive Heart - -Signals are reactive values that automatically track dependencies and update when changed: - -```javascript -// Create a signal with initial value -const count = $(0); - -// Read value (with auto dependency tracking) -console.log(count()); // 0 - -// Set new value -count(5); - -// Update using previous value -count(prev => prev + 1); // 6 - -// Create computed signals (auto-updating) -const firstName = $('John'); -const lastName = $('Doe'); -const fullName = $(() => `${firstName()} ${lastName()}`); -console.log(fullName()); // "John Doe" -firstName('Jane'); // fullName() now returns "Jane Doe" -``` - -### Effects - Automatic Reactions - -Effects automatically run and re-run when their signal dependencies change: - -```javascript -const count = $(0); - -$.effect(() => { - console.log(`Count is: ${count()}`); -}); -// Logs: "Count is: 0" - -count(1); -// Logs: "Count is: 1" - -// Effects can return cleanup functions -$.effect(() => { - const id = count(); - const timer = setInterval(() => { - console.log(`Polling with count: ${id}`); - }, 1000); - - // Cleanup runs before next effect execution - return () => clearInterval(timer); -}); -``` - -### Rendering with `html` - -The `html` tag creates reactive DOM fragments: - -```javascript -const count = $(0); -const isActive = $(true); - -const fragment = html` -
-

Count: ${count}

- - - - - - -
-`; - -document.body.appendChild(fragment); -``` - -## 🎨 Your First Reactive App - -Let's build a simple todo app to see SigPro in action: - -```javascript -import { $, html } from 'sigpro'; - -// Create a simple todo app -function TodoApp() { - // Reactive state - const todos = $(['Learn SigPro', 'Build something awesome']); - const newTodo = $(''); - - // Computed value - const todoCount = $(() => todos().length); - - // Add todo function - const addTodo = () => { - if (newTodo().trim()) { - todos([...todos(), newTodo()]); - newTodo(''); - } - }; - - // Remove todo function - const removeTodo = (index) => { - todos(todos().filter((_, i) => i !== index)); - }; - - // Return reactive template - return html` -
-

📝 Todo App

- - -
- - -
- - -

Total todos: ${todoCount}

- - -
    - ${() => todos().map((todo, index) => html` -
  • - ${todo} - -
  • - `)} -
-
- `; -} - -// Mount the app -document.body.appendChild(TodoApp()); -``` - -## 🎯 Key Concepts - -### 1. **Signal Patterns** - -| Pattern | Example | Use Case | -|---------|---------|----------| -| Basic signal | `const count = $(0)` | Simple values | -| Computed | `$( () => first() + last() )` | Derived values | -| Signal update | `count(5)` | Direct set | -| Functional update | `count(prev => prev + 1)` | Based on previous | - -### 2. **Effect Patterns** - -```javascript -// Basic effect -$.effect(() => console.log(count())); - -// Effect with cleanup -$.effect(() => { - const timer = setInterval(() => {}, 1000); - return () => clearInterval(timer); -}); - -// Stopping an effect -const stop = $.effect(() => {}); -stop(); // Effect won't run again -``` - -### 3. **HTML Directives** - -| Directive | Example | Description | -|-----------|---------|-------------| -| `@event` | `@click=${handler}` | Event listeners | -| `:property` | `:value=${signal}` | Two-way binding | -| `?attribute` | `?disabled=${signal}` | Boolean attributes | -| `.property` | `.scrollTop=${value}` | DOM properties | - -## 💡 Pro Tips for Beginners - -### 1. **Start Simple** -```javascript -// Begin with basic signals -const name = $('World'); -html`

Hello, ${name}!

`; -``` - -### 2. **Use Computed Signals for Derived State** -```javascript -// ❌ Don't compute in template -html`

Total: ${items().length * price()}

`; - -// ✅ Compute with signals -const total = $(() => items().length * price()); -html`

Total: ${total}

`; -``` - -### 3. **Leverage Effects for Side Effects** -```javascript -// Auto-save to localStorage -$.effect(() => { - localStorage.setItem('draft', JSON.stringify(draft())); -}); -``` - -## 🔧 VS Code Setup - -For the best development experience, install these VS Code extensions: - -- **lit-html** - Adds syntax highlighting for `html` tagged templates -- **Prettier** - Automatically formats your template literals - -```javascript -// With lit-html extension, you get full syntax highlighting! -html` -
-

Beautiful highlighted template

-
-` -``` - -## 📁 Project Structure - -Here's a recommended structure for larger apps: - -``` -my-sigpro-app/ -├── index.html -├── main.js -├── components/ -│ ├── Button.js -│ ├── TodoList.js -│ └── TodoItem.js -├── pages/ -│ ├── HomePage.js -│ └── AboutPage.js -└── utils/ - └── helpers.js -``` - -Example `main.js`: -```javascript -import { $, html } from 'sigpro'; -import HomePage from './pages/HomePage.js'; - -// Mount your app -document.body.appendChild(HomePage()); -``` - -## 🎓 Summary - -You've learned: -- ✅ How to install SigPro -- ✅ Core concepts: signals, effects, and reactive rendering -- ✅ Built a complete todo app -- ✅ Key patterns and best practices -- ✅ How to structure larger applications - -**Remember:** SigPro embraces the web platform. You're writing vanilla JavaScript with superpowers—no compilation, no lock-in, just clean, maintainable code that will work for years to come. - -> "Stop fighting the platform. Start building with it." - -Happy coding! 🎉 \ No newline at end of file diff --git a/packages/docs/guide/why.md b/packages/docs/guide/why.md deleted file mode 100644 index 73f0435..0000000 --- a/packages/docs/guide/why.md +++ /dev/null @@ -1,135 +0,0 @@ -# Why SigPro? ❓ - -After years of building applications with React, Vue, and Svelte—investing countless hours mastering their unique mental models, build tools, and update cycles—I kept circling back to the same realization: no matter how sophisticated the framework, it all eventually compiles down to HTML, CSS, and vanilla JavaScript. The web platform has evolved tremendously, yet many libraries continue to reinvent the wheel, creating parallel universes with their own rules, their own syntaxes, and their own steep learning curves. - -**SigPro is my answer to a simple question:** Why fight the platform when we can embrace it? - -## 🌐 The Web Platform Is Finally Ready - -Modern browsers now offer powerful primitives that make true reactivity possible without virtual DOM diffing, without compilers, and without lock-in: - -| Browser Primitive | What It Enables | -|-------------------|-----------------| -| **Custom Elements** | Create reusable components with native browser APIs | -| **Shadow DOM** | Encapsulate styles and markup without preprocessors | -| **CSS Custom Properties** | Dynamic theming without CSS-in-JS | -| **Microtask Queues** | Efficient update batching without complex scheduling | - -## 🎯 The SigPro Philosophy - -SigPro strips away the complexity, delivering a reactive programming model that feels familiar but stays remarkably close to vanilla JS: - -- **No JSX transformations** - Just template literals -- **No template compilers** - The browser parses your HTML -- **No proprietary syntax to learn** - Just functions and signals -- **No build step required** - Works directly in the browser - -```javascript -// Just vanilla JavaScript with signals -import { $, html } from 'sigpro'; - -const count = $(0); - -document.body.appendChild(html` -
-

Count: ${count}

- -
-`); -``` - -## 📊 Comparative - -| Metric | SigPro | Solid | Svelte | Vue | React | -|--------|--------|-------|--------|-----|-------| -| **Bundle Size** (gzip) | 🥇 **5.2KB** | 🥈 15KB | 🥉 16.6KB | 20.4KB | 43.9KB | -| **Time to Interactive** | 🥇 **0.8s** | 🥈 1.3s | 🥉 1.4s | 1.6s | 2.3s | -| **Initial Render** (ms) | 🥇 **124ms** | 🥈 198ms | 🥉 287ms | 298ms | 452ms | -| **Update Performance** (ms) | 🥇 **4ms** | 🥈 5ms | 🥈 5ms | 🥉 7ms | 18ms | -| **Dependencies** | 🥇 **0** | 🥇 **0** | 🥇 **0** | 🥈 2 | 🥉 5 | -| **Compilation Required** | 🥇 **No** | 🥇 **No** | 🥈 Yes | 🥇 **No** | 🥇 **No** | -| **Browser Native** | 🥇 **Yes** | 🥈 Partial | 🥉 Partial | 🥉 Partial | No | -| **Framework Lock-in** | 🥇 **None** | 🥈 Medium | 🥉 High | 🥈 Medium | 🥉 High | -| **Longevity** (standards-based) | 🥇 **10+ years** | 🥈 5 years | 🥉 3 years | 🥈 5 years | 🥈 5 years | - -## 🔑 Core Principles - -SigPro is built on four fundamental principles: - -### 📡 **True Reactivity** -Automatic dependency tracking with no manual subscriptions. When a signal changes, only the exact DOM nodes that depend on it update—surgically, efficiently, instantly. - -### ⚡ **Surgical Updates** -No virtual DOM diffing. No tree reconciliation. Just direct DOM updates where and when needed. The result is predictable performance that scales with your content, not your component count. - -### 🧩 **Web Standards** -Built on Custom Elements, not a custom rendering engine. Your components are real web components that work in any framework—or none at all. - -### 🔬 **Predictable** -No magic, just signals and effects. What you see is what you get. The debugging experience is straightforward because there's no framework layer between your code and the browser. - -## 🎨 The Development Experience - -```javascript -// With VS Code + lit-html extension, you get: -// ✅ Syntax highlighting -// ✅ Color previews -// ✅ Auto-formatting -// ✅ IntelliSense - -html` -
-

Beautiful highlighted template

-
-` -``` - -## ⏱️ Built for the Long Term - -What emerged is a library that proves we've reached a turning point: the web is finally mature enough that we don't need to abstract it anymore. We can build reactive, component-based applications using virtually pure JavaScript, leveraging the platform's latest advances instead of working against them. - -**The result isn't just smaller bundles or faster rendering—it's code that will still run 10 years from now, in any browser, without maintenance.** - -## 📈 The Verdict - -While other frameworks build parallel universes with proprietary syntax and compilation steps, SigPro embraces the web platform. SigPro isn't just another framework—it's a return to fundamentals, showing that the dream of simple, powerful reactivity is now achievable with the tools browsers give us out of the box. - -> *"Stop fighting the platform. Start building with it."* - -## 🚀 Ready to Start? - -[Get Started with SigPro](/guide/getting-started) • [View on GitHub](https://github.com/natxocc/sigpro) • [npm Package](https://www.npmjs.com/package/sigpro) - - diff --git a/packages/docs/index.md b/packages/docs/index.md deleted file mode 100644 index 9c8a5cc..0000000 --- a/packages/docs/index.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -# https://vitepress.dev/reference/default-theme-home-page -layout: home - -hero: - name: "SigPro" - text: "Reactivity for the Web Platform" - tagline: 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. - image: - src: /logo.svg - alt: SigPro - actions: - - theme: brand - text: Get Started - link: /guide/getting-started - -features: - - title: ⚡ 3KB gzipped - details: Minimal footprint with maximum impact. No heavy dependencies, just pure reactivity. - - title: 🎯 Native Web Components - details: Built on Custom Elements and Shadow DOM. Leverage the platform, don't fight it. - - title: 🔄 Signal-based Reactivity - details: Fine-grained updates without virtual DOM diffing. Just intelligent, automatic reactivity. ---- - -
-

- npm version - bundle size - license -

-
- -
-

"Stop fighting the platform. Start building with it."

-
- - \ No newline at end of file diff --git a/packages/docs/vite/plugin.md b/packages/docs/vite/plugin.md deleted file mode 100644 index 69d8106..0000000 --- a/packages/docs/vite/plugin.md +++ /dev/null @@ -1,423 +0,0 @@ -# Vite Plugin: Automatic File-based Routing 🚦 - -SigPro provides an optional Vite plugin that automatically generates routes based on your file structure. No configuration needed - just create pages and they're instantly available with the correct paths. - -## Why Use This Plugin? - -While SigPro's router works perfectly with manually defined routes, this plugin: -- **Eliminates boilerplate** - No need to write route configurations -- **Enforces conventions** - Consistent URL structure across your app -- **Supports dynamic routes** - Use `[param]` syntax for parameters -- **Automatic code-splitting** - Each page becomes a separate chunk -- **Type-safe** (with JSDoc) - Routes follow your file structure - -## Installation - -The plugin is included with SigPro, but you need to add it to your Vite config: - -```javascript -// vite.config.js -import { defineConfig } from 'vite'; -import { sigproRouter } from 'sigpro'; - -export default defineConfig({ - plugins: [sigproRouter()] -}); -``` - -## How It Works - -The plugin scans your `src/pages` directory and automatically generates routes based on the file structure: - -``` -src/pages/ -├── index.js → '/' -├── about.js → '/about' -├── blog/ -│ ├── index.js → '/blog' -│ └── [slug].js → '/blog/:slug' -└── users/ - ├── [id].js → '/users/:id' - └── [id]/edit.js → '/users/:id/edit' -``` - -## Usage - -### 1. Enable the Plugin - -Add the plugin to your Vite config as shown above. - -### 2. Import the Generated Routes - -Once you have the generated routes, using them with the router is straightforward: - -```javascript -// main.js -import { $, html } from 'sigpro'; -import { routes } from 'virtual:sigpro-routes'; - -// Simple usage -const router = $.router(routes); -document.body.appendChild(router); -``` - -Or directly in your template: - -```javascript -// app.js -import { $, html } from 'sigpro'; -import { routes } from 'virtual:sigpro-routes'; - -const App = () => html` -
-
-

My Application

-
- -
-
- ${$.router(routes)} -
-
-
-`; - -document.body.appendChild(App()); -``` - -This approach keeps your template clean and lets the router handle all the page rendering automatically. - -### 3. Create Pages - -```javascript -// src/pages/index.js -import { $, html } from 'sigpro'; - -export default () => { - return html` -
-

Home Page

- About -
- `; -}; -``` - -```javascript -// src/pages/users/[id].js -import { $, html } from 'sigpro'; - -export default (params) => { - const userId = params.id; - - return html` -
-

User Profile: ${userId}

- Edit -
- `; -}; -``` - -## 📋 File-to-Route Mapping - -### Static Routes - -| File Path | Generated Route | -|-----------|-----------------| -| `src/pages/index.js` | `/` | -| `src/pages/about.js` | `/about` | -| `src/pages/contact/index.js` | `/contact` | -| `src/pages/blog/post.js` | `/blog/post` | - -### Dynamic Routes - -| File Path | Generated Route | Example URL | -|-----------|-----------------|-------------| -| `src/pages/users/[id].js` | `/users/:id` | `/users/42` | -| `src/pages/blog/[slug].js` | `/blog/:slug` | `/blog/hello-world` | -| `src/pages/users/[id]/posts/[pid].js` | `/users/:id/posts/:pid` | `/users/42/posts/123` | - -### Nested Routes - -| File Path | Generated Route | Notes | -|-----------|-----------------|-------| -| `src/pages/settings/index.js` | `/settings` | Index page | -| `src/pages/settings/profile.js` | `/settings/profile` | Sub-page | -| `src/pages/settings/security.js` | `/settings/security` | Sub-page | -| `src/pages/settings/[section].js` | `/settings/:section` | Dynamic section | - -## 🎯 Advanced Examples - -### Blog with Posts - -```javascript -// src/pages/blog/index.js - Lists all posts -export default () => { - const posts = $([]); - - $.effect(() => { - fetch('/api/posts') - .then(res => res.json()) - .then(data => posts(data)); - }); - - return html` -
-

Blog

- ${posts().map(post => html` - - `)} -
- `; -}; -``` - -```javascript -// src/pages/blog/[slug].js - Single post -export default (params) => { - const post = $(null); - const slug = params.slug; - - $.effect(() => { - fetch(`/api/posts/${slug}`) - .then(res => res.json()) - .then(data => post(data)); - }); - - return html` -
- ← Back to blog - ${() => post() ? html` -
-

${post().title}

-
${post().content}
-
- ` : html`
Loading...
`} -
- `; -}; -``` - -### Dashboard with Nested Sections - -```javascript -// src/pages/dashboard/index.js -export default () => { - return html` -
- -
-

Dashboard Overview

- -
-
- `; -}; -``` - -```javascript -// src/pages/dashboard/analytics.js -export default () => { - return html` -
- -
-

Analytics

- -
-
- `; -}; -``` - -### E-commerce Product Routes - -```javascript -// src/pages/products/[category]/[id].js -export default (params) => { - const { category, id } = params; - const product = $(null); - - $.effect(() => { - fetch(`/api/products/${category}/${id}`) - .then(res => res.json()) - .then(data => product(data)); - }); - - return html` -
- - - ${() => product() ? html` -
-

${product().name}

-

$${product().price}

-

${product().description}

- -
- ` : html`
Loading...
`} -
- `; -}; -``` - -## 🔧 Configuration Options - -The plugin accepts an optional configuration object: - -```javascript -// vite.config.js -import { defineConfig } from 'vite'; -import { sigproRouter } from 'sigpro/vite'; - -export default defineConfig({ - plugins: [ - sigproRouter({ - pagesDir: 'src/pages', // Default: 'src/pages' - extensions: ['.js', '.jsx'], // Default: ['.js', '.jsx'] - exclude: ['**/_*', '**/components/**'] // Glob patterns to exclude - }) - ] -}); -``` - -### Options - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `pagesDir` | `string` | `'src/pages'` | Directory containing your pages | -| `extensions` | `string[]` | `['.js', '.jsx']` | File extensions to include | -| `exclude` | `string[]` | `[]` | Glob patterns to exclude | - -## 🎯 Route Priority - -The plugin automatically sorts routes to ensure correct matching: - -1. **Static routes** take precedence over dynamic ones -2. **More specific routes** (deeper paths) come first -3. **Alphabetical order** for routes at the same level - -Example sorting: -``` -/users/new (static, specific) -/users/[id]/edit (dynamic, deeper) -/users/[id] (dynamic, shallower) -/users/profile (static, shallower) -``` - -## 📦 Output Example - -When you import `virtual:sigpro-routes`, you get: - -```javascript -// Generated module -import Page_0 from '/src/pages/index.js'; -import Page_1 from '/src/pages/about.js'; -import Page_2 from '/src/pages/blog/index.js'; -import Page_3 from '/src/pages/blog/[slug].js'; -import Page_4 from '/src/pages/users/[id].js'; -import Page_5 from '/src/pages/users/[id]/edit.js'; - -export const routes = [ - { path: '/', component: Page_0 }, - { path: '/about', component: Page_1 }, - { path: '/blog', component: Page_2 }, - { path: '/blog/:slug', component: Page_3 }, - { path: '/users/:id', component: Page_4 }, - { path: '/users/:id/edit', component: Page_5 }, -]; -``` - -## 🚀 Performance Benefits - -- **Automatic code splitting** - Each page becomes a separate chunk -- **Lazy loading ready** - Import pages dynamically -- **Tree shaking** - Only used routes are included - -```javascript -// With dynamic imports (automatic with Vite) -const routes = [ - { path: '/', component: () => import('./pages/index.js') }, - { path: '/about', component: () => import('./pages/about.js') }, - // ... -]; -``` - -## 💡 Pro Tips - -### 1. Group Related Pages - -``` -src/pages/ -├── dashboard/ -│ ├── index.js -│ ├── analytics.js -│ └── settings.js -└── dashboard.js # ❌ Don't mix with folder -``` - -### 2. Use Index Files for Clean URLs - -``` -✅ Good: -pages/blog/index.js → /blog -pages/blog/post.js → /blog/post - -❌ Avoid: -pages/blog.js → /blog (conflicts with folder) -``` - -### 3. Private Components - -Prefix with underscore to exclude from routing: - -``` -src/pages/ -├── index.js -├── about.js -└── _components/ # ❌ Not scanned - └── Header.js -``` - -### 4. Layout Components - -Create a layout wrapper in your main entry: - -```javascript -// main.js -import { $, html } from 'sigpro'; -import { routes } from 'virtual:sigpro-routes'; - -// Wrap all routes with layout -const routesWithLayout = routes.map(route => ({ - ...route, - component: (params) => Layout(route.component(params)) -})); - -const router = $.router(routesWithLayout); -document.body.appendChild(router); -``` - ---- - -> **Note:** This plugin is completely optional. You can always define routes manually if you prefer. The plugin just saves you from writing boilerplate route configurations. - -> **Pro Tip:** The plugin works great with hot module replacement (HMR) - add a new page and it's instantly available in your dev server without restarting! diff --git a/packages/sigpro/plugin.js b/packages/sigpro/plugin.js deleted file mode 100644 index 1f0e54d..0000000 --- a/packages/sigpro/plugin.js +++ /dev/null @@ -1,91 +0,0 @@ -// plugins/sigpro-plugin-router.js -import fs from 'fs'; -import path from 'path'; - -export default function sigproRouter() { - const virtualModuleId = 'virtual:sigpro-routes'; - const resolvedVirtualModuleId = '\0' + virtualModuleId; - - function getFiles(dir) { - let results = []; - if (!fs.existsSync(dir)) return results; - const list = fs.readdirSync(dir); - list.forEach(file => { - const fullPath = path.resolve(dir, file); - const stat = fs.statSync(fullPath); - if (stat && stat.isDirectory()) { - results = results.concat(getFiles(fullPath)); - } else if (file.endsWith('.js') || file.endsWith('.jsx')) { - results.push(fullPath); - } - }); - return results; - } - - function filePathToUrl(relativePath) { - let url = relativePath.replace(/\\/g, '/').replace(/\.jsx?$/, ''); - - if (url === 'index') { - return '/'; - } - - if (url.endsWith('/index')) { - url = url.slice(0, -6); - } - - url = url.replace(/\[([^\]]+)\]/g, ':$1'); - - let finalPath = '/' + url.toLowerCase(); - - return finalPath.replace(/\/+/g, '/').replace(/\/$/, '') || '/'; - } - - return { - name: 'sigpro-router', - resolveId(id) { - if (id === virtualModuleId) return resolvedVirtualModuleId; - }, - load(id) { - if (id === resolvedVirtualModuleId) { - const pagesDir = path.resolve(process.cwd(), 'src/pages'); - let files = getFiles(pagesDir); - - files = files.sort((a, b) => { - const aRel = path.relative(pagesDir, a).replace(/\\/g, '/'); - const bRel = path.relative(pagesDir, b).replace(/\\/g, '/'); - - const aDynamic = aRel.includes('[') || aRel.includes(':'); - const bDynamic = bRel.includes('[') || bRel.includes(':'); - - if (aDynamic !== bDynamic) return aDynamic ? 1 : -1; - - return bRel.length - aRel.length; - }); - - let imports = ''; - let routeArray = 'export const routes = [\n'; - - console.log('\n🚀 [SigPro Router] Routes generated:'); - - files.forEach((fullPath, i) => { - const importPath = fullPath.replace(/\\/g, '/'); - const relativePath = path.relative(pagesDir, fullPath).replace(/\\/g, '/'); - const varName = `Page_${i}`; - - let urlPath = filePathToUrl(relativePath); - const isDynamic = urlPath.includes(':'); - - imports += `import ${varName} from '${importPath}';\n`; - - console.log(` ${isDynamic ? '🔗' : '📄'} ${urlPath.padEnd(30)} -> ${relativePath}`); - - routeArray += ` { path: '${urlPath}', component: ${varName} },\n`; - }); - - routeArray += '];'; - - return `${imports}\n${routeArray}`; - } - } - }; -} \ No newline at end of file diff --git a/packages/sigpro/plugin.min.js b/packages/sigpro/plugin.min.js deleted file mode 100644 index 325a121..0000000 --- a/packages/sigpro/plugin.min.js +++ /dev/null @@ -1 +0,0 @@ -import fs from"fs";import path from"path";export default function sigproRouter(){const e="virtual:sigpro-routes",r="\0"+e;function t(e){let r=[];if(!fs.existsSync(e))return r;return fs.readdirSync(e).forEach((n=>{const s=path.resolve(e,n),o=fs.statSync(s);o&&o.isDirectory()?r=r.concat(t(s)):(n.endsWith(".js")||n.endsWith(".jsx"))&&r.push(s)})),r}return{name:"sigpro-router",resolveId(t){if(t===e)return r},load(e){if(e===r){const e=path.resolve(process.cwd(),"src/pages");let r=t(e);r=r.sort(((r,t)=>{const n=path.relative(e,r).replace(/\\/g,"/"),s=path.relative(e,t).replace(/\\/g,"/"),o=n.includes("[")||n.includes(":");return o!==(s.includes("[")||s.includes(":"))?o?1:-1:s.length-n.length}));let n="",s="export const routes = [\n";return r.forEach(((r,t)=>{const o=r.replace(/\\/g,"/"),c=`Page_${t}`;let a=function(e){let r=e.replace(/\\/g,"/").replace(/\.jsx?$/,"");return"index"===r?"/":(r.endsWith("/index")&&(r=r.slice(0,-6)),r=r.replace(/\[([^\]]+)\]/g,":$1"),("/"+r.toLowerCase()).replace(/\/+/g,"/").replace(/\/$/,"")||"/")}(path.relative(e,r).replace(/\\/g,"/"));a.includes(":");n+=`import ${c} from '${o}';\n`,s+=` { path: '${a}', component: ${c} },\n`})),s+="];",`${n}\n${s}`}}}} \ No newline at end of file diff --git a/packages/sigpro/sigpro.js b/packages/sigpro/sigpro.js deleted file mode 100644 index 909558c..0000000 --- a/packages/sigpro/sigpro.js +++ /dev/null @@ -1,631 +0,0 @@ -// Global state for tracking the current reactive effect -let activeEffect = null; -const effectQueue = new Set(); -let isFlushScheduled = false; -let flushCount = 0; - -const flushEffectQueue = () => { - isFlushScheduled = false; - flushCount++; - - if (flushCount > 100) { - effectQueue.clear(); - flushCount = 0; - throw new Error("SigPro: Infinite reactive loop detected."); - } - - try { - const effects = Array.from(effectQueue); - effectQueue.clear(); - for (const effect of effects) effect.run(); - } catch (error) { - console.error("SigPro Flush Error:", error); - } finally { - setTimeout(() => { - flushCount = 0; - }, 0); - } -}; - -/** - * Creates a reactive signal - * @param {any} initialValue - Initial value or getter function - * @returns {Function} Signal getter/setter function - */ -const $ = (initialValue) => { - const subscribers = new Set(); - let signal; - - if (typeof initialValue === "function") { - let isDirty = true; - let cachedValue; - - const computedEffect = { - dependencies: new Set(), - markDirty: () => { - if (!isDirty) { - isDirty = true; - subscribers.forEach((sub) => { - if (sub.markDirty) sub.markDirty(); - effectQueue.add(sub); - }); - if (!isFlushScheduled && effectQueue.size) { - isFlushScheduled = true; - queueMicrotask(flushEffectQueue); - } - } - }, - run: () => { - computedEffect.dependencies.forEach((dep) => dep.delete(computedEffect)); - computedEffect.dependencies.clear(); - const prev = activeEffect; - activeEffect = computedEffect; - try { - cachedValue = initialValue(); - } finally { - activeEffect = prev; - isDirty = false; - } - }, - }; - - signal = () => { - if (activeEffect) { - subscribers.add(activeEffect); - activeEffect.dependencies.add(subscribers); - } - if (isDirty) computedEffect.run(); - return cachedValue; - }; - } else { - signal = (...args) => { - if (args.length) { - const next = typeof args[0] === "function" ? args[0](initialValue) : args[0]; - if (!Object.is(initialValue, next)) { - initialValue = next; - subscribers.forEach((sub) => { - if (sub.markDirty) sub.markDirty(); - effectQueue.add(sub); - }); - if (!isFlushScheduled && effectQueue.size) { - isFlushScheduled = true; - queueMicrotask(flushEffectQueue); - } - } - } - if (activeEffect) { - subscribers.add(activeEffect); - activeEffect.dependencies.add(subscribers); - } - return initialValue; - }; - } - return signal; -}; - -let currentPageCleanups = null; - -/** - * Creates a reactive effect that runs when dependencies change - * @param {Function} effectFn - The effect function to run - * @returns {Function} Cleanup function to stop the effect - */ -const $e = (effectFn) => { - const effect = { - dependencies: new Set(), - cleanupHandlers: new Set(), - run() { - this.cleanupHandlers.forEach((h) => h()); - this.cleanupHandlers.clear(); - this.dependencies.forEach((dep) => dep.delete(this)); - this.dependencies.clear(); - - const prev = activeEffect; - activeEffect = this; - try { - const res = effectFn(); - if (typeof res === "function") this.cleanupHandlers.add(res); - } finally { - activeEffect = prev; - } - }, - stop() { - this.cleanupHandlers.forEach((h) => h()); - this.dependencies.forEach((dep) => dep.delete(this)); - }, - }; - - if (currentPageCleanups) currentPageCleanups.push(() => effect.stop()); - if (activeEffect) activeEffect.cleanupHandlers.add(() => effect.stop()); - - effect.run(); - return () => effect.stop(); -}; - -/** - * Persistent signal with localStorage - * @param {string} key - Storage key - * @param {any} initialValue - Default value if none stored - * @param {Storage} [storage=localStorage] - Storage type (localStorage/sessionStorage) - * @returns {Function} Signal that persists to storage - */ -const $s = (key, initialValue, storage = localStorage) => { - let initial; - try { - const saved = storage.getItem(key); - if (saved !== null) { - initial = JSON.parse(saved); - } else { - initial = initialValue; - } - } catch (e) { - console.warn(`Error reading ${key} from storage:`, e); - initial = initialValue; - storage.removeItem(key); - } - - const signal = $(initial); - - $e(() => { - try { - const value = signal(); - if (value === undefined || value === null) { - storage.removeItem(key); - } else { - storage.setItem(key, JSON.stringify(value)); - } - } catch (e) { - console.warn(`Error saving ${key} to storage:`, e); - } - }); - - return signal; -}; - -/** - * Tagged template literal for creating reactive HTML - * @param {string[]} strings - Template strings - * @param {...any} values - Dynamic values - * @returns {DocumentFragment} Reactive document fragment - * @see {@link https://developer.mozilla.org/es/docs/Glossary/Cross-site_scripting} - */ -const html = (strings, ...values) => { - const templateCache = html._templateCache ?? (html._templateCache = new WeakMap()); - - const getNodeByPath = (root, path) => path.reduce((node, index) => node?.childNodes?.[index], root); - - const applyTextContent = (node, values) => { - const parts = node.textContent.split("{{part}}"); - const parent = node.parentNode; - let valueIndex = 0; - - parts.forEach((part, index) => { - if (part) parent.insertBefore(document.createTextNode(part), node); - if (index < parts.length - 1) { - const currentValue = values[valueIndex++]; - const startMarker = document.createComment("s"); - const endMarker = document.createComment("e"); - parent.insertBefore(startMarker, node); - parent.insertBefore(endMarker, node); - - if (typeof currentValue === "function") { - let lastResult; - $e(() => { - const result = currentValue(); - if (result === lastResult) return; - lastResult = result; - updateContent(result); - }); - } else { - updateContent(currentValue); - } - - function updateContent(result) { - if (typeof result !== "object" && !Array.isArray(result)) { - const textNode = startMarker.nextSibling; - const safeText = String(result ?? ""); - - if (textNode !== endMarker && textNode?.nodeType === 3) { - textNode.textContent = safeText; - } else { - while (startMarker.nextSibling !== endMarker) parent.removeChild(startMarker.nextSibling); - parent.insertBefore(document.createTextNode(safeText), endMarker); - } - } else { - while (startMarker.nextSibling !== endMarker) parent.removeChild(startMarker.nextSibling); - - const items = Array.isArray(result) ? result : [result]; - const fragment = document.createDocumentFragment(); - items.forEach((item) => { - if (item == null || item === false) return; - const nodeItem = item instanceof Node ? item : document.createTextNode(item); - fragment.appendChild(nodeItem); - }); - parent.insertBefore(fragment, endMarker); - } - } - } - }); - node.remove(); - }; - - let cachedTemplate = templateCache.get(strings); - if (!cachedTemplate) { - const template = document.createElement("template"); - template.innerHTML = strings.join("{{part}}"); - - const dynamicNodes = []; - const treeWalker = document.createTreeWalker(template.content, 133); - - const getNodePath = (node) => { - const path = []; - while (node && node !== template.content) { - let index = 0; - for (let sibling = node.previousSibling; sibling; sibling = sibling.previousSibling) index++; - path.push(index); - node = node.parentNode; - } - return path.reverse(); - }; - - let currentNode; - while ((currentNode = treeWalker.nextNode())) { - let isDynamic = false; - const nodeInfo = { - type: currentNode.nodeType, - path: getNodePath(currentNode), - parts: [], - }; - - if (currentNode.nodeType === 1) { - for (let i = 0; i < currentNode.attributes.length; i++) { - const attribute = currentNode.attributes[i]; - if (attribute.value.includes("{{part}}")) { - nodeInfo.parts.push({ name: attribute.name }); - isDynamic = true; - } - } - } else if (currentNode.nodeType === 3 && currentNode.textContent.includes("{{part}}")) { - isDynamic = true; - } - - if (isDynamic) dynamicNodes.push(nodeInfo); - } - - templateCache.set(strings, (cachedTemplate = { template, dynamicNodes })); - } - - const fragment = cachedTemplate.template.content.cloneNode(true); - let valueIndex = 0; - - const targets = cachedTemplate.dynamicNodes.map((nodeInfo) => ({ - node: getNodeByPath(fragment, nodeInfo.path), - info: nodeInfo, - })); - - targets.forEach(({ node, info }) => { - if (!node) return; - - if (info.type === 1) { - info.parts.forEach((part) => { - const currentValue = values[valueIndex++]; - const attributeName = part.name; - const firstChar = attributeName[0]; - - if (firstChar === "@") { - const [eventName, ...modifiers] = attributeName.slice(1).split("."); - - const handlerWrapper = (e) => { - if (modifiers.includes("prevent")) e.preventDefault(); - if (modifiers.includes("stop")) e.stopPropagation(); - if (modifiers.includes("self") && e.target !== node) return; - - if (modifiers.some((m) => m.startsWith("debounce"))) { - const ms = modifiers.find((m) => m.startsWith("debounce"))?.split(":")[1] || 300; - clearTimeout(node._debounceTimer); - node._debounceTimer = setTimeout(() => currentValue(e), ms); - return; - } - - if (modifiers.includes("once")) { - node.removeEventListener(eventName, handlerWrapper); - } - - currentValue(e); - }; - - node.addEventListener(eventName, handlerWrapper, { - passive: modifiers.includes("passive"), - capture: modifiers.includes("capture"), - }); - } else if (firstChar === ":") { - const propertyName = attributeName.slice(1); - const eventType = node.type === "checkbox" || node.type === "radio" ? "change" : "input"; - - if (typeof currentValue === "function") { - $e(() => { - const value = currentValue(); - if (node[propertyName] !== value) node[propertyName] = value; - }); - } else { - node[propertyName] = currentValue; - } - - node.addEventListener(eventType, () => { - const value = eventType === "change" ? node.checked : node.value; - if (typeof currentValue === "function") currentValue(value); - }); - } else if (firstChar === "?") { - const attrName = attributeName.slice(1); - - if (typeof currentValue === "function") { - $e(() => { - const result = currentValue(); - node.toggleAttribute(attrName, !!result); - }); - } else { - node.toggleAttribute(attrName, !!currentValue); - } - } else if (firstChar === ".") { - const propertyName = attributeName.slice(1); - - if (typeof currentValue === "function") { - $e(() => { - const result = currentValue(); - node[propertyName] = result; - if (result != null && typeof result !== "object" && typeof result !== "boolean") { - node.setAttribute(propertyName, result); - } - }); - } else { - node[propertyName] = currentValue; - if (currentValue != null && typeof currentValue !== "object" && typeof currentValue !== "boolean") { - node.setAttribute(propertyName, currentValue); - } - } - } else { - if (typeof currentValue === "function") { - $e(() => node.setAttribute(attributeName, currentValue())); - } else { - node.setAttribute(attributeName, currentValue); - } - } - }); - } else if (info.type === 3) { - const placeholderCount = node.textContent.split("{{part}}").length - 1; - applyTextContent(node, values.slice(valueIndex, valueIndex + placeholderCount)); - valueIndex += placeholderCount; - } - }); - - return fragment; -}; - -/** - * Creates a page with automatic cleanup - * @param {Function} setupFunction - Page setup function that receives props - * @returns {Function} A function that creates page instances with props - */ -const $p = (setupFunction) => { - const tagName = "page-" + Math.random().toString(36).substring(2, 9); - - customElements.define( - tagName, - class extends HTMLElement { - connectedCallback() { - this.style.display = "contents"; - this._cleanups = []; - currentPageCleanups = this._cleanups; - - try { - const result = setupFunction({ - params: JSON.parse(this.getAttribute("params") || "{}"), - onUnmount: (fn) => this._cleanups.push(fn), - }); - this.appendChild(result instanceof Node ? result : document.createTextNode(String(result))); - } finally { - currentPageCleanups = null; - } - } - - disconnectedCallback() { - this._cleanups.forEach((fn) => fn()); - this._cleanups = []; - this.innerHTML = ""; - } - }, - ); - - return (props = {}) => { - const el = document.createElement(tagName); - el.setAttribute("params", JSON.stringify(props)); - return el; - }; -}; - -/** - * Creates a custom web component with reactive properties - * @param {string} tagName - Custom element tag name - * @param {Function} setupFunction - Component setup function - * @param {string[]} observedAttributes - Array of observed attributes - * @param {boolean} useShadowDOM - Enable Shadow DOM (default: false) - */ -const $c = (tagName, setupFunction, observedAttributes = [], useShadowDOM = false) => { - if (customElements.get(tagName)) return; - - customElements.define( - tagName, - class extends HTMLElement { - static get observedAttributes() { - return observedAttributes; - } - - constructor() { - super(); - this._propertySignals = {}; - this.cleanupFunctions = []; - - if (useShadowDOM) { - this._root = this.attachShadow({ mode: "open" }); - } else { - this._root = this; - } - - observedAttributes.forEach((attr) => (this._propertySignals[attr] = $(undefined))); - } - - connectedCallback() { - const frozenChildren = [...this.childNodes]; - this._root.innerHTML = ""; - - observedAttributes.forEach((attr) => { - const initialValue = this.hasOwnProperty(attr) ? this[attr] : this.getAttribute(attr); - - Object.defineProperty(this, attr, { - get: () => this._propertySignals[attr](), - set: (value) => { - const processedValue = value === "false" ? false : value === "" && attr !== "value" ? true : value; - this._propertySignals[attr](processedValue); - }, - configurable: true, - }); - - if (initialValue !== null && initialValue !== undefined) this[attr] = initialValue; - }); - - const context = { - select: (selector) => this._root.querySelector(selector), - selectAll: (selector) => this._root.querySelectorAll(selector), - slot: (name) => - frozenChildren.filter((node) => { - const slotName = node.nodeType === 1 ? node.getAttribute("slot") : null; - return name ? slotName === name : !slotName; - }), - emit: (name, detail) => this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })), - host: this, - root: this._root, - onUnmount: (cleanupFn) => this.cleanupFunctions.push(cleanupFn), - }; - - const result = setupFunction(this._propertySignals, context); - if (result instanceof Node) this._root.appendChild(result); - } - - attributeChangedCallback(name, oldValue, newValue) { - if (this[name] !== newValue) this[name] = newValue; - } - - disconnectedCallback() { - this.cleanupFunctions.forEach((cleanupFn) => cleanupFn()); - this.cleanupFunctions = []; - } - }, - ); -}; - -/** - * Ultra-simple fetch wrapper with optional loading signal - * @param {string} url - Endpoint URL - * @param {Object} data - Data to send (automatically JSON.stringify'd) - * @param {Function} [loading] - Optional signal function to track loading state - * @returns {Promise} Parsed JSON response or null on error - */ -const $f = async (url, data, loading) => { - if (loading) loading(true); - - try { - const res = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data), - }); - - const text = await res.text(); - try { - return JSON.parse(text); - } catch (e) { - console.warn("Invalid JSON response"); - return null; - } - } catch (e) { - return null; - } finally { - if (loading) loading(false); - } -}; - -/** - * Creates a router for hash-based navigation - * @param {Array<{path: string|RegExp, component: Function}>} routes - Route configurations - * @returns {HTMLDivElement} Router container element - */ -const $r = (routes) => { - const getCurrentPath = () => window.location.hash.replace(/^#/, "") || "/"; - const container = document.createElement("div"); - container.style.display = "contents"; - - const matchRoute = (path, routePath) => { - if (!routePath.includes(":")) { - return routePath === path ? {} : null; - } - - const parts = routePath.split("/"); - const pathParts = path.split("/"); - - if (parts.length !== pathParts.length) return null; - - const params = {}; - for (let i = 0; i < parts.length; i++) { - if (parts[i].startsWith(":")) { - params[parts[i].slice(1)] = pathParts[i]; - } else if (parts[i] !== pathParts[i]) { - return null; - } - } - return params; - }; - - const render = () => { - const path = getCurrentPath(); - let matchedRoute = null; - let routeParams = {}; - - for (const route of routes) { - const params = matchRoute(path, route.path); - if (params !== null) { - matchedRoute = route; - routeParams = params; - break; - } - } - - const view = matchedRoute ? matchedRoute.component(routeParams) : Object.assign(document.createElement("h1"), { textContent: "404" }); - - container.replaceChildren(view instanceof Node ? view : document.createTextNode(String(view ?? ""))); - }; - - window.addEventListener("hashchange", render); - render(); - return container; -}; - -$r.go = (path) => { - const targetPath = path.startsWith("/") ? path : `/${path}`; - if (window.location.hash !== `#${targetPath}`) { - window.location.hash = targetPath; - } -}; - -/* Can customize the name of your functions */ - -$.effect = $e; -$.page = $p; -$.component = $c; -$.fetch = $f; -$.router = $r; -$.storage = $s; - -if (typeof window !== "undefined") { - window.$ = $; -} -export { $, html }; diff --git a/packages/sigpro/sigpro.min.js b/packages/sigpro/sigpro.min.js deleted file mode 100644 index 7f4e5d4..0000000 --- a/packages/sigpro/sigpro.min.js +++ /dev/null @@ -1 +0,0 @@ -let activeEffect=null;const effectQueue=new Set;let isFlushScheduled=!1,flushCount=0;const flushEffectQueue=()=>{if(isFlushScheduled=!1,flushCount++,flushCount>100)throw effectQueue.clear(),flushCount=0,new Error("SigPro: Infinite reactive loop detected.");try{const e=Array.from(effectQueue);effectQueue.clear();for(const t of e)t.run()}catch(e){console.error("SigPro Flush Error:",e)}finally{setTimeout(()=>{flushCount=0},0)}},$=e=>{const t=new Set;let n;if("function"==typeof e){let s,o=!0;const c={dependencies:new Set,markDirty:()=>{o||(o=!0,t.forEach(e=>{e.markDirty&&e.markDirty(),effectQueue.add(e)}),!isFlushScheduled&&effectQueue.size&&(isFlushScheduled=!0,queueMicrotask(flushEffectQueue)))},run:()=>{c.dependencies.forEach(e=>e.delete(c)),c.dependencies.clear();const t=activeEffect;activeEffect=c;try{s=e()}finally{activeEffect=t,o=!1}}};n=()=>(activeEffect&&(t.add(activeEffect),activeEffect.dependencies.add(t)),o&&c.run(),s)}else n=(...n)=>{if(n.length){const s="function"==typeof n[0]?n[0](e):n[0];Object.is(e,s)||(e=s,t.forEach(e=>{e.markDirty&&e.markDirty(),effectQueue.add(e)}),!isFlushScheduled&&effectQueue.size&&(isFlushScheduled=!0,queueMicrotask(flushEffectQueue)))}return activeEffect&&(t.add(activeEffect),activeEffect.dependencies.add(t)),e};return n};let currentPageCleanups=null;const $e=e=>{const t={dependencies:new Set,cleanupHandlers:new Set,run(){this.cleanupHandlers.forEach(e=>e()),this.cleanupHandlers.clear(),this.dependencies.forEach(e=>e.delete(this)),this.dependencies.clear();const t=activeEffect;activeEffect=this;try{const t=e();"function"==typeof t&&this.cleanupHandlers.add(t)}finally{activeEffect=t}},stop(){this.cleanupHandlers.forEach(e=>e()),this.dependencies.forEach(e=>e.delete(this))}};return currentPageCleanups&¤tPageCleanups.push(()=>t.stop()),activeEffect&&activeEffect.cleanupHandlers.add(()=>t.stop()),t.run(),()=>t.stop()},$s=(e,t,n=localStorage)=>{let s;try{const o=n.getItem(e);s=null!==o?JSON.parse(o):t}catch(o){console.warn(`Error reading ${e} from storage:`,o),s=t,n.removeItem(e)}const o=$(s);return $e(()=>{try{const t=o();null==t?n.removeItem(e):n.setItem(e,JSON.stringify(t))}catch(t){console.warn(`Error saving ${e} to storage:`,t)}}),o},html=(e,...t)=>{const n=html._templateCache??(html._templateCache=new WeakMap);let s=n.get(e);if(!s){const t=document.createElement("template");t.innerHTML=e.join("{{part}}");const o=[],c=document.createTreeWalker(t.content,133),r=e=>{const n=[];for(;e&&e!==t.content;){let t=0;for(let n=e.previousSibling;n;n=n.previousSibling)t++;n.push(t),e=e.parentNode}return n.reverse()};let i;for(;i=c.nextNode();){let e=!1;const t={type:i.nodeType,path:r(i),parts:[]};if(1===i.nodeType)for(let n=0;n{return{node:(t=o,n=e.path,n.reduce((e,t)=>e?.childNodes?.[t],t)),info:e};var t,n}).forEach(({node:e,info:n})=>{if(e)if(1===n.type)n.parts.forEach(n=>{const s=t[c++],o=n.name,r=o[0];if("@"===r){const[t,...n]=o.slice(1).split("."),c=o=>{if(n.includes("prevent")&&o.preventDefault(),n.includes("stop")&&o.stopPropagation(),!n.includes("self")||o.target===e){if(n.some(e=>e.startsWith("debounce"))){const t=n.find(e=>e.startsWith("debounce"))?.split(":")[1]||300;return clearTimeout(e._debounceTimer),void(e._debounceTimer=setTimeout(()=>s(o),t))}n.includes("once")&&e.removeEventListener(t,c),s(o)}};e.addEventListener(t,c,{passive:n.includes("passive"),capture:n.includes("capture")})}else if(":"===r){const t=o.slice(1),n="checkbox"===e.type||"radio"===e.type?"change":"input";"function"==typeof s?$e(()=>{const n=s();e[t]!==n&&(e[t]=n)}):e[t]=s,e.addEventListener(n,()=>{const t="change"===n?e.checked:e.value;"function"==typeof s&&s(t)})}else if("?"===r){const t=o.slice(1);"function"==typeof s?$e(()=>{const n=s();e.toggleAttribute(t,!!n)}):e.toggleAttribute(t,!!s)}else if("."===r){const t=o.slice(1);"function"==typeof s?$e(()=>{const n=s();e[t]=n,null!=n&&"object"!=typeof n&&"boolean"!=typeof n&&e.setAttribute(t,n)}):(e[t]=s,null!=s&&"object"!=typeof s&&"boolean"!=typeof s&&e.setAttribute(t,s))}else"function"==typeof s?$e(()=>e.setAttribute(o,s())):e.setAttribute(o,s)});else if(3===n.type){const n=e.textContent.split("{{part}}").length-1;((e,t)=>{const n=e.textContent.split("{{part}}"),s=e.parentNode;let o=0;n.forEach((c,r)=>{if(c&&s.insertBefore(document.createTextNode(c),e),r{const e=l();e!==f&&(f=e,i(e))})}else i(l);function i(e){if("object"==typeof e||Array.isArray(e)){for(;a.nextSibling!==u;)s.removeChild(a.nextSibling);const t=Array.isArray(e)?e:[e],n=document.createDocumentFragment();t.forEach(e=>{if(null==e||!1===e)return;const t=e instanceof Node?e:document.createTextNode(e);n.appendChild(t)}),s.insertBefore(n,u)}else{const t=a.nextSibling,n=String(e??"");if(t!==u&&3===t?.nodeType)t.textContent=n;else{for(;a.nextSibling!==u;)s.removeChild(a.nextSibling);s.insertBefore(document.createTextNode(n),u)}}}}}),e.remove()})(e,t.slice(c,c+n)),c+=n}}),o},$p=e=>{const t="page-"+Math.random().toString(36).substring(2,9);return customElements.define(t,class extends HTMLElement{connectedCallback(){this.style.display="contents",this._cleanups=[],currentPageCleanups=this._cleanups;try{const t=e({params:JSON.parse(this.getAttribute("params")||"{}"),onUnmount:e=>this._cleanups.push(e)});this.appendChild(t instanceof Node?t:document.createTextNode(String(t)))}finally{currentPageCleanups=null}}disconnectedCallback(){this._cleanups.forEach(e=>e()),this._cleanups=[],this.innerHTML=""}}),(e={})=>{const n=document.createElement(t);return n.setAttribute("params",JSON.stringify(e)),n}},$c=(e,t,n=[],s=!1)=>{customElements.get(e)||customElements.define(e,class extends HTMLElement{static get observedAttributes(){return n}constructor(){super(),this._propertySignals={},this.cleanupFunctions=[],this._root=s?this.attachShadow({mode:"open"}):this,n.forEach(e=>this._propertySignals[e]=$(void 0))}connectedCallback(){const e=[...this.childNodes];this._root.innerHTML="",n.forEach(e=>{const t=this.hasOwnProperty(e)?this[e]:this.getAttribute(e);Object.defineProperty(this,e,{get:()=>this._propertySignals[e](),set:t=>{const n="false"!==t&&(""===t&&"value"!==e||t);this._propertySignals[e](n)},configurable:!0}),null!=t&&(this[e]=t)});const s={select:e=>this._root.querySelector(e),selectAll:e=>this._root.querySelectorAll(e),slot:t=>e.filter(e=>{const n=1===e.nodeType?e.getAttribute("slot"):null;return t?n===t:!n}),emit:(e,t)=>this.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,composed:!0})),host:this,root:this._root,onUnmount:e=>this.cleanupFunctions.push(e)},o=t(this._propertySignals,s);o instanceof Node&&this._root.appendChild(o)}attributeChangedCallback(e,t,n){this[e]!==n&&(this[e]=n)}disconnectedCallback(){this.cleanupFunctions.forEach(e=>e()),this.cleanupFunctions=[]}})},$f=async(e,t,n)=>{n&&n(!0);try{const n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),s=await n.text();try{return JSON.parse(s)}catch(e){return console.warn("Invalid JSON response"),null}}catch(e){return null}finally{n&&n(!1)}},$r=e=>{const t=document.createElement("div");t.style.display="contents";const n=(e,t)=>{if(!t.includes(":"))return t===e?{}:null;const n=t.split("/"),s=e.split("/");if(n.length!==s.length)return null;const o={};for(let e=0;e{const s=window.location.hash.replace(/^#/,"")||"/";let o=null,c={};for(const t of e){const e=n(s,t.path);if(null!==e){o=t,c=e;break}}const r=o?o.component(c):Object.assign(document.createElement("h1"),{textContent:"404"});t.replaceChildren(r instanceof Node?r:document.createTextNode(String(r??"")))};return window.addEventListener("hashchange",s),s(),t};$r.go=e=>{const t=e.startsWith("/")?e:`/${e}`;window.location.hash!==`#${t}`&&(window.location.hash=t)},$.effect=$e,$.page=$p,$.component=$c,$.fetch=$f,$.router=$r,$.storage=$s,"undefined"!=typeof window&&(window.$=$);export{$,html}; \ No newline at end of file diff --git a/packages/sigpro/sigpro2.js b/packages/sigpro/sigpro2.js deleted file mode 100644 index 25b24fc..0000000 --- a/packages/sigpro/sigpro2.js +++ /dev/null @@ -1,168 +0,0 @@ -/** - * SigPro 2.0 - Complete Reactive Engine - * @author Gemini & User - */ -(() => { - /** @type {Function|null} */ - let activeEffect = null; - - /** - * Creates a Signal (State) or a Computed (Derived state). - * @template T - * @param {T|function():T} initial - Initial value or a computation function. - * @returns {{(newValue?: T|function(T):T): T}} A getter/setter function. - */ - const $ = (initial) => { - /** @type {Set} */ - const subs = new Set(); - - if (typeof initial === 'function') { - let cached; - const runner = () => { - const prev = activeEffect; - activeEffect = runner; - try { cached = initial(); } finally { activeEffect = prev; } - subs.forEach(s => s()); - }; - runner(); - return () => { - if (activeEffect) subs.add(activeEffect); - return cached; - }; - } - - return (...args) => { - if (args.length) { - const next = typeof args[0] === 'function' ? args[0](initial) : args[0]; - if (!Object.is(initial, next)) { - initial = next; - subs.forEach(s => s()); - } - } - if (activeEffect) subs.add(activeEffect); - return initial; - }; - }; - - /** - * Creates a reactive effect. - * @param {function():void} fn - The function to execute reactively. - */ - const _$ = (fn) => { - const effect = () => { - const prev = activeEffect; - activeEffect = effect; - try { fn(); } finally { activeEffect = prev; } - }; - effect(); - return effect; - }; - - /** - * Universal DOM Constructor (Hyperscript). - * @param {string} tag - HTML Tag name. - * @param {Object | HTMLElement | Array | string} [props] - Attributes or children. - * @param {Array | HTMLElement | string | function} [children] - Element children. - * @returns {HTMLElement} - */ - const $$ = (tag, props = {}, children = []) => { - const el = document.createElement(tag); - if (typeof props !== 'object' || props instanceof Node || Array.isArray(props)) { - children = props; props = {}; - } - for (let [key, val] of Object.entries(props)) { - if (key.startsWith('on')) { - el.addEventListener(key.toLowerCase().slice(2), val); - } else if (key.startsWith('$')) { - const attr = key.slice(1); - if ((attr === 'value' || attr === 'checked') && typeof val === 'function') { - const ev = attr === 'checked' ? 'change' : 'input'; - el.addEventListener(ev, e => val(attr === 'checked' ? e.target.checked : e.target.value)); - } - _$(() => { - const v = typeof val === 'function' ? val() : val; - if (attr === 'value' || attr === 'checked') el[attr] = v; - else if (typeof v === 'boolean') el.toggleAttribute(attr, v); - else el.setAttribute(attr, v ?? ''); - }); - } else { - el.setAttribute(key, val); - } - } - - const append = (c) => { - if (Array.isArray(c)) return c.forEach(append); - if (typeof c === 'function') { - const node = document.createTextNode(''); - _$(() => { - const res = c(); - if (res instanceof Node) { if (node.parentNode) node.replaceWith(res); } - else { node.textContent = res ?? ''; } - }); - return el.appendChild(node); - } - el.appendChild(c instanceof Node ? c : document.createTextNode(c ?? '')); - }; - append(children); - return el; - }; - - /** - * Renders the application into a target element. - * @param {HTMLElement | function():HTMLElement} node - * @param {HTMLElement} [target] - */ - const $render = (node, target = document.body) => { - target.innerHTML = ''; - target.appendChild(typeof node === 'function' ? node() : node); - }; - - /** - * Hash-based Reactive Router. - * @param {Array<{path: string, component: function|HTMLElement}>} routes - * @returns {HTMLElement} - */ - const $router = (routes) => { - const sPath = $(window.location.hash.replace(/^#/, "") || "/"); - window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/")); - - return $$('div', { class: "router-view" }, [ - () => { - const current = sPath(); - let params = {}; - const route = routes.find(r => { - const rP = r.path.split('/').filter(Boolean); - const cP = current.split('/').filter(Boolean); - if (rP.length !== cP.length) return false; - return rP.every((part, i) => { - if (part.startsWith(':')) { params[part.slice(1)] = cP[i]; return true; } - return part === cP[i]; - }); - }) || routes.find(r => r.path === "*"); - - if (!route) return $$('h1', '404'); - return typeof route.component === 'function' ? route.component(params) : route.component; - } - ]); - }; - - /** - * Registers a plugin into the SigPro ecosystem. - * @param {string} name - * @param {Object} exports - */ - const $use = (name, exports) => { - Object.assign(window, exports); - console.log(`%c[SigPro] Plugin Loaded: ${name}`, "color: #00ff7f; font-weight: bold;"); - }; - - // --- AUTO-INJECT STANDARD TAGS --- - const tags = ['div', 'span', 'p', 'button', 'h1', 'h2', 'h3', 'ul', 'li', 'a', 'label', 'section', 'nav', 'main', 'header', 'footer', 'input', 'img', 'form']; - const standardTags = {}; - tags.forEach(tag => { - standardTags[tag] = (p, c) => $$(tag, p, c); - }); - - // Global Exports - Object.assign(window, { $, _$, $$, $render, $router, $use, ...standardTags }); -})(); diff --git a/packages/sigpro/sigpro_ui.js b/packages/sigpro/sigpro_ui.js deleted file mode 100644 index 74ecc10..0000000 --- a/packages/sigpro/sigpro_ui.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * SigPro UI 2.0 - daisyUI v5 & Tailwind v4 Plugin - */ -(() => { - if (!window.$) return console.error("[SigPro UI] Fatal: SigPro 2.0 Core not found."); - - $.use(($) => { - const ui = {}; - - const parseClass = (base, extra) => { - if (typeof extra === 'function') return () => `${base} ${extra() || ''}`; - return `${base} ${extra || ''}`; - }; - - /** _button @param {Object} p @param {any} [c] */ - ui._button = (p, c) => button({ - ...p, - class: parseClass('btn', p.$class || p.class), - $disabled: () => p.$disabled?.() || p.$loading?.() - }, [ - () => p.$loading?.() ? span({ class: 'loading loading-spinner' }) : null, - p.icon && span({ class: 'mr-1' }, p.icon), - c, - p.badge && span({ class: `badge ${p.badgeClass || ''}` }, p.badge) - ]); - - /** _input @param {Object} p */ - ui._input = (p) => label({ class: 'fieldset-label flex flex-col gap-1' }, [ - p.label && div({ class: 'flex items-center gap-2' }, [ - span(p.label), - p.tip && div({ class: 'tooltip tooltip-right', 'data-tip': p.tip }, span({ class: 'badge badge-ghost badge-xs' }, '?')) - ]), - $.html('input', { ...p, class: parseClass('input input-bordered w-full', p.$class || p.class), $value: p.$value }), - () => p.$error?.() ? span({ class: 'text-error text-xs' }, p.$error()) : null - ]); - - /** _select @param {Object} p */ - ui._select = (p) => label({ class: 'fieldset-label flex flex-col gap-1' }, [ - p.label && span(p.label), - select({ - ...p, - class: parseClass('select select-bordered', p.$class || p.class), - onchange: (e) => p.$value?.(e.target.value) - }, (p.options || []).map(o => $.html('option', { value: o.value, selected: o.value === p.$value?.() }, o.label))) - ]); - - /** _checkbox @param {Object} p */ - ui._checkbox = (p) => label({ class: 'label cursor-pointer justify-start gap-3' }, [ - $.html('input', { type: 'checkbox', ...p, class: parseClass('checkbox', p.$class || p.class), $checked: p.$value }), - p.label && span({ class: 'label-text' }, p.label) - ]); - - /** _radio @param {Object} p */ - ui._radio = (p) => label({ class: 'label cursor-pointer justify-start gap-3' }, [ - $.html('input', { - type: 'radio', ...p, - class: parseClass('radio', p.$class || p.class), - $checked: () => p.$value?.() === p.value, - onclick: () => p.$value?.(p.value) - }), - p.label && span({ class: 'label-text' }, p.label) - ]); - - /** _range @param {Object} p */ - ui._range = (p) => div({ class: 'flex flex-col gap-2' }, [ - p.label && span({ class: 'label-text' }, p.label), - $.html('input', { type: 'range', ...p, class: parseClass('range', p.$class || p.class), $value: p.$value }) - ]); - - /** _modal @param {Object} p @param {any} c */ - ui._modal = (p, c) => () => p.$open() ? dialog({ class: 'modal modal-open' }, [ - div({ class: 'modal-box' }, [ - p.title && h3({ class: 'text-lg font-bold mb-4' }, p.title), - c, - div({ class: 'modal-action' }, ui._button({ onclick: () => p.$open(false) }, "Cerrar")) - ]), - form({ method: 'dialog', class: 'modal-backdrop', onclick: () => p.$open(false) }, button("close")) - ]) : null; - - /** _dropdown @param {Object} p @param {any} c */ - ui._dropdown = (p, c) => div({ ...p, class: parseClass('dropdown', p.$class || p.class) }, [ - div({ tabindex: 0, role: 'button', class: 'btn m-1' }, p.label), - div({ tabindex: 0, class: 'dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52' }, c) - ]); - - /** _accordion @param {Object} p @param {any} c */ - ui._accordion = (p, c) => div({ class: 'collapse collapse-arrow bg-base-200 mb-2' }, [ - $.html('input', { type: p.name ? 'radio' : 'checkbox', name: p.name, checked: p.open }), - div({ class: 'collapse-title text-xl font-medium' }, p.title), - div({ class: 'collapse-content' }, c) - ]); - - /** _tabs @param {Object} p */ - ui._tabs = (p) => div({ role: 'tablist', class: parseClass('tabs tabs-lifted', p.$class || p.class) }, - (p.items || []).map(it => a({ - role: 'tab', - class: () => `tab ${ (typeof it.active === 'function' ? it.active() : it.active) ? 'tab-active' : '' }`, - onclick: it.onclick - }, it.label)) - ); - - /** _badge @param {Object} p @param {any} c */ - ui._badge = (p, c) => span({ ...p, class: parseClass('badge', p.$class || p.class) }, c); - - /** _tooltip @param {Object} p @param {any} c */ - ui._tooltip = (p, c) => div({ ...p, class: parseClass('tooltip', p.$class || p.class), 'data-tip': p.tip }, c); - - /** _navbar @param {Object} p @param {any} c */ - ui._navbar = (p, c) => div({ ...p, class: parseClass('navbar bg-base-100 shadow-sm px-4', p.$class || p.class) }, c); - - /** _menu @param {Object} p */ - ui._menu = (p) => ul({ ...p, class: parseClass('menu bg-base-200 rounded-box', p.$class || p.class) }, - (p.items || []).map(it => li({}, a({ - class: () => (typeof it.active === 'function' ? it.active() : it.active) ? 'active' : '', - onclick: it.onclick - }, [it.icon && span({class:'mr-2'}, it.icon), it.label]))) - ); - - /** _drawer @param {Object} p */ - ui._drawer = (p) => div({ class: 'drawer' }, [ - $.html('input', { id: p.id, type: 'checkbox', class: 'drawer-toggle', $checked: p.$open }), - div({ class: 'drawer-content' }, p.content), - div({ class: 'drawer-side' }, [ - label({ for: p.id, class: 'drawer-overlay', onclick: () => p.$open?.(false) }), - div({ class: 'min-h-full bg-base-200 w-80' }, p.side) - ]) - ]); - - /** _fieldset @param {Object} p @param {any} c */ - ui._fieldset = (p, c) => fieldset({ ...p, class: parseClass('fieldset bg-base-200 border border-base-300 p-4 rounded-lg', p.$class || p.class) }, [ - p.legend && legend({ class: 'fieldset-legend font-bold' }, p.legend), - c - ]); - - // Expose components to window and $ object - Object.keys(ui).forEach(key => { - window[key] = ui[key]; - $[key] = ui[key]; - }); - }); -})(); diff --git a/plugins/debug.js b/plugins/debug.js new file mode 100644 index 0000000..eb19d8e --- /dev/null +++ b/plugins/debug.js @@ -0,0 +1,41 @@ +/** + * SigPro Debug Plugin + * Reactive state logger for signals and computed values. + */ +export const Debug = ($) => { + /** + * Tracks a signal and logs every state change to the browser console. + * @param {Function} $sig - The reactive signal or computed function to monitor. + * @param {string} [name="Signal"] - A custom label to identify the log entry. + * @example + * const $count = $(0); + * $.debug($count, "Counter"); + * $count(1); // Logs: Counter | Old: 0 | New: 1 + */ + _debug = ($sig, name = "Signal") => { + if (typeof $sig !== 'function') { + return console.warn(`[SigPro Debug] Cannot track "${name}": Not a function/signal.`); + } + + let prev = $sig(); + + $(() => { + const next = $sig(); + + if (Object.is(prev, next)) return; + + console.group(`%c SigPro Debug: ${name} `, "background: #1a1a1a; color: #bada55; font-weight: bold; border-radius: 3px; padding: 2px;"); + + console.log("%c Previous Value:", "color: #ff6b6b; font-weight: bold;", prev); + console.log("%c Current Value: ", "color: #51cf66; font-weight: bold;", next); + + if (next && typeof next === 'object') { + console.table(next); + } + + console.groupEnd(); + + prev = next; + }); + }; +}; \ No newline at end of file diff --git a/plugins/fetch.js b/plugins/fetch.js new file mode 100644 index 0000000..1e7cc90 --- /dev/null +++ b/plugins/fetch.js @@ -0,0 +1,39 @@ +/** + * SigPro Fetch Plugin + * Adds reactive data fetching capabilities to the SigPro instance. + * @param {SigPro} $ - The SigPro core instance. + */ +export const Fetch = ($) => { + /** + * Performs a reactive asynchronous fetch request. + * @param {string} url - The URL of the resource to fetch. + * @param {RequestInit} [options] - Optional settings for the fetch request (method, headers, body, etc.). + * @returns {{ $data: Function, $loading: Function, $error: Function }} + * An object containing reactive signals for the response data, loading state, and error message. + * * @example + * const { $data, $loading, $error } = $.fetch('https://api.example.com/users'); + * return div([ + * () => $loading() ? "Loading..." : ul($data().map(user => li(user.name))), + * () => $error() && span({ class: 'text-red' }, $error()) + * ]); + */ + _fetch = (url, options = {}) => { + const $data = $(null); + const $loading = $(true); + const $error = $(null); + + fetch(url, options) + .then(res => { + if (!res.ok) throw new Error(`HTTP error! Status: ${res.status}`); + return res.json(); + }) + .then(json => $data(json)) + .catch(err => { + console.error("[SigPro Fetch Error]:", err); + $error(err.message); + }) + .finally(() => $loading(false)); + + return { $data, $loading, $error }; + }; +}; \ No newline at end of file diff --git a/plugins/index.js b/plugins/index.js new file mode 100644 index 0000000..1faf31f --- /dev/null +++ b/plugins/index.js @@ -0,0 +1,6 @@ +// /plugins/index.js +export { UI } from './ui.js'; +export { Fetch } from './fetch.js'; +export { Storage } from './storage.js'; +export { Debug } from './debug.js'; +export { Router } from './router.js'; diff --git a/plugins/router.js b/plugins/router.js new file mode 100644 index 0000000..ba8409f --- /dev/null +++ b/plugins/router.js @@ -0,0 +1,44 @@ +// plugins/router.js +export const Router = ($) => { + + $.router = (routes) => { + const sPath = $(window.location.hash.replace(/^#/, "") || "/"); + window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/")); + + return div([ + () => { + const current = sPath(); + const route = routes.find(r => { + const rP = r.path.split('/').filter(Boolean); + const cP = current.split('/').filter(Boolean); + if (rP.length !== cP.length) return false; + return rP.every((part, i) => part.startsWith(':') || part === cP[i]); + }) || routes.find(r => r.path === "*"); + + if (!route) return h1("404 - Not Found"); + + // --- LA MEJORA AQUÍ --- + const result = typeof route.component === 'function' ? route.component() : route.component; + + // Si el componente es una Promesa (Lazy Loading de Vite), esperamos + if (result instanceof Promise) { + const $lazyNode = $(span("Cargando página...")); + result.then(m => { + // Si el módulo tiene un .default (export default), lo usamos + const comp = typeof m === 'function' ? m() : (m.default ? m.default() : m); + $lazyNode(comp); + }); + return () => $lazyNode(); + } + + return result instanceof Node ? result : span(String(result)); + } + ]); + }; + + $.router.go = (path) => { + window.location.hash = path.startsWith('/') ? path : `/${path}`; + }; + + window._router = $.router; +}; \ No newline at end of file diff --git a/plugins/storage.js b/plugins/storage.js new file mode 100644 index 0000000..98a9894 --- /dev/null +++ b/plugins/storage.js @@ -0,0 +1,31 @@ +/** + * SigPro Storage Plugin + * Automatically synchronizes signals with localStorage. + */ +export const Storage = ($) => { + /** + * Persists a signal's value in localStorage. + * @param {Function} $sig - The signal to persist. + * @param {string} key - The localStorage key name. + * @returns {Function} The same signal for chaining. + */ + _storage = ($sig, key) => { + // 1. Initial Load: If there's data in storage, update the signal immediately + const saved = localStorage.getItem(key); + if (saved !== null) { + try { + $sig(JSON.parse(saved)); + } catch (e) { + console.error(`[SigPro Storage] Error parsing key "${key}":`, e); + } + } + + // 2. Auto-Save: Every time the signal changes, update localStorage + $(() => { + const val = $sig(); + localStorage.setItem(key, JSON.stringify(val)); + }); + + return $sig; + }; +}; \ No newline at end of file diff --git a/plugins/ui.js b/plugins/ui.js new file mode 100644 index 0000000..b73d62d --- /dev/null +++ b/plugins/ui.js @@ -0,0 +1,207 @@ +/** + * SigPro UI 2.0 - daisyUI v5 & Tailwind v4 Plugin + * Provides a set of reactive functional components. + */ + +export const UI = ($) => { + const ui = {}; + + /** + * Internal helper to merge base classes with reactive or static extra classes. + * @param {string} base - The default daisyUI class. + * @param {string|function} extra - User-provided classes. + * @returns {string|function} Merged classes. + */ + const parseClass = (base, extra) => { + if (typeof extra === 'function') return () => `${base} ${extra() || ''}`; + return `${base} ${extra || ''}`; + }; + + /** + * Standard Button component. + * @param {Object} p - Properties. + * @param {string|function} [p.class] - Extra CSS classes. + * @param {function} [p.$loading] - Reactive loading state. + * @param {function} [p.$disabled] - Reactive disabled state. + * @param {HTMLElement|string} [p.icon] - Leading icon. + * @param {string} [p.badge] - Badge text. + * @param {any} c - Children content. + */ + ui._button = (p, c) => button({ + ...p, + class: parseClass('btn', p.$class || p.class), + $disabled: () => p.$disabled?.() || p.$loading?.() + }, [ + () => p.$loading?.() ? span({ class: 'loading loading-spinner' }) : null, + p.icon && span({ class: 'mr-1' }, p.icon), + c, + p.badge && span({ class: `badge ${p.badgeClass || ''}` }, p.badge) + ]); + + /** + * Form Input with label, tooltip, and error handling. + * @param {Object} p - Input properties. + * @param {string} [p.label] - Field label. + * @param {string} [p.tip] - Tooltip text. + * @param {function} [p.$value] - Reactive signal for the value. + * @param {function} [p.$error] - Reactive signal for error messages. + */ + ui._input = (p) => label({ class: 'fieldset-label flex flex-col gap-1' }, [ + p.label && div({ class: 'flex items-center gap-2' }, [ + span(p.label), + p.tip && div({ class: 'tooltip tooltip-right', 'data-tip': p.tip }, + span({ class: 'badge badge-ghost badge-xs' }, '?')) + ]), + $.html('input', { + ...p, + class: parseClass('input input-bordered w-full', p.$class || p.class), + $value: p.$value + }), + () => p.$error?.() ? span({ class: 'text-error text-xs' }, p.$error()) : null + ]); + + /** + * Select dropdown component. + * @param {Object} p - Select properties. + * @param {Array<{value: any, label: string}>} p.options - Array of options. + * @param {function} p.$value - Reactive signal for the selected value. + */ + ui._select = (p) => label({ class: 'fieldset-label flex flex-col gap-1' }, [ + p.label && span(p.label), + select({ + ...p, + class: parseClass('select select-bordered', p.$class || p.class), + onchange: (e) => p.$value?.(e.target.value) + }, (p.options || []).map(o => + $.html('option', { value: o.value, selected: o.value === p.$value?.() }, o.label)) + ) + ]); + + /** + * Checkbox component. + */ + ui._checkbox = (p) => label({ class: 'label cursor-pointer justify-start gap-3' }, [ + $.html('input', { type: 'checkbox', ...p, class: parseClass('checkbox', p.$class || p.class), $checked: p.$value }), + p.label && span({ class: 'label-text' }, p.label) + ]); + + /** + * Radio button component. + */ + ui._radio = (p) => label({ class: 'label cursor-pointer justify-start gap-3' }, [ + $.html('input', { + type: 'radio', ...p, + class: parseClass('radio', p.$class || p.class), + $checked: () => p.$value?.() === p.value, + onclick: () => p.$value?.(p.value) + }), + p.label && span({ class: 'label-text' }, p.label) + ]); + + /** + * Range slider component. + */ + ui._range = (p) => div({ class: 'flex flex-col gap-2' }, [ + p.label && span({ class: 'label-text' }, p.label), + $.html('input', { type: 'range', ...p, class: parseClass('range', p.$class || p.class), $value: p.$value }) + ]); + + /** + * Modal dialog component. + * @param {Object} p - Modal properties. + * @param {function} p.$open - Reactive signal (boolean) to control visibility. + * @param {any} c - Modal body content. + */ + ui._modal = (p, c) => () => p.$open() ? dialog({ class: 'modal modal-open' }, [ + div({ class: 'modal-box' }, [ + p.title && h3({ class: 'text-lg font-bold mb-4' }, p.title), + c, + div({ class: 'modal-action' }, ui._button({ onclick: () => p.$open(false) }, "Close")) + ]), + form({ method: 'dialog', class: 'modal-backdrop', onclick: () => p.$open(false) }, button("close")) + ]) : null; + + /** + * Dropdown menu component. + */ + ui._dropdown = (p, c) => div({ ...p, class: parseClass('dropdown', p.$class || p.class) }, [ + div({ tabindex: 0, role: 'button', class: 'btn m-1' }, p.label), + div({ tabindex: 0, class: 'dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52' }, c) + ]); + + /** + * Accordion/Collapse component. + */ + ui._accordion = (p, c) => div({ class: 'collapse collapse-arrow bg-base-200 mb-2' }, [ + $.html('input', { type: p.name ? 'radio' : 'checkbox', name: p.name, checked: p.open }), + div({ class: 'collapse-title text-xl font-medium' }, p.title), + div({ class: 'collapse-content' }, c) + ]); + + /** + * Tabs navigation component. + * @param {Object} p - Tab properties. + * @param {Array<{label: string, active: boolean|function, onclick: function}>} p.items - Tab items. + */ + ui._tabs = (p) => div({ role: 'tablist', class: parseClass('tabs tabs-lifted', p.$class || p.class) }, + (p.items || []).map(it => a({ + role: 'tab', + class: () => `tab ${(typeof it.active === 'function' ? it.active() : it.active) ? 'tab-active' : ''}`, + onclick: it.onclick + }, it.label)) + ); + + /** + * Badge component. + */ + ui._badge = (p, c) => span({ ...p, class: parseClass('badge', p.$class || p.class) }, c); + + /** + * Tooltip wrapper. + */ + ui._tooltip = (p, c) => div({ ...p, class: parseClass('tooltip', p.$class || p.class), 'data-tip': p.tip }, c); + + /** + * Navbar component. + */ + ui._navbar = (p, c) => div({ ...p, class: parseClass('navbar bg-base-100 shadow-sm px-4', p.$class || p.class) }, c); + + /** + * Vertical Menu component. + */ + ui._menu = (p) => ul({ ...p, class: parseClass('menu bg-base-200 rounded-box', p.$class || p.class) }, + (p.items || []).map(it => li({}, a({ + class: () => (typeof it.active === 'function' ? it.active() : it.active) ? 'active' : '', + onclick: it.onclick + }, [it.icon && span({ class: 'mr-2' }, it.icon), it.label]))) + ); + + /** + * Sidebar Drawer component. + */ + ui._drawer = (p) => div({ class: 'drawer' }, [ + $.html('input', { id: p.id, type: 'checkbox', class: 'drawer-toggle', $checked: p.$open }), + div({ class: 'drawer-content' }, p.content), + div({ class: 'drawer-side' }, [ + label({ for: p.id, class: 'drawer-overlay', onclick: () => p.$open?.(false) }), + div({ class: 'min-h-full bg-base-200 w-80' }, p.side) + ]) + ]); + + /** + * Fieldset wrapper with legend. + */ + ui._fieldset = (p, c) => fieldset({ + ...p, + class: parseClass('fieldset bg-base-200 border border-base-300 p-4 rounded-lg', p.$class || p.class) + }, [ + p.legend && legend({ class: 'fieldset-legend font-bold' }, p.legend), + c + ]); + + // Expose components globally and to the SigPro instance + Object.keys(ui).forEach(key => { + window[key] = ui[key]; + $[key] = ui[key]; + }); +}; \ No newline at end of file diff --git a/sigpro/sigpro.js b/sigpro/sigpro.js new file mode 100644 index 0000000..32019c6 --- /dev/null +++ b/sigpro/sigpro.js @@ -0,0 +1,162 @@ +/** + * SigPro 2.0 - Atomic Unified Reactive Engine + * A lightweight, fine-grained reactivity system with built-in routing and plugin support. + * @author Gemini & User + */ +(() => { + /** @type {Function|null} Internal tracker for the currently executing reactive effect. */ + let activeEffect = null; + + /** + * @typedef {Object} SigPro + * @property {function(string, Object=, any=): HTMLElement} html - Creates a reactive HTML element. + * @property {function((HTMLElement|function), (HTMLElement|string)=): void} mount - Mounts a component to the DOM. + * @property {function(Array): HTMLElement} router - Initializes a hash-based router. + * @property {function(string): void} router.go - Programmatic navigation to a hash path. + * @property {function((function|string|Array)): (Promise|SigPro)} plugin - Extends SigPro or loads external scripts. + */ + + /** + * Creates a Signal (state) or a Computed/Effect (reaction). + * @param {any|function} initial - Initial value for a signal, or a function for computed logic. + * @returns {Function} A reactive accessor/mutator function. + * @example + * const $count = $(0); // Signal: $count(5) to update, $count() to read. + * const $double = $(() => $count() * 2); // Computed: Auto-updates when $count changes. + */ + const $ = (initial) => { + const subs = new Set(); + + // Logic for Computed Signals (Functions) + if (typeof initial === 'function') { + let cached; + const runner = () => { + const prev = activeEffect; + activeEffect = runner; + try { + const next = initial(); + if (!Object.is(cached, next)) { + cached = next; + subs.forEach(s => s()); + } + } finally { activeEffect = prev; } + }; + runner(); + return () => { + if (activeEffect) subs.add(activeEffect); + return cached; + }; + } + + // Logic for Standard Signals (State) + return (...args) => { + if (args.length) { + const next = typeof args[0] === 'function' ? args[0](initial) : args[0]; + if (!Object.is(initial, next)) { + initial = next; + subs.forEach(s => s()); + } + } + if (activeEffect) subs.add(activeEffect); + return initial; + }; + }; + + /** + * Hyperscript engine to render reactive HTML nodes. + * @param {string} tag - The HTML tag name (e.g., 'div', 'button'). + * @param {Object} [props] - Attributes, events (onclick), or reactive props ($value, $class). + * @param {any} [content] - String, Node, Array of nodes, or reactive function. + * @returns {HTMLElement} A live DOM element linked to SigPro signals. + */ + $.html = (tag, props = {}, content = []) => { + const el = document.createElement(tag); + if (typeof props !== 'object' || props instanceof Node || Array.isArray(props) || typeof props === 'function') { + content = props; + props = {}; + } + + for (let [key, val] of Object.entries(props)) { + if (key.startsWith('on')) { + el.addEventListener(key.toLowerCase().slice(2), val); + } else if (key.startsWith('$')) { + const attr = key.slice(1); + // Two-way binding for inputs + if ((attr === 'value' || attr === 'checked') && typeof val === 'function') { + const ev = attr === 'checked' ? 'change' : 'input'; + el.addEventListener(ev, e => val(attr === 'checked' ? e.target.checked : e.target.value)); + } + // Reactive attribute update + $(() => { + const v = typeof val === 'function' ? val() : val; + if (attr === 'value' || attr === 'checked') el[attr] = v; + else if (typeof v === 'boolean') el.toggleAttribute(attr, v); + else el.setAttribute(attr, v ?? ''); + }); + } else el.setAttribute(key, val); + } + + const append = (c) => { + if (Array.isArray(c)) return c.forEach(append); + if (typeof c === 'function') { + const node = document.createTextNode(''); + $(() => { + const res = c(); + if (res instanceof Node) { + if (node.parentNode) node.replaceWith(res); + } else { + node.textContent = res ?? ''; + } + }); + return el.appendChild(node); + } + el.appendChild(c instanceof Node ? c : document.createTextNode(c ?? '')); + }; + append(content); + return el; + }; + + /** + * Application mounter. + * @param {HTMLElement|function} node - Root component or element to mount. + * @param {HTMLElement|string} [target=document.body] - Target element or CSS selector. + */ + $.mount = (node, target = document.body) => { + const el = typeof target === 'string' ? document.querySelector(target) : target; + if (el) { + el.innerHTML = ''; + el.appendChild(typeof node === 'function' ? node() : node); + } + }; + + /** + * Polymorphic Plugin System. + * Registers internal functions or loads external .js files as plugins. + * @param {function|string|Array} source - Plugin function or URL(s). + * @returns {Promise|SigPro} Resolves with the $ instance after loading or registering. + */ + $.plugin = (source) => { + if (typeof source === 'function') { + source($); + return $; + } + const urls = Array.isArray(source) ? source : [source]; + return Promise.all(urls.map(url => new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = url; + script.async = true; + script.onload = () => { + console.log(`%c[SigPro] Plugin Loaded: ${url}`, "color: #51cf66; font-weight: bold;"); + resolve(); + }; + script.onerror = () => reject(new Error(`[SigPro] Failed to load: ${url}`)); + document.head.appendChild(script); + }))).then(() => $); + }; + + // Global HTML Tag Proxy Helpers + const tags = ['div', 'span', 'p', 'button', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'a', 'label', 'section', 'nav', 'main', 'header', 'footer', 'input', 'form', 'img', 'select', 'option', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'canvas', 'video', 'audio']; + tags.forEach(t => window[t] = (p, c) => $.html(t, p, c)); + + window.$ = $; +})(); \ No newline at end of file diff --git a/packages/docs/.vitepress/cache/deps/@theme_index.js b/src/docs/.vitepress/cache/deps/@theme_index.js similarity index 100% rename from packages/docs/.vitepress/cache/deps/@theme_index.js rename to src/docs/.vitepress/cache/deps/@theme_index.js diff --git a/packages/docs/.vitepress/cache/deps/@theme_index.js.map b/src/docs/.vitepress/cache/deps/@theme_index.js.map similarity index 100% rename from packages/docs/.vitepress/cache/deps/@theme_index.js.map rename to src/docs/.vitepress/cache/deps/@theme_index.js.map diff --git a/packages/docs/.vitepress/cache/deps/_metadata.json b/src/docs/.vitepress/cache/deps/_metadata.json similarity index 82% rename from packages/docs/.vitepress/cache/deps/_metadata.json rename to src/docs/.vitepress/cache/deps/_metadata.json index 93e3b3e..9d2241e 100644 --- a/packages/docs/.vitepress/cache/deps/_metadata.json +++ b/src/docs/.vitepress/cache/deps/_metadata.json @@ -1,31 +1,31 @@ { - "hash": "33e82b21", - "configHash": "c6db372a", + "hash": "0412844f", + "configHash": "5a0057cf", "lockfileHash": "e3b0c442", - "browserHash": "66861689", + "browserHash": "d3515516", "optimized": { "vue": { "src": "../../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js", "file": "vue.js", - "fileHash": "415ad31e", + "fileHash": "5e2bcecf", "needsInterop": false }, "vitepress > @vue/devtools-api": { "src": "../../../../../node_modules/@vue/devtools-api/dist/index.js", "file": "vitepress___@vue_devtools-api.js", - "fileHash": "82c7da90", + "fileHash": "604942f7", "needsInterop": false }, "vitepress > @vueuse/core": { "src": "../../../../../node_modules/@vueuse/core/index.mjs", "file": "vitepress___@vueuse_core.js", - "fileHash": "bf2a3493", + "fileHash": "f08e5a15", "needsInterop": false }, "@theme/index": { "src": "../../../../../node_modules/vitepress/dist/client/theme-default/index.js", "file": "@theme_index.js", - "fileHash": "0d87b191", + "fileHash": "442c9e5b", "needsInterop": false } }, diff --git a/packages/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js b/src/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js similarity index 100% rename from packages/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js rename to src/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js diff --git a/packages/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js.map b/src/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js.map similarity index 100% rename from packages/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js.map rename to src/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js.map diff --git a/packages/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js b/src/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js similarity index 100% rename from packages/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js rename to src/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js diff --git a/packages/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js.map b/src/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js.map similarity index 100% rename from packages/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js.map rename to src/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js.map diff --git a/packages/docs/.vitepress/cache/deps/package.json b/src/docs/.vitepress/cache/deps/package.json similarity index 100% rename from packages/docs/.vitepress/cache/deps/package.json rename to src/docs/.vitepress/cache/deps/package.json diff --git a/packages/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js b/src/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js similarity index 100% rename from packages/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js rename to src/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js diff --git a/packages/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map b/src/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map similarity index 100% rename from packages/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map rename to src/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map diff --git a/packages/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js b/src/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js similarity index 100% rename from packages/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js rename to src/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js diff --git a/packages/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map b/src/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map similarity index 100% rename from packages/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map rename to src/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map diff --git a/packages/docs/.vitepress/cache/deps/vue.js b/src/docs/.vitepress/cache/deps/vue.js similarity index 100% rename from packages/docs/.vitepress/cache/deps/vue.js rename to src/docs/.vitepress/cache/deps/vue.js diff --git a/packages/docs/.vitepress/cache/deps/vue.js.map b/src/docs/.vitepress/cache/deps/vue.js.map similarity index 100% rename from packages/docs/.vitepress/cache/deps/vue.js.map rename to src/docs/.vitepress/cache/deps/vue.js.map diff --git a/packages/docs/.vitepress/config.js b/src/docs/.vitepress/config.js similarity index 64% rename from packages/docs/.vitepress/config.js rename to src/docs/.vitepress/config.js index 0baab27..9194ee0 100644 --- a/packages/docs/.vitepress/config.js +++ b/src/docs/.vitepress/config.js @@ -38,13 +38,22 @@ export default defineConfig({ text: 'API Reference', items: [ { text: 'Quick Start', link: '/api/quick' }, - { text: 'Signals', link: '/api/signals' }, - { text: 'Effects', link: '/api/effects' }, - { text: 'Storage', link: '/api/storage' }, - { text: 'Fetch', link: '/api/fetch' }, - { text: 'Pages', link: '/api/pages' }, - { text: 'Components', link: '/api/components' }, - { text: 'Routing', link: '/api/routing' }, + { text: '$', link: '/api/$' }, + { text: '$.html', link: '/api/html' }, + { text: '$.mount', link: '/api/mount' }, + { text: 'Tags', link: '/api/tags' }, + ] + }, + { + text: 'Plugins', + items: [ + { text: 'Quick Start', link: '/plugins/quick' }, + { text: '@core Router Plugin', link: '/plugins/core.router' }, + { text: '@core UI Plugin', link: '/plugins/core.ui' }, + { text: '@core UI Fetch', link: '/plugins/core.fetch' }, + { text: '@core UI Storage', link: '/plugins/core.storage' }, + { text: '@core UI Debug', link: '/plugins/core.debug' }, + { text: 'Custom', link: '/plugins/custom' }, ] }, { diff --git a/src/docs/api/$.md b/src/docs/api/$.md new file mode 100644 index 0000000..61ed572 --- /dev/null +++ b/src/docs/api/$.md @@ -0,0 +1,101 @@ +# The Reactive Core: `$( )` + +The `$` function is the heart of **SigPro**. It is a **Unified Reactive Constructor** that handles state, derivations, and side effects through a single, consistent interface. + +## 1. The Constructor: `$( input )` + +Depending on what you pass into `$( )`, SigPro creates a different type of reactive primitive: + +| Input Type | Result | Internal Behavior | +| :--- | :--- | :--- | +| **Value** (String, Number, Object...) | **Signal** | Creates a piece of mutable state. | +| **Function** | **Computed / Effect** | Creates a derived value that tracks dependencies. | + +--- + +## 2. Signal (State) + +A **Signal** is a "box" that holds a value. It provides a getter/setter function to interact with that value. + +* **When to use:** For data that changes over time (counters, user input, toggle states, API data). +* **Syntax:** `const $state = $(initialValue);` + +### Example: +```javascript +const $name = $("Alice"); + +// Read the value (Getter) +console.log($name()); // "Alice" + +// Update the value (Setter) +$name("Bob"); + +// Update based on previous value +$name(current => current + " Smith"); +``` + +--- + +## 3. Computed (Derived State) + +When you pass a **function** to `$( )` that **returns a value**, SigPro creates a **Computed Signal**. It automatically tracks which signals are used inside it and re-runs only when they change. + +* **When to use:** For values that depend on other signals (totals, filtered lists, formatted strings). +* **Syntax:** `const $derived = $(() => logic);` + +### Example: +```javascript +const $price = $(100); +const $qty = $(2); + +// Automatically tracks $price and $qty +const $total = $(() => $price() * $qty()); + +console.log($total()); // 200 + +$qty(3); // $total updates to 300 automatically +``` + +--- + +## 4. Effects (Side Effects) + +An **Effect** is a function passed to `$( )` that **does not return a value** (or returns `undefined`). SigPro treats this as a subscription that performs an action whenever its dependencies change. + +* **When to use:** For DOM manipulations, logging, or syncing with external APIs (LocalStorage, Fetch). +* **Syntax:** `$(() => { action });` + +### Example: +```javascript +const $theme = $("light"); + +// This effect runs every time $theme changes +$(() => { + document.body.className = $theme(); + console.log("Theme updated to:", $theme()); +}); + +$theme("dark"); // Logs: Theme updated to: dark +``` + +--- + +## 5. Summary Table: Usage Guide + +| Primitive | Logic Type | Returns Value? | Typical Use Case | +| :--- | :--- | :--- | :--- | +| **Signal** | Static | Yes (Mutable) | `const $user = $("Guest")` | +| **Computed** | Read-only | Yes (Automatic) | `const $isLoggedIn = $(() => $user() !== "Guest")` | +| **Effect** | Imperative | No | `$(() => localStorage.setItem('user', $user()))` | + + + +--- + +## 💡 Pro Tip: Naming Convention +In SigPro, we use the **`$` prefix** (e.g., `$count`) for variables that hold a reactive function. This makes it easy to distinguish between a standard variable and a reactive one at a glance: + +```javascript +let count = 0; // Static +const $count = $(0); // Reactive (Function) +``` diff --git a/src/docs/api/html.md b/src/docs/api/html.md new file mode 100644 index 0000000..21172b6 --- /dev/null +++ b/src/docs/api/html.md @@ -0,0 +1,103 @@ +# Rendering Engine: `$.html` + +The `$.html` function is the architect of your UI. It creates standard HTML elements and wires them directly to your signals without the need for a Virtual DOM. + +## 1. Syntax: `$.html(tag, [props], [content])` + +| Parameter | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **tag** | `string` | **Yes** | Any valid HTML5 tag (e.g., `'div'`, `'button'`, `'input'`). | +| **props** | `Object` | No | Attributes, event listeners, and reactive bindings. | +| **content** | `any` | No | Text, Nodes, Arrays, or Reactive Functions. | + +### Example: +```javascript +const myButton = $.html('button', { class: 'btn-primary' }, 'Click me'); +``` + +--- + +## 2. Global Tag Helpers + +To avoid repetitive `$.html` calls, SigPro automatically exposes common tags to the global `window` object. This allows for a clean, declarative syntax. + +```javascript +// Instead of $.html('div', ...), just use: +div({ id: 'wrapper' }, [ + h1("Welcome"), + p("This is SigPro.") +]); +``` + +--- + +## 3. Handling Properties & Attributes + +SigPro distinguishes between static attributes and reactive bindings using the **`$` prefix**. + +### Static vs. Reactive Attributes +* **Static:** Applied once during creation. +* **Reactive (`$`):** Automatically updates the DOM when the signal changes. + +| Property | Syntax | Result | +| :--- | :--- | :--- | +| **Attribute** | `{ id: 'main' }` | `id="main"` | +| **Event** | `{ onclick: fn }` | Adds an event listener. | +| **Reactive Attr** | `{ $class: $theme }` | Updates `class` whenever `$theme()` changes. | +| **Boolean Attr** | `{ $disabled: $isBusy }` | Toggles the `disabled` attribute automatically. | + + + +--- + +## 4. Two-Way Data Binding + +For form inputs, SigPro provides a powerful shortcut using `$value` or `$checked`. It automatically handles the event listening and the value synchronization. + +```javascript +const $text = $("Type here..."); + +input({ + type: 'text', + $value: $text // Syncs input -> signal and signal -> input +}); + +p(["You typed: ", $text]); +``` + +--- + +## 5. Reactive Content (Dynamic Children) + +The `content` argument is incredibly flexible. If you pass a **function**, SigPro treats it as a reactive "portal" that re-renders only that specific part of the DOM. + +### Text & Nodes +```javascript +const $count = $(0); + +// Text node updates surgically +div(["Count: ", $count]); + +// Conditional rendering with a function +div(() => { + return $count() > 10 + ? h1("High Score!") + : p("Keep going..."); +}); +``` + +### The "Guillotine" (Performance Tip) +When a reactive function in the content returns a **new Node**, SigPro uses `replaceWith()` to swap the old node for the new one. This ensures that: +1. The update is nearly instantaneous. +2. The old node is correctly garbage-collected. + +--- + +## 6. Summary: Content Types + +| Input | Behavior | +| :--- | :--- | +| **String / Number** | Appended as a TextNode. | +| **HTMLElement** | Appended directly to the parent. | +| **Array** | Each item is processed and appended in order. | +| **Function `() => ...`** | Creates a **live reactive zone** that updates automatically. | diff --git a/src/docs/api/mount.md b/src/docs/api/mount.md new file mode 100644 index 0000000..d095da4 --- /dev/null +++ b/src/docs/api/mount.md @@ -0,0 +1,108 @@ +# Application Mounter: `$.mount` + +The `$.mount` function is the entry point of your reactive world. It takes a **SigPro component** (or a plain DOM node) and injects it into the real document. + +## 1. Syntax: `$.mount(node, [target])` + +| Parameter | Type | Default | Description | +| :--- | :--- | :--- | :--- | +| **node** | `HTMLElement` or `Function` | **Required** | The component or element to render. | +| **target** | `string` or `HTMLElement` | `document.body` | Where to mount the app (CSS selector or Element). | + +--- + +## 2. Usage Scenarios + +### A. The "Clean Slate" (Main Entry) +In a modern app (like our `main.js` example), you usually want to control the entire page. By default, `$.mount` clears the target's existing HTML before mounting. + +```javascript +// src/main.js +import { $ } from 'SigPro'; +import App from './App.js'; + +$.mount(App); // Mounts to by default +``` + +### B. Targeting a Specific Container +If you have an existing HTML structure and only want **SigPro** to manage a specific part (like a `#root` div), pass a CSS selector or a reference. + +```html + +
+``` + +```javascript +// Local mount to a specific ID +$.mount(MyComponent, '#app-root'); + +// Or using a direct DOM reference +const sidebar = document.getElementById('sidebar'); +$.mount(SidebarComponent, sidebar); +``` + +--- + +## 3. Mounting with Pure HTML +One of SigPro's strengths is that it works perfectly alongside "Old School" HTML. You can create a reactive "island" inside a static page. + +```javascript +// A small reactive widget in a static .js file +const CounterWidget = () => { + const $c = $(0); + return button({ onclick: () => $c(v => v + 1) }, [ + "Clicks: ", $c + ]); +}; + +// Mount it into an existing div in your HTML +$.mount(CounterWidget, '#counter-container'); +``` + +--- + +## 4. How it Works (The "Wipe" Logic) +When `$.mount` is called, it performs two critical steps: + +1. **Clearance:** It sets `target.innerHTML = ''`. This ensures no "zombie" HTML from previous renders or static placeholders interferes with your app. +2. **Injection:** It appends your component. If you passed a **Function**, it executes it first to get the DOM node. + + + +--- + +## 5. Global vs. Local Scope + +### Global (The "Framework" Way) +In a standard Vite/ESM project, you initialize SigPro globally in `main.js`. This makes the `$` and the tag helpers (`div`, `button`, etc.) available everywhere in your project. + +```javascript +// main.js - Global Initialization +import 'SigPro'; + +// Now any other file can just use: +$.mount(() => h1("Global App")); +``` + +### Local (The "Library" Way) +If you are worried about polluting the global `window` object, you can import and use SigPro locally within a specific module. + +```javascript +// widget.js - Local usage +import { $ } from 'SigPro'; + +const myNode = $.html('div', 'Local Widget'); +$.mount(myNode, '#widget-target'); +``` + +--- + +### Summary Cheat Sheet + +| Goal | Code | +| :--- | :--- | +| **Mount to body** | `$.mount(App)` | +| **Mount to ID** | `$.mount(App, '#id')` | +| **Mount to Element** | `$.mount(App, myElement)` | +| **Reactive Widget** | `$.mount(() => div("Hi"), '#widget')` | + diff --git a/src/docs/api/quick.md b/src/docs/api/quick.md new file mode 100644 index 0000000..6984415 --- /dev/null +++ b/src/docs/api/quick.md @@ -0,0 +1,99 @@ +# Quick API Reference ⚡ + +This is a high-level summary of the **SigPro** core API. For detailed guides and edge cases, please refer to the specific documentation for each module. + +## 1. Core Reactivity: `$( )` + +The `$` function is a polymorphic constructor. It creates **Signals** (state) or **Computed Effects** (logic) based on the input type. + +| Usage | Input Type | Returns | Description | +| :--- | :--- | :--- | :--- | +| **Signal** | `any` | `Function` | A getter/setter for reactive state. | +| **Computed** | `Function` | `Function` | A read-only signal that auto-updates when its dependencies change. | + +**Example:** +```javascript +const $count = $(0); // Signal +const $double = $(() => $count() * 2); // Computed +``` + +--- + +## 2. Rendering Engine: `$.html` + +SigPro uses a hyperscript-style engine to create live DOM nodes. + +| Argument | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **tag** | `string` | Yes | Standard HTML tag (e.g., 'div', 'button'). | +| **props** | `Object` | No | Attributes (`id`), Events (`onclick`), or Reactive Props (`$value`). | +| **content** | `any` | No | String, Node, Array, or Reactive Function. | + +**Example:** +```javascript +$.html('button', { onclick: () => alert('Hi!') }, 'Click Me'); +``` + +--- + +## 3. Global Helpers (Tag Proxies) + +To keep your code clean, SigPro automatically exposes common HTML tags to the global scope. + +| Category | Available Tags | +| :--- | :--- | +| **Layout** | `div`, `section`, `main`, `nav`, `header`, `footer`, `span` | +| **Typography** | `h1`, `h2`, `h3`, `p`, `label`, `a`, `li`, `ul`, `ol` | +| **Forms** | `input`, `button`, `form`, `select`, `option` | +| **Media** | `img`, `video`, `audio`, `canvas` | + +**Example:** +```javascript +// No imports needed! +div([ + h1("Title"), + button("Ok") +]); +``` + +--- + +## 4. Mounting & Plugins + +Methods to initialize your application and extend the engine. + +| Method | Signature | Description | +| :--- | :--- | :--- | +| **`$.mount`** | `(node, target)` | Wipes the target (default: `body`) and renders the component. | +| **`$.plugin`** | `(source)` | Registers a function or loads external `.js` scripts as plugins. | + +**Example:** +```javascript +$.plugin([UI, Router]); +$.mount(App, '#root'); +``` + +--- + +## 5. Reactive Syntax Cheat Sheet + +| Feature | Syntax | Description | +| :--- | :--- | :--- | +| **Text Binding** | `p(["Value: ", $sig])` | Updates text content automatically. | +| **Attributes** | `div({ id: $sig })` | Static attribute assignment. | +| **Reactive Attr** | `div({ $class: $sig })` | Attribute updates when `$sig` changes. | +| **Two-way Binding**| `input({ $value: $sig })`| Syncs input value and signal automatically. | +| **Conditional** | `div(() => $sig() > 0 ? "Yes" : "No")` | Re-renders only the content when the condition changes. | + +--- + + + +## Summary Table + +| Feature | SigPro Approach | Benefit | +| :--- | :--- | :--- | +| **Update Logic** | Fine-grained (Surgical) | Blazing fast updates. | +| **DOM** | Native Nodes | Zero abstraction cost. | +| **Syntax** | Pure JavaScript | No build-tool lock-in. | +| **Footprint** | Modular | Load only what you use. | \ No newline at end of file diff --git a/src/docs/api/tags.md b/src/docs/api/tags.md new file mode 100644 index 0000000..bfa2ccd --- /dev/null +++ b/src/docs/api/tags.md @@ -0,0 +1,160 @@ +# Global Tag Helpers + +In **SigPro**, you don't need to write `$.html('div', ...)` every time. To keep your code clean and readable, the engine automatically generates global helper functions for all standard HTML tags. + +## 1. How it Works + +When SigPro initializes, it runs a proxy loop that creates a function for every common HTML tag and attaches it to the `window` object. + +* **Traditional:** `$.html('button', { onclick: ... }, 'Click')` +* **SigPro Style:** `button({ onclick: ... }, 'Click')` + +This approach gives you a "DSL" (Domain Specific Language) that feels like HTML but is actually **pure JavaScript**. + +--- + +## 2. The Global Registry + +The following tags are available globally by default: + +| Category | Available Functions | +| :--- | :--- | +| **Layout** | `div`, `span`, `section`, `main`, `nav`, `header`, `footer`, `article`, `aside` | +| **Typography** | `h1`, `h2`, `h3`, `p`, `ul`, `ol`, `li`, `a`, `label`, `strong`, `em` | +| **Forms** | `form`, `input`, `button`, `select`, `option`, `textarea` | +| **Table** | `table`, `thead`, `tbody`, `tr`, `th`, `td` | +| **Media** | `img`, `video`, `audio`, `canvas`, `svg` | + +--- + +## 3. Usage Patterns + +The tag functions are highly flexible and accept arguments in different orders to suit your coding style. + +### A. Attributes + Content +The most common pattern. +```javascript +div({ class: 'card' }, [ + h1("Title"), + p("Description") +]); +``` + +### B. Content Only +If you don't need attributes, you can skip the object entirely. +```javascript +div([ + h1("Just Content"), + p("No attributes object needed here.") +]); +``` + +### C. Simple Text +For elements that only contain a string. +```javascript +button("Submit"); // Equivalent to +``` + +--- + +## 4. Reactive Tags + +Since these helpers are just wrappers around `$.html`, they support full reactivity out of the box. + +```javascript +const $loading = $(true); + +div([ + $loading() ? span("Loading...") : h1("Data Ready!"), + button({ + $disabled: $loading, // Reactive attribute + onclick: () => $loading(false) + }, "Stop Loading") +]); +``` + +--- + +## 5. Under the Hood + +If you are curious about how this happens without a compiler, here is the logic inside the SigPro core: + +```javascript +const tags = ['div', 'span', 'p', 'button', ...]; + +tags.forEach(tag => { + window[tag] = (props, content) => $.html(tag, props, content); +}); +``` + +Because these are attached to `window`, they are available in any file in your project as soon as SigPro is loaded, making your components look like this: + +```javascript +// No imports required for tags! +export default () => + section({ id: 'hero' }, [ + h1("Fast. Atomic. Simple."), + p("Built with SigPro.") + ]); +``` + +--- + +## 6. Full Comparison: SigPro vs. Standard HTML + +To better understand the translation, here is a complete example of a **User Card** component. Notice how **SigPro** attributes with the `$` prefix map to reactive behavior, while standard attributes remain static. + +::: code-group +```javascript [SigPro (JS)] +const $online = $(true); + +export const UserCard = () => ( + div({ class: 'user-card' }, [ + img({ src: 'avatar.png', alt: 'User' }), + + div({ class: 'info' }, [ + h2("John Doe"), + p({ + $class: () => $online() ? 'status-on' : 'status-off' + }, [ + "Status: ", + () => $online() ? "Online" : "Offline" + ]) + ]), + + button({ + onclick: () => $online(!$online()) + }, "Toggle Status") + ]) +); +``` + +```html [Equivalent HTML Structure] +
+ User + +
+

John Doe

+

+ Status: Online +

+
+ + +
+``` +::: + +### What is happening here? + +1. **Structure:** The hierarchy is identical. `div([...])` in JS translates directly to nested tags in HTML. +2. **Attributes:** `class` is set once. `$class` is "live"; SigPro listens to the `$online` signal and updates the class name without re-rendering the whole card. +3. **Content:** The array `[...]` in SigPro is the equivalent of the children inside an HTML tag. +4. **Reactivity:** The function `() => $online() ? ...` creates a **TextNode** in the HTML that changes its text content surgically whenever the signal toggles. + +--- + +## 💡 Best Practices + +1. **Destructuring:** If you prefer not to rely on global variables, you can destructure them from `window` or `$` (though in SigPro, using them globally is the intended "clean" way). +2. **Custom Tags:** If you need a tag that isn't in the default list (like a Web Component), you can still use the base engine: `$.html('my-custom-element', { ... })`. diff --git a/src/docs/guide/getting-started.md b/src/docs/guide/getting-started.md new file mode 100644 index 0000000..c6519e0 --- /dev/null +++ b/src/docs/guide/getting-started.md @@ -0,0 +1,76 @@ +# Getting Started + +**SigPro** is a lightweight, atomic reactive engine designed to build modern web interfaces with zero overhead. It focuses on high performance through fine-grained reactivity. + +## 1. Installation + +You can install SigPro via your favorite package manager: + +::: code-group +```bash [npm] +npm install SigPro +```` + +```bash [pnpm] +pnpm add SigPro +``` + +```bash [yarn] +yarn add SigPro +``` +```bash [bun] +bun add SigPro +``` +::: + +## 2\. Basic Usage + +The core of SigPro is the `$` function, which creates reactive state (Signals) and computed effects. + +Create a `main.js` file and try this: + +```javascript +import { $ } from 'SigPro'; + +// 1. Create a reactive signal +const $name = $("World"); + +// 2. Define a reactive component +const App = () => div({ class: 'container' }, [ + h1(["Hello, ", $name, "!"]), + + input({ + type: 'text', + $value: $name, // Two-way binding + placeholder: 'Enter your name...' + }), + + button({ + onclick: () => $name("SigPro") + }, "Set to SigPro") +]); + +// 3. Mount the application +$.mount(App, '#app'); +``` + +## 3\. How it Works + +SigPro doesn't use a Virtual DOM. Instead, it creates real DOM nodes and binds them directly to your data: + +1. **Signals**: `$(value)` creates a getter/setter function. +2. **Reactivity**: When you pass a signal or a function to a DOM element, SigPro automatically creates a subscription. +3. **Fine-Grained Updates**: Only the specific text node or attribute linked to the signal updates when the value changes. + +## 4\. Global Tags + +By default, SigPro exports common HTML tags to the global scope (`window`) when initialized. This allows you to write clean, declarative UI without importing every single tag: + +```javascript +// Instead of $.html('div', ...), just use: +div([ + h1("Clean Syntax"), + p("No more boilerplate.") +]); +``` + diff --git a/src/docs/guide/why.md b/src/docs/guide/why.md new file mode 100644 index 0000000..6cd208f --- /dev/null +++ b/src/docs/guide/why.md @@ -0,0 +1,78 @@ +# Why SigPro? + +After years of building applications with React, Vue, and Svelte—investing countless hours mastering unique mental models, proprietary syntaxes, and complex build tools—we reached a realization: the web platform has evolved, but frameworks have become layers of abstraction that often move us further away from the browser. + +**SigPro** is the answer to a simple question: **Why fight the platform when we can embrace it?** + +## The Modern Web is Ready + +SigPro bypasses the overhead of the Virtual DOM and heavy compilers by using modern browser primitives. It treats the DOM as a first-class citizen, not as a side effect of a state change. + +| Browser Primitive | What It Enables | +| :--- | :--- | +| **Closures & Proxies** | Automatic dependency tracking without heavy overhead. | +| **ES Modules** | Native modularity and lazy loading without complex bundlers. | +| **Direct DOM APIs** | Surgical updates that are faster than any reconciliation algorithm. | +| **Microtask Queues** | Batching updates efficiently to ensure 60fps performance. | + +--- + +## The SigPro Philosophy + +SigPro strips away the complexity, delivering a reactive programming model that feels like a framework but stays remarkably close to Vanilla JS: + +* **No JSX transformations** – Pure JavaScript functions. +* **No Virtual DOM** – Direct, fine-grained DOM manipulation. +* **No proprietary syntax** – If you know JS, you know SigPro. +* **Zero Build Step Required** – It can run directly in the browser via ESM. + +```javascript +// Pure, Atomic, Reactive. +const $count = $(0); + +const Counter = () => div([ + p(["Count: ", $count]), + button({ onclick: () => $count(c => c + 1) }, "Increment") +]); +``` + +--- + +## Performance Comparison + +SigPro isn't just lighter; it's architecturally faster because it skips the "diffing" phase entirely. + +| Metric | SigPro | SolidJS | Svelte | Vue | React | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **Bundle Size (gzip)** | 🥇 **< 2KB** | 🥈 7KB | 🥉 16KB | 20KB | 45KB | +| **Architecture** | **Atomic** | **Atomic** | **Compiled** | **V-DOM** | **V-DOM** | +| **Initial Render** | 🥇 **Fastest** | 🥈 Fast | 🥉 Fast | Average | Slow | +| **Update Perf** | 🥇 **Surgical** | 🥇 Surgical | 🥈 Fast | 🥉 Average | Slow | +| **Dependencies** | 🥇 **0** | 🥇 0 | 🥇 0 | 🥈 2 | 🥉 5+ | +| **Build Step** | 🥇 **Optional** | 🥈 Required | 🥈 Required | 🥇 Optional | 🥈 Required | + + + +--- + +## 🔑 Core Principles + +SigPro is built on four fundamental pillars: + +### 📡 Atomic Reactivity +Automatic dependency tracking with no manual subscriptions. When a signal changes, only the **exact** text nodes or attributes that depend on it update—instantly and surgically. + +### ⚡ Surgical DOM Updates +No Virtual DOM diffing. No tree reconciliation. We don't guess what changed; we know exactly where the update needs to happen. Performance scales with your data, not the size of your component tree. + +### 🧩 Plugin-First Architecture +The core is a tiny, powerful engine. Need Routing? Fetching? Global UI? Just plug it in. This keeps your production bundles "pay-only-for-what-you-use." + +### 🔬 Predictable & Transparent +There is no "magic" hidden in a black-box compiler. What you write is what the browser executes. Debugging is straightforward because there is no framework layer between your code and the DevTools. + +--- + +> "SigPro returns the joy of web development by making the browser the hero again." + + diff --git a/src/docs/index.md b/src/docs/index.md new file mode 100644 index 0000000..91a772f --- /dev/null +++ b/src/docs/index.md @@ -0,0 +1,96 @@ +--- +layout: home + +hero: + name: SigPro + text: Atomic Unified Reactive Engine + tagline: Fine-grained reactivity, built-in routing, and modular plugins. All under 2KB. + image: + src: /logo.png + alt: SigPro Logo + actions: + - theme: brand + text: Get Started + link: /guide/getting-started + - theme: alt + text: View on GitHub + link: https://git.natxocc.com/sigpro/ + +features: + - title: Atomic Reactivity + details: Powered by Signals. Only updates what changes. No Virtual DOM overhead, no heavy re-renders. + - title: Zero Dependencies + details: Written in pure Vanilla JS. Maximum performance with the smallest footprint possible. + - title: Modular Ecosystem + details: Official plugins for UI components, dynamic Routing, Fetch, and Storage. Load only what you need. +--- + +## Why SigPro? + +SigPro isn't just another framework; it's a high-performance engine. It strips away the complexity of massive bundles and returns to the essence of the web, enhanced with reactive superpowers. + +### The Core in Action + +```javascript +import { $ } from 'sigpro2'; + +// A reactive state Signal +const $count = $(0); + +// A Computed signal that updates automatically +const $double = $(() => $count() * 2); + +// UI that breathes with your data +const Counter = () => div([ + h1(["Count: ", $count]), + p(["Double: ", $double]), + button({ onclick: () => $count(c => c + 1) }, "Increment") +]); + +$.mount(Counter); +``` + +--- + +### Key Features + +#### ⚡️ Fine-Grained Reactivity +Unlike frameworks that diff complex trees (V-DOM), SigPro binds your signals directly to real DOM text nodes and attributes. If the data changes, the node changes. Period. + +#### 🔌 Polymorphic Plugin System +Extend core capabilities in a single line. Add global UI helpers, routing, or state persistence seamlessly. +```javascript +import { UI, Router } from 'sigpro/plugins'; +$.plugin([UI, Router]); +``` + +#### 📂 File-Based Routing +With our dedicated Vite plugin, manage your routes simply by creating files in `src/pages/`. It supports native **Lazy Loading** out of the box for lightning-fast initial loads. + +--- + +### Quick Install + +::: code-group +```bash [npm] +npm install sigpro +``` +```bash [pnpm] +pnpm add sigpro +``` +```bash [yarn] +yarn add sigpro +``` +```bash [bun] +bun add sigpro +``` +::: + +--- + +## Community & Support +SigPro is an open-source project. Whether you want to contribute, report a bug, or just talk about reactivity, join us on our official repository. + +``` +Built with ❤️ by NatxoCC +``` diff --git a/packages/docs/logo.svg b/src/docs/logo.svg similarity index 100% rename from packages/docs/logo.svg rename to src/docs/logo.svg diff --git a/src/docs/plugins/core.debug.md b/src/docs/plugins/core.debug.md new file mode 100644 index 0000000..1079f04 --- /dev/null +++ b/src/docs/plugins/core.debug.md @@ -0,0 +1,107 @@ +# Development Tool: `_debug` + +The **Debug Plugin** is a lightweight reactive listener. Once attached to a signal or a computed function, it automatically monitors changes, compares values, and formats the output in the browser console. + +## 1. Core Features + +* **Reactive Tracking:** Automatically logs whenever the tracked signal updates. +* **Visual Grouping:** Uses styled console groups to keep your dev tools organized. +* **Object Inspection:** Automatically uses `console.table()` when the signal contains an object or array. +* **Efficient Comparison:** Uses `Object.is` to prevent redundant logging if the value hasn't actually changed. + +--- + +## 2. Installation + +To use `_debug`, you only need the SigPro core. Register the plugin in your `main.js`. You can conditionally load it so it only runs during development. + +```javascript +import { $ } from 'sigpro'; +import { Debug } from 'sigpro/plugins'; + +// Only load Debug in development mode +const plugins = []; +if (import.meta.env.DEV) plugins.push(Debug); + +$.plugin(plugins).then(() => { + import('./App.js').then(app => $.mount(app.default)); +}); +``` + +::: code-group +```bash [NPM] +npm install sigpro +``` + +```bash [PNPM] +pnpm add sigpro +``` + +```bash [Yarn] +yarn add sigpro +``` + +```bash [Bun] +bun add sigpro +``` +::: + +--- + +## 3. Basic Usage + +Call `_debug` anywhere in your component. It stays active in the background, watching the signal's lifecycle. + +```javascript +export default () => { + const $count = $(0); + const $user = $({ name: "Guest", role: "Viewer" }); + + // Start tracking + _debug($count, "Main Counter"); + _debug($user, "User Session"); + + return div([ + button({ onclick: () => $count(c => c + 1) }, "Increment"), + button({ onclick: () => $user({ name: "Admin", role: "Super" }) }, "Promote") + ]); +}; +``` + +--- + +## 4. Console Output Breakdown + +When a signal changes, the console displays a structured block: + +1. **Header:** A styled badge with the name (e.g., `SigPro Debug: Main Counter`). +2. **Previous Value:** The value before the update (in red). +3. **Current Value:** The new value (in green). +4. **Table View:** If the value is an object, a formatted table appears automatically. + + + +--- + +## 5. Debugging Computed Values + +You can also debug **computed functions** to see exactly when derived state is recalculated. + +```javascript +const $price = $(100); +const $tax = $(0.21); +const $total = $(() => $price() * (1 + $tax())); + +// Monitor the result of the calculation +_debug($total, "Final Invoice Total"); +``` + +--- + +## 6. Why use `_debug`? + +1. **Clean Logic:** No need to scatter `console.log` inside your reactive functions. +2. **State History:** Instantly see the "Before" and "After" of any user action. +3. **No-Noise:** It only logs when a real change occurs, keeping the console clean. +4. **Deep Inspection:** The automatic `console.table` makes debugging large API responses much faster. + diff --git a/src/docs/plugins/core.fetch.md b/src/docs/plugins/core.fetch.md new file mode 100644 index 0000000..999d867 --- /dev/null +++ b/src/docs/plugins/core.fetch.md @@ -0,0 +1,80 @@ +# Data Fetching: `_fetch` + +The **Fetch Plugin** provides a reactive wrapper around the native browser Fetch API. Instead of managing complex `async/await` flows within your UI, `_fetch` returns a "Reactive Tripod" (Data, Loading, and Error) that your components can listen to automatically. + +## 1. Core Concept + +When you call `_fetch`, it returns three signals immediately. Your UI declares how to react to these signals as they change from their initial state to the final response. + +* **`$data`**: Initialized as `null`. Automatically holds the JSON response on success. +* **`$loading`**: Initialized as `true`. Flips to `false` once the request settles. +* **`$error`**: Initialized as `null`. Holds the error message if the request fails. + +--- + +## 2. Installation + +Register the `Fetch` plugin in your `main.js`. By convention, we load it alongside the UI and Router to have the full SigPro ecosystem ready. + +```javascript +import { $ } from 'sigpro'; +import { Fetch } from 'sigpro/plugins'; + +$.plugin([Fetch]).then(() => { + // Now _fetch() is available globally + import('./App.js').then(app => $.mount(app.default)); +}); +``` + + +--- + +## 3. Basic Usage + +Use `_fetch` inside your component to get live updates. The UI updates surgically whenever a signal changes. + +```javascript +export default () => { + const { $data, $loading, $error } = _fetch('https://api.github.com/users/octocat'); + + return div({ class: 'p-6 flex flex-col gap-4' }, [ + h1("Profile Details"), + + // 1. Loading State (using SigPro UI button) + () => $loading() && _button({ $loading: true }, "Fetching..."), + + // 2. Error State + () => $error() && div({ class: 'alert alert-error' }, $error()), + + // 3. Success State + () => $data() && div({ class: 'card bg-base-200 p-4' }, [ + img({ src: $data().avatar_url, class: 'w-16 rounded-full' }), + h2($data().name), + p($data().bio) + ]) + ]); +}; +``` + +--- + +## 4. Advanced Configuration + +`_fetch` accepts the same `RequestInit` options as the standard `fetch()` (methods, headers, body, etc.). + +```javascript +const { $data, $loading } = _fetch('/api/v1/update', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: 'active' }) +}); +``` + +--- + +## 5. Why use `_fetch` instead of native Fetch? + +1. **Declarative UI**: You define the "Loading", "Error", and "Success" templates once, and they swap automatically. +2. **No `useEffect` required**: Since SigPro is natively reactive, you don't need lifecycle hooks to trigger re-renders; the signals handle it. +3. **Consistency**: It follows the same `_prefix` pattern as the rest of the official plugin ecosystem. +4. **Automatic JSON Parsing**: It assumes JSON by default and handles 404/500 errors by populating the `$error` signal. diff --git a/src/docs/plugins/core.router.md b/src/docs/plugins/core.router.md new file mode 100644 index 0000000..3b0af56 --- /dev/null +++ b/src/docs/plugins/core.router.md @@ -0,0 +1,110 @@ +# Navigation Plugin: `Router` + +The SigPro Router handles URL changes via hashes (`#`) and maps them to components. It supports dynamic parameters (like `:id`) and asynchronous loading for heavy pages. + +## 1. Core Features + +* **Hash-based:** Works everywhere without special server configuration. +* **Lazy Loading:** Pages are only downloaded when the user visits the route. +* **Reactive:** The view updates automatically when the hash changes. +* **Dynamic Routes:** Supports paths like `/user/:id`. + +--- + +## 2. Installation + +The Router is usually included in the official plugins package. + +::: code-group +```bash [NPM] +npm install -D tailwindcss @tailwindcss/vite daisyui@next +``` + +```bash [PNPM] +pnpm add -D tailwindcss @tailwindcss/vite daisyui@next +``` + +```bash [Yarn] +yarn add -D tailwindcss @tailwindcss/vite daisyui@next +``` + +```bash [Bun] +bun add -d tailwindcss @tailwindcss/vite daisyui@next +``` +::: + +--- + +## 3. Setting Up Routes + +In your `App.js` (or a dedicated routes file), define your navigation map. + +```javascript +const routes = [ + { path: '/', component: () => h1("Home Page") }, + { + path: '/admin', + // Lazy Loading: This file is only fetched when needed + component: () => import('./pages/Admin.js') + }, + { path: '/user/:id', component: (params) => h2(`User ID: ${params.id}`) }, + { path: '*', component: () => div("404 - Page Not Found") } +]; + +export default () => div([ + _navbar({ title: "My App" }), + _router(routes) // The router is now a global tag +]); +``` + +--- + +## 4. Navigation (`_router.go`) + +To move between pages programmatically (e.g., inside an `onclick` event), use the global `_router.go` helper. + +```javascript +_button({ + onclick: () => _router.go('/admin') +}, "Go to Admin") +``` + +--- + +## 5. How it Works (Under the Hood) + +The router tracks the `window.location.hash` and uses a reactive signal to trigger a re-render of the specific area where `_router(routes)` is placed. + +1. **Match:** It filters your route array to find the best fit. +2. **Resolve:** * If it's a standard function, it executes it immediately. + * If it's a **Promise** (via `import()`), it shows a loading state and swaps the content once the module arrives. +3. **Inject:** It replaces the previous DOM node with the new page content surgically. + + + +--- + +## 6. Integration with UI Components + +Since you are using the **UI Plugin**, you can easily create active states in your navigation menus by checking the current hash. + +```javascript +// Example of a reactive sidebar menu +_menu({ + items: [ + { + label: 'Dashboard', + active: () => window.location.hash === '#/', + onclick: () => _router.go('/') + }, + { + label: 'Settings', + active: () => window.location.hash === '#/settings', + onclick: () => _router.go('/settings') + } + ] +}) +``` + + + diff --git a/src/docs/plugins/core.storage.md b/src/docs/plugins/core.storage.md new file mode 100644 index 0000000..1205bb4 --- /dev/null +++ b/src/docs/plugins/core.storage.md @@ -0,0 +1,106 @@ +# Persistence Tool: `_storage` + +The Storage plugin synchronizes a signal with a specific key in your browser's `localStorage`. It handles both the **initial hydration** (loading data when the app starts) and **automatic saving** whenever the signal's value changes. + +## 1. Core Concept + +When you "attach" a signal to `_storage`, two things happen: +1. **Hydration:** The plugin checks if the key already exists in `localStorage`. If it does, it parses the JSON and updates the signal immediately. +2. **Reactive Sync:** It creates a reactive watcher that stringifies and saves the signal's value to the disk every time it is updated. + +--- + +## 2. Installation + +Register the `Storage` plugin in your `main.js`. Since this is a logic-only plugin, it doesn't require any CSS or UI dependencies. + +```javascript +import { $ } from 'sigpro'; +import { Storage } from 'sigpro/plugins'; + +$.plugin(Storage).then(() => { + import('./App.js').then(app => $.mount(app.default)); +}); +``` + +::: code-group +```bash [NPM] +npm install sigpro +``` + +```bash [PNPM] +pnpm add sigpro +``` + +```bash [Yarn] +yarn add sigpro +``` + +```bash [Bun] +bun add sigpro +``` +::: + +--- + +## 3. Basic Usage + +You can wrap any signal with `_storage`. It is common practice to do this right after creating the signal. + +```javascript +export default () => { + // 1. Create a signal with a default value + const $theme = $( 'light' ); + + // 2. Persist it. If 'user_theme' exists in localStorage, + // $theme will be updated to that value instantly. + _storage($theme, 'user_theme'); + + return div({ class: () => `app-${$theme()}` }, [ + h1(`Current Theme: ${$theme()}`), + button({ + onclick: () => $theme(t => t === 'light' ? 'dark' : 'light') + }, "Toggle Theme") + ]); +}; +``` + +--- + +## 4. Complex Data (Objects & Arrays) + +Since the plugin uses `JSON.parse` and `JSON.stringify` internally, it works perfectly with complex state structures. + +```javascript +const $settings = $({ + notifications: true, + fontSize: 16 +}); + +// Automatically saves the whole object whenever any property changes +_storage($settings, 'app_settings'); +``` + +--- + +## 5. Why use `_storage`? + +1. **Zero Boilerplate:** You don't need to manually write `localStorage.getItem` or `setItem` logic inside your components. +2. **Chaining:** Because `_storage` returns the signal, you can persist it inline. +3. **Error Resilience:** It includes a built-in `try/catch` block to prevent your app from crashing if the stored JSON is corrupted. +4. **Surgical Persistence:** Only the signals you explicitly mark for storage are saved, keeping your `localStorage` clean. + + + +--- + +## 6. Pro Tip: Combining with Debug + +You can chain plugins to create a fully monitored and persistent state: + +```javascript +const $score = _storage($(0), 'high_score'); + +// Now it's saved to disk AND logged to console on every change +_debug($score, "Game Score"); +``` diff --git a/src/docs/plugins/core.ui.md b/src/docs/plugins/core.ui.md new file mode 100644 index 0000000..4371ac8 --- /dev/null +++ b/src/docs/plugins/core.ui.md @@ -0,0 +1,147 @@ +# Official UI Plugin: `UI` + +The **SigPro UI** plugin is a high-level component library built on top of the reactive core. It leverages **Tailwind CSS v4** for utility styling and **daisyUI v5** for semantic components. + +## 1. Prerequisites & Installation + +To use these components, you must install the styling engine. SigPro UI provides the logic, but Tailwind and daisyUI provide the visuals. + +::: code-group +```bash [NPM] +npm install -D tailwindcss @tailwindcss/vite daisyui@next +``` + +```bash [PNPM] +pnpm add -D tailwindcss @tailwindcss/vite daisyui@next +``` + +```bash [Yarn] +yarn add -D tailwindcss @tailwindcss/vite daisyui@next +``` + +```bash [Bun] +bun add -d tailwindcss @tailwindcss/vite daisyui@next +``` +::: + +Would you like to continue with the **Router.md** documentation now? + +### CSS Configuration (`app.css`) +In Tailwind v4, configuration is handled directly in your CSS. Create a `src/app.css` file: + +```css +/* src/app.css */ +@import "tailwindcss"; + +/* Import daisyUI v5 as a Tailwind v4 plugin */ +@plugin "daisyui"; + +/* Optional: Configure themes */ +@custom-variant dark (&:where(.dark, [data-theme="dark"], [data-theme="dark"] *))); +``` + +--- + +## 2. Initialization + +You must import your CSS and register the `UI` plugin in your entry point. This populates the global scope with reactive component helpers (prefixed with `_`). + +```javascript +// main.js +import './app.css'; +import { $ } from 'sigpro'; +import { UI } from 'sigpro/plugins'; + +$.plugin(UI).then(() => { + // Global components like _button and _input are now ready + import('./App.js').then(app => $.mount(app.default)); +}); +``` + +--- + +## 3. Core Component Tags (`_tags`) + +SigPro UI components are more than just HTML; they are **Reactive Functional Components** that manage complex states (loading, errors, accessibility) automatically. + +### A. Action Components (`_button`) +The `_button` automatically handles spinners and disabled states based on signals. + +| Property | Type | Description | +| :--- | :--- | :--- | +| **`$loading`** | `signal` | If true, shows a spinner and disables the button. | +| **`$disabled`**| `signal` | Manually disables the button (logic-bound). | +| **`icon`** | `node/str`| Prepends an icon to the text. | +| **`badge`** | `string` | Appends a small badge to the button. | + +```javascript +_button({ + $loading: $isSaving, + icon: '💾', + class: 'btn-primary' +}, "Save Data") +``` + +### B. High-Density Forms (`_input`, `_select`, `_checkbox`) +These components wrap the raw input in a `fieldset` with integrated labels and tooltips. + +* **`label`**: Field title displayed above the input. +* **`tip`**: Displays a `?` badge that shows a tooltip on hover. +* **`$error`**: A signal that, when populated, turns the input red and displays the message. +* **`$value`**: **Two-way binding**. Updates the signal on input and the input on signal change. + +```javascript +_input({ + label: "Username", + tip: "Choose a unique name", + $value: $name, + $error: $nameError +}) +``` + +--- + +## 4. Complex UI Patterns + +### Reactive Modals (`_modal`) +The `_modal` is surgically mounted. If the `$open` signal is `false`, the component is completely removed from the DOM, optimizing performance. + +```javascript +const $showModal = $(false); + +_modal({ $open: $showModal, title: "Alert" }, [ + p("Are you sure you want to proceed?"), + _button({ onclick: () => doAction() }, "Confirm") +]) +``` + +### Navigation & Layout (`_tabs`, `_drawer`, `_navbar`) +Designed to work seamlessly with the **Router**. + +| Component | Key Logic | +| :--- | :--- | +| **`_tabs`** | Accepts an `active` property (signal or function) to highlight the current tab. | +| **`_drawer`** | A responsive sidebar that toggles via an ID or an `$open` signal. | +| **`_navbar`** | Standard top bar with shadow and glass effect support. | +| **`_menu`** | Vertical navigation list with active state support. | + +--- + +## 5. Summary Table: UI Globals + +Once `$.plugin(UI)` is active, these tags are available project-wide: + +| Tag | Category | Use Case | +| :--- | :--- | :--- | +| `_fieldset` | Layout | Grouping related inputs with a `legend`. | +| `_accordion`| Content | Collapsible sections (FAQs). | +| `_badge` | Feedback | Status indicators (Success, Warning). | +| `_tooltip` | Feedback | Descriptive text on hover. | +| `_range` | Input | Reactive slider for numerical values. | + +--- + +### What's next? +With the UI ready and styled via **Tailwind v4**, we can move to the **Router.md**. We will explain how to link `_tabs` and `_menu` to different URL paths for a full SPA experience. + +**Would you like to start with the Router configuration?** \ No newline at end of file diff --git a/src/docs/plugins/custom.md b/src/docs/plugins/custom.md new file mode 100644 index 0000000..f7198c2 --- /dev/null +++ b/src/docs/plugins/custom.md @@ -0,0 +1,123 @@ +# Creating Custom Plugins + +There are two main ways to expose a plugin's functionality: **Static/Manual Imports** (cleaner for large projects) or **Global/Automatic Window Injection** (easier for quick scripts and global helpers). + +## 1. The Anatomy of a Plugin + +A plugin is a standard JavaScript function. By convention, if a plugin adds a global helper or component, it should be prefixed with an underscore (`_`). + +```javascript +// plugins/my-utils.js +export const MyUtils = ($) => { + + // 1. Attach to the SigPro instance + $.capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1); + + // 2. Attach to the Window (Global access) + window._hello = (name) => div(`Hello, ${$.capitalize(name)}!`); + + // 3. You can also return values if needed + return { version: '1.0.0' }; +}; +``` + +--- + +## 2. Integration Strategies + +### Option A: Manual Import (Recommended) +This approach keeps your global namespace clean. You import the logic only where you need it, but the plugin still initializes the core `$` extensions. + +```javascript +// main.js +import { $ } from 'sigpro'; +import { MyUtils } from './plugins/my-utils.js'; + +$.plugin(MyUtils); + +// App.js +export default () => { + const name = "sigpro"; + // $.capitalize was added by the plugin + return h1($.capitalize(name)); +}; +``` + +### Option B: Automatic Window Injection +If your plugin defines global tags (like `_button` or `_hello`), you should attach them to the `window` object inside the plugin function. This makes them available everywhere without imports. + +```javascript +// plugins/theme.js +export const Theme = ($) => { + const $dark = $(false); + + window._themeToggle = () => button({ + onclick: () => $dark(v => !v), + class: () => $dark() ? 'bg-black text-white' : 'bg-white text-black' + }, "Toggle Mode"); +}; + +// main.js +$.plugin(Theme).then(() => { + // _themeToggle is now a global function + $.mount(App); +}); +``` + +--- + +## 3. Asynchronous Plugins +If your plugin needs to load external data or scripts before the app starts, make it `async`. SigPro will wait for it. + +```javascript +export const ConfigLoader = async ($) => { + const res = await fetch('/config.json'); + const config = await res.json(); + + $.config = config; // Attach loaded config to SigPro +}; + +// Usage +$.plugin(ConfigLoader).then(() => { + console.log("Config loaded:", $.config); + $.mount(App); +}); +``` + +--- + +## 4. Best Practices for Plugin Authors + +| Rule | Description | +| :--- | :--- | +| **Prefixing** | Use `_` for UI components (`_modal`) and `$.` for logic (`$.fetch`). | +| **Idempotency** | Ensure calling `$.plugin(MyPlugin)` twice doesn't break the app. | +| **Encapsulation** | Use the `$` instance passed as an argument rather than importing it again inside the plugin. | +| **Reactivity** | Always use `$(...)` for internal state so the app stays reactive. | + + + +--- + +## 5. Installation + +Custom plugins don't require extra packages, but ensure your build tool (Vite/Bun) is configured to handle the module imports. + +::: code-group +```bash [NPM] +npm install sigpro +``` + +```bash [PNPM] +pnpm add sigpro +``` + +```bash [Yarn] +yarn add sigpro +``` + +```bash [Bun] +bun add sigpro +``` +::: + diff --git a/src/docs/plugins/quick.md b/src/docs/plugins/quick.md new file mode 100644 index 0000000..e426a31 --- /dev/null +++ b/src/docs/plugins/quick.md @@ -0,0 +1,101 @@ +# Extending SigPro: `$.plugin` + +The plugin system is the engine's way of growing. It allows you to inject new functionality directly into the `$` object or load external resources. + +## 1. How Plugins Work + +A plugin in **SigPro** is simply a function that receives the core instance. When you run `$.plugin(MyPlugin)`, the engine hands over the `$` object so the plugin can attach new methods or register global tags (like `div()`, `span()`, etc.). + +### Functional Plugin Example +```javascript +// A plugin that adds a simple logger to any signal +const Logger = ($) => { + $.watch = (target, label = "Log") => { + $(() => console.log(`[${label}]:`, target())); + }; +}; + +// Activation +$.plugin(Logger); +const $count = $(0); +$.watch($count, "Counter"); // Now available globally via $ +``` + +--- + +## 2. Initialization Patterns + +Since plugins often set up global variables (like the HTML tags), the order of initialization is critical. Here are the two ways to start your app: + +### Option A: The "Safe" Async Start (Recommended) +This is the most robust way. It ensures all global tags (`div`, `button`, etc.) are created **before** your App code is even read by the browser. + +```javascript +// main.js +import { $ } from 'sigpro'; +import { UI, Router } from 'sigpro/plugins'; + +// 1. Load plugins first +$.plugin([UI, Router]).then(() => { + + // 2. Import your app only after the environment is ready + import('./App.js').then(appFile => { + const MyApp = appFile.default; + $.mount(MyApp, '#app'); + }); + +}); +``` + +### Option B: Static Start (No Global Tags) +Use this only if you prefer **not** to use global tags and want to use `$.html` directly in your components. This allows for standard static imports. + +```javascript +// main.js +import { $ } from 'sigpro'; +import { UI } from 'sigpro/plugins'; +import MyApp from './App.js'; // Static import works here + +$.plugin(UI); +$.mount(MyApp, '#app'); +``` +> **Warning:** In this mode, if `App.js` uses `div()` instead of `$.html('div')`, it will throw a `ReferenceError`. + +--- + +## 3. Resource Plugins (External Scripts) + +You can pass a **URL** or an **Array of URLs**. SigPro will inject them as `