diff --git a/.gitignore b/.gitignore index 3b717cd..fcb1b90 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ node_modules/ jspm_packages/ # Build and Distribution -dist/ lib-cov/ coverage/ *.lcov diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 5f2529c..5e98765 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -56,7 +56,7 @@ export default defineConfig({ { text: 'Vite Router Plugin', items: [ - { text: 'Vite Plugin', link: '/vite/router' }, + { text: 'Vite Plugin', link: '/vite/plugin' }, ] } ], diff --git a/docs/.vitepress/dist/404.html b/docs/.vitepress/dist/404.html new file mode 100644 index 0000000..f498666 --- /dev/null +++ b/docs/.vitepress/dist/404.html @@ -0,0 +1,22 @@ + + + + + + 404 | SigPro + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/components.html b/docs/.vitepress/dist/api/components.html new file mode 100644 index 0000000..fd7e4b2 --- /dev/null +++ b/docs/.vitepress/dist/api/components.html @@ -0,0 +1,595 @@ + + + + + + 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/.vitepress/dist/api/effects.html b/docs/.vitepress/dist/api/effects.html new file mode 100644 index 0000000..2605246 --- /dev/null +++ b/docs/.vitepress/dist/api/effects.html @@ -0,0 +1,787 @@ + + + + + + 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/.vitepress/dist/api/fetch.html b/docs/.vitepress/dist/api/fetch.html new file mode 100644 index 0000000..eff7102 --- /dev/null +++ b/docs/.vitepress/dist/api/fetch.html @@ -0,0 +1,873 @@ + + + + + + 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/.vitepress/dist/api/pages.html b/docs/.vitepress/dist/api/pages.html new file mode 100644 index 0000000..5fc0ee0 --- /dev/null +++ b/docs/.vitepress/dist/api/pages.html @@ -0,0 +1,405 @@ + + + + + + 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/.vitepress/dist/api/quick.html b/docs/.vitepress/dist/api/quick.html new file mode 100644 index 0000000..06a0f4d --- /dev/null +++ b/docs/.vitepress/dist/api/quick.html @@ -0,0 +1,217 @@ + + + + + + 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
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/routing.html b/docs/.vitepress/dist/api/routing.html new file mode 100644 index 0000000..0965ca1 --- /dev/null +++ b/docs/.vitepress/dist/api/routing.html @@ -0,0 +1,628 @@ + + + + + + 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/.vitepress/dist/api/signals.html b/docs/.vitepress/dist/api/signals.html new file mode 100644 index 0000000..4c90ffc --- /dev/null +++ b/docs/.vitepress/dist/api/signals.html @@ -0,0 +1,683 @@ + + + + + + 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/.vitepress/dist/api/storage.html b/docs/.vitepress/dist/api/storage.html new file mode 100644 index 0000000..124ff87 --- /dev/null +++ b/docs/.vitepress/dist/api/storage.html @@ -0,0 +1,820 @@ + + + + + + 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/.vitepress/dist/assets/api_components.md.BlFwj17l.js b/docs/.vitepress/dist/assets/api_components.md.BlFwj17l.js new file mode 100644 index 0000000..8386adc --- /dev/null +++ b/docs/.vitepress/dist/assets/api_components.md.BlFwj17l.js @@ -0,0 +1,571 @@ +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/.vitepress/dist/assets/api_components.md.BlFwj17l.lean.js b/docs/.vitepress/dist/assets/api_components.md.BlFwj17l.lean.js new file mode 100644 index 0000000..f21251e --- /dev/null +++ b/docs/.vitepress/dist/assets/api_components.md.BlFwj17l.lean.js @@ -0,0 +1 @@ +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/.vitepress/dist/assets/api_effects.md.Br_yStBS.js b/docs/.vitepress/dist/assets/api_effects.md.Br_yStBS.js new file mode 100644 index 0000000..8d2bef7 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_effects.md.Br_yStBS.js @@ -0,0 +1,763 @@ +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:

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/.vitepress/dist/assets/api_effects.md.Br_yStBS.lean.js b/docs/.vitepress/dist/assets/api_effects.md.Br_yStBS.lean.js new file mode 100644 index 0000000..8ad5dff --- /dev/null +++ b/docs/.vitepress/dist/assets/api_effects.md.Br_yStBS.lean.js @@ -0,0 +1 @@ +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/.vitepress/dist/assets/api_fetch.md.DQLBJSoq.js b/docs/.vitepress/dist/assets/api_fetch.md.DQLBJSoq.js new file mode 100644 index 0000000..ffcb610 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_fetch.md.DQLBJSoq.js @@ -0,0 +1,849 @@ +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:

$.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/.vitepress/dist/assets/api_fetch.md.DQLBJSoq.lean.js b/docs/.vitepress/dist/assets/api_fetch.md.DQLBJSoq.lean.js new file mode 100644 index 0000000..0a44344 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_fetch.md.DQLBJSoq.lean.js @@ -0,0 +1 @@ +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/.vitepress/dist/assets/api_pages.md.BP19nHXw.js b/docs/.vitepress/dist/assets/api_pages.md.BP19nHXw.js new file mode 100644 index 0000000..d038a11 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_pages.md.BP19nHXw.js @@ -0,0 +1,381 @@ +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/.vitepress/dist/assets/api_pages.md.BP19nHXw.lean.js b/docs/.vitepress/dist/assets/api_pages.md.BP19nHXw.lean.js new file mode 100644 index 0000000..8a2f4b3 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_pages.md.BP19nHXw.lean.js @@ -0,0 +1 @@ +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/.vitepress/dist/assets/api_quick.md.BDS3ttnt.js b/docs/.vitepress/dist/assets/api_quick.md.BDS3ttnt.js new file mode 100644 index 0000000..652a1a7 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_quick.md.BDS3ttnt.js @@ -0,0 +1,193 @@ +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/.vitepress/dist/assets/api_quick.md.BDS3ttnt.lean.js b/docs/.vitepress/dist/assets/api_quick.md.BDS3ttnt.lean.js new file mode 100644 index 0000000..012ea6d --- /dev/null +++ b/docs/.vitepress/dist/assets/api_quick.md.BDS3ttnt.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":"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/.vitepress/dist/assets/api_routing.md.7SNAZXtp.js b/docs/.vitepress/dist/assets/api_routing.md.7SNAZXtp.js new file mode 100644 index 0000000..f44d17d --- /dev/null +++ b/docs/.vitepress/dist/assets/api_routing.md.7SNAZXtp.js @@ -0,0 +1,604 @@ +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:

$.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/.vitepress/dist/assets/api_routing.md.7SNAZXtp.lean.js b/docs/.vitepress/dist/assets/api_routing.md.7SNAZXtp.lean.js new file mode 100644 index 0000000..37f6f1a --- /dev/null +++ b/docs/.vitepress/dist/assets/api_routing.md.7SNAZXtp.lean.js @@ -0,0 +1 @@ +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/.vitepress/dist/assets/api_signals.md.CrW68-BA.js b/docs/.vitepress/dist/assets/api_signals.md.CrW68-BA.js new file mode 100644 index 0000000..acf5267 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_signals.md.CrW68-BA.js @@ -0,0 +1,659 @@ +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:

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:

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/.vitepress/dist/assets/api_signals.md.CrW68-BA.lean.js b/docs/.vitepress/dist/assets/api_signals.md.CrW68-BA.lean.js new file mode 100644 index 0000000..089705f --- /dev/null +++ b/docs/.vitepress/dist/assets/api_signals.md.CrW68-BA.lean.js @@ -0,0 +1 @@ +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/.vitepress/dist/assets/api_storage.md.COEWBXHk.js b/docs/.vitepress/dist/assets/api_storage.md.COEWBXHk.js new file mode 100644 index 0000000..430118a --- /dev/null +++ b/docs/.vitepress/dist/assets/api_storage.md.COEWBXHk.js @@ -0,0 +1,796 @@ +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:

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/.vitepress/dist/assets/api_storage.md.COEWBXHk.lean.js b/docs/.vitepress/dist/assets/api_storage.md.COEWBXHk.lean.js new file mode 100644 index 0000000..2b86ef1 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_storage.md.COEWBXHk.lean.js @@ -0,0 +1 @@ +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/.vitepress/dist/assets/app.vxcCtRjT.js b/docs/.vitepress/dist/assets/app.vxcCtRjT.js new file mode 100644 index 0000000..0a653eb --- /dev/null +++ b/docs/.vitepress/dist/assets/app.vxcCtRjT.js @@ -0,0 +1 @@ +import{t as p}from"./chunks/theme.KNngRPLo.js";import{R as s,a0 as i,a1 as u,a2 as c,a3 as l,a4 as f,a5 as d,a6 as m,a7 as h,a8 as g,a9 as A,d as v,u as y,v as C,s as P,aa as b,ab as w,ac as R,ad as E}from"./chunks/framework.C8AWLET_.js";function r(e){if(e.extends){const a=r(e.extends);return{...a,...e,async enhanceApp(t){a.enhanceApp&&await a.enhanceApp(t),e.enhanceApp&&await e.enhanceApp(t)}}}return e}const n=r(p),S=v({name:"VitePressApp",setup(){const{site:e,lang:a,dir:t}=y();return C(()=>{P(()=>{document.documentElement.lang=a.value,document.documentElement.dir=t.value})}),e.value.router.prefetchLinks&&b(),w(),R(),n.setup&&n.setup(),()=>E(n.Layout)}});async function T(){globalThis.__VITEPRESS__=!0;const e=_(),a=D();a.provide(u,e);const t=c(e.route);return a.provide(l,t),a.component("Content",f),a.component("ClientOnly",d),Object.defineProperties(a.config.globalProperties,{$frontmatter:{get(){return t.frontmatter.value}},$params:{get(){return t.page.value.params}}}),n.enhanceApp&&await n.enhanceApp({app:a,router:e,siteData:m}),{app:a,router:e,data:t}}function D(){return A(S)}function _(){let e=s;return h(a=>{let t=g(a),o=null;return t&&(e&&(t=t.replace(/\.js$/,".lean.js")),o=import(t)),s&&(e=!1),o},n.NotFound)}s&&T().then(({app:e,router:a,data:t})=>{a.go().then(()=>{i(a.route,t.site),e.mount("#app")})});export{T as createApp}; diff --git a/docs/.vitepress/dist/assets/chunks/framework.C8AWLET_.js b/docs/.vitepress/dist/assets/chunks/framework.C8AWLET_.js new file mode 100644 index 0000000..eb7fa98 --- /dev/null +++ b/docs/.vitepress/dist/assets/chunks/framework.C8AWLET_.js @@ -0,0 +1,19 @@ +/** +* @vue/shared v3.5.30 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Fs(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const re={},Mt=[],qe=()=>{},Qr=()=>!1,sn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Hs=e=>e.startsWith("onUpdate:"),fe=Object.assign,Ds=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},Do=Object.prototype.hasOwnProperty,Z=(e,t)=>Do.call(e,t),K=Array.isArray,Pt=e=>rn(e)==="[object Map]",Zr=e=>rn(e)==="[object Set]",ir=e=>rn(e)==="[object Date]",G=e=>typeof e=="function",le=e=>typeof e=="string",He=e=>typeof e=="symbol",Q=e=>e!==null&&typeof e=="object",ei=e=>(Q(e)||G(e))&&G(e.then)&&G(e.catch),ti=Object.prototype.toString,rn=e=>ti.call(e),jo=e=>rn(e).slice(8,-1),ni=e=>rn(e)==="[object Object]",Nn=e=>le(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,mt=Fs(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Fn=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},$o=/-\w/g,Se=Fn(e=>e.replace($o,t=>t.slice(1).toUpperCase())),Vo=/\B([A-Z])/g,at=Fn(e=>e.replace(Vo,"-$1").toLowerCase()),Hn=Fn(e=>e.charAt(0).toUpperCase()+e.slice(1)),bn=Fn(e=>e?`on${Hn(e)}`:""),Ke=(e,t)=>!Object.is(e,t),Zn=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},ko=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Wo=e=>{const t=le(e)?Number(e):NaN;return isNaN(t)?e:t};let or;const Dn=()=>or||(or=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function js(e){if(K(e)){const t={};for(let n=0;n{if(n){const s=n.split(Bo);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function $s(e){let t="";if(le(e))t=e;else if(K(e))for(let n=0;n!!(e&&e.__v_isRef===!0),Jo=e=>le(e)?e:e==null?"":K(e)||Q(e)&&(e.toString===ti||!G(e.toString))?ii(e)?Jo(e.value):JSON.stringify(e,oi,2):String(e),oi=(e,t)=>ii(t)?oi(e,t.value):Pt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],i)=>(n[es(s,i)+" =>"]=r,n),{})}:Zr(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>es(n))}:He(t)?es(t):Q(t)&&!K(t)&&!ni(t)?String(t):t,es=(e,t="")=>{var n;return He(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.5.30 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let pe;class zo{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.__v_skip=!0,this.parent=pe,!t&&pe&&(this.index=(pe.scopes||(pe.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t0&&--this._on===0&&(pe=this.prevScope,this.prevScope=void 0)}stop(t){if(this._active){this._active=!1;let n,s;for(n=0,s=this.effects.length;n0)return;if(Bt){let t=Bt;for(Bt=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;Ut;){let t=Ut;for(Ut=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function ui(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function di(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),Us(s),Zo(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function Ss(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(hi(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function hi(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Xt)||(e.globalVersion=Xt,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Ss(e))))return;e.flags|=2;const t=e.dep,n=se,s=Fe;se=e,Fe=!0;try{ui(e);const r=e.fn(e._value);(t.version===0||Ke(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{se=n,Fe=s,di(e),e.flags&=-3}}function Us(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let i=n.computed.deps;i;i=i.nextDep)Us(i,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function Zo(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let Fe=!0;const pi=[];function Qe(){pi.push(Fe),Fe=!1}function Ze(){const e=pi.pop();Fe=e===void 0?!0:e}function lr(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=se;se=void 0;try{t()}finally{se=n}}}let Xt=0;class el{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class jn{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!se||!Fe||se===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==se)n=this.activeLink=new el(se,this),se.deps?(n.prevDep=se.depsTail,se.depsTail.nextDep=n,se.depsTail=n):se.deps=se.depsTail=n,gi(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=se.depsTail,n.nextDep=void 0,se.depsTail.nextDep=n,se.depsTail=n,se.deps===n&&(se.deps=s)}return n}trigger(t){this.version++,Xt++,this.notify(t)}notify(t){ks();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{Ws()}}}function gi(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)gi(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const Cn=new WeakMap,vt=Symbol(""),xs=Symbol(""),Yt=Symbol("");function me(e,t,n){if(Fe&&se){let s=Cn.get(e);s||Cn.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new jn),r.map=s,r.key=n),r.track()}}function ze(e,t,n,s,r,i){const l=Cn.get(e);if(!l){Xt++;return}const o=c=>{c&&c.trigger()};if(ks(),t==="clear")l.forEach(o);else{const c=K(e),f=c&&Nn(n);if(c&&n==="length"){const a=Number(s);l.forEach((h,v)=>{(v==="length"||v===Yt||!He(v)&&v>=a)&&o(h)})}else switch((n!==void 0||l.has(void 0))&&o(l.get(n)),f&&o(l.get(Yt)),t){case"add":c?f&&o(l.get("length")):(o(l.get(vt)),Pt(e)&&o(l.get(xs)));break;case"delete":c||(o(l.get(vt)),Pt(e)&&o(l.get(xs)));break;case"set":Pt(e)&&o(l.get(vt));break}}Ws()}function tl(e,t){const n=Cn.get(e);return n&&n.get(t)}function At(e){const t=z(e);return t===e?t:(me(t,"iterate",Yt),Re(e)?t:t.map(De))}function $n(e){return me(e=z(e),"iterate",Yt),e}function Be(e,t){return et(e)?Ft(lt(e)?De(t):t):De(t)}const nl={__proto__:null,[Symbol.iterator](){return ns(this,Symbol.iterator,e=>Be(this,e))},concat(...e){return At(this).concat(...e.map(t=>K(t)?At(t):t))},entries(){return ns(this,"entries",e=>(e[1]=Be(this,e[1]),e))},every(e,t){return Ge(this,"every",e,t,void 0,arguments)},filter(e,t){return Ge(this,"filter",e,t,n=>n.map(s=>Be(this,s)),arguments)},find(e,t){return Ge(this,"find",e,t,n=>Be(this,n),arguments)},findIndex(e,t){return Ge(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Ge(this,"findLast",e,t,n=>Be(this,n),arguments)},findLastIndex(e,t){return Ge(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Ge(this,"forEach",e,t,void 0,arguments)},includes(...e){return ss(this,"includes",e)},indexOf(...e){return ss(this,"indexOf",e)},join(e){return At(this).join(e)},lastIndexOf(...e){return ss(this,"lastIndexOf",e)},map(e,t){return Ge(this,"map",e,t,void 0,arguments)},pop(){return $t(this,"pop")},push(...e){return $t(this,"push",e)},reduce(e,...t){return cr(this,"reduce",e,t)},reduceRight(e,...t){return cr(this,"reduceRight",e,t)},shift(){return $t(this,"shift")},some(e,t){return Ge(this,"some",e,t,void 0,arguments)},splice(...e){return $t(this,"splice",e)},toReversed(){return At(this).toReversed()},toSorted(e){return At(this).toSorted(e)},toSpliced(...e){return At(this).toSpliced(...e)},unshift(...e){return $t(this,"unshift",e)},values(){return ns(this,"values",e=>Be(this,e))}};function ns(e,t,n){const s=$n(e),r=s[t]();return s!==e&&!Re(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.done||(i.value=n(i.value)),i}),r}const sl=Array.prototype;function Ge(e,t,n,s,r,i){const l=$n(e),o=l!==e&&!Re(e),c=l[t];if(c!==sl[t]){const h=c.apply(e,i);return o?De(h):h}let f=n;l!==e&&(o?f=function(h,v){return n.call(this,Be(e,h),v,e)}:n.length>2&&(f=function(h,v){return n.call(this,h,v,e)}));const a=c.call(l,f,s);return o&&r?r(a):a}function cr(e,t,n,s){const r=$n(e),i=r!==e&&!Re(e);let l=n,o=!1;r!==e&&(i?(o=s.length===0,l=function(f,a,h){return o&&(o=!1,f=Be(e,f)),n.call(this,f,Be(e,a),h,e)}):n.length>3&&(l=function(f,a,h){return n.call(this,f,a,h,e)}));const c=r[t](l,...s);return o?Be(e,c):c}function ss(e,t,n){const s=z(e);me(s,"iterate",Yt);const r=s[t](...n);return(r===-1||r===!1)&&Vn(n[0])?(n[0]=z(n[0]),s[t](...n)):r}function $t(e,t,n=[]){Qe(),ks();const s=z(e)[t].apply(e,n);return Ws(),Ze(),s}const rl=Fs("__proto__,__v_isRef,__isVue"),mi=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(He));function il(e){He(e)||(e=String(e));const t=z(this);return me(t,"has",e),t.hasOwnProperty(e)}class vi{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return i;if(n==="__v_raw")return s===(r?i?gl:wi:i?bi:_i).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const l=K(t);if(!r){let c;if(l&&(c=nl[n]))return c;if(n==="hasOwnProperty")return il}const o=Reflect.get(t,n,ae(t)?t:s);if((He(n)?mi.has(n):rl(n))||(r||me(t,"get",n),i))return o;if(ae(o)){const c=l&&Nn(n)?o:o.value;return r&&Q(c)?Jt(c):c}return Q(o)?r?Jt(o):Nt(o):o}}class yi extends vi{constructor(t=!1){super(!1,t)}set(t,n,s,r){let i=t[n];const l=K(t)&&Nn(n);if(!this._isShallow){const f=et(i);if(!Re(s)&&!et(s)&&(i=z(i),s=z(s)),!l&&ae(i)&&!ae(s))return f||(i.value=s),!0}const o=l?Number(n)e,fn=e=>Reflect.getPrototypeOf(e);function fl(e,t,n){return function(...s){const r=this.__v_raw,i=z(r),l=Pt(i),o=e==="entries"||e===Symbol.iterator&&l,c=e==="keys"&&l,f=r[e](...s),a=n?Ts:t?Ft:De;return!t&&me(i,"iterate",c?xs:vt),fe(Object.create(f),{next(){const{value:h,done:v}=f.next();return v?{value:h,done:v}:{value:o?[a(h[0]),a(h[1])]:a(h),done:v}}})}}function un(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function ul(e,t){const n={get(r){const i=this.__v_raw,l=z(i),o=z(r);e||(Ke(r,o)&&me(l,"get",r),me(l,"get",o));const{has:c}=fn(l),f=t?Ts:e?Ft:De;if(c.call(l,r))return f(i.get(r));if(c.call(l,o))return f(i.get(o));i!==l&&i.get(r)},get size(){const r=this.__v_raw;return!e&&me(z(r),"iterate",vt),r.size},has(r){const i=this.__v_raw,l=z(i),o=z(r);return e||(Ke(r,o)&&me(l,"has",r),me(l,"has",o)),r===o?i.has(r):i.has(r)||i.has(o)},forEach(r,i){const l=this,o=l.__v_raw,c=z(o),f=t?Ts:e?Ft:De;return!e&&me(c,"iterate",vt),o.forEach((a,h)=>r.call(i,f(a),f(h),l))}};return fe(n,e?{add:un("add"),set:un("set"),delete:un("delete"),clear:un("clear")}:{add(r){const i=z(this),l=fn(i),o=z(r),c=!t&&!Re(r)&&!et(r)?o:r;return l.has.call(i,c)||Ke(r,c)&&l.has.call(i,r)||Ke(o,c)&&l.has.call(i,o)||(i.add(c),ze(i,"add",c,c)),this},set(r,i){!t&&!Re(i)&&!et(i)&&(i=z(i));const l=z(this),{has:o,get:c}=fn(l);let f=o.call(l,r);f||(r=z(r),f=o.call(l,r));const a=c.call(l,r);return l.set(r,i),f?Ke(i,a)&&ze(l,"set",r,i):ze(l,"add",r,i),this},delete(r){const i=z(this),{has:l,get:o}=fn(i);let c=l.call(i,r);c||(r=z(r),c=l.call(i,r)),o&&o.call(i,r);const f=i.delete(r);return c&&ze(i,"delete",r,void 0),f},clear(){const r=z(this),i=r.size!==0,l=r.clear();return i&&ze(r,"clear",void 0,void 0),l}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=fl(r,e,t)}),n}function Bs(e,t){const n=ul(e,t);return(s,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(Z(n,r)&&r in s?n:s,r,i)}const dl={get:Bs(!1,!1)},hl={get:Bs(!1,!0)},pl={get:Bs(!0,!1)};const _i=new WeakMap,bi=new WeakMap,wi=new WeakMap,gl=new WeakMap;function ml(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function vl(e){return e.__v_skip||!Object.isExtensible(e)?0:ml(jo(e))}function Nt(e){return et(e)?e:Ks(e,!1,ll,dl,_i)}function yl(e){return Ks(e,!1,al,hl,bi)}function Jt(e){return Ks(e,!0,cl,pl,wi)}function Ks(e,t,n,s,r){if(!Q(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=vl(e);if(i===0)return e;const l=r.get(e);if(l)return l;const o=new Proxy(e,i===2?s:n);return r.set(e,o),o}function lt(e){return et(e)?lt(e.__v_raw):!!(e&&e.__v_isReactive)}function et(e){return!!(e&&e.__v_isReadonly)}function Re(e){return!!(e&&e.__v_isShallow)}function Vn(e){return e?!!e.__v_raw:!1}function z(e){const t=e&&e.__v_raw;return t?z(t):e}function wn(e){return!Z(e,"__v_skip")&&Object.isExtensible(e)&&si(e,"__v_skip",!0),e}const De=e=>Q(e)?Nt(e):e,Ft=e=>Q(e)?Jt(e):e;function ae(e){return e?e.__v_isRef===!0:!1}function yt(e){return Si(e,!1)}function Ie(e){return Si(e,!0)}function Si(e,t){return ae(e)?e:new _l(e,t)}class _l{constructor(t,n){this.dep=new jn,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:z(t),this._value=n?t:De(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||Re(t)||et(t);t=s?t:z(t),Ke(t,n)&&(this._rawValue=t,this._value=s?t:De(t),this.dep.trigger())}}function kn(e){return ae(e)?e.value:e}function ce(e){return G(e)?e():kn(e)}const bl={get:(e,t,n)=>t==="__v_raw"?e:kn(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return ae(r)&&!ae(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function xi(e){return lt(e)?e:new Proxy(e,bl)}class wl{constructor(t){this.__v_isRef=!0,this._value=void 0;const n=this.dep=new jn,{get:s,set:r}=t(n.track.bind(n),n.trigger.bind(n));this._get=s,this._set=r}get value(){return this._value=this._get()}set value(t){this._set(t)}}function Sl(e){return new wl(e)}class xl{constructor(t,n,s){this._object=t,this._key=n,this._defaultValue=s,this.__v_isRef=!0,this._value=void 0,this._raw=z(t);let r=!0,i=t;if(!K(t)||!Nn(String(n)))do r=!Vn(i)||Re(i);while(r&&(i=i.__v_raw));this._shallow=r}get value(){let t=this._object[this._key];return this._shallow&&(t=kn(t)),this._value=t===void 0?this._defaultValue:t}set value(t){if(this._shallow&&ae(this._raw[this._key])){const n=this._object[this._key];if(ae(n)){n.value=t;return}}this._object[this._key]=t}get dep(){return tl(this._raw,this._key)}}class Tl{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function Cl(e,t,n){return ae(e)?e:G(e)?new Tl(e):Q(e)&&arguments.length>1?El(e,t,n):yt(e)}function El(e,t,n){return new xl(e,t,n)}class Al{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new jn(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Xt-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&se!==this)return fi(this,!0),!0}get value(){const t=this.dep.track();return hi(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Rl(e,t,n=!1){let s,r;return G(e)?s=e:(s=e.get,r=e.set),new Al(s,r,n)}const dn={},En=new WeakMap;let pt;function Ol(e,t=!1,n=pt){if(n){let s=En.get(n);s||En.set(n,s=[]),s.push(e)}}function Ml(e,t,n=re){const{immediate:s,deep:r,once:i,scheduler:l,augmentJob:o,call:c}=n,f=g=>r?g:Re(g)||r===!1||r===0?ot(g,1):ot(g);let a,h,v,_,I=!1,T=!1;if(ae(e)?(h=()=>e.value,I=Re(e)):lt(e)?(h=()=>f(e),I=!0):K(e)?(T=!0,I=e.some(g=>lt(g)||Re(g)),h=()=>e.map(g=>{if(ae(g))return g.value;if(lt(g))return f(g);if(G(g))return c?c(g,2):g()})):G(e)?t?h=c?()=>c(e,2):e:h=()=>{if(v){Qe();try{v()}finally{Ze()}}const g=pt;pt=a;try{return c?c(e,3,[_]):e(_)}finally{pt=g}}:h=qe,t&&r){const g=h,R=r===!0?1/0:r;h=()=>ot(g(),R)}const W=li(),H=()=>{a.stop(),W&&W.active&&Ds(W.effects,a)};if(i&&t){const g=t;t=(...R)=>{g(...R),H()}}let D=T?new Array(e.length).fill(dn):dn;const p=g=>{if(!(!(a.flags&1)||!a.dirty&&!g))if(t){const R=a.run();if(r||I||(T?R.some((k,O)=>Ke(k,D[O])):Ke(R,D))){v&&v();const k=pt;pt=a;try{const O=[R,D===dn?void 0:T&&D[0]===dn?[]:D,_];D=R,c?c(t,3,O):t(...O)}finally{pt=k}}}else a.run()};return o&&o(p),a=new ci(h),a.scheduler=l?()=>l(p,!1):p,_=g=>Ol(g,!1,a),v=a.onStop=()=>{const g=En.get(a);if(g){if(c)c(g,4);else for(const R of g)R();En.delete(a)}},t?s?p(!0):D=a.run():l?l(p.bind(null,!0),!0):a.run(),H.pause=a.pause.bind(a),H.resume=a.resume.bind(a),H.stop=H,H}function ot(e,t=1/0,n){if(t<=0||!Q(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,ae(e))ot(e.value,t,n);else if(K(e))for(let s=0;s{ot(s,t,n)});else if(ni(e)){for(const s in e)ot(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&ot(e[s],t,n)}return e}/** +* @vue/runtime-core v3.5.30 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function on(e,t,n,s){try{return s?e(...s):e()}catch(r){Wn(r,t,n)}}function je(e,t,n,s){if(G(e)){const r=on(e,t,n,s);return r&&ei(r)&&r.catch(i=>{Wn(i,t,n)}),r}if(K(e)){const r=[];for(let i=0;i>>1,r=_e[s],i=zt(r);i=zt(n)?_e.push(e):_e.splice(Il(t),0,e),e.flags|=1,Ci()}}function Ci(){An||(An=Ti.then(Ei))}function Ll(e){K(e)?It.push(...e):it&&e.id===-1?it.splice(Ot+1,0,e):e.flags&1||(It.push(e),e.flags|=1),Ci()}function ar(e,t,n=ke+1){for(;n<_e.length;n++){const s=_e[n];if(s&&s.flags&2){if(e&&s.id!==e.uid)continue;_e.splice(n,1),n--,s.flags&4&&(s.flags&=-2),s(),s.flags&4||(s.flags&=-2)}}}function Rn(e){if(It.length){const t=[...new Set(It)].sort((n,s)=>zt(n)-zt(s));if(It.length=0,it){it.push(...t);return}for(it=t,Ot=0;Ote.id==null?e.flags&2?-1:1/0:e.id;function Ei(e){try{for(ke=0;ke<_e.length;ke++){const t=_e[ke];t&&!(t.flags&8)&&(t.flags&4&&(t.flags&=-2),on(t,t.i,t.i?15:14),t.flags&4||(t.flags&=-2))}}finally{for(;ke<_e.length;ke++){const t=_e[ke];t&&(t.flags&=-2)}ke=-1,_e.length=0,Rn(),An=null,(_e.length||It.length)&&Ei()}}let we=null,Ai=null;function On(e){const t=we;return we=e,Ai=e&&e.type.__scopeId||null,t}function Nl(e,t=we,n){if(!t||e._n)return e;const s=(...r)=>{s._d&&In(-1);const i=On(t);let l;try{l=e(...r)}finally{On(i),s._d&&In(1)}return l};return s._n=!0,s._c=!0,s._d=!0,s}function We(e,t,n,s){const r=e.dirs,i=t&&t.dirs;for(let l=0;l1)return n&&G(t)?t.call(s&&s.proxy):t}}function Ri(){return!!(Ct()||wt)}const Hl=Symbol.for("v-scx"),Dl=()=>_t(Hl);function Oi(e,t){return Bn(e,null,t)}function Sf(e,t){return Bn(e,null,{flush:"post"})}function Le(e,t,n){return Bn(e,t,n)}function Bn(e,t,n=re){const{immediate:s,deep:r,flush:i,once:l}=n,o=fe({},n),c=t&&s||!t&&i!=="post";let f;if(tn){if(i==="sync"){const _=Dl();f=_.__watcherHandles||(_.__watcherHandles=[])}else if(!c){const _=()=>{};return _.stop=qe,_.resume=qe,_.pause=qe,_}}const a=ve;o.call=(_,I,T)=>je(_,a,I,T);let h=!1;i==="post"?o.scheduler=_=>{Te(_,a&&a.suspense)}:i!=="sync"&&(h=!0,o.scheduler=(_,I)=>{I?_():qs(_)}),o.augmentJob=_=>{t&&(_.flags|=4),h&&(_.flags|=2,a&&(_.id=a.uid,_.i=a))};const v=Ml(e,t,o);return tn&&(f?f.push(v):c&&v()),v}function jl(e,t,n){const s=this.proxy,r=le(e)?e.includes(".")?Mi(s,e):()=>s[e]:e.bind(s,s);let i;G(t)?i=t:(i=t.handler,n=t);const l=ln(this),o=Bn(r,i.bind(s),n);return l(),o}function Mi(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;re.__isTeleport,Ue=Symbol("_leaveCb"),Vt=Symbol("_enterCb");function Vl(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return Ht(()=>{e.isMounted=!0}),Vi(()=>{e.isUnmounting=!0}),e}const Oe=[Function,Array],Ii={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Oe,onEnter:Oe,onAfterEnter:Oe,onEnterCancelled:Oe,onBeforeLeave:Oe,onLeave:Oe,onAfterLeave:Oe,onLeaveCancelled:Oe,onBeforeAppear:Oe,onAppear:Oe,onAfterAppear:Oe,onAppearCancelled:Oe},Li=e=>{const t=e.subTree;return t.component?Li(t.component):t},kl={name:"BaseTransition",props:Ii,setup(e,{slots:t}){const n=Ct(),s=Vl();return()=>{const r=t.default&&Hi(t.default(),!0);if(!r||!r.length)return;const i=Ni(r),l=z(e),{mode:o}=l;if(s.isLeaving)return rs(i);const c=fr(i);if(!c)return rs(i);let f=Cs(c,l,s,n,h=>f=h);c.type!==ue&&Qt(c,f);let a=n.subTree&&fr(n.subTree);if(a&&a.type!==ue&&!gt(a,c)&&Li(n).type!==ue){let h=Cs(a,l,s,n);if(Qt(a,h),o==="out-in"&&c.type!==ue)return s.isLeaving=!0,h.afterLeave=()=>{s.isLeaving=!1,n.job.flags&8||n.update(),delete h.afterLeave,a=void 0},rs(i);o==="in-out"&&c.type!==ue?h.delayLeave=(v,_,I)=>{const T=Fi(s,a);T[String(a.key)]=a,v[Ue]=()=>{_(),v[Ue]=void 0,delete f.delayedLeave,a=void 0},f.delayedLeave=()=>{I(),delete f.delayedLeave,a=void 0}}:a=void 0}else a&&(a=void 0);return i}}};function Ni(e){let t=e[0];if(e.length>1){for(const n of e)if(n.type!==ue){t=n;break}}return t}const Wl=kl;function Fi(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function Cs(e,t,n,s,r){const{appear:i,mode:l,persisted:o=!1,onBeforeEnter:c,onEnter:f,onAfterEnter:a,onEnterCancelled:h,onBeforeLeave:v,onLeave:_,onAfterLeave:I,onLeaveCancelled:T,onBeforeAppear:W,onAppear:H,onAfterAppear:D,onAppearCancelled:p}=t,g=String(e.key),R=Fi(n,e),k=(C,M)=>{C&&je(C,s,9,M)},O=(C,M)=>{const E=M[1];k(C,M),K(C)?C.every(y=>y.length<=1)&&E():C.length<=1&&E()},B={mode:l,persisted:o,beforeEnter(C){let M=c;if(!n.isMounted)if(i)M=W||c;else return;C[Ue]&&C[Ue](!0);const E=R[g];E&>(e,E)&&E.el[Ue]&&E.el[Ue](),k(M,[C])},enter(C){if(R[g]===e)return;let M=f,E=a,y=h;if(!n.isMounted)if(i)M=H||f,E=D||a,y=p||h;else return;let N=!1;C[Vt]=ie=>{N||(N=!0,ie?k(y,[C]):k(E,[C]),B.delayedLeave&&B.delayedLeave(),C[Vt]=void 0)};const Y=C[Vt].bind(null,!1);M?O(M,[C,Y]):Y()},leave(C,M){const E=String(e.key);if(C[Vt]&&C[Vt](!0),n.isUnmounting)return M();k(v,[C]);let y=!1;C[Ue]=Y=>{y||(y=!0,M(),Y?k(T,[C]):k(I,[C]),C[Ue]=void 0,R[E]===e&&delete R[E])};const N=C[Ue].bind(null,!1);R[E]=e,_?O(_,[C,N]):N()},clone(C){const M=Cs(C,t,n,s,r);return r&&r(M),M}};return B}function rs(e){if(Kn(e))return e=ct(e),e.children=null,e}function fr(e){if(!Kn(e))return Pi(e.type)&&e.children?Ni(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&G(n.default))return n.default()}}function Qt(e,t){e.shapeFlag&6&&e.component?(e.transition=t,Qt(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Hi(e,t=!1,n){let s=[],r=0;for(let i=0;i1)for(let i=0;iLt(T,t&&(K(t)?t[W]:t),n,s,r));return}if(bt(s)&&!r){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&Lt(e,t,n,s.component.subTree);return}const i=s.shapeFlag&4?Js(s.component):s.el,l=r?null:i,{i:o,r:c}=e,f=t&&t.r,a=o.refs===re?o.refs={}:o.refs,h=o.setupState,v=z(h),_=h===re?Qr:T=>ur(a,T)?!1:Z(v,T),I=(T,W)=>!(W&&ur(a,W));if(f!=null&&f!==c){if(dr(t),le(f))a[f]=null,_(f)&&(h[f]=null);else if(ae(f)){const T=t;I(f,T.k)&&(f.value=null),T.k&&(a[T.k]=null)}}if(G(c))on(c,o,12,[l,a]);else{const T=le(c),W=ae(c);if(T||W){const H=()=>{if(e.f){const D=T?_(c)?h[c]:a[c]:I()||!e.k?c.value:a[e.k];if(r)K(D)&&Ds(D,i);else if(K(D))D.includes(i)||D.push(i);else if(T)a[c]=[i],_(c)&&(h[c]=a[c]);else{const p=[i];I(c,e.k)&&(c.value=p),e.k&&(a[e.k]=p)}}else T?(a[c]=l,_(c)&&(h[c]=l)):W&&(I(c,e.k)&&(c.value=l),e.k&&(a[e.k]=l))};if(l){const D=()=>{H(),Mn.delete(e)};D.id=-1,Mn.set(e,D),Te(D,n)}else dr(e),H()}}}function dr(e){const t=Mn.get(e);t&&(t.flags|=8,Mn.delete(e))}let hr=!1;const Rt=()=>{hr||(console.error("Hydration completed but contains mismatches."),hr=!0)},Ul=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",Bl=e=>e.namespaceURI.includes("MathML"),hn=e=>{if(e.nodeType===1){if(Ul(e))return"svg";if(Bl(e))return"mathml"}},pn=e=>e.nodeType===8;function Kl(e){const{mt:t,p:n,o:{patchProp:s,createText:r,nextSibling:i,parentNode:l,remove:o,insert:c,createComment:f}}=e,a=(p,g)=>{if(!g.hasChildNodes()){n(null,p,g),Rn(),g._vnode=p;return}h(g.firstChild,p,null,null,null),Rn(),g._vnode=p},h=(p,g,R,k,O,B=!1)=>{B=B||!!g.dynamicChildren;const C=pn(p)&&p.data==="[",M=()=>T(p,g,R,k,O,C),{type:E,ref:y,shapeFlag:N,patchFlag:Y}=g;let ie=p.nodeType;g.el=p,Y===-2&&(B=!1,g.dynamicChildren=null);let V=null;switch(E){case St:ie!==3?g.children===""?(c(g.el=r(""),l(p),p),V=p):V=M():(p.data!==g.children&&(Rt(),p.data=g.children),V=i(p));break;case ue:D(p)?(V=i(p),H(g.el=p.content.firstChild,p,R)):ie!==8||C?V=M():V=i(p);break;case qt:if(C&&(p=i(p),ie=p.nodeType),ie===1||ie===3){V=p;const X=!g.children.length;for(let j=0;j{B=B||!!g.dynamicChildren;const{type:C,props:M,patchFlag:E,shapeFlag:y,dirs:N,transition:Y}=g,ie=C==="input"||C==="option";if(ie||E!==-1){N&&We(g,null,R,"created");let V=!1;if(D(p)){V=io(null,Y)&&R&&R.vnode.props&&R.vnode.props.appear;const j=p.content.firstChild;if(V){const te=j.getAttribute("class");te&&(j.$cls=te),Y.beforeEnter(j)}H(j,p,R),g.el=p=j}if(y&16&&!(M&&(M.innerHTML||M.textContent))){let j=_(p.firstChild,g,p,R,k,O,B);for(;j;){gn(p,1)||Rt();const te=j;j=j.nextSibling,o(te)}}else if(y&8){let j=g.children;j[0]===` +`&&(p.tagName==="PRE"||p.tagName==="TEXTAREA")&&(j=j.slice(1));const{textContent:te}=p;te!==j&&te!==j.replace(/\r\n|\r/g,` +`)&&(gn(p,0)||Rt(),p.textContent=g.children)}if(M){if(ie||!B||E&48){const j=p.tagName.includes("-");for(const te in M)(ie&&(te.endsWith("value")||te==="indeterminate")||sn(te)&&!mt(te)||te[0]==="."||j&&!mt(te))&&s(p,te,null,M[te],void 0,R)}else if(M.onClick)s(p,"onClick",null,M.onClick,void 0,R);else if(E&4&<(M.style))for(const j in M.style)M.style[j]}let X;(X=M&&M.onVnodeBeforeMount)&&Me(X,R,g),N&&We(g,null,R,"beforeMount"),((X=M&&M.onVnodeMounted)||N||V)&&fo(()=>{X&&Me(X,R,g),V&&Y.enter(p),N&&We(g,null,R,"mounted")},k)}return p.nextSibling},_=(p,g,R,k,O,B,C)=>{C=C||!!g.dynamicChildren;const M=g.children,E=M.length;for(let y=0;y{const{slotScopeIds:C}=g;C&&(O=O?O.concat(C):C);const M=l(p),E=_(i(p),g,M,R,k,O,B);return E&&pn(E)&&E.data==="]"?i(g.anchor=E):(Rt(),c(g.anchor=f("]"),M,E),E)},T=(p,g,R,k,O,B)=>{if(gn(p.parentElement,1)||Rt(),g.el=null,B){const E=W(p);for(;;){const y=i(p);if(y&&y!==E)o(y);else break}}const C=i(p),M=l(p);return o(p),n(null,g,M,C,R,k,hn(M),O),R&&(R.vnode.el=g.el,Ji(R,g.el)),C},W=(p,g="[",R="]")=>{let k=0;for(;p;)if(p=i(p),p&&pn(p)&&(p.data===g&&k++,p.data===R)){if(k===0)return i(p);k--}return p},H=(p,g,R)=>{const k=g.parentNode;k&&k.replaceChild(p,g);let O=R;for(;O;)O.vnode.el===g&&(O.vnode.el=O.subTree.el=p),O=O.parent},D=p=>p.nodeType===1&&p.tagName==="TEMPLATE";return[a,h]}const pr="data-allow-mismatch",ql={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function gn(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(pr);)e=e.parentElement;const n=e&&e.getAttribute(pr);if(n==null)return!1;if(n==="")return!0;{const s=n.split(",");return t===0&&s.includes("children")?!0:s.includes(ql[t])}}Dn().requestIdleCallback;Dn().cancelIdleCallback;const bt=e=>!!e.type.__asyncLoader,Kn=e=>e.type.__isKeepAlive;function Gl(e,t){$i(e,"a",t)}function Xl(e,t){$i(e,"da",t)}function $i(e,t,n=ve){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(qn(t,s,n),n){let r=n.parent;for(;r&&r.parent;)Kn(r.parent.vnode)&&Yl(s,t,n,r),r=r.parent}}function Yl(e,t,n,s){const r=qn(t,e,s,!0);Gn(()=>{Ds(s[t],r)},n)}function qn(e,t,n=ve,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...l)=>{Qe();const o=ln(n),c=je(t,n,e,l);return o(),Ze(),c});return s?r.unshift(i):r.push(i),i}}const st=e=>(t,n=ve)=>{(!tn||e==="sp")&&qn(e,(...s)=>t(...s),n)},Jl=st("bm"),Ht=st("m"),zl=st("bu"),Ql=st("u"),Vi=st("bum"),Gn=st("um"),Zl=st("sp"),ec=st("rtg"),tc=st("rtc");function nc(e,t=ve){qn("ec",e,t)}const ki="components";function xf(e,t){return Ui(ki,e,!0,t)||e}const Wi=Symbol.for("v-ndc");function Tf(e){return le(e)?Ui(ki,e,!1)||e:e||Wi}function Ui(e,t,n=!0,s=!1){const r=we||ve;if(r){const i=r.type;{const o=Dc(i,!1);if(o&&(o===t||o===Se(t)||o===Hn(Se(t))))return i}const l=gr(r[e]||i[e],t)||gr(r.appContext[e],t);return!l&&s?i:l}}function gr(e,t){return e&&(e[t]||e[Se(t)]||e[Hn(Se(t))])}function Cf(e,t,n,s){let r;const i=n,l=K(e);if(l||le(e)){const o=l&<(e);let c=!1,f=!1;o&&(c=!Re(e),f=et(e),e=$n(e)),r=new Array(e.length);for(let a=0,h=e.length;at(o,c,void 0,i));else{const o=Object.keys(e);r=new Array(o.length);for(let c=0,f=o.length;c0;return t!=="default"&&(n.name=t),Ms(),Ps(be,null,[de("slot",n,s&&s())],f?-2:64)}let i=e[t];i&&i._c&&(i._d=!1),Ms();const l=i&&Bi(i(n)),o=n.key||l&&l.key,c=Ps(be,{key:(o&&!He(o)?o:`_${t}`)+(!l&&s?"_fb":"")},l||(s?s():[]),l&&e._===1?64:-2);return!r&&c.scopeId&&(c.slotScopeIds=[c.scopeId+"-s"]),i&&i._c&&(i._d=!0),c}function Bi(e){return e.some(t=>en(t)?!(t.type===ue||t.type===be&&!Bi(t.children)):!0)?e:null}function Af(e,t){const n={};for(const s in e)n[/[A-Z]/.test(s)?`on:${s}`:bn(s)]=e[s];return n}const Es=e=>e?mo(e)?Js(e):Es(e.parent):null,Kt=fe(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Es(e.parent),$root:e=>Es(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>qi(e),$forceUpdate:e=>e.f||(e.f=()=>{qs(e.update)}),$nextTick:e=>e.n||(e.n=Un.bind(e.proxy)),$watch:e=>jl.bind(e)}),is=(e,t)=>e!==re&&!e.__isScriptSetup&&Z(e,t),sc={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:i,accessCache:l,type:o,appContext:c}=e;if(t[0]!=="$"){const v=l[t];if(v!==void 0)switch(v){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(is(s,t))return l[t]=1,s[t];if(r!==re&&Z(r,t))return l[t]=2,r[t];if(Z(i,t))return l[t]=3,i[t];if(n!==re&&Z(n,t))return l[t]=4,n[t];As&&(l[t]=0)}}const f=Kt[t];let a,h;if(f)return t==="$attrs"&&me(e.attrs,"get",""),f(e);if((a=o.__cssModules)&&(a=a[t]))return a;if(n!==re&&Z(n,t))return l[t]=4,n[t];if(h=c.config.globalProperties,Z(h,t))return h[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return is(r,t)?(r[t]=n,!0):s!==re&&Z(s,t)?(s[t]=n,!0):Z(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,props:i,type:l}},o){let c;return!!(n[o]||e!==re&&o[0]!=="$"&&Z(e,o)||is(t,o)||Z(i,o)||Z(s,o)||Z(Kt,o)||Z(r.config.globalProperties,o)||(c=l.__cssModules)&&c[o])},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:Z(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Rf(){return rc().slots}function rc(e){const t=Ct();return t.setupContext||(t.setupContext=yo(t))}function mr(e){return K(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let As=!0;function ic(e){const t=qi(e),n=e.proxy,s=e.ctx;As=!1,t.beforeCreate&&vr(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:l,watch:o,provide:c,inject:f,created:a,beforeMount:h,mounted:v,beforeUpdate:_,updated:I,activated:T,deactivated:W,beforeDestroy:H,beforeUnmount:D,destroyed:p,unmounted:g,render:R,renderTracked:k,renderTriggered:O,errorCaptured:B,serverPrefetch:C,expose:M,inheritAttrs:E,components:y,directives:N,filters:Y}=t;if(f&&oc(f,s,null),l)for(const X in l){const j=l[X];G(j)&&(s[X]=j.bind(n))}if(r){const X=r.call(n,n);Q(X)&&(e.data=Nt(X))}if(As=!0,i)for(const X in i){const j=i[X],te=G(j)?j.bind(n,n):G(j.get)?j.get.bind(n,n):qe,cn=!G(j)&&G(j.set)?j.set.bind(n):qe,ft=oe({get:te,set:cn});Object.defineProperty(s,X,{enumerable:!0,configurable:!0,get:()=>ft.value,set:$e=>ft.value=$e})}if(o)for(const X in o)Ki(o[X],s,n,X);if(c){const X=G(c)?c.call(n):c;Reflect.ownKeys(X).forEach(j=>{Fl(j,X[j])})}a&&vr(a,e,"c");function V(X,j){K(j)?j.forEach(te=>X(te.bind(n))):j&&X(j.bind(n))}if(V(Jl,h),V(Ht,v),V(zl,_),V(Ql,I),V(Gl,T),V(Xl,W),V(nc,B),V(tc,k),V(ec,O),V(Vi,D),V(Gn,g),V(Zl,C),K(M))if(M.length){const X=e.exposed||(e.exposed={});M.forEach(j=>{Object.defineProperty(X,j,{get:()=>n[j],set:te=>n[j]=te,enumerable:!0})})}else e.exposed||(e.exposed={});R&&e.render===qe&&(e.render=R),E!=null&&(e.inheritAttrs=E),y&&(e.components=y),N&&(e.directives=N),C&&ji(e)}function oc(e,t,n=qe){K(e)&&(e=Rs(e));for(const s in e){const r=e[s];let i;Q(r)?"default"in r?i=_t(r.from||s,r.default,!0):i=_t(r.from||s):i=_t(r),ae(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:l=>i.value=l}):t[s]=i}}function vr(e,t,n){je(K(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function Ki(e,t,n,s){let r=s.includes(".")?Mi(n,s):()=>n[s];if(le(e)){const i=t[e];G(i)&&Le(r,i)}else if(G(e))Le(r,e.bind(n));else if(Q(e))if(K(e))e.forEach(i=>Ki(i,t,n,s));else{const i=G(e.handler)?e.handler.bind(n):t[e.handler];G(i)&&Le(r,i,e)}}function qi(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:l}}=e.appContext,o=i.get(t);let c;return o?c=o:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(f=>Pn(c,f,l,!0)),Pn(c,t,l)),Q(t)&&i.set(t,c),c}function Pn(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&Pn(e,i,n,!0),r&&r.forEach(l=>Pn(e,l,n,!0));for(const l in t)if(!(s&&l==="expose")){const o=lc[l]||n&&n[l];e[l]=o?o(e[l],t[l]):t[l]}return e}const lc={data:yr,props:_r,emits:_r,methods:Wt,computed:Wt,beforeCreate:ye,created:ye,beforeMount:ye,mounted:ye,beforeUpdate:ye,updated:ye,beforeDestroy:ye,beforeUnmount:ye,destroyed:ye,unmounted:ye,activated:ye,deactivated:ye,errorCaptured:ye,serverPrefetch:ye,components:Wt,directives:Wt,watch:ac,provide:yr,inject:cc};function yr(e,t){return t?e?function(){return fe(G(e)?e.call(this,this):e,G(t)?t.call(this,this):t)}:t:e}function cc(e,t){return Wt(Rs(e),Rs(t))}function Rs(e){if(K(e)){const t={};for(let n=0;nt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Se(t)}Modifiers`]||e[`${at(t)}Modifiers`];function hc(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||re;let r=n;const i=t.startsWith("update:"),l=i&&dc(s,t.slice(7));l&&(l.trim&&(r=n.map(a=>le(a)?a.trim():a)),l.number&&(r=n.map(ko)));let o,c=s[o=bn(t)]||s[o=bn(Se(t))];!c&&i&&(c=s[o=bn(at(t))]),c&&je(c,e,6,r);const f=s[o+"Once"];if(f){if(!e.emitted)e.emitted={};else if(e.emitted[o])return;e.emitted[o]=!0,je(f,e,6,r)}}const pc=new WeakMap;function Xi(e,t,n=!1){const s=n?pc:t.emitsCache,r=s.get(e);if(r!==void 0)return r;const i=e.emits;let l={},o=!1;if(!G(e)){const c=f=>{const a=Xi(f,t,!0);a&&(o=!0,fe(l,a))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!i&&!o?(Q(e)&&s.set(e,null),null):(K(i)?i.forEach(c=>l[c]=null):fe(l,i),Q(e)&&s.set(e,l),l)}function Xn(e,t){return!e||!sn(t)?!1:(t=t.slice(2).replace(/Once$/,""),Z(e,t[0].toLowerCase()+t.slice(1))||Z(e,at(t))||Z(e,t))}function os(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[i],slots:l,attrs:o,emit:c,render:f,renderCache:a,props:h,data:v,setupState:_,ctx:I,inheritAttrs:T}=e,W=On(e);let H,D;try{if(n.shapeFlag&4){const g=r||s,R=g;H=Pe(f.call(R,g,a,h,_,v,I)),D=o}else{const g=t;H=Pe(g.length>1?g(h,{attrs:o,slots:l,emit:c}):g(h,null)),D=t.props?o:gc(o)}}catch(g){Gt.length=0,Wn(g,e,1),H=de(ue)}let p=H;if(D&&T!==!1){const g=Object.keys(D),{shapeFlag:R}=p;g.length&&R&7&&(i&&g.some(Hs)&&(D=mc(D,i)),p=ct(p,D,!1,!0))}return n.dirs&&(p=ct(p,null,!1,!0),p.dirs=p.dirs?p.dirs.concat(n.dirs):n.dirs),n.transition&&Qt(p,n.transition),H=p,On(W),H}const gc=e=>{let t;for(const n in e)(n==="class"||n==="style"||sn(n))&&((t||(t={}))[n]=e[n]);return t},mc=(e,t)=>{const n={};for(const s in e)(!Hs(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function vc(e,t,n){const{props:s,children:r,component:i}=e,{props:l,children:o,patchFlag:c}=t,f=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?br(s,l,f):!!l;if(c&8){const a=t.dynamicProps;for(let h=0;hObject.create(zi),Zi=e=>Object.getPrototypeOf(e)===zi;function yc(e,t,n,s=!1){const r={},i=Qi();e.propsDefaults=Object.create(null),eo(e,t,r,i);for(const l in e.propsOptions[0])l in r||(r[l]=void 0);n?e.props=s?r:yl(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function _c(e,t,n,s){const{props:r,attrs:i,vnode:{patchFlag:l}}=e,o=z(r),[c]=e.propsOptions;let f=!1;if((s||l>0)&&!(l&16)){if(l&8){const a=e.vnode.dynamicProps;for(let h=0;h{c=!0;const[v,_]=to(h,t,!0);fe(l,v),_&&o.push(..._)};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!i&&!c)return Q(e)&&s.set(e,Mt),Mt;if(K(i))for(let a=0;ae==="_"||e==="_ctx"||e==="$stable",Xs=e=>K(e)?e.map(Pe):[Pe(e)],wc=(e,t,n)=>{if(t._n)return t;const s=Nl((...r)=>Xs(t(...r)),n);return s._c=!1,s},no=(e,t,n)=>{const s=e._ctx;for(const r in e){if(Gs(r))continue;const i=e[r];if(G(i))t[r]=wc(r,i,s);else if(i!=null){const l=Xs(i);t[r]=()=>l}}},so=(e,t)=>{const n=Xs(t);e.slots.default=()=>n},ro=(e,t,n)=>{for(const s in t)(n||!Gs(s))&&(e[s]=t[s])},Sc=(e,t,n)=>{const s=e.slots=Qi();if(e.vnode.shapeFlag&32){const r=t._;r?(ro(s,t,n),n&&si(s,"_",r,!0)):no(t,s)}else t&&so(e,t)},xc=(e,t,n)=>{const{vnode:s,slots:r}=e;let i=!0,l=re;if(s.shapeFlag&32){const o=t._;o?n&&o===1?i=!1:ro(r,t,n):(i=!t.$stable,no(t,r)),l=t}else t&&(so(e,t),l={default:1});if(i)for(const o in r)!Gs(o)&&l[o]==null&&delete r[o]},Te=fo;function Tc(e){return Cc(e,Kl)}function Cc(e,t){const n=Dn();n.__VUE__=!0;const{insert:s,remove:r,patchProp:i,createElement:l,createText:o,createComment:c,setText:f,setElementText:a,parentNode:h,nextSibling:v,setScopeId:_=qe,insertStaticContent:I}=e,T=(u,d,m,x=null,b=null,w=null,L=void 0,P=null,A=!!d.dynamicChildren)=>{if(u===d)return;u&&!gt(u,d)&&(x=an(u),$e(u,b,w,!0),u=null),d.patchFlag===-2&&(A=!1,d.dynamicChildren=null);const{type:S,ref:U,shapeFlag:F}=d;switch(S){case St:W(u,d,m,x);break;case ue:H(u,d,m,x);break;case qt:u==null&&D(d,m,x,L);break;case be:y(u,d,m,x,b,w,L,P,A);break;default:F&1?R(u,d,m,x,b,w,L,P,A):F&6?N(u,d,m,x,b,w,L,P,A):(F&64||F&128)&&S.process(u,d,m,x,b,w,L,P,A,Et)}U!=null&&b?Lt(U,u&&u.ref,w,d||u,!d):U==null&&u&&u.ref!=null&&Lt(u.ref,null,w,u,!0)},W=(u,d,m,x)=>{if(u==null)s(d.el=o(d.children),m,x);else{const b=d.el=u.el;d.children!==u.children&&f(b,d.children)}},H=(u,d,m,x)=>{u==null?s(d.el=c(d.children||""),m,x):d.el=u.el},D=(u,d,m,x)=>{[u.el,u.anchor]=I(u.children,d,m,x,u.el,u.anchor)},p=({el:u,anchor:d},m,x)=>{let b;for(;u&&u!==d;)b=v(u),s(u,m,x),u=b;s(d,m,x)},g=({el:u,anchor:d})=>{let m;for(;u&&u!==d;)m=v(u),r(u),u=m;r(d)},R=(u,d,m,x,b,w,L,P,A)=>{if(d.type==="svg"?L="svg":d.type==="math"&&(L="mathml"),u==null)k(d,m,x,b,w,L,P,A);else{const S=u.el&&u.el._isVueCE?u.el:null;try{S&&S._beginPatch(),C(u,d,b,w,L,P,A)}finally{S&&S._endPatch()}}},k=(u,d,m,x,b,w,L,P)=>{let A,S;const{props:U,shapeFlag:F,transition:$,dirs:q}=u;if(A=u.el=l(u.type,w,U&&U.is,U),F&8?a(A,u.children):F&16&&B(u.children,A,null,x,b,ls(u,w),L,P),q&&We(u,null,x,"created"),O(A,u,u.scopeId,L,x),U){for(const ne in U)ne!=="value"&&!mt(ne)&&i(A,ne,null,U[ne],w,x);"value"in U&&i(A,"value",null,U.value,w),(S=U.onVnodeBeforeMount)&&Me(S,x,u)}q&&We(u,null,x,"beforeMount");const J=io(b,$);J&&$.beforeEnter(A),s(A,d,m),((S=U&&U.onVnodeMounted)||J||q)&&Te(()=>{S&&Me(S,x,u),J&&$.enter(A),q&&We(u,null,x,"mounted")},b)},O=(u,d,m,x,b)=>{if(m&&_(u,m),x)for(let w=0;w{for(let S=A;S{const P=d.el=u.el;let{patchFlag:A,dynamicChildren:S,dirs:U}=d;A|=u.patchFlag&16;const F=u.props||re,$=d.props||re;let q;if(m&&ut(m,!1),(q=$.onVnodeBeforeUpdate)&&Me(q,m,d,u),U&&We(d,u,m,"beforeUpdate"),m&&ut(m,!0),(F.innerHTML&&$.innerHTML==null||F.textContent&&$.textContent==null)&&a(P,""),S?M(u.dynamicChildren,S,P,m,x,ls(d,b),w):L||j(u,d,P,null,m,x,ls(d,b),w,!1),A>0){if(A&16)E(P,F,$,m,b);else if(A&2&&F.class!==$.class&&i(P,"class",null,$.class,b),A&4&&i(P,"style",F.style,$.style,b),A&8){const J=d.dynamicProps;for(let ne=0;ne{q&&Me(q,m,d,u),U&&We(d,u,m,"updated")},x)},M=(u,d,m,x,b,w,L)=>{for(let P=0;P{if(d!==m){if(d!==re)for(const w in d)!mt(w)&&!(w in m)&&i(u,w,d[w],null,b,x);for(const w in m){if(mt(w))continue;const L=m[w],P=d[w];L!==P&&w!=="value"&&i(u,w,P,L,b,x)}"value"in m&&i(u,"value",d.value,m.value,b)}},y=(u,d,m,x,b,w,L,P,A)=>{const S=d.el=u?u.el:o(""),U=d.anchor=u?u.anchor:o("");let{patchFlag:F,dynamicChildren:$,slotScopeIds:q}=d;q&&(P=P?P.concat(q):q),u==null?(s(S,m,x),s(U,m,x),B(d.children||[],m,U,b,w,L,P,A)):F>0&&F&64&&$&&u.dynamicChildren&&u.dynamicChildren.length===$.length?(M(u.dynamicChildren,$,m,b,w,L,P),(d.key!=null||b&&d===b.subTree)&&oo(u,d,!0)):j(u,d,m,U,b,w,L,P,A)},N=(u,d,m,x,b,w,L,P,A)=>{d.slotScopeIds=P,u==null?d.shapeFlag&512?b.ctx.activate(d,m,x,L,A):Y(d,m,x,b,w,L,A):ie(u,d,A)},Y=(u,d,m,x,b,w,L)=>{const P=u.component=Lc(u,x,b);if(Kn(u)&&(P.ctx.renderer=Et),Nc(P,!1,L),P.asyncDep){if(b&&b.registerDep(P,V,L),!u.el){const A=P.subTree=de(ue);H(null,A,d,m),u.placeholder=A.el}}else V(P,u,d,m,b,w,L)},ie=(u,d,m)=>{const x=d.component=u.component;if(vc(u,d,m))if(x.asyncDep&&!x.asyncResolved){X(x,d,m);return}else x.next=d,x.update();else d.el=u.el,x.vnode=d},V=(u,d,m,x,b,w,L)=>{const P=()=>{if(u.isMounted){let{next:F,bu:$,u:q,parent:J,vnode:ne}=u;{const Ce=lo(u);if(Ce){F&&(F.el=ne.el,X(u,F,L)),Ce.asyncDep.then(()=>{Te(()=>{u.isUnmounted||S()},b)});return}}let ee=F,xe;ut(u,!1),F?(F.el=ne.el,X(u,F,L)):F=ne,$&&Zn($),(xe=F.props&&F.props.onVnodeBeforeUpdate)&&Me(xe,J,F,ne),ut(u,!0);const he=os(u),Ne=u.subTree;u.subTree=he,T(Ne,he,h(Ne.el),an(Ne),u,b,w),F.el=he.el,ee===null&&Ji(u,he.el),q&&Te(q,b),(xe=F.props&&F.props.onVnodeUpdated)&&Te(()=>Me(xe,J,F,ne),b)}else{let F;const{el:$,props:q}=d,{bm:J,m:ne,parent:ee,root:xe,type:he}=u,Ne=bt(d);if(ut(u,!1),J&&Zn(J),!Ne&&(F=q&&q.onVnodeBeforeMount)&&Me(F,ee,d),ut(u,!0),$&&Qn){const Ce=()=>{u.subTree=os(u),Qn($,u.subTree,u,b,null)};Ne&&he.__asyncHydrate?he.__asyncHydrate($,u,Ce):Ce()}else{xe.ce&&xe.ce._hasShadowRoot()&&xe.ce._injectChildStyle(he,u.parent?u.parent.type:void 0);const Ce=u.subTree=os(u);T(null,Ce,m,x,u,b,w),d.el=Ce.el}if(ne&&Te(ne,b),!Ne&&(F=q&&q.onVnodeMounted)){const Ce=d;Te(()=>Me(F,ee,Ce),b)}(d.shapeFlag&256||ee&&bt(ee.vnode)&&ee.vnode.shapeFlag&256)&&u.a&&Te(u.a,b),u.isMounted=!0,d=m=x=null}};u.scope.on();const A=u.effect=new ci(P);u.scope.off();const S=u.update=A.run.bind(A),U=u.job=A.runIfDirty.bind(A);U.i=u,U.id=u.uid,A.scheduler=()=>qs(U),ut(u,!0),S()},X=(u,d,m)=>{d.component=u;const x=u.vnode.props;u.vnode=d,u.next=null,_c(u,d.props,x,m),xc(u,d.children,m),Qe(),ar(u),Ze()},j=(u,d,m,x,b,w,L,P,A=!1)=>{const S=u&&u.children,U=u?u.shapeFlag:0,F=d.children,{patchFlag:$,shapeFlag:q}=d;if($>0){if($&128){cn(S,F,m,x,b,w,L,P,A);return}else if($&256){te(S,F,m,x,b,w,L,P,A);return}}q&8?(U&16&&Dt(S,b,w),F!==S&&a(m,F)):U&16?q&16?cn(S,F,m,x,b,w,L,P,A):Dt(S,b,w,!0):(U&8&&a(m,""),q&16&&B(F,m,x,b,w,L,P,A))},te=(u,d,m,x,b,w,L,P,A)=>{u=u||Mt,d=d||Mt;const S=u.length,U=d.length,F=Math.min(S,U);let $;for($=0;$U?Dt(u,b,w,!0,!1,F):B(d,m,x,b,w,L,P,A,F)},cn=(u,d,m,x,b,w,L,P,A)=>{let S=0;const U=d.length;let F=u.length-1,$=U-1;for(;S<=F&&S<=$;){const q=u[S],J=d[S]=A?Je(d[S]):Pe(d[S]);if(gt(q,J))T(q,J,m,null,b,w,L,P,A);else break;S++}for(;S<=F&&S<=$;){const q=u[F],J=d[$]=A?Je(d[$]):Pe(d[$]);if(gt(q,J))T(q,J,m,null,b,w,L,P,A);else break;F--,$--}if(S>F){if(S<=$){const q=$+1,J=q$)for(;S<=F;)$e(u[S],b,w,!0),S++;else{const q=S,J=S,ne=new Map;for(S=J;S<=$;S++){const Ee=d[S]=A?Je(d[S]):Pe(d[S]);Ee.key!=null&&ne.set(Ee.key,S)}let ee,xe=0;const he=$-J+1;let Ne=!1,Ce=0;const jt=new Array(he);for(S=0;S=he){$e(Ee,b,w,!0);continue}let Ve;if(Ee.key!=null)Ve=ne.get(Ee.key);else for(ee=J;ee<=$;ee++)if(jt[ee-J]===0&>(Ee,d[ee])){Ve=ee;break}Ve===void 0?$e(Ee,b,w,!0):(jt[Ve-J]=S+1,Ve>=Ce?Ce=Ve:Ne=!0,T(Ee,d[Ve],m,null,b,w,L,P,A),xe++)}const nr=Ne?Ec(jt):Mt;for(ee=nr.length-1,S=he-1;S>=0;S--){const Ee=J+S,Ve=d[Ee],sr=d[Ee+1],rr=Ee+1{const{el:w,type:L,transition:P,children:A,shapeFlag:S}=u;if(S&6){ft(u.component.subTree,d,m,x);return}if(S&128){u.suspense.move(d,m,x);return}if(S&64){L.move(u,d,m,Et);return}if(L===be){s(w,d,m);for(let F=0;FP.enter(w),b);else{const{leave:F,delayLeave:$,afterLeave:q}=P,J=()=>{u.ctx.isUnmounted?r(w):s(w,d,m)},ne=()=>{w._isLeaving&&w[Ue](!0),F(w,()=>{J(),q&&q()})};$?$(w,J,ne):ne()}else s(w,d,m)},$e=(u,d,m,x=!1,b=!1)=>{const{type:w,props:L,ref:P,children:A,dynamicChildren:S,shapeFlag:U,patchFlag:F,dirs:$,cacheIndex:q}=u;if(F===-2&&(b=!1),P!=null&&(Qe(),Lt(P,null,m,u,!0),Ze()),q!=null&&(d.renderCache[q]=void 0),U&256){d.ctx.deactivate(u);return}const J=U&1&&$,ne=!bt(u);let ee;if(ne&&(ee=L&&L.onVnodeBeforeUnmount)&&Me(ee,d,u),U&6)Ho(u.component,m,x);else{if(U&128){u.suspense.unmount(m,x);return}J&&We(u,null,d,"beforeUnmount"),U&64?u.type.remove(u,d,m,Et,x):S&&!S.hasOnce&&(w!==be||F>0&&F&64)?Dt(S,d,m,!1,!0):(w===be&&F&384||!b&&U&16)&&Dt(A,d,m),x&&er(u)}(ne&&(ee=L&&L.onVnodeUnmounted)||J)&&Te(()=>{ee&&Me(ee,d,u),J&&We(u,null,d,"unmounted")},m)},er=u=>{const{type:d,el:m,anchor:x,transition:b}=u;if(d===be){Fo(m,x);return}if(d===qt){g(u);return}const w=()=>{r(m),b&&!b.persisted&&b.afterLeave&&b.afterLeave()};if(u.shapeFlag&1&&b&&!b.persisted){const{leave:L,delayLeave:P}=b,A=()=>L(m,w);P?P(u.el,w,A):A()}else w()},Fo=(u,d)=>{let m;for(;u!==d;)m=v(u),r(u),u=m;r(d)},Ho=(u,d,m)=>{const{bum:x,scope:b,job:w,subTree:L,um:P,m:A,a:S}=u;Sr(A),Sr(S),x&&Zn(x),b.stop(),w&&(w.flags|=8,$e(L,u,d,m)),P&&Te(P,d),Te(()=>{u.isUnmounted=!0},d)},Dt=(u,d,m,x=!1,b=!1,w=0)=>{for(let L=w;L{if(u.shapeFlag&6)return an(u.component.subTree);if(u.shapeFlag&128)return u.suspense.next();const d=v(u.anchor||u.el),m=d&&d[$l];return m?v(m):d};let Jn=!1;const tr=(u,d,m)=>{let x;u==null?d._vnode&&($e(d._vnode,null,null,!0),x=d._vnode.component):T(d._vnode||null,u,d,null,null,null,m),d._vnode=u,Jn||(Jn=!0,ar(x),Rn(),Jn=!1)},Et={p:T,um:$e,m:ft,r:er,mt:Y,mc:B,pc:j,pbc:M,n:an,o:e};let zn,Qn;return t&&([zn,Qn]=t(Et)),{render:tr,hydrate:zn,createApp:uc(tr,zn)}}function ls({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function ut({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function io(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function oo(e,t,n=!1){const s=e.children,r=t.children;if(K(s)&&K(r))for(let i=0;i>1,e[n[o]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,l=n[i-1];i-- >0;)n[i]=l,l=t[l];return n}function lo(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:lo(t)}function Sr(e){if(e)for(let t=0;te.__isSuspense;function fo(e,t){t&&t.pendingBranch?K(e)?t.effects.push(...e):t.effects.push(e):Ll(e)}const be=Symbol.for("v-fgt"),St=Symbol.for("v-txt"),ue=Symbol.for("v-cmt"),qt=Symbol.for("v-stc"),Gt=[];let Ae=null;function Ms(e=!1){Gt.push(Ae=e?null:[])}function Ac(){Gt.pop(),Ae=Gt[Gt.length-1]||null}let Zt=1;function In(e,t=!1){Zt+=e,e<0&&Ae&&t&&(Ae.hasOnce=!0)}function uo(e){return e.dynamicChildren=Zt>0?Ae||Mt:null,Ac(),Zt>0&&Ae&&Ae.push(e),e}function Of(e,t,n,s,r,i){return uo(po(e,t,n,s,r,i,!0))}function Ps(e,t,n,s,r){return uo(de(e,t,n,s,r,!0))}function en(e){return e?e.__v_isVNode===!0:!1}function gt(e,t){return e.type===t.type&&e.key===t.key}const ho=({key:e})=>e??null,Sn=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?le(e)||ae(e)||G(e)?{i:we,r:e,k:t,f:!!n}:e:null);function po(e,t=null,n=null,s=0,r=null,i=e===be?0:1,l=!1,o=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&ho(t),ref:t&&Sn(t),scopeId:Ai,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:we};return o?(Ys(c,n),i&128&&e.normalize(c)):n&&(c.shapeFlag|=le(n)?8:16),Zt>0&&!l&&Ae&&(c.patchFlag>0||i&6)&&c.patchFlag!==32&&Ae.push(c),c}const de=Rc;function Rc(e,t=null,n=null,s=0,r=null,i=!1){if((!e||e===Wi)&&(e=ue),en(e)){const o=ct(e,t,!0);return n&&Ys(o,n),Zt>0&&!i&&Ae&&(o.shapeFlag&6?Ae[Ae.indexOf(e)]=o:Ae.push(o)),o.patchFlag=-2,o}if(jc(e)&&(e=e.__vccOpts),t){t=Oc(t);let{class:o,style:c}=t;o&&!le(o)&&(t.class=$s(o)),Q(c)&&(Vn(c)&&!K(c)&&(c=fe({},c)),t.style=js(c))}const l=le(e)?1:ao(e)?128:Pi(e)?64:Q(e)?4:G(e)?2:0;return po(e,t,n,s,r,l,i,!0)}function Oc(e){return e?Vn(e)||Zi(e)?fe({},e):e:null}function ct(e,t,n=!1,s=!1){const{props:r,ref:i,patchFlag:l,children:o,transition:c}=e,f=t?Mc(r||{},t):r,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:f,key:f&&ho(f),ref:t&&t.ref?n&&i?K(i)?i.concat(Sn(t)):[i,Sn(t)]:Sn(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:o,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==be?l===-1?16:l|16:l,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&ct(e.ssContent),ssFallback:e.ssFallback&&ct(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&Qt(a,c.clone(a)),a}function go(e=" ",t=0){return de(St,null,e,t)}function Mf(e,t){const n=de(qt,null,e);return n.staticCount=t,n}function Pf(e="",t=!1){return t?(Ms(),Ps(ue,null,e)):de(ue,null,e)}function Pe(e){return e==null||typeof e=="boolean"?de(ue):K(e)?de(be,null,e.slice()):en(e)?Je(e):de(St,null,String(e))}function Je(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:ct(e)}function Ys(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(K(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),Ys(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!Zi(t)?t._ctx=we:r===3&&we&&(we.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else G(t)?(t={default:t,_ctx:we},n=32):(t=String(t),s&64?(n=16,t=[go(t)]):n=8);e.children=t,e.shapeFlag|=n}function Mc(...e){const t={};for(let n=0;nve||we;let Ln,Is;{const e=Dn(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),i=>{r.length>1?r.forEach(l=>l(i)):r[0](i)}};Ln=t("__VUE_INSTANCE_SETTERS__",n=>ve=n),Is=t("__VUE_SSR_SETTERS__",n=>tn=n)}const ln=e=>{const t=ve;return Ln(e),e.scope.on(),()=>{e.scope.off(),Ln(t)}},xr=()=>{ve&&ve.scope.off(),Ln(null)};function mo(e){return e.vnode.shapeFlag&4}let tn=!1;function Nc(e,t=!1,n=!1){t&&Is(t);const{props:s,children:r}=e.vnode,i=mo(e);yc(e,s,i,t),Sc(e,r,n||t);const l=i?Fc(e,t):void 0;return t&&Is(!1),l}function Fc(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,sc);const{setup:s}=n;if(s){Qe();const r=e.setupContext=s.length>1?yo(e):null,i=ln(e),l=on(s,e,0,[e.props,r]),o=ei(l);if(Ze(),i(),(o||e.sp)&&!bt(e)&&ji(e),o){if(l.then(xr,xr),t)return l.then(c=>{Tr(e,c)}).catch(c=>{Wn(c,e,0)});e.asyncDep=l}else Tr(e,l)}else vo(e)}function Tr(e,t,n){G(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:Q(t)&&(e.setupState=xi(t)),vo(e)}function vo(e,t,n){const s=e.type;e.render||(e.render=s.render||qe);{const r=ln(e);Qe();try{ic(e)}finally{Ze(),r()}}}const Hc={get(e,t){return me(e,"get",""),e[t]}};function yo(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,Hc),slots:e.slots,emit:e.emit,expose:t}}function Js(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(xi(wn(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Kt)return Kt[n](e)},has(t,n){return n in t||n in Kt}})):e.proxy}function Dc(e,t=!0){return G(e)?e.displayName||e.name:e.name||t&&e.__name}function jc(e){return G(e)&&"__vccOpts"in e}const oe=(e,t)=>Rl(e,t,tn);function Ls(e,t,n){try{In(-1);const s=arguments.length;return s===2?Q(t)&&!K(t)?en(t)?de(e,null,[t]):de(e,t):de(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&en(n)&&(n=[n]),de(e,t,n))}finally{In(1)}}const $c="3.5.30";/** +* @vue/runtime-dom v3.5.30 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Ns;const Cr=typeof window<"u"&&window.trustedTypes;if(Cr)try{Ns=Cr.createPolicy("vue",{createHTML:e=>e})}catch{}const _o=Ns?e=>Ns.createHTML(e):e=>e,Vc="http://www.w3.org/2000/svg",kc="http://www.w3.org/1998/Math/MathML",Ye=typeof document<"u"?document:null,Er=Ye&&Ye.createElement("template"),Wc={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?Ye.createElementNS(Vc,e):t==="mathml"?Ye.createElementNS(kc,e):n?Ye.createElement(e,{is:n}):Ye.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>Ye.createTextNode(e),createComment:e=>Ye.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Ye.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,i){const l=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{Er.innerHTML=_o(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const o=Er.content;if(s==="svg"||s==="mathml"){const c=o.firstChild;for(;c.firstChild;)o.appendChild(c.firstChild);o.removeChild(c)}t.insertBefore(o,n)}return[l?l.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},rt="transition",kt="animation",nn=Symbol("_vtc"),bo={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Uc=fe({},Ii,bo),Bc=e=>(e.displayName="Transition",e.props=Uc,e),If=Bc((e,{slots:t})=>Ls(Wl,Kc(e),t)),dt=(e,t=[])=>{K(e)?e.forEach(n=>n(...t)):e&&e(...t)},Ar=e=>e?K(e)?e.some(t=>t.length>1):e.length>1:!1;function Kc(e){const t={};for(const y in e)y in bo||(t[y]=e[y]);if(e.css===!1)return t;const{name:n="v",type:s,duration:r,enterFromClass:i=`${n}-enter-from`,enterActiveClass:l=`${n}-enter-active`,enterToClass:o=`${n}-enter-to`,appearFromClass:c=i,appearActiveClass:f=l,appearToClass:a=o,leaveFromClass:h=`${n}-leave-from`,leaveActiveClass:v=`${n}-leave-active`,leaveToClass:_=`${n}-leave-to`}=e,I=qc(r),T=I&&I[0],W=I&&I[1],{onBeforeEnter:H,onEnter:D,onEnterCancelled:p,onLeave:g,onLeaveCancelled:R,onBeforeAppear:k=H,onAppear:O=D,onAppearCancelled:B=p}=t,C=(y,N,Y,ie)=>{y._enterCancelled=ie,ht(y,N?a:o),ht(y,N?f:l),Y&&Y()},M=(y,N)=>{y._isLeaving=!1,ht(y,h),ht(y,_),ht(y,v),N&&N()},E=y=>(N,Y)=>{const ie=y?O:D,V=()=>C(N,y,Y);dt(ie,[N,V]),Rr(()=>{ht(N,y?c:i),Xe(N,y?a:o),Ar(ie)||Or(N,s,T,V)})};return fe(t,{onBeforeEnter(y){dt(H,[y]),Xe(y,i),Xe(y,l)},onBeforeAppear(y){dt(k,[y]),Xe(y,c),Xe(y,f)},onEnter:E(!1),onAppear:E(!0),onLeave(y,N){y._isLeaving=!0;const Y=()=>M(y,N);Xe(y,h),y._enterCancelled?(Xe(y,v),Ir(y)):(Ir(y),Xe(y,v)),Rr(()=>{y._isLeaving&&(ht(y,h),Xe(y,_),Ar(g)||Or(y,s,W,Y))}),dt(g,[y,Y])},onEnterCancelled(y){C(y,!1,void 0,!0),dt(p,[y])},onAppearCancelled(y){C(y,!0,void 0,!0),dt(B,[y])},onLeaveCancelled(y){M(y),dt(R,[y])}})}function qc(e){if(e==null)return null;if(Q(e))return[cs(e.enter),cs(e.leave)];{const t=cs(e);return[t,t]}}function cs(e){return Wo(e)}function Xe(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[nn]||(e[nn]=new Set)).add(t)}function ht(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.remove(s));const n=e[nn];n&&(n.delete(t),n.size||(e[nn]=void 0))}function Rr(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let Gc=0;function Or(e,t,n,s){const r=e._endId=++Gc,i=()=>{r===e._endId&&s()};if(n!=null)return setTimeout(i,n);const{type:l,timeout:o,propCount:c}=Xc(e,t);if(!l)return s();const f=l+"end";let a=0;const h=()=>{e.removeEventListener(f,v),i()},v=_=>{_.target===e&&++a>=c&&h()};setTimeout(()=>{a(n[I]||"").split(", "),r=s(`${rt}Delay`),i=s(`${rt}Duration`),l=Mr(r,i),o=s(`${kt}Delay`),c=s(`${kt}Duration`),f=Mr(o,c);let a=null,h=0,v=0;t===rt?l>0&&(a=rt,h=l,v=i.length):t===kt?f>0&&(a=kt,h=f,v=c.length):(h=Math.max(l,f),a=h>0?l>f?rt:kt:null,v=a?a===rt?i.length:c.length:0);const _=a===rt&&/\b(?:transform|all)(?:,|$)/.test(s(`${rt}Property`).toString());return{type:a,timeout:h,propCount:v,hasTransform:_}}function Mr(e,t){for(;e.lengthPr(n)+Pr(e[s])))}function Pr(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Ir(e){return(e?e.ownerDocument:document).body.offsetHeight}function Yc(e,t,n){const s=e[nn];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Lr=Symbol("_vod"),Jc=Symbol("_vsh"),zc=Symbol(""),Qc=/(?:^|;)\s*display\s*:/;function Zc(e,t,n){const s=e.style,r=le(n);let i=!1;if(n&&!r){if(t)if(le(t))for(const l of t.split(";")){const o=l.slice(0,l.indexOf(":")).trim();n[o]==null&&xn(s,o,"")}else for(const l in t)n[l]==null&&xn(s,l,"");for(const l in n)l==="display"&&(i=!0),xn(s,l,n[l])}else if(r){if(t!==n){const l=s[zc];l&&(n+=";"+l),s.cssText=n,i=Qc.test(n)}}else t&&e.removeAttribute("style");Lr in e&&(e[Lr]=i?s.display:"",e[Jc]&&(s.display="none"))}const Nr=/\s*!important$/;function xn(e,t,n){if(K(n))n.forEach(s=>xn(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=ea(e,t);Nr.test(n)?e.setProperty(at(s),n.replace(Nr,""),"important"):e[s]=n}}const Fr=["Webkit","Moz","ms"],as={};function ea(e,t){const n=as[t];if(n)return n;let s=Se(t);if(s!=="filter"&&s in e)return as[t]=s;s=Hn(s);for(let r=0;rfs||(ia.then(()=>fs=0),fs=Date.now());function la(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;je(ca(s,n.value),t,5,[s])};return n.value=e,n.attached=oa(),n}function ca(e,t){if(K(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const kr=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,aa=(e,t,n,s,r,i)=>{const l=r==="svg";t==="class"?Yc(e,s,l):t==="style"?Zc(e,n,s):sn(t)?Hs(t)||sa(e,t,n,s,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):fa(e,t,s,l))?(jr(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Dr(e,t,s,l,i,t!=="value")):e._isVueCE&&(ua(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!le(s)))?jr(e,Se(t),s,i,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),Dr(e,t,s,l))};function fa(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&kr(t)&&G(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return kr(t)&&le(n)?!1:t in e}function ua(e,t){const n=e._def.props;if(!n)return!1;const s=Se(t);return Array.isArray(n)?n.some(r=>Se(r)===s):Object.keys(n).some(r=>Se(r)===s)}const da=["ctrl","shift","alt","meta"],ha={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>da.some(n=>e[`${n}Key`]&&!t.includes(n))},Lf=(e,t)=>{if(!e)return e;const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=(r,...i)=>{for(let l=0;l{const n=e._withKeys||(e._withKeys={}),s=t.join(".");return n[s]||(n[s]=r=>{if(!("key"in r))return;const i=at(r.key);if(t.some(l=>l===i||pa[l]===i))return e(r)})},ga=fe({patchProp:aa},Wc);let us,Wr=!1;function ma(){return us=Wr?us:Tc(ga),Wr=!0,us}const Ff=(...e)=>{const t=ma().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=ya(s);if(r)return n(r,!0,va(r))},t};function va(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function ya(e){return le(e)?document.querySelector(e):e}const _a=window.__VP_SITE_DATA__;function wo(e){return li()?(Qo(e),!0):!1}const ds=new WeakMap,ba=(...e)=>{var t;const n=e[0],s=(t=Ct())==null?void 0:t.proxy;if(s==null&&!Ri())throw new Error("injectLocal must be called in setup");return s&&ds.has(s)&&n in ds.get(s)?ds.get(s)[n]:_t(...e)},So=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const wa=Object.prototype.toString,Sa=e=>wa.call(e)==="[object Object]",Tt=()=>{},Ur=xa();function xa(){var e,t;return So&&((e=window==null?void 0:window.navigator)==null?void 0:e.userAgent)&&(/iP(?:ad|hone|od)/.test(window.navigator.userAgent)||((t=window==null?void 0:window.navigator)==null?void 0:t.maxTouchPoints)>2&&/iPad|Macintosh/.test(window==null?void 0:window.navigator.userAgent))}function zs(e,t){function n(...s){return new Promise((r,i)=>{Promise.resolve(e(()=>t.apply(this,s),{fn:t,thisArg:this,args:s})).then(r).catch(i)})}return n}const xo=e=>e();function Ta(e,t={}){let n,s,r=Tt;const i=c=>{clearTimeout(c),r(),r=Tt};let l;return c=>{const f=ce(e),a=ce(t.maxWait);return n&&i(n),f<=0||a!==void 0&&a<=0?(s&&(i(s),s=null),Promise.resolve(c())):new Promise((h,v)=>{r=t.rejectOnCancel?v:h,l=c,a&&!s&&(s=setTimeout(()=>{n&&i(n),s=null,h(l())},a)),n=setTimeout(()=>{s&&i(s),s=null,h(c())},f)})}}function Ca(...e){let t=0,n,s=!0,r=Tt,i,l,o,c,f;!ae(e[0])&&typeof e[0]=="object"?{delay:l,trailing:o=!0,leading:c=!0,rejectOnCancel:f=!1}=e[0]:[l,o=!0,c=!0,f=!1]=e;const a=()=>{n&&(clearTimeout(n),n=void 0,r(),r=Tt)};return v=>{const _=ce(l),I=Date.now()-t,T=()=>i=v();return a(),_<=0?(t=Date.now(),T()):(I>_&&(c||!s)?(t=Date.now(),T()):o&&(i=new Promise((W,H)=>{r=f?H:W,n=setTimeout(()=>{t=Date.now(),s=!0,W(T()),a()},Math.max(0,_-I))})),!c&&!n&&(n=setTimeout(()=>s=!0,_)),s=!1,i)}}function Ea(e=xo,t={}){const{initialState:n="active"}=t,s=Qs(n==="active");function r(){s.value=!1}function i(){s.value=!0}return{isActive:Jt(s),pause:r,resume:i,eventFilter:(...o)=>{s.value&&e(...o)}}}function Br(e){return e.endsWith("rem")?Number.parseFloat(e)*16:Number.parseFloat(e)}function Aa(e){return Ct()}function hs(e){return Array.isArray(e)?e:[e]}function Qs(...e){if(e.length!==1)return Cl(...e);const t=e[0];return typeof t=="function"?Jt(Sl(()=>({get:t,set:Tt}))):yt(t)}function Ra(e,t=200,n={}){return zs(Ta(t,n),e)}function Oa(e,t=200,n=!1,s=!0,r=!1){return zs(Ca(t,n,s,r),e)}function Ma(e,t,n={}){const{eventFilter:s=xo,...r}=n;return Le(e,zs(s,t),r)}function Pa(e,t,n={}){const{eventFilter:s,initialState:r="active",...i}=n,{eventFilter:l,pause:o,resume:c,isActive:f}=Ea(s,{initialState:r});return{stop:Ma(e,t,{...i,eventFilter:l}),pause:o,resume:c,isActive:f}}function Yn(e,t=!0,n){Aa()?Ht(e,n):t?e():Un(e)}function Ia(e,t,n){return Le(e,t,{...n,immediate:!0})}const tt=So?window:void 0;function Zs(e){var t;const n=ce(e);return(t=n==null?void 0:n.$el)!=null?t:n}function nt(...e){const t=[],n=()=>{t.forEach(o=>o()),t.length=0},s=(o,c,f,a)=>(o.addEventListener(c,f,a),()=>o.removeEventListener(c,f,a)),r=oe(()=>{const o=hs(ce(e[0])).filter(c=>c!=null);return o.every(c=>typeof c!="string")?o:void 0}),i=Ia(()=>{var o,c;return[(c=(o=r.value)==null?void 0:o.map(f=>Zs(f)))!=null?c:[tt].filter(f=>f!=null),hs(ce(r.value?e[1]:e[0])),hs(kn(r.value?e[2]:e[1])),ce(r.value?e[3]:e[2])]},([o,c,f,a])=>{if(n(),!(o!=null&&o.length)||!(c!=null&&c.length)||!(f!=null&&f.length))return;const h=Sa(a)?{...a}:a;t.push(...o.flatMap(v=>c.flatMap(_=>f.map(I=>s(v,_,I,h)))))},{flush:"post"}),l=()=>{i(),n()};return wo(n),l}function La(){const e=Ie(!1),t=Ct();return t&&Ht(()=>{e.value=!0},t),e}function Na(e){const t=La();return oe(()=>(t.value,!!e()))}function Fa(e){return typeof e=="function"?e:typeof e=="string"?t=>t.key===e:Array.isArray(e)?t=>e.includes(t.key):()=>!0}function Hf(...e){let t,n,s={};e.length===3?(t=e[0],n=e[1],s=e[2]):e.length===2?typeof e[1]=="object"?(t=!0,n=e[0],s=e[1]):(t=e[0],n=e[1]):(t=!0,n=e[0]);const{target:r=tt,eventName:i="keydown",passive:l=!1,dedupe:o=!1}=s,c=Fa(t);return nt(r,i,a=>{a.repeat&&ce(o)||c(a)&&n(a)},l)}const Ha=Symbol("vueuse-ssr-width");function Da(){const e=Ri()?ba(Ha,null):null;return typeof e=="number"?e:void 0}function To(e,t={}){const{window:n=tt,ssrWidth:s=Da()}=t,r=Na(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function"),i=Ie(typeof s=="number"),l=Ie(),o=Ie(!1),c=f=>{o.value=f.matches};return Oi(()=>{if(i.value){i.value=!r.value;const f=ce(e).split(",");o.value=f.some(a=>{const h=a.includes("not all"),v=a.match(/\(\s*min-width:\s*(-?\d+(?:\.\d*)?[a-z]+\s*)\)/),_=a.match(/\(\s*max-width:\s*(-?\d+(?:\.\d*)?[a-z]+\s*)\)/);let I=!!(v||_);return v&&I&&(I=s>=Br(v[1])),_&&I&&(I=s<=Br(_[1])),h?!I:I});return}r.value&&(l.value=n.matchMedia(ce(e)),o.value=l.value.matches)}),nt(l,"change",c,{passive:!0}),oe(()=>o.value)}const mn=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},vn="__vueuse_ssr_handlers__",ja=$a();function $a(){return vn in mn||(mn[vn]=mn[vn]||{}),mn[vn]}function Co(e,t){return ja[e]||t}function Eo(e){return To("(prefers-color-scheme: dark)",e)}function Va(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}const ka={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},Kr="vueuse-storage";function Wa(e,t,n,s={}){var r;const{flush:i="pre",deep:l=!0,listenToStorageChanges:o=!0,writeDefaults:c=!0,mergeDefaults:f=!1,shallow:a,window:h=tt,eventFilter:v,onError:_=E=>{console.error(E)},initOnMounted:I}=s,T=(a?Ie:yt)(typeof t=="function"?t():t),W=oe(()=>ce(e));if(!n)try{n=Co("getDefaultStorage",()=>{var E;return(E=tt)==null?void 0:E.localStorage})()}catch(E){_(E)}if(!n)return T;const H=ce(t),D=Va(H),p=(r=s.serializer)!=null?r:ka[D],{pause:g,resume:R}=Pa(T,()=>O(T.value),{flush:i,deep:l,eventFilter:v});Le(W,()=>C(),{flush:i}),h&&o&&Yn(()=>{n instanceof Storage?nt(h,"storage",C,{passive:!0}):nt(h,Kr,M),I&&C()}),I||C();function k(E,y){if(h){const N={key:W.value,oldValue:E,newValue:y,storageArea:n};h.dispatchEvent(n instanceof Storage?new StorageEvent("storage",N):new CustomEvent(Kr,{detail:N}))}}function O(E){try{const y=n.getItem(W.value);if(E==null)k(y,null),n.removeItem(W.value);else{const N=p.write(E);y!==N&&(n.setItem(W.value,N),k(y,N))}}catch(y){_(y)}}function B(E){const y=E?E.newValue:n.getItem(W.value);if(y==null)return c&&H!=null&&n.setItem(W.value,p.write(H)),H;if(!E&&f){const N=p.read(y);return typeof f=="function"?f(N,H):D==="object"&&!Array.isArray(N)?{...H,...N}:N}else return typeof y!="string"?y:p.read(y)}function C(E){if(!(E&&E.storageArea!==n)){if(E&&E.key==null){T.value=H;return}if(!(E&&E.key!==W.value)){g();try{(E==null?void 0:E.newValue)!==p.write(T.value)&&(T.value=B(E))}catch(y){_(y)}finally{E?Un(R):R()}}}}function M(E){C(E.detail)}return T}const Ua="*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}";function Ba(e={}){const{selector:t="html",attribute:n="class",initialValue:s="auto",window:r=tt,storage:i,storageKey:l="vueuse-color-scheme",listenToStorageChanges:o=!0,storageRef:c,emitAuto:f,disableTransition:a=!0}=e,h={auto:"",light:"light",dark:"dark",...e.modes||{}},v=Eo({window:r}),_=oe(()=>v.value?"dark":"light"),I=c||(l==null?Qs(s):Wa(l,s,i,{window:r,listenToStorageChanges:o})),T=oe(()=>I.value==="auto"?_.value:I.value),W=Co("updateHTMLAttrs",(g,R,k)=>{const O=typeof g=="string"?r==null?void 0:r.document.querySelector(g):Zs(g);if(!O)return;const B=new Set,C=new Set;let M=null;if(R==="class"){const y=k.split(/\s/g);Object.values(h).flatMap(N=>(N||"").split(/\s/g)).filter(Boolean).forEach(N=>{y.includes(N)?B.add(N):C.add(N)})}else M={key:R,value:k};if(B.size===0&&C.size===0&&M===null)return;let E;a&&(E=r.document.createElement("style"),E.appendChild(document.createTextNode(Ua)),r.document.head.appendChild(E));for(const y of B)O.classList.add(y);for(const y of C)O.classList.remove(y);M&&O.setAttribute(M.key,M.value),a&&(r.getComputedStyle(E).opacity,document.head.removeChild(E))});function H(g){var R;W(t,n,(R=h[g])!=null?R:g)}function D(g){e.onChanged?e.onChanged(g,H):H(g)}Le(T,D,{flush:"post",immediate:!0}),Yn(()=>D(T.value));const p=oe({get(){return f?I.value:T.value},set(g){I.value=g}});return Object.assign(p,{store:I,system:_,state:T})}function Ka(e={}){const{valueDark:t="dark",valueLight:n=""}=e,s=Ba({...e,onChanged:(l,o)=>{var c;e.onChanged?(c=e.onChanged)==null||c.call(e,l==="dark",o,l):o(l)},modes:{dark:t,light:n}}),r=oe(()=>s.system.value);return oe({get(){return s.value==="dark"},set(l){const o=l?"dark":"light";r.value===o?s.value="auto":s.value=o}})}function ps(e){return typeof Window<"u"&&e instanceof Window?e.document.documentElement:typeof Document<"u"&&e instanceof Document?e.documentElement:e}const qr=1;function qa(e,t={}){const{throttle:n=0,idle:s=200,onStop:r=Tt,onScroll:i=Tt,offset:l={left:0,right:0,top:0,bottom:0},eventListenerOptions:o={capture:!1,passive:!0},behavior:c="auto",window:f=tt,onError:a=O=>{console.error(O)}}=t,h=Ie(0),v=Ie(0),_=oe({get(){return h.value},set(O){T(O,void 0)}}),I=oe({get(){return v.value},set(O){T(void 0,O)}});function T(O,B){var C,M,E,y;if(!f)return;const N=ce(e);if(!N)return;(E=N instanceof Document?f.document.body:N)==null||E.scrollTo({top:(C=ce(B))!=null?C:I.value,left:(M=ce(O))!=null?M:_.value,behavior:ce(c)});const Y=((y=N==null?void 0:N.document)==null?void 0:y.documentElement)||(N==null?void 0:N.documentElement)||N;_!=null&&(h.value=Y.scrollLeft),I!=null&&(v.value=Y.scrollTop)}const W=Ie(!1),H=Nt({left:!0,right:!1,top:!0,bottom:!1}),D=Nt({left:!1,right:!1,top:!1,bottom:!1}),p=O=>{W.value&&(W.value=!1,D.left=!1,D.right=!1,D.top=!1,D.bottom=!1,r(O))},g=Ra(p,n+s),R=O=>{var B;if(!f)return;const C=((B=O==null?void 0:O.document)==null?void 0:B.documentElement)||(O==null?void 0:O.documentElement)||Zs(O),{display:M,flexDirection:E,direction:y}=getComputedStyle(C),N=y==="rtl"?-1:1,Y=C.scrollLeft;D.left=Yh.value;const ie=Math.abs(Y*N)<=(l.left||0),V=Math.abs(Y*N)+C.clientWidth>=C.scrollWidth-(l.right||0)-qr;M==="flex"&&E==="row-reverse"?(H.left=V,H.right=ie):(H.left=ie,H.right=V),h.value=Y;let X=C.scrollTop;O===f.document&&!X&&(X=f.document.body.scrollTop),D.top=Xv.value;const j=Math.abs(X)<=(l.top||0),te=Math.abs(X)+C.clientHeight>=C.scrollHeight-(l.bottom||0)-qr;M==="flex"&&E==="column-reverse"?(H.top=te,H.bottom=j):(H.top=j,H.bottom=te),v.value=X},k=O=>{var B;if(!f)return;const C=(B=O.target.documentElement)!=null?B:O.target;R(C),W.value=!0,g(O),i(O)};return nt(e,"scroll",n?Oa(k,n,!0,!1):k,o),Yn(()=>{try{const O=ce(e);if(!O)return;R(O)}catch(O){a(O)}}),nt(e,"scrollend",p,o),{x:_,y:I,isScrolling:W,arrivedState:H,directions:D,measure(){const O=ce(e);f&&O&&R(O)}}}function Ao(e){const t=window.getComputedStyle(e);if(t.overflowX==="scroll"||t.overflowY==="scroll"||t.overflowX==="auto"&&e.clientWidth1?!0:(t.preventDefault&&t.preventDefault(),!1)}const gs=new WeakMap;function Df(e,t=!1){const n=Ie(t);let s=null,r="";Le(Qs(e),o=>{const c=ps(ce(o));if(c){const f=c;if(gs.get(f)||gs.set(f,f.style.overflow),f.style.overflow!=="hidden"&&(r=f.style.overflow),f.style.overflow==="hidden")return n.value=!0;if(n.value)return f.style.overflow="hidden"}},{immediate:!0});const i=()=>{const o=ps(ce(e));!o||n.value||(Ur&&(s=nt(o,"touchmove",c=>{Ga(c)},{passive:!1})),o.style.overflow="hidden",n.value=!0)},l=()=>{const o=ps(ce(e));!o||!n.value||(Ur&&(s==null||s()),o.style.overflow=r,gs.delete(o),n.value=!1)};return wo(l),oe({get(){return n.value},set(o){o?i():l()}})}function jf(e={}){const{window:t=tt,...n}=e;return qa(t,n)}function $f(e={}){const{window:t=tt,initialWidth:n=Number.POSITIVE_INFINITY,initialHeight:s=Number.POSITIVE_INFINITY,listenOrientation:r=!0,includeScrollbar:i=!0,type:l="inner"}=e,o=Ie(n),c=Ie(s),f=()=>{if(t)if(l==="outer")o.value=t.outerWidth,c.value=t.outerHeight;else if(l==="visual"&&t.visualViewport){const{width:h,height:v,scale:_}=t.visualViewport;o.value=Math.round(h*_),c.value=Math.round(v*_)}else i?(o.value=t.innerWidth,c.value=t.innerHeight):(o.value=t.document.documentElement.clientWidth,c.value=t.document.documentElement.clientHeight)};f(),Yn(f);const a={passive:!0};if(nt("resize",f,a),t&&l==="visual"&&t.visualViewport&&nt(t.visualViewport,"resize",f,a),r){const h=To("(orientation: portrait)");Le(h,()=>f())}return{width:o,height:c}}const ms={};var vs={};const Ro=/^(?:[a-z]+:|\/\/)/i,Xa="vitepress-theme-appearance",Ya=/#.*$/,Ja=/[?#].*$/,za=/(?:(^|\/)index)?\.(?:md|html)$/,ge=typeof document<"u",Oo={relativePath:"404.md",filePath:"",title:"404",description:"Not Found",headers:[],frontmatter:{sidebar:!1,layout:"page"},lastUpdated:0,isNotFound:!0};function Qa(e,t,n=!1){if(t===void 0)return!1;if(e=Gr(`/${e}`),n)return new RegExp(t).test(e);if(Gr(t)!==e)return!1;const s=t.match(Ya);return s?(ge?location.hash:"")===s[0]:!0}function Gr(e){return decodeURI(e).replace(Ja,"").replace(za,"$1")}function Za(e){return Ro.test(e)}function ef(e,t){return Object.keys((e==null?void 0:e.locales)||{}).find(n=>n!=="root"&&!Za(n)&&Qa(t,`/${n}/`,!0))||"root"}function tf(e,t){var s,r,i,l,o,c,f;const n=ef(e,t);return Object.assign({},e,{localeIndex:n,lang:((s=e.locales[n])==null?void 0:s.lang)??e.lang,dir:((r=e.locales[n])==null?void 0:r.dir)??e.dir,title:((i=e.locales[n])==null?void 0:i.title)??e.title,titleTemplate:((l=e.locales[n])==null?void 0:l.titleTemplate)??e.titleTemplate,description:((o=e.locales[n])==null?void 0:o.description)??e.description,head:Po(e.head,((c=e.locales[n])==null?void 0:c.head)??[]),themeConfig:{...e.themeConfig,...(f=e.locales[n])==null?void 0:f.themeConfig}})}function Mo(e,t){const n=t.title||e.title,s=t.titleTemplate??e.titleTemplate;if(typeof s=="string"&&s.includes(":title"))return s.replace(/:title/g,n);const r=nf(e.title,s);return n===r.slice(3)?n:`${n}${r}`}function nf(e,t){return t===!1?"":t===!0||t===void 0?` | ${e}`:e===t?"":` | ${t}`}function sf(e,t){const[n,s]=t;if(n!=="meta")return!1;const r=Object.entries(s)[0];return r==null?!1:e.some(([i,l])=>i===n&&l[r[0]]===r[1])}function Po(e,t){return[...e.filter(n=>!sf(t,n)),...t]}const rf=/[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g,of=/^[a-z]:/i;function Xr(e){const t=of.exec(e),n=t?t[0]:"";return n+e.slice(n.length).replace(rf,"_").replace(/(^|\/)_+(?=[^/]*$)/,"$1")}const ys=new Set;function lf(e){if(ys.size===0){const n=typeof process=="object"&&(vs==null?void 0:vs.VITE_EXTRA_EXTENSIONS)||(ms==null?void 0:ms.VITE_EXTRA_EXTENSIONS)||"";("3g2,3gp,aac,ai,apng,au,avif,bin,bmp,cer,class,conf,crl,css,csv,dll,doc,eps,epub,exe,gif,gz,ics,ief,jar,jpe,jpeg,jpg,js,json,jsonld,m4a,man,mid,midi,mjs,mov,mp2,mp3,mp4,mpe,mpeg,mpg,mpp,oga,ogg,ogv,ogx,opus,otf,p10,p7c,p7m,p7s,pdf,png,ps,qt,roff,rtf,rtx,ser,svg,t,tif,tiff,tr,ts,tsv,ttf,txt,vtt,wav,weba,webm,webp,woff,woff2,xhtml,xml,yaml,yml,zip"+(n&&typeof n=="string"?","+n:"")).split(",").forEach(s=>ys.add(s))}const t=e.split(".").pop();return t==null||!ys.has(t.toLowerCase())}const cf=Symbol(),xt=Ie(_a);function Vf(e){const t=oe(()=>tf(xt.value,e.data.relativePath)),n=t.value.appearance,s=n==="force-dark"?yt(!0):n==="force-auto"?Eo():n?Ka({storageKey:Xa,initialValue:()=>n==="dark"?"dark":"auto",...typeof n=="object"?n:{}}):yt(!1),r=yt(ge?location.hash:"");return ge&&window.addEventListener("hashchange",()=>{r.value=location.hash}),Le(()=>e.data,()=>{r.value=ge?location.hash:""}),{site:t,theme:oe(()=>t.value.themeConfig),page:oe(()=>e.data),frontmatter:oe(()=>e.data.frontmatter),params:oe(()=>e.data.params),lang:oe(()=>t.value.lang),dir:oe(()=>e.data.frontmatter.dir||t.value.dir),localeIndex:oe(()=>t.value.localeIndex||"root"),title:oe(()=>Mo(t.value,e.data)),description:oe(()=>e.data.description||t.value.description),isDark:s,hash:oe(()=>r.value)}}function af(){const e=_t(cf);if(!e)throw new Error("vitepress data not properly injected in app");return e}function ff(e,t){return`${e}${t}`.replace(/\/+/g,"/")}function Yr(e){return Ro.test(e)||!e.startsWith("/")?e:ff(xt.value.base,e)}function uf(e){let t=e.replace(/\.html$/,"");if(t=decodeURIComponent(t),t=t.replace(/\/$/,"/index"),ge){const n="/sigpro/";t=Xr(t.slice(n.length).replace(/\//g,"_")||"index")+".md";let s=__VP_HASH_MAP__[t.toLowerCase()];if(s||(t=t.endsWith("_index.md")?t.slice(0,-9)+".md":t.slice(0,-3)+"_index.md",s=__VP_HASH_MAP__[t.toLowerCase()]),!s)return null;t=`${n}assets/${t}.${s}.js`}else t=`./${Xr(t.slice(1).replace(/\//g,"_"))}.md.js`;return t}let Tn=[];function kf(e){Tn.push(e),Gn(()=>{Tn=Tn.filter(t=>t!==e)})}function df(){let e=xt.value.scrollOffset,t=0,n=24;if(typeof e=="object"&&"padding"in e&&(n=e.padding,e=e.selector),typeof e=="number")t=e;else if(typeof e=="string")t=Jr(e,n);else if(Array.isArray(e))for(const s of e){const r=Jr(s,n);if(r){t=r;break}}return t}function Jr(e,t){const n=document.querySelector(e);if(!n)return 0;const s=n.getBoundingClientRect().bottom;return s<0?0:s+t}const hf=Symbol(),Io="http://a.com",pf=()=>({path:"/",component:null,data:Oo});function Wf(e,t){const n=Nt(pf()),s={route:n,go:r};async function r(o=ge?location.href:"/"){var c,f;o=_s(o),await((c=s.onBeforeRouteChange)==null?void 0:c.call(s,o))!==!1&&(ge&&o!==_s(location.href)&&(history.replaceState({scrollPosition:window.scrollY},""),history.pushState({},"",o)),await l(o),await((f=s.onAfterRouteChange??s.onAfterRouteChanged)==null?void 0:f(o)))}let i=null;async function l(o,c=0,f=!1){var v,_;if(await((v=s.onBeforePageLoad)==null?void 0:v.call(s,o))===!1)return;const a=new URL(o,Io),h=i=a.pathname;try{let I=await e(h);if(!I)throw new Error(`Page not found: ${h}`);if(i===h){i=null;const{default:T,__pageData:W}=I;if(!T)throw new Error(`Invalid route component: ${T}`);await((_=s.onAfterPageLoad)==null?void 0:_.call(s,o)),n.path=ge?h:Yr(h),n.component=wn(T),n.data=wn(W),ge&&Un(()=>{let H=xt.value.base+W.relativePath.replace(/(?:(^|\/)index)?\.md$/,"$1");if(!xt.value.cleanUrls&&!H.endsWith("/")&&(H+=".html"),H!==a.pathname&&(a.pathname=H,o=H+a.search+a.hash,history.replaceState({},"",o)),a.hash&&!c){let D=null;try{D=document.getElementById(decodeURIComponent(a.hash).slice(1))}catch(p){console.warn(p)}if(D){zr(D,a.hash);return}}window.scrollTo(0,c)})}}catch(I){if(!/fetch|Page not found/.test(I.message)&&!/^\/404(\.html|\/)?$/.test(o)&&console.error(I),!f)try{const T=await fetch(xt.value.base+"hashmap.json");window.__VP_HASH_MAP__=await T.json(),await l(o,c,!0);return}catch{}if(i===h){i=null,n.path=ge?h:Yr(h),n.component=t?wn(t):null;const T=ge?h.replace(/(^|\/)$/,"$1index").replace(/(\.html)?$/,".md").replace(/^\//,""):"404.md";n.data={...Oo,relativePath:T}}}}return ge&&(history.state===null&&history.replaceState({},""),window.addEventListener("click",o=>{if(o.defaultPrevented||!(o.target instanceof Element)||o.target.closest("button")||o.button!==0||o.ctrlKey||o.shiftKey||o.altKey||o.metaKey)return;const c=o.target.closest("a");if(!c||c.closest(".vp-raw")||c.hasAttribute("download")||c.hasAttribute("target"))return;const f=c.getAttribute("href")??(c instanceof SVGAElement?c.getAttribute("xlink:href"):null);if(f==null)return;const{href:a,origin:h,pathname:v,hash:_,search:I}=new URL(f,c.baseURI),T=new URL(location.href);h===T.origin&&lf(v)&&(o.preventDefault(),v===T.pathname&&I===T.search?(_!==T.hash&&(history.pushState({},"",a),window.dispatchEvent(new HashChangeEvent("hashchange",{oldURL:T.href,newURL:a}))),_?zr(c,_,c.classList.contains("header-anchor")):window.scrollTo(0,0)):r(a))},{capture:!0}),window.addEventListener("popstate",async o=>{var f;if(o.state===null)return;const c=_s(location.href);await l(c,o.state&&o.state.scrollPosition||0),await((f=s.onAfterRouteChange??s.onAfterRouteChanged)==null?void 0:f(c))}),window.addEventListener("hashchange",o=>{o.preventDefault()})),s}function gf(){const e=_t(hf);if(!e)throw new Error("useRouter() is called without provider.");return e}function Lo(){return gf().route}function zr(e,t,n=!1){let s=null;try{s=e.classList.contains("header-anchor")?e:document.getElementById(decodeURIComponent(t).slice(1))}catch(r){console.warn(r)}if(s){let r=function(){!n||Math.abs(l-window.scrollY)>window.innerHeight?window.scrollTo(0,l):window.scrollTo({left:0,top:l,behavior:"smooth"})};const i=parseInt(window.getComputedStyle(s).paddingTop,10),l=window.scrollY+s.getBoundingClientRect().top-df()+i;requestAnimationFrame(r)}}function _s(e){const t=new URL(e,Io);return t.pathname=t.pathname.replace(/(^|\/)index(\.html)?$/,"$1"),xt.value.cleanUrls?t.pathname=t.pathname.replace(/\.html$/,""):!t.pathname.endsWith("/")&&!t.pathname.endsWith(".html")&&(t.pathname+=".html"),t.pathname+t.search+t.hash}const yn=()=>Tn.forEach(e=>e()),Uf=Di({name:"VitePressContent",props:{as:{type:[Object,String],default:"div"}},setup(e){const t=Lo(),{frontmatter:n,site:s}=af();return Le(n,yn,{deep:!0,flush:"post"}),()=>Ls(e.as,s.value.contentProps??{style:{position:"relative"}},[t.component?Ls(t.component,{onVnodeMounted:yn,onVnodeUpdated:yn,onVnodeUnmounted:yn}):"404 Page Not Found"])}}),Bf=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n},Kf=Di({setup(e,{slots:t}){const n=yt(!1);return Ht(()=>{n.value=!0}),()=>n.value&&t.default?t.default():null}});function qf(){ge&&window.addEventListener("click",e=>{var n;const t=e.target;if(t.matches(".vp-code-group input")){const s=(n=t.parentElement)==null?void 0:n.parentElement;if(!s)return;const r=Array.from(s.querySelectorAll("input")).indexOf(t);if(r<0)return;const i=s.querySelector(".blocks");if(!i)return;const l=Array.from(i.children).find(f=>f.classList.contains("active"));if(!l)return;const o=i.children[r];if(!o||l===o)return;l.classList.remove("active"),o.classList.add("active");const c=s==null?void 0:s.querySelector(`label[for="${t.id}"]`);c==null||c.scrollIntoView({block:"nearest"})}})}function Gf(){if(ge){const e=new WeakMap;window.addEventListener("click",t=>{var s;const n=t.target;if(n.matches('div[class*="language-"] > button.copy')){const r=n.parentElement,i=(s=n.nextElementSibling)==null?void 0:s.nextElementSibling;if(!r||!i)return;const l=/language-(shellscript|shell|bash|sh|zsh)/.test(r.className),o=[".vp-copy-ignore",".diff.remove"],c=i.cloneNode(!0);c.querySelectorAll(o.join(",")).forEach(a=>a.remove());let f=c.textContent||"";l&&(f=f.replace(/^ *(\$|>) /gm,"").trim()),mf(f).then(()=>{n.classList.add("copied"),clearTimeout(e.get(n));const a=setTimeout(()=>{n.classList.remove("copied"),n.blur(),e.delete(n)},2e3);e.set(n,a)})}})}}async function mf(e){try{return navigator.clipboard.writeText(e)}catch{const t=document.createElement("textarea"),n=document.activeElement;t.value=e,t.setAttribute("readonly",""),t.style.contain="strict",t.style.position="absolute",t.style.left="-9999px",t.style.fontSize="12pt";const s=document.getSelection(),r=s?s.rangeCount>0&&s.getRangeAt(0):null;document.body.appendChild(t),t.select(),t.selectionStart=0,t.selectionEnd=e.length,document.execCommand("copy"),document.body.removeChild(t),r&&(s.removeAllRanges(),s.addRange(r)),n&&n.focus()}}function Xf(e,t){let n=!0,s=[];const r=i=>{if(n){n=!1,i.forEach(o=>{const c=bs(o);for(const f of document.head.children)if(f.isEqualNode(c)){s.push(f);return}});return}const l=i.map(bs);s.forEach((o,c)=>{const f=l.findIndex(a=>a==null?void 0:a.isEqualNode(o??null));f!==-1?delete l[f]:(o==null||o.remove(),delete s[c])}),l.forEach(o=>o&&document.head.appendChild(o)),s=[...s,...l].filter(Boolean)};Oi(()=>{const i=e.data,l=t.value,o=i&&i.description,c=i&&i.frontmatter.head||[],f=Mo(l,i);f!==document.title&&(document.title=f);const a=o||l.description;let h=document.querySelector("meta[name=description]");h?h.getAttribute("content")!==a&&h.setAttribute("content",a):bs(["meta",{name:"description",content:a}]),r(Po(l.head,yf(c)))})}function bs([e,t,n]){const s=document.createElement(e);for(const r in t)s.setAttribute(r,t[r]);return n&&(s.innerHTML=n),e==="script"&&t.async==null&&(s.async=!1),s}function vf(e){return e[0]==="meta"&&e[1]&&e[1].name==="description"}function yf(e){return e.filter(t=>!vf(t))}const ws=new Set,No=()=>document.createElement("link"),_f=e=>{const t=No();t.rel="prefetch",t.href=e,document.head.appendChild(t)},bf=e=>{const t=new XMLHttpRequest;t.open("GET",e,t.withCredentials=!0),t.send()};let _n;const wf=ge&&(_n=No())&&_n.relList&&_n.relList.supports&&_n.relList.supports("prefetch")?_f:bf;function Yf(){if(!ge||!window.IntersectionObserver)return;let e;if((e=navigator.connection)&&(e.saveData||/2g/.test(e.effectiveType)))return;const t=window.requestIdleCallback||setTimeout;let n=null;const s=()=>{n&&n.disconnect(),n=new IntersectionObserver(i=>{i.forEach(l=>{if(l.isIntersecting){const o=l.target;n.unobserve(o);const{pathname:c}=o;if(!ws.has(c)){ws.add(c);const f=uf(c);f&&wf(f)}}})}),t(()=>{document.querySelectorAll("#app a").forEach(i=>{const{hostname:l,pathname:o}=new URL(i.href instanceof SVGAnimatedString?i.href.animVal:i.href,i.baseURI),c=o.match(/\.\w+$/);c&&c[0]!==".html"||i.target!=="_blank"&&l===location.hostname&&(o!==location.pathname?n.observe(i):ws.add(o))})})};Ht(s);const r=Lo();Le(()=>r.path,s),Gn(()=>{n&&n.disconnect()})}export{Rf as $,df as A,Cf as B,xf as C,kf as D,de as E,be as F,Ie as G,Tf as H,Ro as I,Lo as J,Mc as K,_t as L,$f as M,js as N,Hf as O,Un as P,jf as Q,ge as R,Jt as S,If as T,Df as U,Fl as V,Af as W,Nf as X,Vi as Y,Lf as Z,Bf as _,go as a,Xf as a0,hf as a1,Vf as a2,cf as a3,Uf as a4,Kf as a5,xt as a6,Wf as a7,uf as a8,Ff as a9,Yf as aa,Gf as ab,qf as ac,Ls as ad,Mf as ae,Ps as b,Of as c,Di as d,Pf as e,lf as f,Yr as g,oe as h,Za as i,po as j,kn as k,Qa as l,To as m,$s as n,Ms as o,yt as p,Le as q,Ef as r,Oi as s,Jo as t,af as u,Ht as v,Nl as w,Gn as x,Sf as y,Ql as z}; diff --git a/docs/.vitepress/dist/assets/chunks/theme.KNngRPLo.js b/docs/.vitepress/dist/assets/chunks/theme.KNngRPLo.js new file mode 100644 index 0000000..5e07a93 --- /dev/null +++ b/docs/.vitepress/dist/assets/chunks/theme.KNngRPLo.js @@ -0,0 +1 @@ +import{d as p,c as u,r as c,n as T,o as s,a as j,t as N,b as _,w as h,T as ce,e as m,_ as b,u as He,i as Ae,f as Be,g as ue,h as g,j as v,k as i,l as z,m as se,p as S,q as F,s as Y,v as U,x as de,y as ve,z as Ce,A as Ee,F as x,B as H,C as W,D as Q,E as k,G as ge,H as C,I as $e,J as X,K as G,L as Z,M as Fe,N as ye,O as De,P as Pe,Q as Le,R as ee,S as Oe,U as Ve,V as Se,W as Ge,X as Ue,Y as je,Z as ze,$ as We}from"./framework.C8AWLET_.js";const qe=p({__name:"VPBadge",props:{text:{},type:{default:"tip"}},setup(e){return(t,n)=>(s(),u("span",{class:T(["VPBadge",e.type])},[c(t.$slots,"default",{},()=>[j(N(e.text),1)])],2))}}),Ke={key:0,class:"VPBackdrop"},Re=p({__name:"VPBackdrop",props:{show:{type:Boolean}},setup(e){return(t,n)=>(s(),_(ce,{name:"fade"},{default:h(()=>[e.show?(s(),u("div",Ke)):m("",!0)]),_:1}))}}),Je=b(Re,[["__scopeId","data-v-c79a1216"]]),P=He;function Ye(e,t){let n,a=!1;return()=>{n&&clearTimeout(n),a?n=setTimeout(e,t):(e(),(a=!0)&&setTimeout(()=>a=!1,t))}}function ie(e){return e.startsWith("/")?e:`/${e}`}function fe(e){const{pathname:t,search:n,hash:a,protocol:o}=new URL(e,"http://a.com");if(Ae(e)||e.startsWith("#")||!o.startsWith("http")||!Be(t))return e;const{site:r}=P(),l=t.endsWith("/")||t.endsWith(".html")?e:e.replace(/(?:(^\.+)\/)?.*$/,`$1${t.replace(/(\.md)?$/,r.value.cleanUrls?"":".html")}${n}${a}`);return ue(l)}function K({correspondingLink:e=!1}={}){const{site:t,localeIndex:n,page:a,theme:o,hash:r}=P(),l=g(()=>{var d,y;return{label:(d=t.value.locales[n.value])==null?void 0:d.label,link:((y=t.value.locales[n.value])==null?void 0:y.link)||(n.value==="root"?"/":`/${n.value}/`)}});return{localeLinks:g(()=>Object.entries(t.value.locales).flatMap(([d,y])=>l.value.label===y.label?[]:{text:y.label,link:Qe(y.link||(d==="root"?"/":`/${d}/`),o.value.i18nRouting!==!1&&e,a.value.relativePath.slice(l.value.link.length-1),!t.value.cleanUrls)+r.value})),currentLang:l}}function Qe(e,t,n,a){return t?e.replace(/\/$/,"")+ie(n.replace(/(^|\/)index\.md$/,"$1").replace(/\.md$/,a?".html":"")):e}const Xe={class:"NotFound"},Ze={class:"code"},et={class:"title"},tt={class:"quote"},nt={class:"action"},at=["href","aria-label"],ot=p({__name:"NotFound",setup(e){const{theme:t}=P(),{currentLang:n}=K();return(a,o)=>{var r,l,f,d,y;return s(),u("div",Xe,[v("p",Ze,N(((r=i(t).notFound)==null?void 0:r.code)??"404"),1),v("h1",et,N(((l=i(t).notFound)==null?void 0:l.title)??"PAGE NOT FOUND"),1),o[0]||(o[0]=v("div",{class:"divider"},null,-1)),v("blockquote",tt,N(((f=i(t).notFound)==null?void 0:f.quote)??"But if you don't change your direction, and if you keep looking, you may end up where you are heading."),1),v("div",nt,[v("a",{class:"link",href:i(ue)(i(n).link),"aria-label":((d=i(t).notFound)==null?void 0:d.linkLabel)??"go to home"},N(((y=i(t).notFound)==null?void 0:y.linkText)??"Take me home"),9,at)])])}}}),st=b(ot,[["__scopeId","data-v-d6be1790"]]);function Te(e,t){if(Array.isArray(e))return R(e);if(e==null)return[];t=ie(t);const n=Object.keys(e).sort((o,r)=>r.split("/").length-o.split("/").length).find(o=>t.startsWith(ie(o))),a=n?e[n]:[];return Array.isArray(a)?R(a):R(a.items,a.base)}function it(e){const t=[];let n=0;for(const a in e){const o=e[a];if(o.items){n=t.push(o);continue}t[n]||t.push({items:[]}),t[n].items.push(o)}return t}function rt(e){const t=[];function n(a){for(const o of a)o.text&&o.link&&t.push({text:o.text,link:o.link,docFooterText:o.docFooterText}),o.items&&n(o.items)}return n(e),t}function re(e,t){return Array.isArray(t)?t.some(n=>re(e,n)):z(e,t.link)?!0:t.items?re(e,t.items):!1}function R(e,t){return[...e].map(n=>{const a={...n},o=a.base||t;return o&&a.link&&(a.link=o+a.link),a.items&&(a.items=R(a.items,o)),a})}function D(){const{frontmatter:e,page:t,theme:n}=P(),a=se("(min-width: 960px)"),o=S(!1),r=g(()=>{const w=n.value.sidebar,A=t.value.relativePath;return w?Te(w,A):[]}),l=S(r.value);F(r,(w,A)=>{JSON.stringify(w)!==JSON.stringify(A)&&(l.value=r.value)});const f=g(()=>e.value.sidebar!==!1&&l.value.length>0&&e.value.layout!=="home"),d=g(()=>y?e.value.aside==null?n.value.aside==="left":e.value.aside==="left":!1),y=g(()=>e.value.layout==="home"?!1:e.value.aside!=null?!!e.value.aside:n.value.aside!==!1),L=g(()=>f.value&&a.value),$=g(()=>f.value?it(l.value):[]);function V(){o.value=!0}function M(){o.value=!1}function I(){o.value?M():V()}return{isOpen:o,sidebar:l,sidebarGroups:$,hasSidebar:f,hasAside:y,leftAside:d,isSidebarEnabled:L,open:V,close:M,toggle:I}}function lt(e,t){let n;Y(()=>{n=e.value?document.activeElement:void 0}),U(()=>{window.addEventListener("keyup",a)}),de(()=>{window.removeEventListener("keyup",a)});function a(o){o.key==="Escape"&&e.value&&(t(),n==null||n.focus())}}function ct(e){const{page:t,hash:n}=P(),a=S(!1),o=g(()=>e.value.collapsed!=null),r=g(()=>!!e.value.link),l=S(!1),f=()=>{l.value=z(t.value.relativePath,e.value.link)};F([t,e,n],f),U(f);const d=g(()=>l.value?!0:e.value.items?re(t.value.relativePath,e.value.items):!1),y=g(()=>!!(e.value.items&&e.value.items.length));Y(()=>{a.value=!!(o.value&&e.value.collapsed)}),ve(()=>{(l.value||d.value)&&(a.value=!1)});function L(){o.value&&(a.value=!a.value)}return{collapsed:a,collapsible:o,isLink:r,isActiveLink:l,hasActiveLink:d,hasChildren:y,toggle:L}}function ut(){const{hasSidebar:e}=D(),t=se("(min-width: 960px)"),n=se("(min-width: 1280px)");return{isAsideEnabled:g(()=>!n.value&&!t.value?!1:e.value?n.value:t.value)}}const dt=/\b(?:VPBadge|header-anchor|footnote-ref|ignore-header)\b/,le=[];function Ne(e){return typeof e.outline=="object"&&!Array.isArray(e.outline)&&e.outline.label||e.outlineTitle||"On this page"}function he(e){const t=[...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")].filter(n=>n.id&&n.hasChildNodes()).map(n=>{const a=Number(n.tagName[1]);return{element:n,title:vt(n),link:"#"+n.id,level:a}});return ft(t,e)}function vt(e){let t="";for(const n of e.childNodes)if(n.nodeType===1){if(dt.test(n.className))continue;t+=n.textContent}else n.nodeType===3&&(t+=n.textContent);return t.trim()}function ft(e,t){if(t===!1)return[];const n=(typeof t=="object"&&!Array.isArray(t)?t.level:t)||2,[a,o]=typeof n=="number"?[n,n]:n==="deep"?[2,6]:n;return pt(e,a,o)}function ht(e,t){const{isAsideEnabled:n}=ut(),a=Ye(r,100);let o=null;U(()=>{requestAnimationFrame(r),window.addEventListener("scroll",a)}),Ce(()=>{l(location.hash)}),de(()=>{window.removeEventListener("scroll",a)});function r(){if(!n.value)return;const f=window.scrollY,d=window.innerHeight,y=document.body.offsetHeight,L=Math.abs(f+d-y)<1,$=le.map(({element:M,link:I})=>({link:I,top:mt(M)})).filter(({top:M})=>!Number.isNaN(M)).sort((M,I)=>M.top-I.top);if(!$.length){l(null);return}if(f<1){l(null);return}if(L){l($[$.length-1].link);return}let V=null;for(const{link:M,top:I}of $){if(I>f+Ee()+4)break;V=M}l(V)}function l(f){o&&o.classList.remove("active"),f==null?o=null:o=e.value.querySelector(`a[href="${decodeURIComponent(f)}"]`);const d=o;d?(d.classList.add("active"),t.value.style.top=d.offsetTop+39+"px",t.value.style.opacity="1"):(t.value.style.top="33px",t.value.style.opacity="0")}}function mt(e){let t=0;for(;e!==document.body;){if(e===null)return NaN;t+=e.offsetTop,e=e.offsetParent}return t}function pt(e,t,n){le.length=0;const a=[],o=[];return e.forEach(r=>{const l={...r,children:[]};let f=o[o.length-1];for(;f&&f.level>=l.level;)o.pop(),f=o[o.length-1];if(l.element.classList.contains("ignore-header")||f&&"shouldIgnore"in f){o.push({level:l.level,shouldIgnore:!0});return}l.level>n||l.level{const o=W("VPDocOutlineItem",!0);return s(),u("ul",{class:T(["VPDocOutlineItem",e.root?"root":"nested"])},[(s(!0),u(x,null,H(e.headers,({children:r,link:l,title:f})=>(s(),u("li",null,[v("a",{class:"outline-link",href:l,onClick:t,title:f},N(f),9,kt),r!=null&&r.length?(s(),_(o,{key:0,headers:r},null,8,["headers"])):m("",!0)]))),256))],2)}}}),Me=b(_t,[["__scopeId","data-v-b933a997"]]),bt={class:"content"},gt={"aria-level":"2",class:"outline-title",id:"doc-outline-aria-label",role:"heading"},$t=p({__name:"VPDocAsideOutline",setup(e){const{frontmatter:t,theme:n}=P(),a=ge([]);Q(()=>{a.value=he(t.value.outline??n.value.outline)});const o=S(),r=S();return ht(o,r),(l,f)=>(s(),u("nav",{"aria-labelledby":"doc-outline-aria-label",class:T(["VPDocAsideOutline",{"has-outline":a.value.length>0}]),ref_key:"container",ref:o},[v("div",bt,[v("div",{class:"outline-marker",ref_key:"marker",ref:r},null,512),v("div",gt,N(i(Ne)(i(n))),1),k(Me,{headers:a.value,root:!0},null,8,["headers"])])],2))}}),yt=b($t,[["__scopeId","data-v-a5bbad30"]]),Pt={class:"VPDocAsideCarbonAds"},Lt=p({__name:"VPDocAsideCarbonAds",props:{carbonAds:{}},setup(e){const t=()=>null;return(n,a)=>(s(),u("div",Pt,[k(i(t),{"carbon-ads":e.carbonAds},null,8,["carbon-ads"])]))}}),Vt={class:"VPDocAside"},St=p({__name:"VPDocAside",setup(e){const{theme:t}=P();return(n,a)=>(s(),u("div",Vt,[c(n.$slots,"aside-top",{},void 0,!0),c(n.$slots,"aside-outline-before",{},void 0,!0),k(yt),c(n.$slots,"aside-outline-after",{},void 0,!0),a[0]||(a[0]=v("div",{class:"spacer"},null,-1)),c(n.$slots,"aside-ads-before",{},void 0,!0),i(t).carbonAds?(s(),_(Lt,{key:0,"carbon-ads":i(t).carbonAds},null,8,["carbon-ads"])):m("",!0),c(n.$slots,"aside-ads-after",{},void 0,!0),c(n.$slots,"aside-bottom",{},void 0,!0)]))}}),Tt=b(St,[["__scopeId","data-v-3f215769"]]);function Nt(){const{theme:e,page:t}=P();return g(()=>{const{text:n="Edit this page",pattern:a=""}=e.value.editLink||{};let o;return typeof a=="function"?o=a(t.value):o=a.replace(/:path/g,t.value.filePath),{url:o,text:n}})}function Mt(){const{page:e,theme:t,frontmatter:n}=P();return g(()=>{var y,L,$,V,M,I,w,A;const a=Te(t.value.sidebar,e.value.relativePath),o=rt(a),r=xt(o,B=>B.link.replace(/[?#].*$/,"")),l=r.findIndex(B=>z(e.value.relativePath,B.link)),f=((y=t.value.docFooter)==null?void 0:y.prev)===!1&&!n.value.prev||n.value.prev===!1,d=((L=t.value.docFooter)==null?void 0:L.next)===!1&&!n.value.next||n.value.next===!1;return{prev:f?void 0:{text:(typeof n.value.prev=="string"?n.value.prev:typeof n.value.prev=="object"?n.value.prev.text:void 0)??(($=r[l-1])==null?void 0:$.docFooterText)??((V=r[l-1])==null?void 0:V.text),link:(typeof n.value.prev=="object"?n.value.prev.link:void 0)??((M=r[l-1])==null?void 0:M.link)},next:d?void 0:{text:(typeof n.value.next=="string"?n.value.next:typeof n.value.next=="object"?n.value.next.text:void 0)??((I=r[l+1])==null?void 0:I.docFooterText)??((w=r[l+1])==null?void 0:w.text),link:(typeof n.value.next=="object"?n.value.next.link:void 0)??((A=r[l+1])==null?void 0:A.link)}}})}function xt(e,t){const n=new Set;return e.filter(a=>{const o=t(a);return n.has(o)?!1:n.add(o)})}const E=p({__name:"VPLink",props:{tag:{},href:{},noIcon:{type:Boolean},target:{},rel:{}},setup(e){const t=e,n=g(()=>t.tag??(t.href?"a":"span")),a=g(()=>t.href&&$e.test(t.href)||t.target==="_blank");return(o,r)=>(s(),_(C(n.value),{class:T(["VPLink",{link:e.href,"vp-external-link-icon":a.value,"no-icon":e.noIcon}]),href:e.href?i(fe)(e.href):void 0,target:e.target??(a.value?"_blank":void 0),rel:e.rel??(a.value?"noreferrer":void 0)},{default:h(()=>[c(o.$slots,"default")]),_:3},8,["class","href","target","rel"]))}}),It={class:"VPLastUpdated"},wt=["datetime"],Ht=p({__name:"VPDocFooterLastUpdated",setup(e){const{theme:t,page:n,lang:a}=P(),o=g(()=>new Date(n.value.lastUpdated)),r=g(()=>o.value.toISOString()),l=S("");return U(()=>{Y(()=>{var f,d,y;l.value=new Intl.DateTimeFormat((d=(f=t.value.lastUpdated)==null?void 0:f.formatOptions)!=null&&d.forceLocale?a.value:void 0,((y=t.value.lastUpdated)==null?void 0:y.formatOptions)??{dateStyle:"short",timeStyle:"short"}).format(o.value)})}),(f,d)=>{var y;return s(),u("p",It,[j(N(((y=i(t).lastUpdated)==null?void 0:y.text)||i(t).lastUpdatedText||"Last updated")+": ",1),v("time",{datetime:r.value},N(l.value),9,wt)])}}}),At=b(Ht,[["__scopeId","data-v-e98dd255"]]),Bt={key:0,class:"VPDocFooter"},Ct={key:0,class:"edit-info"},Et={key:0,class:"edit-link"},Ft={key:1,class:"last-updated"},Dt={key:1,class:"prev-next","aria-labelledby":"doc-footer-aria-label"},Ot={class:"pager"},Gt=["innerHTML"],Ut=["innerHTML"],jt={class:"pager"},zt=["innerHTML"],Wt=["innerHTML"],qt=p({__name:"VPDocFooter",setup(e){const{theme:t,page:n,frontmatter:a}=P(),o=Nt(),r=Mt(),l=g(()=>t.value.editLink&&a.value.editLink!==!1),f=g(()=>n.value.lastUpdated),d=g(()=>l.value||f.value||r.value.prev||r.value.next);return(y,L)=>{var $,V,M,I;return d.value?(s(),u("footer",Bt,[c(y.$slots,"doc-footer-before",{},void 0,!0),l.value||f.value?(s(),u("div",Ct,[l.value?(s(),u("div",Et,[k(E,{class:"edit-link-button",href:i(o).url,"no-icon":!0},{default:h(()=>[L[0]||(L[0]=v("span",{class:"vpi-square-pen edit-link-icon"},null,-1)),j(" "+N(i(o).text),1)]),_:1},8,["href"])])):m("",!0),f.value?(s(),u("div",Ft,[k(At)])):m("",!0)])):m("",!0),($=i(r).prev)!=null&&$.link||(V=i(r).next)!=null&&V.link?(s(),u("nav",Dt,[L[1]||(L[1]=v("span",{class:"visually-hidden",id:"doc-footer-aria-label"},"Pager",-1)),v("div",Ot,[(M=i(r).prev)!=null&&M.link?(s(),_(E,{key:0,class:"pager-link prev",href:i(r).prev.link},{default:h(()=>{var w;return[v("span",{class:"desc",innerHTML:((w=i(t).docFooter)==null?void 0:w.prev)||"Previous page"},null,8,Gt),v("span",{class:"title",innerHTML:i(r).prev.text},null,8,Ut)]}),_:1},8,["href"])):m("",!0)]),v("div",jt,[(I=i(r).next)!=null&&I.link?(s(),_(E,{key:0,class:"pager-link next",href:i(r).next.link},{default:h(()=>{var w;return[v("span",{class:"desc",innerHTML:((w=i(t).docFooter)==null?void 0:w.next)||"Next page"},null,8,zt),v("span",{class:"title",innerHTML:i(r).next.text},null,8,Wt)]}),_:1},8,["href"])):m("",!0)])])):m("",!0)])):m("",!0)}}}),Kt=b(qt,[["__scopeId","data-v-e257564d"]]),Rt={class:"container"},Jt={class:"aside-container"},Yt={class:"aside-content"},Qt={class:"content"},Xt={class:"content-container"},Zt={class:"main"},en=p({__name:"VPDoc",setup(e){const{theme:t}=P(),n=X(),{hasSidebar:a,hasAside:o,leftAside:r}=D(),l=g(()=>n.path.replace(/[./]+/g,"_").replace(/_html$/,""));return(f,d)=>{const y=W("Content");return s(),u("div",{class:T(["VPDoc",{"has-sidebar":i(a),"has-aside":i(o)}])},[c(f.$slots,"doc-top",{},void 0,!0),v("div",Rt,[i(o)?(s(),u("div",{key:0,class:T(["aside",{"left-aside":i(r)}])},[d[0]||(d[0]=v("div",{class:"aside-curtain"},null,-1)),v("div",Jt,[v("div",Yt,[k(Tt,null,{"aside-top":h(()=>[c(f.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":h(()=>[c(f.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":h(()=>[c(f.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":h(()=>[c(f.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":h(()=>[c(f.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":h(()=>[c(f.$slots,"aside-ads-after",{},void 0,!0)]),_:3})])])],2)):m("",!0),v("div",Qt,[v("div",Xt,[c(f.$slots,"doc-before",{},void 0,!0),v("main",Zt,[k(y,{class:T(["vp-doc",[l.value,i(t).externalLinkIcon&&"external-link-icon-enabled"]])},null,8,["class"])]),k(Kt,null,{"doc-footer-before":h(()=>[c(f.$slots,"doc-footer-before",{},void 0,!0)]),_:3}),c(f.$slots,"doc-after",{},void 0,!0)])])]),c(f.$slots,"doc-bottom",{},void 0,!0)],2)}}}),tn=b(en,[["__scopeId","data-v-39a288b8"]]),nn=p({__name:"VPButton",props:{tag:{},size:{default:"medium"},theme:{default:"brand"},text:{},href:{},target:{},rel:{}},setup(e){const t=e,n=g(()=>t.href&&$e.test(t.href)),a=g(()=>t.tag||(t.href?"a":"button"));return(o,r)=>(s(),_(C(a.value),{class:T(["VPButton",[e.size,e.theme]]),href:e.href?i(fe)(e.href):void 0,target:t.target??(n.value?"_blank":void 0),rel:t.rel??(n.value?"noreferrer":void 0)},{default:h(()=>[j(N(e.text),1)]),_:1},8,["class","href","target","rel"]))}}),an=b(nn,[["__scopeId","data-v-fa7799d5"]]),on=["src","alt"],sn=p({inheritAttrs:!1,__name:"VPImage",props:{image:{},alt:{}},setup(e){return(t,n)=>{const a=W("VPImage",!0);return e.image?(s(),u(x,{key:0},[typeof e.image=="string"||"src"in e.image?(s(),u("img",G({key:0,class:"VPImage"},typeof e.image=="string"?t.$attrs:{...e.image,...t.$attrs},{src:i(ue)(typeof e.image=="string"?e.image:e.image.src),alt:e.alt??(typeof e.image=="string"?"":e.image.alt||"")}),null,16,on)):(s(),u(x,{key:1},[k(a,G({class:"dark",image:e.image.dark,alt:e.image.alt},t.$attrs),null,16,["image","alt"]),k(a,G({class:"light",image:e.image.light,alt:e.image.alt},t.$attrs),null,16,["image","alt"])],64))],64)):m("",!0)}}}),J=b(sn,[["__scopeId","data-v-8426fc1a"]]),rn={class:"container"},ln={class:"main"},cn={class:"heading"},un=["innerHTML"],dn=["innerHTML"],vn=["innerHTML"],fn={key:0,class:"actions"},hn={key:0,class:"image"},mn={class:"image-container"},pn=p({__name:"VPHero",props:{name:{},text:{},tagline:{},image:{},actions:{}},setup(e){const t=Z("hero-image-slot-exists");return(n,a)=>(s(),u("div",{class:T(["VPHero",{"has-image":e.image||i(t)}])},[v("div",rn,[v("div",ln,[c(n.$slots,"home-hero-info-before",{},void 0,!0),c(n.$slots,"home-hero-info",{},()=>[v("h1",cn,[e.name?(s(),u("span",{key:0,innerHTML:e.name,class:"name clip"},null,8,un)):m("",!0),e.text?(s(),u("span",{key:1,innerHTML:e.text,class:"text"},null,8,dn)):m("",!0)]),e.tagline?(s(),u("p",{key:0,innerHTML:e.tagline,class:"tagline"},null,8,vn)):m("",!0)],!0),c(n.$slots,"home-hero-info-after",{},void 0,!0),e.actions?(s(),u("div",fn,[(s(!0),u(x,null,H(e.actions,o=>(s(),u("div",{key:o.link,class:"action"},[k(an,{tag:"a",size:"medium",theme:o.theme,text:o.text,href:o.link,target:o.target,rel:o.rel},null,8,["theme","text","href","target","rel"])]))),128))])):m("",!0),c(n.$slots,"home-hero-actions-after",{},void 0,!0)]),e.image||i(t)?(s(),u("div",hn,[v("div",mn,[a[0]||(a[0]=v("div",{class:"image-bg"},null,-1)),c(n.$slots,"home-hero-image",{},()=>[e.image?(s(),_(J,{key:0,class:"image-src",image:e.image},null,8,["image"])):m("",!0)],!0)])])):m("",!0)])],2))}}),kn=b(pn,[["__scopeId","data-v-4f9c455b"]]),_n=p({__name:"VPHomeHero",setup(e){const{frontmatter:t}=P();return(n,a)=>i(t).hero?(s(),_(kn,{key:0,class:"VPHomeHero",name:i(t).hero.name,text:i(t).hero.text,tagline:i(t).hero.tagline,image:i(t).hero.image,actions:i(t).hero.actions},{"home-hero-info-before":h(()=>[c(n.$slots,"home-hero-info-before")]),"home-hero-info":h(()=>[c(n.$slots,"home-hero-info")]),"home-hero-info-after":h(()=>[c(n.$slots,"home-hero-info-after")]),"home-hero-actions-after":h(()=>[c(n.$slots,"home-hero-actions-after")]),"home-hero-image":h(()=>[c(n.$slots,"home-hero-image")]),_:3},8,["name","text","tagline","image","actions"])):m("",!0)}}),bn={class:"box"},gn={key:0,class:"icon"},$n=["innerHTML"],yn=["innerHTML"],Pn=["innerHTML"],Ln={key:4,class:"link-text"},Vn={class:"link-text-value"},Sn=p({__name:"VPFeature",props:{icon:{},title:{},details:{},link:{},linkText:{},rel:{},target:{}},setup(e){return(t,n)=>(s(),_(E,{class:"VPFeature",href:e.link,rel:e.rel,target:e.target,"no-icon":!0,tag:e.link?"a":"div"},{default:h(()=>[v("article",bn,[typeof e.icon=="object"&&e.icon.wrap?(s(),u("div",gn,[k(J,{image:e.icon,alt:e.icon.alt,height:e.icon.height||48,width:e.icon.width||48},null,8,["image","alt","height","width"])])):typeof e.icon=="object"?(s(),_(J,{key:1,image:e.icon,alt:e.icon.alt,height:e.icon.height||48,width:e.icon.width||48},null,8,["image","alt","height","width"])):e.icon?(s(),u("div",{key:2,class:"icon",innerHTML:e.icon},null,8,$n)):m("",!0),v("h2",{class:"title",innerHTML:e.title},null,8,yn),e.details?(s(),u("p",{key:3,class:"details",innerHTML:e.details},null,8,Pn)):m("",!0),e.linkText?(s(),u("div",Ln,[v("p",Vn,[j(N(e.linkText)+" ",1),n[0]||(n[0]=v("span",{class:"vpi-arrow-right link-text-icon"},null,-1))])])):m("",!0)])]),_:1},8,["href","rel","target","tag"]))}}),Tn=b(Sn,[["__scopeId","data-v-a3976bdc"]]),Nn={key:0,class:"VPFeatures"},Mn={class:"container"},xn={class:"items"},In=p({__name:"VPFeatures",props:{features:{}},setup(e){const t=e,n=g(()=>{const a=t.features.length;if(a){if(a===2)return"grid-2";if(a===3)return"grid-3";if(a%3===0)return"grid-6";if(a>3)return"grid-4"}else return});return(a,o)=>e.features?(s(),u("div",Nn,[v("div",Mn,[v("div",xn,[(s(!0),u(x,null,H(e.features,r=>(s(),u("div",{key:r.title,class:T(["item",[n.value]])},[k(Tn,{icon:r.icon,title:r.title,details:r.details,link:r.link,"link-text":r.linkText,rel:r.rel,target:r.target},null,8,["icon","title","details","link","link-text","rel","target"])],2))),128))])])])):m("",!0)}}),wn=b(In,[["__scopeId","data-v-a6181336"]]),Hn=p({__name:"VPHomeFeatures",setup(e){const{frontmatter:t}=P();return(n,a)=>i(t).features?(s(),_(wn,{key:0,class:"VPHomeFeatures",features:i(t).features},null,8,["features"])):m("",!0)}}),An=p({__name:"VPHomeContent",setup(e){const{width:t}=Fe({initialWidth:0,includeScrollbar:!1});return(n,a)=>(s(),u("div",{class:"vp-doc container",style:ye(i(t)?{"--vp-offset":`calc(50% - ${i(t)/2}px)`}:{})},[c(n.$slots,"default",{},void 0,!0)],4))}}),Bn=b(An,[["__scopeId","data-v-8e2d4988"]]),Cn=p({__name:"VPHome",setup(e){const{frontmatter:t,theme:n}=P();return(a,o)=>{const r=W("Content");return s(),u("div",{class:T(["VPHome",{"external-link-icon-enabled":i(n).externalLinkIcon}])},[c(a.$slots,"home-hero-before",{},void 0,!0),k(_n,null,{"home-hero-info-before":h(()=>[c(a.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":h(()=>[c(a.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":h(()=>[c(a.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":h(()=>[c(a.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":h(()=>[c(a.$slots,"home-hero-image",{},void 0,!0)]),_:3}),c(a.$slots,"home-hero-after",{},void 0,!0),c(a.$slots,"home-features-before",{},void 0,!0),k(Hn),c(a.$slots,"home-features-after",{},void 0,!0),i(t).markdownStyles!==!1?(s(),_(Bn,{key:0},{default:h(()=>[k(r)]),_:1})):(s(),_(r,{key:1}))],2)}}}),En=b(Cn,[["__scopeId","data-v-8b561e3d"]]),Fn={},Dn={class:"VPPage"};function On(e,t){const n=W("Content");return s(),u("div",Dn,[c(e.$slots,"page-top"),k(n),c(e.$slots,"page-bottom")])}const Gn=b(Fn,[["render",On]]),Un=p({__name:"VPContent",setup(e){const{page:t,frontmatter:n}=P(),{hasSidebar:a}=D();return(o,r)=>(s(),u("div",{class:T(["VPContent",{"has-sidebar":i(a),"is-home":i(n).layout==="home"}]),id:"VPContent"},[i(t).isNotFound?c(o.$slots,"not-found",{key:0},()=>[k(st)],!0):i(n).layout==="page"?(s(),_(Gn,{key:1},{"page-top":h(()=>[c(o.$slots,"page-top",{},void 0,!0)]),"page-bottom":h(()=>[c(o.$slots,"page-bottom",{},void 0,!0)]),_:3})):i(n).layout==="home"?(s(),_(En,{key:2},{"home-hero-before":h(()=>[c(o.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info-before":h(()=>[c(o.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":h(()=>[c(o.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":h(()=>[c(o.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":h(()=>[c(o.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":h(()=>[c(o.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":h(()=>[c(o.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":h(()=>[c(o.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":h(()=>[c(o.$slots,"home-features-after",{},void 0,!0)]),_:3})):i(n).layout&&i(n).layout!=="doc"?(s(),_(C(i(n).layout),{key:3})):(s(),_(tn,{key:4},{"doc-top":h(()=>[c(o.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":h(()=>[c(o.$slots,"doc-bottom",{},void 0,!0)]),"doc-footer-before":h(()=>[c(o.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":h(()=>[c(o.$slots,"doc-before",{},void 0,!0)]),"doc-after":h(()=>[c(o.$slots,"doc-after",{},void 0,!0)]),"aside-top":h(()=>[c(o.$slots,"aside-top",{},void 0,!0)]),"aside-outline-before":h(()=>[c(o.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":h(()=>[c(o.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":h(()=>[c(o.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":h(()=>[c(o.$slots,"aside-ads-after",{},void 0,!0)]),"aside-bottom":h(()=>[c(o.$slots,"aside-bottom",{},void 0,!0)]),_:3}))],2))}}),jn=b(Un,[["__scopeId","data-v-1428d186"]]),zn={class:"container"},Wn=["innerHTML"],qn=["innerHTML"],Kn=p({__name:"VPFooter",setup(e){const{theme:t,frontmatter:n}=P(),{hasSidebar:a}=D();return(o,r)=>i(t).footer&&i(n).footer!==!1?(s(),u("footer",{key:0,class:T(["VPFooter",{"has-sidebar":i(a)}])},[v("div",zn,[i(t).footer.message?(s(),u("p",{key:0,class:"message",innerHTML:i(t).footer.message},null,8,Wn)):m("",!0),i(t).footer.copyright?(s(),u("p",{key:1,class:"copyright",innerHTML:i(t).footer.copyright},null,8,qn)):m("",!0)])],2)):m("",!0)}}),Rn=b(Kn,[["__scopeId","data-v-e315a0ad"]]);function Jn(){const{theme:e,frontmatter:t}=P(),n=ge([]),a=g(()=>n.value.length>0);return Q(()=>{n.value=he(t.value.outline??e.value.outline)}),{headers:n,hasLocalNav:a}}const Yn={class:"menu-text"},Qn={class:"header"},Xn={class:"outline"},Zn=p({__name:"VPLocalNavOutlineDropdown",props:{headers:{},navHeight:{}},setup(e){const t=e,{theme:n}=P(),a=S(!1),o=S(0),r=S(),l=S();function f($){var V;(V=r.value)!=null&&V.contains($.target)||(a.value=!1)}F(a,$=>{if($){document.addEventListener("click",f);return}document.removeEventListener("click",f)}),De("Escape",()=>{a.value=!1}),Q(()=>{a.value=!1});function d(){a.value=!a.value,o.value=window.innerHeight+Math.min(window.scrollY-t.navHeight,0)}function y($){$.target.classList.contains("outline-link")&&(l.value&&(l.value.style.transition="none"),Pe(()=>{a.value=!1}))}function L(){a.value=!1,window.scrollTo({top:0,left:0,behavior:"smooth"})}return($,V)=>(s(),u("div",{class:"VPLocalNavOutlineDropdown",style:ye({"--vp-vh":o.value+"px"}),ref_key:"main",ref:r},[e.headers.length>0?(s(),u("button",{key:0,onClick:d,class:T({open:a.value})},[v("span",Yn,N(i(Ne)(i(n))),1),V[0]||(V[0]=v("span",{class:"vpi-chevron-right icon"},null,-1))],2)):(s(),u("button",{key:1,onClick:L},N(i(n).returnToTopLabel||"Return to top"),1)),k(ce,{name:"flyout"},{default:h(()=>[a.value?(s(),u("div",{key:0,ref_key:"items",ref:l,class:"items",onClick:y},[v("div",Qn,[v("a",{class:"top-link",href:"#",onClick:L},N(i(n).returnToTopLabel||"Return to top"),1)]),v("div",Xn,[k(Me,{headers:e.headers},null,8,["headers"])])],512)):m("",!0)]),_:1})],4))}}),ea=b(Zn,[["__scopeId","data-v-8a42e2b4"]]),ta={class:"container"},na=["aria-expanded"],aa={class:"menu-text"},oa=p({__name:"VPLocalNav",props:{open:{type:Boolean}},emits:["open-menu"],setup(e){const{theme:t,frontmatter:n}=P(),{hasSidebar:a}=D(),{headers:o}=Jn(),{y:r}=Le(),l=S(0);U(()=>{l.value=parseInt(getComputedStyle(document.documentElement).getPropertyValue("--vp-nav-height"))}),Q(()=>{o.value=he(n.value.outline??t.value.outline)});const f=g(()=>o.value.length===0),d=g(()=>f.value&&!a.value),y=g(()=>({VPLocalNav:!0,"has-sidebar":a.value,empty:f.value,fixed:d.value}));return(L,$)=>i(n).layout!=="home"&&(!d.value||i(r)>=l.value)?(s(),u("div",{key:0,class:T(y.value)},[v("div",ta,[i(a)?(s(),u("button",{key:0,class:"menu","aria-expanded":e.open,"aria-controls":"VPSidebarNav",onClick:$[0]||($[0]=V=>L.$emit("open-menu"))},[$[1]||($[1]=v("span",{class:"vpi-align-left menu-icon"},null,-1)),v("span",aa,N(i(t).sidebarMenuLabel||"Menu"),1)],8,na)):m("",!0),k(ea,{headers:i(o),navHeight:l.value},null,8,["headers","navHeight"])])],2)):m("",!0)}}),sa=b(oa,[["__scopeId","data-v-a6f0e41e"]]);function ia(){const e=S(!1);function t(){e.value=!0,window.addEventListener("resize",o)}function n(){e.value=!1,window.removeEventListener("resize",o)}function a(){e.value?n():t()}function o(){window.outerWidth>=768&&n()}const r=X();return F(()=>r.path,n),{isScreenOpen:e,openScreen:t,closeScreen:n,toggleScreen:a}}const ra={},la={class:"VPSwitch",type:"button",role:"switch"},ca={class:"check"},ua={key:0,class:"icon"};function da(e,t){return s(),u("button",la,[v("span",ca,[e.$slots.default?(s(),u("span",ua,[c(e.$slots,"default",{},void 0,!0)])):m("",!0)])])}const va=b(ra,[["render",da],["__scopeId","data-v-1d5665e3"]]),fa=p({__name:"VPSwitchAppearance",setup(e){const{isDark:t,theme:n}=P(),a=Z("toggle-appearance",()=>{t.value=!t.value}),o=S("");return ve(()=>{o.value=t.value?n.value.lightModeSwitchTitle||"Switch to light theme":n.value.darkModeSwitchTitle||"Switch to dark theme"}),(r,l)=>(s(),_(va,{title:o.value,class:"VPSwitchAppearance","aria-checked":i(t),onClick:i(a)},{default:h(()=>[...l[0]||(l[0]=[v("span",{class:"vpi-sun sun"},null,-1),v("span",{class:"vpi-moon moon"},null,-1)])]),_:1},8,["title","aria-checked","onClick"]))}}),me=b(fa,[["__scopeId","data-v-5337faa4"]]),ha={key:0,class:"VPNavBarAppearance"},ma=p({__name:"VPNavBarAppearance",setup(e){const{site:t}=P();return(n,a)=>i(t).appearance&&i(t).appearance!=="force-dark"&&i(t).appearance!=="force-auto"?(s(),u("div",ha,[k(me)])):m("",!0)}}),pa=b(ma,[["__scopeId","data-v-6c893767"]]),pe=S();let xe=!1,oe=0;function ka(e){const t=S(!1);if(ee){!xe&&_a(),oe++;const n=F(pe,a=>{var o,r,l;a===e.el.value||(o=e.el.value)!=null&&o.contains(a)?(t.value=!0,(r=e.onFocus)==null||r.call(e)):(t.value=!1,(l=e.onBlur)==null||l.call(e))});de(()=>{n(),oe--,oe||ba()})}return Oe(t)}function _a(){document.addEventListener("focusin",Ie),xe=!0,pe.value=document.activeElement}function ba(){document.removeEventListener("focusin",Ie)}function Ie(){pe.value=document.activeElement}const ga={class:"VPMenuLink"},$a=["innerHTML"],ya=p({__name:"VPMenuLink",props:{item:{}},setup(e){const{page:t}=P();return(n,a)=>(s(),u("div",ga,[k(E,{class:T({active:i(z)(i(t).relativePath,e.item.activeMatch||e.item.link,!!e.item.activeMatch)}),href:e.item.link,target:e.item.target,rel:e.item.rel,"no-icon":e.item.noIcon},{default:h(()=>[v("span",{innerHTML:e.item.text},null,8,$a)]),_:1},8,["class","href","target","rel","no-icon"])]))}}),te=b(ya,[["__scopeId","data-v-35975db6"]]),Pa={class:"VPMenuGroup"},La={key:0,class:"title"},Va=p({__name:"VPMenuGroup",props:{text:{},items:{}},setup(e){return(t,n)=>(s(),u("div",Pa,[e.text?(s(),u("p",La,N(e.text),1)):m("",!0),(s(!0),u(x,null,H(e.items,a=>(s(),u(x,null,["link"in a?(s(),_(te,{key:0,item:a},null,8,["item"])):m("",!0)],64))),256))]))}}),Sa=b(Va,[["__scopeId","data-v-69e747b5"]]),Ta={class:"VPMenu"},Na={key:0,class:"items"},Ma=p({__name:"VPMenu",props:{items:{}},setup(e){return(t,n)=>(s(),u("div",Ta,[e.items?(s(),u("div",Na,[(s(!0),u(x,null,H(e.items,a=>(s(),u(x,{key:JSON.stringify(a)},["link"in a?(s(),_(te,{key:0,item:a},null,8,["item"])):"component"in a?(s(),_(C(a.component),G({key:1,ref_for:!0},a.props),null,16)):(s(),_(Sa,{key:2,text:a.text,items:a.items},null,8,["text","items"]))],64))),128))])):m("",!0),c(t.$slots,"default",{},void 0,!0)]))}}),xa=b(Ma,[["__scopeId","data-v-b98bc113"]]),Ia=["aria-expanded","aria-label"],wa={key:0,class:"text"},Ha=["innerHTML"],Aa={key:1,class:"vpi-more-horizontal icon"},Ba={class:"menu"},Ca=p({__name:"VPFlyout",props:{icon:{},button:{},label:{},items:{}},setup(e){const t=S(!1),n=S();ka({el:n,onBlur:a});function a(){t.value=!1}return(o,r)=>(s(),u("div",{class:"VPFlyout",ref_key:"el",ref:n,onMouseenter:r[1]||(r[1]=l=>t.value=!0),onMouseleave:r[2]||(r[2]=l=>t.value=!1)},[v("button",{type:"button",class:"button","aria-haspopup":"true","aria-expanded":t.value,"aria-label":e.label,onClick:r[0]||(r[0]=l=>t.value=!t.value)},[e.button||e.icon?(s(),u("span",wa,[e.icon?(s(),u("span",{key:0,class:T([e.icon,"option-icon"])},null,2)):m("",!0),e.button?(s(),u("span",{key:1,innerHTML:e.button},null,8,Ha)):m("",!0),r[3]||(r[3]=v("span",{class:"vpi-chevron-down text-icon"},null,-1))])):(s(),u("span",Aa))],8,Ia),v("div",Ba,[k(xa,{items:e.items},{default:h(()=>[c(o.$slots,"default",{},void 0,!0)]),_:3},8,["items"])])],544))}}),ke=b(Ca,[["__scopeId","data-v-cf11d7a2"]]),Ea=["href","aria-label","innerHTML"],Fa=p({__name:"VPSocialLink",props:{icon:{},link:{},ariaLabel:{}},setup(e){const t=e,n=S();U(async()=>{var r;await Pe();const o=(r=n.value)==null?void 0:r.children[0];o instanceof HTMLElement&&o.className.startsWith("vpi-social-")&&(getComputedStyle(o).maskImage||getComputedStyle(o).webkitMaskImage)==="none"&&o.style.setProperty("--icon",`url('https://api.iconify.design/simple-icons/${t.icon}.svg')`)});const a=g(()=>typeof t.icon=="object"?t.icon.svg:``);return(o,r)=>(s(),u("a",{ref_key:"el",ref:n,class:"VPSocialLink no-icon",href:e.link,"aria-label":e.ariaLabel??(typeof e.icon=="string"?e.icon:""),target:"_blank",rel:"noopener",innerHTML:a.value},null,8,Ea))}}),Da=b(Fa,[["__scopeId","data-v-bd121fe5"]]),Oa={class:"VPSocialLinks"},Ga=p({__name:"VPSocialLinks",props:{links:{}},setup(e){return(t,n)=>(s(),u("div",Oa,[(s(!0),u(x,null,H(e.links,({link:a,icon:o,ariaLabel:r})=>(s(),_(Da,{key:a,icon:o,link:a,ariaLabel:r},null,8,["icon","link","ariaLabel"]))),128))]))}}),_e=b(Ga,[["__scopeId","data-v-7bc22406"]]),Ua={key:0,class:"group translations"},ja={class:"trans-title"},za={key:1,class:"group"},Wa={class:"item appearance"},qa={class:"label"},Ka={class:"appearance-action"},Ra={key:2,class:"group"},Ja={class:"item social-links"},Ya=p({__name:"VPNavBarExtra",setup(e){const{site:t,theme:n}=P(),{localeLinks:a,currentLang:o}=K({correspondingLink:!0}),r=g(()=>a.value.length&&o.value.label||t.value.appearance||n.value.socialLinks);return(l,f)=>r.value?(s(),_(ke,{key:0,class:"VPNavBarExtra",label:"extra navigation"},{default:h(()=>[i(a).length&&i(o).label?(s(),u("div",Ua,[v("p",ja,N(i(o).label),1),(s(!0),u(x,null,H(i(a),d=>(s(),_(te,{key:d.link,item:d},null,8,["item"]))),128))])):m("",!0),i(t).appearance&&i(t).appearance!=="force-dark"&&i(t).appearance!=="force-auto"?(s(),u("div",za,[v("div",Wa,[v("p",qa,N(i(n).darkModeSwitchLabel||"Appearance"),1),v("div",Ka,[k(me)])])])):m("",!0),i(n).socialLinks?(s(),u("div",Ra,[v("div",Ja,[k(_e,{class:"social-links-list",links:i(n).socialLinks},null,8,["links"])])])):m("",!0)]),_:1})):m("",!0)}}),Qa=b(Ya,[["__scopeId","data-v-bb2aa2f0"]]),Xa=["aria-expanded"],Za=p({__name:"VPNavBarHamburger",props:{active:{type:Boolean}},emits:["click"],setup(e){return(t,n)=>(s(),u("button",{type:"button",class:T(["VPNavBarHamburger",{active:e.active}]),"aria-label":"mobile navigation","aria-expanded":e.active,"aria-controls":"VPNavScreen",onClick:n[0]||(n[0]=a=>t.$emit("click"))},[...n[1]||(n[1]=[v("span",{class:"container"},[v("span",{class:"top"}),v("span",{class:"middle"}),v("span",{class:"bottom"})],-1)])],10,Xa))}}),eo=b(Za,[["__scopeId","data-v-e5dd9c1c"]]),to=["innerHTML"],no=p({__name:"VPNavBarMenuLink",props:{item:{}},setup(e){const{page:t}=P();return(n,a)=>(s(),_(E,{class:T({VPNavBarMenuLink:!0,active:i(z)(i(t).relativePath,e.item.activeMatch||e.item.link,!!e.item.activeMatch)}),href:e.item.link,target:e.item.target,rel:e.item.rel,"no-icon":e.item.noIcon,tabindex:"0"},{default:h(()=>[v("span",{innerHTML:e.item.text},null,8,to)]),_:1},8,["class","href","target","rel","no-icon"]))}}),ao=b(no,[["__scopeId","data-v-e56f3d57"]]),oo=p({__name:"VPNavBarMenuGroup",props:{item:{}},setup(e){const t=e,{page:n}=P(),a=r=>"component"in r?!1:"link"in r?z(n.value.relativePath,r.link,!!t.item.activeMatch):r.items.some(a),o=g(()=>a(t.item));return(r,l)=>(s(),_(ke,{class:T({VPNavBarMenuGroup:!0,active:i(z)(i(n).relativePath,e.item.activeMatch,!!e.item.activeMatch)||o.value}),button:e.item.text,items:e.item.items},null,8,["class","button","items"]))}}),so={key:0,"aria-labelledby":"main-nav-aria-label",class:"VPNavBarMenu"},io=p({__name:"VPNavBarMenu",setup(e){const{theme:t}=P();return(n,a)=>i(t).nav?(s(),u("nav",so,[a[0]||(a[0]=v("span",{id:"main-nav-aria-label",class:"visually-hidden"}," Main Navigation ",-1)),(s(!0),u(x,null,H(i(t).nav,o=>(s(),u(x,{key:JSON.stringify(o)},["link"in o?(s(),_(ao,{key:0,item:o},null,8,["item"])):"component"in o?(s(),_(C(o.component),G({key:1,ref_for:!0},o.props),null,16)):(s(),_(oo,{key:2,item:o},null,8,["item"]))],64))),128))])):m("",!0)}}),ro=b(io,[["__scopeId","data-v-dc692963"]]);function lo(e){const{localeIndex:t,theme:n}=P();function a(o){var I,w,A;const r=o.split("."),l=(I=n.value.search)==null?void 0:I.options,f=l&&typeof l=="object",d=f&&((A=(w=l.locales)==null?void 0:w[t.value])==null?void 0:A.translations)||null,y=f&&l.translations||null;let L=d,$=y,V=e;const M=r.pop();for(const B of r){let O=null;const q=V==null?void 0:V[B];q&&(O=V=q);const ne=$==null?void 0:$[B];ne&&(O=$=ne);const ae=L==null?void 0:L[B];ae&&(O=L=ae),q||(V=O),ne||($=O),ae||(L=O)}return(L==null?void 0:L[M])??($==null?void 0:$[M])??(V==null?void 0:V[M])??""}return a}const co=["aria-label"],uo={class:"DocSearch-Button-Container"},vo={class:"DocSearch-Button-Placeholder"},be=p({__name:"VPNavBarSearchButton",setup(e){const n=lo({button:{buttonText:"Search",buttonAriaLabel:"Search"}});return(a,o)=>(s(),u("button",{type:"button",class:"DocSearch DocSearch-Button","aria-label":i(n)("button.buttonAriaLabel")},[v("span",uo,[o[0]||(o[0]=v("span",{class:"vp-icon DocSearch-Search-Icon"},null,-1)),v("span",vo,N(i(n)("button.buttonText")),1)]),o[1]||(o[1]=v("span",{class:"DocSearch-Button-Keys"},[v("kbd",{class:"DocSearch-Button-Key"}),v("kbd",{class:"DocSearch-Button-Key"},"K")],-1))],8,co))}}),fo={class:"VPNavBarSearch"},ho={id:"local-search"},mo={key:1,id:"docsearch"},po=p({__name:"VPNavBarSearch",setup(e){const t=()=>null,n=()=>null,{theme:a}=P(),o=S(!1),r=S(!1);U(()=>{});function l(){o.value||(o.value=!0,setTimeout(f,16))}function f(){const L=new Event("keydown");L.key="k",L.metaKey=!0,window.dispatchEvent(L),setTimeout(()=>{document.querySelector(".DocSearch-Modal")||f()},16)}const d=S(!1),y="";return(L,$)=>{var V;return s(),u("div",fo,[i(y)==="local"?(s(),u(x,{key:0},[d.value?(s(),_(i(t),{key:0,onClose:$[0]||($[0]=M=>d.value=!1)})):m("",!0),v("div",ho,[k(be,{onClick:$[1]||($[1]=M=>d.value=!0)})])],64)):i(y)==="algolia"?(s(),u(x,{key:1},[o.value?(s(),_(i(n),{key:0,algolia:((V=i(a).search)==null?void 0:V.options)??i(a).algolia,onVnodeBeforeMount:$[2]||($[2]=M=>r.value=!0)},null,8,["algolia"])):m("",!0),r.value?m("",!0):(s(),u("div",mo,[k(be,{onClick:l})]))],64)):m("",!0)])}}}),ko=p({__name:"VPNavBarSocialLinks",setup(e){const{theme:t}=P();return(n,a)=>i(t).socialLinks?(s(),_(_e,{key:0,class:"VPNavBarSocialLinks",links:i(t).socialLinks},null,8,["links"])):m("",!0)}}),_o=b(ko,[["__scopeId","data-v-0394ad82"]]),bo=["href","rel","target"],go=["innerHTML"],$o={key:2},yo=p({__name:"VPNavBarTitle",setup(e){const{site:t,theme:n}=P(),{hasSidebar:a}=D(),{currentLang:o}=K(),r=g(()=>{var d;return typeof n.value.logoLink=="string"?n.value.logoLink:(d=n.value.logoLink)==null?void 0:d.link}),l=g(()=>{var d;return typeof n.value.logoLink=="string"||(d=n.value.logoLink)==null?void 0:d.rel}),f=g(()=>{var d;return typeof n.value.logoLink=="string"||(d=n.value.logoLink)==null?void 0:d.target});return(d,y)=>(s(),u("div",{class:T(["VPNavBarTitle",{"has-sidebar":i(a)}])},[v("a",{class:"title",href:r.value??i(fe)(i(o).link),rel:l.value,target:f.value},[c(d.$slots,"nav-bar-title-before",{},void 0,!0),i(n).logo?(s(),_(J,{key:0,class:"logo",image:i(n).logo},null,8,["image"])):m("",!0),i(n).siteTitle?(s(),u("span",{key:1,innerHTML:i(n).siteTitle},null,8,go)):i(n).siteTitle===void 0?(s(),u("span",$o,N(i(t).title),1)):m("",!0),c(d.$slots,"nav-bar-title-after",{},void 0,!0)],8,bo)],2))}}),Po=b(yo,[["__scopeId","data-v-1168a8e4"]]),Lo={class:"items"},Vo={class:"title"},So=p({__name:"VPNavBarTranslations",setup(e){const{theme:t}=P(),{localeLinks:n,currentLang:a}=K({correspondingLink:!0});return(o,r)=>i(n).length&&i(a).label?(s(),_(ke,{key:0,class:"VPNavBarTranslations",icon:"vpi-languages",label:i(t).langMenuLabel||"Change language"},{default:h(()=>[v("div",Lo,[v("p",Vo,N(i(a).label),1),(s(!0),u(x,null,H(i(n),l=>(s(),_(te,{key:l.link,item:l},null,8,["item"]))),128))])]),_:1},8,["label"])):m("",!0)}}),To=b(So,[["__scopeId","data-v-88af2de4"]]),No={class:"wrapper"},Mo={class:"container"},xo={class:"title"},Io={class:"content"},wo={class:"content-body"},Ho=p({__name:"VPNavBar",props:{isScreenOpen:{type:Boolean}},emits:["toggle-screen"],setup(e){const t=e,{y:n}=Le(),{hasSidebar:a}=D(),{frontmatter:o}=P(),r=S({});return ve(()=>{r.value={"has-sidebar":a.value,home:o.value.layout==="home",top:n.value===0,"screen-open":t.isScreenOpen}}),(l,f)=>(s(),u("div",{class:T(["VPNavBar",r.value])},[v("div",No,[v("div",Mo,[v("div",xo,[k(Po,null,{"nav-bar-title-before":h(()=>[c(l.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":h(()=>[c(l.$slots,"nav-bar-title-after",{},void 0,!0)]),_:3})]),v("div",Io,[v("div",wo,[c(l.$slots,"nav-bar-content-before",{},void 0,!0),k(po,{class:"search"}),k(ro,{class:"menu"}),k(To,{class:"translations"}),k(pa,{class:"appearance"}),k(_o,{class:"social-links"}),k(Qa,{class:"extra"}),c(l.$slots,"nav-bar-content-after",{},void 0,!0),k(eo,{class:"hamburger",active:e.isScreenOpen,onClick:f[0]||(f[0]=d=>l.$emit("toggle-screen"))},null,8,["active"])])])])]),f[1]||(f[1]=v("div",{class:"divider"},[v("div",{class:"divider-line"})],-1))],2))}}),Ao=b(Ho,[["__scopeId","data-v-6aa21345"]]),Bo={key:0,class:"VPNavScreenAppearance"},Co={class:"text"},Eo=p({__name:"VPNavScreenAppearance",setup(e){const{site:t,theme:n}=P();return(a,o)=>i(t).appearance&&i(t).appearance!=="force-dark"&&i(t).appearance!=="force-auto"?(s(),u("div",Bo,[v("p",Co,N(i(n).darkModeSwitchLabel||"Appearance"),1),k(me)])):m("",!0)}}),Fo=b(Eo,[["__scopeId","data-v-b44890b2"]]),Do=["innerHTML"],Oo=p({__name:"VPNavScreenMenuLink",props:{item:{}},setup(e){const t=Z("close-screen");return(n,a)=>(s(),_(E,{class:"VPNavScreenMenuLink",href:e.item.link,target:e.item.target,rel:e.item.rel,"no-icon":e.item.noIcon,onClick:i(t)},{default:h(()=>[v("span",{innerHTML:e.item.text},null,8,Do)]),_:1},8,["href","target","rel","no-icon","onClick"]))}}),Go=b(Oo,[["__scopeId","data-v-df37e6dd"]]),Uo=["innerHTML"],jo=p({__name:"VPNavScreenMenuGroupLink",props:{item:{}},setup(e){const t=Z("close-screen");return(n,a)=>(s(),_(E,{class:"VPNavScreenMenuGroupLink",href:e.item.link,target:e.item.target,rel:e.item.rel,"no-icon":e.item.noIcon,onClick:i(t)},{default:h(()=>[v("span",{innerHTML:e.item.text},null,8,Uo)]),_:1},8,["href","target","rel","no-icon","onClick"]))}}),we=b(jo,[["__scopeId","data-v-3e9c20e4"]]),zo={class:"VPNavScreenMenuGroupSection"},Wo={key:0,class:"title"},qo=p({__name:"VPNavScreenMenuGroupSection",props:{text:{},items:{}},setup(e){return(t,n)=>(s(),u("div",zo,[e.text?(s(),u("p",Wo,N(e.text),1)):m("",!0),(s(!0),u(x,null,H(e.items,a=>(s(),_(we,{key:a.text,item:a},null,8,["item"]))),128))]))}}),Ko=b(qo,[["__scopeId","data-v-8133b170"]]),Ro=["aria-controls","aria-expanded"],Jo=["innerHTML"],Yo=["id"],Qo={key:0,class:"item"},Xo={key:1,class:"item"},Zo={key:2,class:"group"},es=p({__name:"VPNavScreenMenuGroup",props:{text:{},items:{}},setup(e){const t=e,n=S(!1),a=g(()=>`NavScreenGroup-${t.text.replace(" ","-").toLowerCase()}`);function o(){n.value=!n.value}return(r,l)=>(s(),u("div",{class:T(["VPNavScreenMenuGroup",{open:n.value}])},[v("button",{class:"button","aria-controls":a.value,"aria-expanded":n.value,onClick:o},[v("span",{class:"button-text",innerHTML:e.text},null,8,Jo),l[0]||(l[0]=v("span",{class:"vpi-plus button-icon"},null,-1))],8,Ro),v("div",{id:a.value,class:"items"},[(s(!0),u(x,null,H(e.items,f=>(s(),u(x,{key:JSON.stringify(f)},["link"in f?(s(),u("div",Qo,[k(we,{item:f},null,8,["item"])])):"component"in f?(s(),u("div",Xo,[(s(),_(C(f.component),G({ref_for:!0},f.props,{"screen-menu":""}),null,16))])):(s(),u("div",Zo,[k(Ko,{text:f.text,items:f.items},null,8,["text","items"])]))],64))),128))],8,Yo)],2))}}),ts=b(es,[["__scopeId","data-v-b9ab8c58"]]),ns={key:0,class:"VPNavScreenMenu"},as=p({__name:"VPNavScreenMenu",setup(e){const{theme:t}=P();return(n,a)=>i(t).nav?(s(),u("nav",ns,[(s(!0),u(x,null,H(i(t).nav,o=>(s(),u(x,{key:JSON.stringify(o)},["link"in o?(s(),_(Go,{key:0,item:o},null,8,["item"])):"component"in o?(s(),_(C(o.component),G({key:1,ref_for:!0},o.props,{"screen-menu":""}),null,16)):(s(),_(ts,{key:2,text:o.text||"",items:o.items},null,8,["text","items"]))],64))),128))])):m("",!0)}}),os=p({__name:"VPNavScreenSocialLinks",setup(e){const{theme:t}=P();return(n,a)=>i(t).socialLinks?(s(),_(_e,{key:0,class:"VPNavScreenSocialLinks",links:i(t).socialLinks},null,8,["links"])):m("",!0)}}),ss={class:"list"},is=p({__name:"VPNavScreenTranslations",setup(e){const{localeLinks:t,currentLang:n}=K({correspondingLink:!0}),a=S(!1);function o(){a.value=!a.value}return(r,l)=>i(t).length&&i(n).label?(s(),u("div",{key:0,class:T(["VPNavScreenTranslations",{open:a.value}])},[v("button",{class:"title",onClick:o},[l[0]||(l[0]=v("span",{class:"vpi-languages icon lang"},null,-1)),j(" "+N(i(n).label)+" ",1),l[1]||(l[1]=v("span",{class:"vpi-chevron-down icon chevron"},null,-1))]),v("ul",ss,[(s(!0),u(x,null,H(i(t),f=>(s(),u("li",{key:f.link,class:"item"},[k(E,{class:"link",href:f.link},{default:h(()=>[j(N(f.text),1)]),_:2},1032,["href"])]))),128))])],2)):m("",!0)}}),rs=b(is,[["__scopeId","data-v-858fe1a4"]]),ls={class:"container"},cs=p({__name:"VPNavScreen",props:{open:{type:Boolean}},setup(e){const t=S(null),n=Ve(ee?document.body:null);return(a,o)=>(s(),_(ce,{name:"fade",onEnter:o[0]||(o[0]=r=>n.value=!0),onAfterLeave:o[1]||(o[1]=r=>n.value=!1)},{default:h(()=>[e.open?(s(),u("div",{key:0,class:"VPNavScreen",ref_key:"screen",ref:t,id:"VPNavScreen"},[v("div",ls,[c(a.$slots,"nav-screen-content-before",{},void 0,!0),k(as,{class:"menu"}),k(rs,{class:"translations"}),k(Fo,{class:"appearance"}),k(os,{class:"social-links"}),c(a.$slots,"nav-screen-content-after",{},void 0,!0)])],512)):m("",!0)]),_:3}))}}),us=b(cs,[["__scopeId","data-v-f2779853"]]),ds={key:0,class:"VPNav"},vs=p({__name:"VPNav",setup(e){const{isScreenOpen:t,closeScreen:n,toggleScreen:a}=ia(),{frontmatter:o}=P(),r=g(()=>o.value.navbar!==!1);return Se("close-screen",n),Y(()=>{ee&&document.documentElement.classList.toggle("hide-nav",!r.value)}),(l,f)=>r.value?(s(),u("header",ds,[k(Ao,{"is-screen-open":i(t),onToggleScreen:i(a)},{"nav-bar-title-before":h(()=>[c(l.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":h(()=>[c(l.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":h(()=>[c(l.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":h(()=>[c(l.$slots,"nav-bar-content-after",{},void 0,!0)]),_:3},8,["is-screen-open","onToggleScreen"]),k(us,{open:i(t)},{"nav-screen-content-before":h(()=>[c(l.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":h(()=>[c(l.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3},8,["open"])])):m("",!0)}}),fs=b(vs,[["__scopeId","data-v-ae24b3ad"]]),hs=["role","tabindex"],ms={key:1,class:"items"},ps=p({__name:"VPSidebarItem",props:{item:{},depth:{}},setup(e){const t=e,{collapsed:n,collapsible:a,isLink:o,isActiveLink:r,hasActiveLink:l,hasChildren:f,toggle:d}=ct(g(()=>t.item)),y=g(()=>f.value?"section":"div"),L=g(()=>o.value?"a":"div"),$=g(()=>f.value?t.depth+2===7?"p":`h${t.depth+2}`:"p"),V=g(()=>o.value?void 0:"button"),M=g(()=>[[`level-${t.depth}`],{collapsible:a.value},{collapsed:n.value},{"is-link":o.value},{"is-active":r.value},{"has-active":l.value}]);function I(A){"key"in A&&A.key!=="Enter"||!t.item.link&&d()}function w(){t.item.link&&d()}return(A,B)=>{const O=W("VPSidebarItem",!0);return s(),_(C(y.value),{class:T(["VPSidebarItem",M.value])},{default:h(()=>[e.item.text?(s(),u("div",G({key:0,class:"item",role:V.value},Ge(e.item.items?{click:I,keydown:I}:{},!0),{tabindex:e.item.items&&0}),[B[1]||(B[1]=v("div",{class:"indicator"},null,-1)),e.item.link?(s(),_(E,{key:0,tag:L.value,class:"link",href:e.item.link,rel:e.item.rel,target:e.item.target},{default:h(()=>[(s(),_(C($.value),{class:"text",innerHTML:e.item.text},null,8,["innerHTML"]))]),_:1},8,["tag","href","rel","target"])):(s(),_(C($.value),{key:1,class:"text",innerHTML:e.item.text},null,8,["innerHTML"])),e.item.collapsed!=null&&e.item.items&&e.item.items.length?(s(),u("div",{key:2,class:"caret",role:"button","aria-label":"toggle section",onClick:w,onKeydown:Ue(w,["enter"]),tabindex:"0"},[...B[0]||(B[0]=[v("span",{class:"vpi-chevron-right caret-icon"},null,-1)])],32)):m("",!0)],16,hs)):m("",!0),e.item.items&&e.item.items.length?(s(),u("div",ms,[e.depth<5?(s(!0),u(x,{key:0},H(e.item.items,q=>(s(),_(O,{key:q.text,item:q,depth:e.depth+1},null,8,["item","depth"]))),128)):m("",!0)])):m("",!0)]),_:1},8,["class"])}}}),ks=b(ps,[["__scopeId","data-v-b3fd67f8"]]),_s=p({__name:"VPSidebarGroup",props:{items:{}},setup(e){const t=S(!0);let n=null;return U(()=>{n=setTimeout(()=>{n=null,t.value=!1},300)}),je(()=>{n!=null&&(clearTimeout(n),n=null)}),(a,o)=>(s(!0),u(x,null,H(e.items,r=>(s(),u("div",{key:r.text,class:T(["group",{"no-transition":t.value}])},[k(ks,{item:r,depth:0},null,8,["item"])],2))),128))}}),bs=b(_s,[["__scopeId","data-v-c40bc020"]]),gs={class:"nav",id:"VPSidebarNav","aria-labelledby":"sidebar-aria-label",tabindex:"-1"},$s=p({__name:"VPSidebar",props:{open:{type:Boolean}},setup(e){const{sidebarGroups:t,hasSidebar:n}=D(),a=e,o=S(null),r=Ve(ee?document.body:null);F([a,o],()=>{var f;a.open?(r.value=!0,(f=o.value)==null||f.focus()):r.value=!1},{immediate:!0,flush:"post"});const l=S(0);return F(t,()=>{l.value+=1},{deep:!0}),(f,d)=>i(n)?(s(),u("aside",{key:0,class:T(["VPSidebar",{open:e.open}]),ref_key:"navEl",ref:o,onClick:d[0]||(d[0]=ze(()=>{},["stop"]))},[d[2]||(d[2]=v("div",{class:"curtain"},null,-1)),v("nav",gs,[d[1]||(d[1]=v("span",{class:"visually-hidden",id:"sidebar-aria-label"}," Sidebar Navigation ",-1)),c(f.$slots,"sidebar-nav-before",{},void 0,!0),(s(),_(bs,{items:i(t),key:l.value},null,8,["items"])),c(f.$slots,"sidebar-nav-after",{},void 0,!0)])],2)):m("",!0)}}),ys=b($s,[["__scopeId","data-v-319d5ca6"]]),Ps=p({__name:"VPSkipLink",setup(e){const{theme:t}=P(),n=X(),a=S();F(()=>n.path,()=>a.value.focus());function o({target:r}){const l=document.getElementById(decodeURIComponent(r.hash).slice(1));if(l){const f=()=>{l.removeAttribute("tabindex"),l.removeEventListener("blur",f)};l.setAttribute("tabindex","-1"),l.addEventListener("blur",f),l.focus(),window.scrollTo(0,0)}}return(r,l)=>(s(),u(x,null,[v("span",{ref_key:"backToTop",ref:a,tabindex:"-1"},null,512),v("a",{href:"#VPContent",class:"VPSkipLink visually-hidden",onClick:o},N(i(t).skipToContentLabel||"Skip to content"),1)],64))}}),Ls=b(Ps,[["__scopeId","data-v-0b0ada53"]]),Vs=p({__name:"Layout",setup(e){const{isOpen:t,open:n,close:a}=D(),o=X();F(()=>o.path,a),lt(t,a);const{frontmatter:r}=P(),l=We(),f=g(()=>!!l["home-hero-image"]);return Se("hero-image-slot-exists",f),(d,y)=>{const L=W("Content");return i(r).layout!==!1?(s(),u("div",{key:0,class:T(["Layout",i(r).pageClass])},[c(d.$slots,"layout-top",{},void 0,!0),k(Ls),k(Je,{class:"backdrop",show:i(t),onClick:i(a)},null,8,["show","onClick"]),k(fs,null,{"nav-bar-title-before":h(()=>[c(d.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":h(()=>[c(d.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":h(()=>[c(d.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":h(()=>[c(d.$slots,"nav-bar-content-after",{},void 0,!0)]),"nav-screen-content-before":h(()=>[c(d.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":h(()=>[c(d.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3}),k(sa,{open:i(t),onOpenMenu:i(n)},null,8,["open","onOpenMenu"]),k(ys,{open:i(t)},{"sidebar-nav-before":h(()=>[c(d.$slots,"sidebar-nav-before",{},void 0,!0)]),"sidebar-nav-after":h(()=>[c(d.$slots,"sidebar-nav-after",{},void 0,!0)]),_:3},8,["open"]),k(jn,null,{"page-top":h(()=>[c(d.$slots,"page-top",{},void 0,!0)]),"page-bottom":h(()=>[c(d.$slots,"page-bottom",{},void 0,!0)]),"not-found":h(()=>[c(d.$slots,"not-found",{},void 0,!0)]),"home-hero-before":h(()=>[c(d.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info-before":h(()=>[c(d.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":h(()=>[c(d.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":h(()=>[c(d.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":h(()=>[c(d.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":h(()=>[c(d.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":h(()=>[c(d.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":h(()=>[c(d.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":h(()=>[c(d.$slots,"home-features-after",{},void 0,!0)]),"doc-footer-before":h(()=>[c(d.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":h(()=>[c(d.$slots,"doc-before",{},void 0,!0)]),"doc-after":h(()=>[c(d.$slots,"doc-after",{},void 0,!0)]),"doc-top":h(()=>[c(d.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":h(()=>[c(d.$slots,"doc-bottom",{},void 0,!0)]),"aside-top":h(()=>[c(d.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":h(()=>[c(d.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":h(()=>[c(d.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":h(()=>[c(d.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":h(()=>[c(d.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":h(()=>[c(d.$slots,"aside-ads-after",{},void 0,!0)]),_:3}),k(Rn),c(d.$slots,"layout-bottom",{},void 0,!0)],2)):(s(),_(L,{key:1}))}}}),Ss=b(Vs,[["__scopeId","data-v-5d98c3a5"]]),Ns={Layout:Ss,enhanceApp:({app:e})=>{e.component("Badge",qe)}};export{Ns as t}; diff --git a/docs/.vitepress/dist/assets/guide_getting-started.md.BeQpK3vd.js b/docs/.vitepress/dist/assets/guide_getting-started.md.BeQpK3vd.js new file mode 100644 index 0000000..95e5ed5 --- /dev/null +++ b/docs/.vitepress/dist/assets/guide_getting-started.md.BeQpK3vd.js @@ -0,0 +1,172 @@ +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/.vitepress/dist/assets/guide_getting-started.md.BeQpK3vd.lean.js b/docs/.vitepress/dist/assets/guide_getting-started.md.BeQpK3vd.lean.js new file mode 100644 index 0000000..7e0ddbd --- /dev/null +++ b/docs/.vitepress/dist/assets/guide_getting-started.md.BeQpK3vd.lean.js @@ -0,0 +1 @@ +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/.vitepress/dist/assets/guide_why.md.DXchYMN-.js b/docs/.vitepress/dist/assets/guide_why.md.DXchYMN-.js new file mode 100644 index 0000000..9f92563 --- /dev/null +++ b/docs/.vitepress/dist/assets/guide_why.md.DXchYMN-.js @@ -0,0 +1,23 @@ +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/.vitepress/dist/assets/guide_why.md.DXchYMN-.lean.js b/docs/.vitepress/dist/assets/guide_why.md.DXchYMN-.lean.js new file mode 100644 index 0000000..763900c --- /dev/null +++ b/docs/.vitepress/dist/assets/guide_why.md.DXchYMN-.lean.js @@ -0,0 +1 @@ +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/.vitepress/dist/assets/index.md.dTY448ug.js b/docs/.vitepress/dist/assets/index.md.dTY448ug.js new file mode 100644 index 0000000..be55934 --- /dev/null +++ b/docs/.vitepress/dist/assets/index.md.dTY448ug.js @@ -0,0 +1 @@ +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"},{"theme":"alt","text":"Why SigPro?","link":"/guide/why"}]},"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,m){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/.vitepress/dist/assets/index.md.dTY448ug.lean.js b/docs/.vitepress/dist/assets/index.md.dTY448ug.lean.js new file mode 100644 index 0000000..d8d8d2e --- /dev/null +++ b/docs/.vitepress/dist/assets/index.md.dTY448ug.lean.js @@ -0,0 +1 @@ +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"},{"theme":"alt","text":"Why SigPro?","link":"/guide/why"}]},"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,m){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/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 b/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 new file mode 100644 index 0000000..b6b603d Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 b/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 new file mode 100644 index 0000000..def40a4 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 b/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 new file mode 100644 index 0000000..e070c3d Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 b/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 new file mode 100644 index 0000000..a3c16ca Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 b/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 new file mode 100644 index 0000000..2210a89 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 b/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 new file mode 100644 index 0000000..790d62d Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 b/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 new file mode 100644 index 0000000..1eec077 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 b/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 new file mode 100644 index 0000000..2cfe615 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 b/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 new file mode 100644 index 0000000..e3886dd Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 b/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 new file mode 100644 index 0000000..36d6748 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 b/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 new file mode 100644 index 0000000..2bed1e8 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 b/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 new file mode 100644 index 0000000..9a8d1e2 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 b/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 new file mode 100644 index 0000000..07d3c53 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 b/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 new file mode 100644 index 0000000..57bdc22 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 differ diff --git a/docs/.vitepress/dist/assets/style.BRh89-fO.css b/docs/.vitepress/dist/assets/style.BRh89-fO.css new file mode 100644 index 0000000..636367e --- /dev/null +++ b/docs/.vitepress/dist/assets/style.BRh89-fO.css @@ -0,0 +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-c79a1216]{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-c79a1216],.VPBackdrop.fade-leave-to[data-v-c79a1216]{opacity:0}.VPBackdrop.fade-leave-active[data-v-c79a1216]{transition-duration:.25s}@media (min-width: 1280px){.VPBackdrop[data-v-c79a1216]{display:none}}.NotFound[data-v-d6be1790]{padding:64px 24px 96px;text-align:center}@media (min-width: 768px){.NotFound[data-v-d6be1790]{padding:96px 32px 168px}}.code[data-v-d6be1790]{line-height:64px;font-size:64px;font-weight:600}.title[data-v-d6be1790]{padding-top:12px;letter-spacing:2px;line-height:20px;font-size:20px;font-weight:700}.divider[data-v-d6be1790]{margin:24px auto 18px;width:64px;height:1px;background-color:var(--vp-c-divider)}.quote[data-v-d6be1790]{margin:0 auto;max-width:256px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.action[data-v-d6be1790]{padding-top:20px}.link[data-v-d6be1790]{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-d6be1790]:hover{border-color:var(--vp-c-brand-2);color:var(--vp-c-brand-2)}.root[data-v-b933a997]{position:relative;z-index:1}.nested[data-v-b933a997]{padding-right:16px;padding-left:16px}.outline-link[data-v-b933a997]{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-b933a997]:hover,.outline-link.active[data-v-b933a997]{color:var(--vp-c-text-1);transition:color .25s}.outline-link.nested[data-v-b933a997]{padding-left:13px}.VPDocAsideOutline[data-v-a5bbad30]{display:none}.VPDocAsideOutline.has-outline[data-v-a5bbad30]{display:block}.content[data-v-a5bbad30]{position:relative;border-left:1px solid var(--vp-c-divider);padding-left:16px;font-size:13px;font-weight:500}.outline-marker[data-v-a5bbad30]{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-a5bbad30]{line-height:32px;font-size:14px;font-weight:600}.VPDocAside[data-v-3f215769]{display:flex;flex-direction:column;flex-grow:1}.spacer[data-v-3f215769]{flex-grow:1}.VPDocAside[data-v-3f215769] .spacer+.VPDocAsideSponsors,.VPDocAside[data-v-3f215769] .spacer+.VPDocAsideCarbonAds{margin-top:24px}.VPDocAside[data-v-3f215769] .VPDocAsideSponsors+.VPDocAsideCarbonAds{margin-top:16px}.VPLastUpdated[data-v-e98dd255]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 640px){.VPLastUpdated[data-v-e98dd255]{line-height:32px;font-size:14px;font-weight:500}}.VPDocFooter[data-v-e257564d]{margin-top:64px}.edit-info[data-v-e257564d]{padding-bottom:18px}@media (min-width: 640px){.edit-info[data-v-e257564d]{display:flex;justify-content:space-between;align-items:center;padding-bottom:14px}}.edit-link-button[data-v-e257564d]{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-e257564d]:hover{color:var(--vp-c-brand-2)}.edit-link-icon[data-v-e257564d]{margin-right:8px}.prev-next[data-v-e257564d]{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-e257564d]{grid-template-columns:repeat(2,1fr);grid-column-gap:16px}}.pager-link[data-v-e257564d]{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-e257564d]:hover{border-color:var(--vp-c-brand-1)}.pager-link.next[data-v-e257564d]{margin-left:auto;text-align:right}.desc[data-v-e257564d]{display:block;line-height:20px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.title[data-v-e257564d]{display:block;line-height:20px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.VPDoc[data-v-39a288b8]{padding:32px 24px 96px;width:100%}@media (min-width: 768px){.VPDoc[data-v-39a288b8]{padding:48px 32px 128px}}@media (min-width: 960px){.VPDoc[data-v-39a288b8]{padding:48px 32px 0}.VPDoc:not(.has-sidebar) .container[data-v-39a288b8]{display:flex;justify-content:center;max-width:992px}.VPDoc:not(.has-sidebar) .content[data-v-39a288b8]{max-width:752px}}@media (min-width: 1280px){.VPDoc .container[data-v-39a288b8]{display:flex;justify-content:center}.VPDoc .aside[data-v-39a288b8]{display:block}}@media (min-width: 1440px){.VPDoc:not(.has-sidebar) .content[data-v-39a288b8]{max-width:784px}.VPDoc:not(.has-sidebar) .container[data-v-39a288b8]{max-width:1104px}}.container[data-v-39a288b8]{margin:0 auto;width:100%}.aside[data-v-39a288b8]{position:relative;display:none;order:2;flex-grow:1;padding-left:32px;width:100%;max-width:256px}.left-aside[data-v-39a288b8]{order:1;padding-left:unset;padding-right:32px}.aside-container[data-v-39a288b8]{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-39a288b8]::-webkit-scrollbar{display:none}.aside-curtain[data-v-39a288b8]{position:fixed;bottom:0;z-index:10;width:224px;height:32px;background:linear-gradient(transparent,var(--vp-c-bg) 70%)}.aside-content[data-v-39a288b8]{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-39a288b8]{position:relative;margin:0 auto;width:100%}@media (min-width: 960px){.content[data-v-39a288b8]{padding:0 32px 128px}}@media (min-width: 1280px){.content[data-v-39a288b8]{order:1;margin:0;min-width:640px}}.content-container[data-v-39a288b8]{margin:0 auto}.VPDoc.has-aside .content-container[data-v-39a288b8]{max-width:688px}.VPButton[data-v-fa7799d5]{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-fa7799d5]:active{transition:color .1s,border-color .1s,background-color .1s}.VPButton.medium[data-v-fa7799d5]{border-radius:20px;padding:0 20px;line-height:38px;font-size:14px}.VPButton.big[data-v-fa7799d5]{border-radius:24px;padding:0 24px;line-height:46px;font-size:16px}.VPButton.brand[data-v-fa7799d5]{border-color:var(--vp-button-brand-border);color:var(--vp-button-brand-text);background-color:var(--vp-button-brand-bg)}.VPButton.brand[data-v-fa7799d5]: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-fa7799d5]: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-fa7799d5]{border-color:var(--vp-button-alt-border);color:var(--vp-button-alt-text);background-color:var(--vp-button-alt-bg)}.VPButton.alt[data-v-fa7799d5]: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-fa7799d5]: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-fa7799d5]{border-color:var(--vp-button-sponsor-border);color:var(--vp-button-sponsor-text);background-color:var(--vp-button-sponsor-bg)}.VPButton.sponsor[data-v-fa7799d5]: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-fa7799d5]: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-8426fc1a]{display:none}.dark .VPImage.light[data-v-8426fc1a]{display:none}.VPHero[data-v-4f9c455b]{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-4f9c455b]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px}}@media (min-width: 960px){.VPHero[data-v-4f9c455b]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px}}.container[data-v-4f9c455b]{display:flex;flex-direction:column;margin:0 auto;max-width:1152px}@media (min-width: 960px){.container[data-v-4f9c455b]{flex-direction:row}}.main[data-v-4f9c455b]{position:relative;z-index:10;order:2;flex-grow:1;flex-shrink:0}.VPHero.has-image .container[data-v-4f9c455b]{text-align:center}@media (min-width: 960px){.VPHero.has-image .container[data-v-4f9c455b]{text-align:left}}@media (min-width: 960px){.main[data-v-4f9c455b]{order:1;width:calc((100% / 3) * 2)}.VPHero.has-image .main[data-v-4f9c455b]{max-width:592px}}.heading[data-v-4f9c455b]{display:flex;flex-direction:column}.name[data-v-4f9c455b],.text[data-v-4f9c455b]{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-4f9c455b],.VPHero.has-image .text[data-v-4f9c455b]{margin:0 auto}.name[data-v-4f9c455b]{color:var(--vp-home-hero-name-color)}.clip[data-v-4f9c455b]{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-4f9c455b],.text[data-v-4f9c455b]{max-width:576px;line-height:56px;font-size:48px}}@media (min-width: 960px){.name[data-v-4f9c455b],.text[data-v-4f9c455b]{line-height:64px;font-size:56px}.VPHero.has-image .name[data-v-4f9c455b],.VPHero.has-image .text[data-v-4f9c455b]{margin:0}}.tagline[data-v-4f9c455b]{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-4f9c455b]{margin:0 auto}@media (min-width: 640px){.tagline[data-v-4f9c455b]{padding-top:12px;max-width:576px;line-height:32px;font-size:20px}}@media (min-width: 960px){.tagline[data-v-4f9c455b]{line-height:36px;font-size:24px}.VPHero.has-image .tagline[data-v-4f9c455b]{margin:0}}.actions[data-v-4f9c455b]{display:flex;flex-wrap:wrap;margin:-6px;padding-top:24px}.VPHero.has-image .actions[data-v-4f9c455b]{justify-content:center}@media (min-width: 640px){.actions[data-v-4f9c455b]{padding-top:32px}}@media (min-width: 960px){.VPHero.has-image .actions[data-v-4f9c455b]{justify-content:flex-start}}.action[data-v-4f9c455b]{flex-shrink:0;padding:6px}.image[data-v-4f9c455b]{order:1;margin:-76px -24px -48px}@media (min-width: 640px){.image[data-v-4f9c455b]{margin:-108px -24px -48px}}@media (min-width: 960px){.image[data-v-4f9c455b]{flex-grow:1;order:2;margin:0;min-height:100%}}.image-container[data-v-4f9c455b]{position:relative;margin:0 auto;width:320px;height:320px}@media (min-width: 640px){.image-container[data-v-4f9c455b]{width:392px;height:392px}}@media (min-width: 960px){.image-container[data-v-4f9c455b]{display:flex;justify-content:center;align-items:center;width:100%;height:100%;transform:translate(-32px,-32px)}}.image-bg[data-v-4f9c455b]{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-4f9c455b]{width:256px;height:256px}}@media (min-width: 960px){.image-bg[data-v-4f9c455b]{width:320px;height:320px}}[data-v-4f9c455b] .image-src{position:absolute;top:50%;left:50%;max-width:192px;max-height:192px;transform:translate(-50%,-50%)}@media (min-width: 640px){[data-v-4f9c455b] .image-src{max-width:256px;max-height:256px}}@media (min-width: 960px){[data-v-4f9c455b] .image-src{max-width:320px;max-height:320px}}.VPFeature[data-v-a3976bdc]{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-a3976bdc]:hover{border-color:var(--vp-c-brand-1)}.box[data-v-a3976bdc]{display:flex;flex-direction:column;padding:24px;height:100%}.box[data-v-a3976bdc]>.VPImage{margin-bottom:20px}.icon[data-v-a3976bdc]{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-a3976bdc]{line-height:24px;font-size:16px;font-weight:600}.details[data-v-a3976bdc]{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-a3976bdc]{padding-top:8px}.link-text-value[data-v-a3976bdc]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.link-text-icon[data-v-a3976bdc]{margin-left:6px}.VPFeatures[data-v-a6181336]{position:relative;padding:0 24px}@media (min-width: 640px){.VPFeatures[data-v-a6181336]{padding:0 48px}}@media (min-width: 960px){.VPFeatures[data-v-a6181336]{padding:0 64px}}.container[data-v-a6181336]{margin:0 auto;max-width:1152px}.items[data-v-a6181336]{display:flex;flex-wrap:wrap;margin:-8px}.item[data-v-a6181336]{padding:8px;width:100%}@media (min-width: 640px){.item.grid-2[data-v-a6181336],.item.grid-4[data-v-a6181336],.item.grid-6[data-v-a6181336]{width:50%}}@media (min-width: 768px){.item.grid-2[data-v-a6181336],.item.grid-4[data-v-a6181336]{width:50%}.item.grid-3[data-v-a6181336],.item.grid-6[data-v-a6181336]{width:calc(100% / 3)}}@media (min-width: 960px){.item.grid-4[data-v-a6181336]{width:25%}}.container[data-v-8e2d4988]{margin:auto;width:100%;max-width:1280px;padding:0 24px}@media (min-width: 640px){.container[data-v-8e2d4988]{padding:0 48px}}@media (min-width: 960px){.container[data-v-8e2d4988]{width:100%;padding:0 64px}}.vp-doc[data-v-8e2d4988] .VPHomeSponsors,.vp-doc[data-v-8e2d4988] .VPTeamPage{margin-left:var(--vp-offset, calc(50% - 50vw) );margin-right:var(--vp-offset, calc(50% - 50vw) )}.vp-doc[data-v-8e2d4988] .VPHomeSponsors h2{border-top:none;letter-spacing:normal}.vp-doc[data-v-8e2d4988] .VPHomeSponsors a,.vp-doc[data-v-8e2d4988] .VPTeamPage a{text-decoration:none}.VPHome[data-v-8b561e3d]{margin-bottom:96px}@media (min-width: 768px){.VPHome[data-v-8b561e3d]{margin-bottom:128px}}.VPContent[data-v-1428d186]{flex-grow:1;flex-shrink:0;margin:var(--vp-layout-top-height, 0px) auto 0;width:100%}.VPContent.is-home[data-v-1428d186]{width:100%;max-width:100%}.VPContent.has-sidebar[data-v-1428d186]{margin:0}@media (min-width: 960px){.VPContent[data-v-1428d186]{padding-top:var(--vp-nav-height)}.VPContent.has-sidebar[data-v-1428d186]{margin:var(--vp-layout-top-height, 0px) 0 0;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPContent.has-sidebar[data-v-1428d186]{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-e315a0ad]{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-e315a0ad]{display:none}.VPFooter[data-v-e315a0ad] a{text-decoration-line:underline;text-underline-offset:2px;transition:color .25s}.VPFooter[data-v-e315a0ad] a:hover{color:var(--vp-c-text-1)}@media (min-width: 768px){.VPFooter[data-v-e315a0ad]{padding:32px}}.container[data-v-e315a0ad]{margin:0 auto;max-width:var(--vp-layout-max-width);text-align:center}.message[data-v-e315a0ad],.copyright[data-v-e315a0ad]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.VPLocalNavOutlineDropdown[data-v-8a42e2b4]{padding:12px 20px 11px}@media (min-width: 960px){.VPLocalNavOutlineDropdown[data-v-8a42e2b4]{padding:12px 36px 11px}}.VPLocalNavOutlineDropdown button[data-v-8a42e2b4]{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-8a42e2b4]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPLocalNavOutlineDropdown button.open[data-v-8a42e2b4]{color:var(--vp-c-text-1)}.icon[data-v-8a42e2b4]{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-8a42e2b4]{font-size:14px}.icon[data-v-8a42e2b4]{font-size:16px}}.open>.icon[data-v-8a42e2b4]{transform:rotate(90deg)}.items[data-v-8a42e2b4]{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-8a42e2b4]{right:auto;left:calc(var(--vp-sidebar-width) + 32px);width:320px}}.header[data-v-8a42e2b4]{background-color:var(--vp-c-bg-soft)}.top-link[data-v-8a42e2b4]{display:block;padding:0 16px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.outline[data-v-8a42e2b4]{padding:8px 0;background-color:var(--vp-c-bg-soft)}.flyout-enter-active[data-v-8a42e2b4]{transition:all .2s ease-out}.flyout-leave-active[data-v-8a42e2b4]{transition:all .15s ease-in}.flyout-enter-from[data-v-8a42e2b4],.flyout-leave-to[data-v-8a42e2b4]{opacity:0;transform:translateY(-16px)}.VPLocalNav[data-v-a6f0e41e]{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-a6f0e41e]{position:fixed}@media (min-width: 960px){.VPLocalNav[data-v-a6f0e41e]{top:var(--vp-nav-height)}.VPLocalNav.has-sidebar[data-v-a6f0e41e]{padding-left:var(--vp-sidebar-width)}.VPLocalNav.empty[data-v-a6f0e41e]{display:none}}@media (min-width: 1280px){.VPLocalNav[data-v-a6f0e41e]{display:none}}@media (min-width: 1440px){.VPLocalNav.has-sidebar[data-v-a6f0e41e]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.container[data-v-a6f0e41e]{display:flex;justify-content:space-between;align-items:center}.menu[data-v-a6f0e41e]{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-a6f0e41e]:hover{color:var(--vp-c-text-1);transition:color .25s}@media (min-width: 768px){.menu[data-v-a6f0e41e]{padding:0 32px}}@media (min-width: 960px){.menu[data-v-a6f0e41e]{display:none}}.menu-icon[data-v-a6f0e41e]{margin-right:8px;font-size:14px}.VPOutlineDropdown[data-v-a6f0e41e]{padding:12px 24px 11px}@media (min-width: 768px){.VPOutlineDropdown[data-v-a6f0e41e]{padding:12px 32px 11px}}.VPSwitch[data-v-1d5665e3]{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-1d5665e3]:hover{border-color:var(--vp-c-brand-1)}.check[data-v-1d5665e3]{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-1d5665e3]{position:relative;display:block;width:18px;height:18px;border-radius:50%;overflow:hidden}.icon[data-v-1d5665e3] [class^=vpi-]{position:absolute;top:3px;left:3px;width:12px;height:12px;color:var(--vp-c-text-2)}.dark .icon[data-v-1d5665e3] [class^=vpi-]{color:var(--vp-c-text-1);transition:opacity .25s!important}.sun[data-v-5337faa4]{opacity:1}.moon[data-v-5337faa4],.dark .sun[data-v-5337faa4]{opacity:0}.dark .moon[data-v-5337faa4]{opacity:1}.dark .VPSwitchAppearance[data-v-5337faa4] .check{transform:translate(18px)}.VPNavBarAppearance[data-v-6c893767]{display:none}@media (min-width: 1280px){.VPNavBarAppearance[data-v-6c893767]{display:flex;align-items:center}}.VPMenuGroup+.VPMenuLink[data-v-35975db6]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.link[data-v-35975db6]{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-35975db6]:hover{color:var(--vp-c-brand-1);background-color:var(--vp-c-default-soft)}.link.active[data-v-35975db6]{color:var(--vp-c-brand-1)}.VPMenuGroup[data-v-69e747b5]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.VPMenuGroup[data-v-69e747b5]:first-child{margin-top:0;border-top:0;padding-top:0}.VPMenuGroup+.VPMenuGroup[data-v-69e747b5]{margin-top:12px;border-top:1px solid var(--vp-c-divider)}.title[data-v-69e747b5]{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-b98bc113]{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-b98bc113] .group{margin:0 -12px;padding:0 12px 12px}.VPMenu[data-v-b98bc113] .group+.group{border-top:1px solid var(--vp-c-divider);padding:11px 12px 12px}.VPMenu[data-v-b98bc113] .group:last-child{padding-bottom:0}.VPMenu[data-v-b98bc113] .group+.item{border-top:1px solid var(--vp-c-divider);padding:11px 16px 0}.VPMenu[data-v-b98bc113] .item{padding:0 16px;white-space:nowrap}.VPMenu[data-v-b98bc113] .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-b98bc113] .action{padding-left:24px}.VPFlyout[data-v-cf11d7a2]{position:relative}.VPFlyout[data-v-cf11d7a2]:hover{color:var(--vp-c-brand-1);transition:color .25s}.VPFlyout:hover .text[data-v-cf11d7a2]{color:var(--vp-c-text-2)}.VPFlyout:hover .icon[data-v-cf11d7a2]{fill:var(--vp-c-text-2)}.VPFlyout.active .text[data-v-cf11d7a2]{color:var(--vp-c-brand-1)}.VPFlyout.active:hover .text[data-v-cf11d7a2]{color:var(--vp-c-brand-2)}.button[aria-expanded=false]+.menu[data-v-cf11d7a2]{opacity:0;visibility:hidden;transform:translateY(0)}.VPFlyout:hover .menu[data-v-cf11d7a2],.button[aria-expanded=true]+.menu[data-v-cf11d7a2]{opacity:1;visibility:visible;transform:translateY(0)}.button[data-v-cf11d7a2]{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-cf11d7a2]{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-cf11d7a2]{margin-right:0;font-size:16px}.text-icon[data-v-cf11d7a2]{margin-left:4px;font-size:14px}.icon[data-v-cf11d7a2]{font-size:20px;transition:fill .25s}.menu[data-v-cf11d7a2]{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-bd121fe5]{display:flex;justify-content:center;align-items:center;width:36px;height:36px;color:var(--vp-c-text-2);transition:color .5s}.VPSocialLink[data-v-bd121fe5]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPSocialLink[data-v-bd121fe5]>svg,.VPSocialLink[data-v-bd121fe5]>[class^=vpi-social-]{width:20px;height:20px;fill:currentColor}.VPSocialLinks[data-v-7bc22406]{display:flex;justify-content:center}.VPNavBarExtra[data-v-bb2aa2f0]{display:none;margin-right:-12px}@media (min-width: 768px){.VPNavBarExtra[data-v-bb2aa2f0]{display:block}}@media (min-width: 1280px){.VPNavBarExtra[data-v-bb2aa2f0]{display:none}}.trans-title[data-v-bb2aa2f0]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.item.appearance[data-v-bb2aa2f0],.item.social-links[data-v-bb2aa2f0]{display:flex;align-items:center;padding:0 12px}.item.appearance[data-v-bb2aa2f0]{min-width:176px}.appearance-action[data-v-bb2aa2f0]{margin-right:-2px}.social-links-list[data-v-bb2aa2f0]{margin:-4px -8px}.VPNavBarHamburger[data-v-e5dd9c1c]{display:flex;justify-content:center;align-items:center;width:48px;height:var(--vp-nav-height)}@media (min-width: 768px){.VPNavBarHamburger[data-v-e5dd9c1c]{display:none}}.container[data-v-e5dd9c1c]{position:relative;width:16px;height:14px;overflow:hidden}.VPNavBarHamburger:hover .top[data-v-e5dd9c1c]{top:0;left:0;transform:translate(4px)}.VPNavBarHamburger:hover .middle[data-v-e5dd9c1c]{top:6px;left:0;transform:translate(0)}.VPNavBarHamburger:hover .bottom[data-v-e5dd9c1c]{top:12px;left:0;transform:translate(8px)}.VPNavBarHamburger.active .top[data-v-e5dd9c1c]{top:6px;transform:translate(0) rotate(225deg)}.VPNavBarHamburger.active .middle[data-v-e5dd9c1c]{top:6px;transform:translate(16px)}.VPNavBarHamburger.active .bottom[data-v-e5dd9c1c]{top:6px;transform:translate(0) rotate(135deg)}.VPNavBarHamburger.active:hover .top[data-v-e5dd9c1c],.VPNavBarHamburger.active:hover .middle[data-v-e5dd9c1c],.VPNavBarHamburger.active:hover .bottom[data-v-e5dd9c1c]{background-color:var(--vp-c-text-2);transition:top .25s,background-color .25s,transform .25s}.top[data-v-e5dd9c1c],.middle[data-v-e5dd9c1c],.bottom[data-v-e5dd9c1c]{position:absolute;width:16px;height:2px;background-color:var(--vp-c-text-1);transition:top .25s,background-color .5s,transform .25s}.top[data-v-e5dd9c1c]{top:0;left:0;transform:translate(0)}.middle[data-v-e5dd9c1c]{top:6px;left:0;transform:translate(8px)}.bottom[data-v-e5dd9c1c]{top:12px;left:0;transform:translate(4px)}.VPNavBarMenuLink[data-v-e56f3d57]{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-e56f3d57],.VPNavBarMenuLink[data-v-e56f3d57]:hover{color:var(--vp-c-brand-1)}.VPNavBarMenu[data-v-dc692963]{display:none}@media (min-width: 768px){.VPNavBarMenu[data-v-dc692963]{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-0394ad82]{display:none}@media (min-width: 1280px){.VPNavBarSocialLinks[data-v-0394ad82]{display:flex;align-items:center}}.title[data-v-1168a8e4]{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-1168a8e4]{flex-shrink:0}.VPNavBarTitle.has-sidebar .title[data-v-1168a8e4]{border-bottom-color:var(--vp-c-divider)}}[data-v-1168a8e4] .logo{margin-right:8px;height:var(--vp-nav-logo-height)}.VPNavBarTranslations[data-v-88af2de4]{display:none}@media (min-width: 1280px){.VPNavBarTranslations[data-v-88af2de4]{display:flex;align-items:center}}.title[data-v-88af2de4]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.VPNavBar[data-v-6aa21345]{position:relative;height:var(--vp-nav-height);pointer-events:none;white-space:nowrap;transition:background-color .25s}.VPNavBar.screen-open[data-v-6aa21345]{transition:none;background-color:var(--vp-nav-bg-color);border-bottom:1px solid var(--vp-c-divider)}.VPNavBar[data-v-6aa21345]:not(.home){background-color:var(--vp-nav-bg-color)}@media (min-width: 960px){.VPNavBar[data-v-6aa21345]:not(.home){background-color:transparent}.VPNavBar[data-v-6aa21345]:not(.has-sidebar):not(.home.top){background-color:var(--vp-nav-bg-color)}}.wrapper[data-v-6aa21345]{padding:0 8px 0 24px}@media (min-width: 768px){.wrapper[data-v-6aa21345]{padding:0 32px}}@media (min-width: 960px){.VPNavBar.has-sidebar .wrapper[data-v-6aa21345]{padding:0}}.container[data-v-6aa21345]{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-6aa21345],.container>.content[data-v-6aa21345]{pointer-events:none}.container[data-v-6aa21345] *{pointer-events:auto}@media (min-width: 960px){.VPNavBar.has-sidebar .container[data-v-6aa21345]{max-width:100%}}.title[data-v-6aa21345]{flex-shrink:0;height:calc(var(--vp-nav-height) - 1px);transition:background-color .5s}@media (min-width: 960px){.VPNavBar.has-sidebar .title[data-v-6aa21345]{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-6aa21345]{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-6aa21345]{flex-grow:1}@media (min-width: 960px){.VPNavBar.has-sidebar .content[data-v-6aa21345]{position:relative;z-index:1;padding-right:32px;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .content[data-v-6aa21345]{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-6aa21345]{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-6aa21345]{position:relative;background-color:var(--vp-nav-bg-color)}.VPNavBar:not(.has-sidebar):not(.home.top) .content-body[data-v-6aa21345]{background-color:transparent}}@media (max-width: 767px){.content-body[data-v-6aa21345]{column-gap:.5rem}}.menu+.translations[data-v-6aa21345]:before,.menu+.appearance[data-v-6aa21345]:before,.menu+.social-links[data-v-6aa21345]:before,.translations+.appearance[data-v-6aa21345]:before,.appearance+.social-links[data-v-6aa21345]:before{margin-right:8px;margin-left:8px;width:1px;height:24px;background-color:var(--vp-c-divider);content:""}.menu+.appearance[data-v-6aa21345]:before,.translations+.appearance[data-v-6aa21345]:before{margin-right:16px}.appearance+.social-links[data-v-6aa21345]:before{margin-left:16px}.social-links[data-v-6aa21345]{margin-right:-8px}.divider[data-v-6aa21345]{width:100%;height:1px}@media (min-width: 960px){.VPNavBar.has-sidebar .divider[data-v-6aa21345]{padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .divider[data-v-6aa21345]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.divider-line[data-v-6aa21345]{width:100%;height:1px;transition:background-color .5s}.VPNavBar:not(.home) .divider-line[data-v-6aa21345]{background-color:var(--vp-c-gutter)}@media (min-width: 960px){.VPNavBar:not(.home.top) .divider-line[data-v-6aa21345]{background-color:var(--vp-c-gutter)}.VPNavBar:not(.has-sidebar):not(.home.top) .divider[data-v-6aa21345]{background-color:var(--vp-c-gutter)}}.VPNavScreenAppearance[data-v-b44890b2]{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-b44890b2]{line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.VPNavScreenMenuLink[data-v-df37e6dd]{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-df37e6dd]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupLink[data-v-3e9c20e4]{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-3e9c20e4]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupSection[data-v-8133b170]{display:block}.title[data-v-8133b170]{line-height:32px;font-size:13px;font-weight:700;color:var(--vp-c-text-2);transition:color .25s}.VPNavScreenMenuGroup[data-v-b9ab8c58]{border-bottom:1px solid var(--vp-c-divider);height:48px;overflow:hidden;transition:border-color .5s}.VPNavScreenMenuGroup .items[data-v-b9ab8c58]{visibility:hidden}.VPNavScreenMenuGroup.open .items[data-v-b9ab8c58]{visibility:visible}.VPNavScreenMenuGroup.open[data-v-b9ab8c58]{padding-bottom:10px;height:auto}.VPNavScreenMenuGroup.open .button[data-v-b9ab8c58]{padding-bottom:6px;color:var(--vp-c-brand-1)}.VPNavScreenMenuGroup.open .button-icon[data-v-b9ab8c58]{transform:rotate(45deg)}.button[data-v-b9ab8c58]{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-b9ab8c58]:hover{color:var(--vp-c-brand-1)}.button-icon[data-v-b9ab8c58]{transition:transform .25s}.group[data-v-b9ab8c58]:first-child{padding-top:0}.group+.group[data-v-b9ab8c58],.group+.item[data-v-b9ab8c58]{padding-top:4px}.VPNavScreenTranslations[data-v-858fe1a4]{height:24px;overflow:hidden}.VPNavScreenTranslations.open[data-v-858fe1a4]{height:auto}.title[data-v-858fe1a4]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-text-1)}.icon[data-v-858fe1a4]{font-size:16px}.icon.lang[data-v-858fe1a4]{margin-right:8px}.icon.chevron[data-v-858fe1a4]{margin-left:4px}.list[data-v-858fe1a4]{padding:4px 0 0 24px}.link[data-v-858fe1a4]{line-height:32px;font-size:13px;color:var(--vp-c-text-1)}.VPNavScreen[data-v-f2779853]{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-f2779853],.VPNavScreen.fade-leave-active[data-v-f2779853]{transition:opacity .25s}.VPNavScreen.fade-enter-active .container[data-v-f2779853],.VPNavScreen.fade-leave-active .container[data-v-f2779853]{transition:transform .25s ease}.VPNavScreen.fade-enter-from[data-v-f2779853],.VPNavScreen.fade-leave-to[data-v-f2779853]{opacity:0}.VPNavScreen.fade-enter-from .container[data-v-f2779853],.VPNavScreen.fade-leave-to .container[data-v-f2779853]{transform:translateY(-8px)}@media (min-width: 768px){.VPNavScreen[data-v-f2779853]{display:none}}.container[data-v-f2779853]{margin:0 auto;padding:24px 0 96px;max-width:288px}.menu+.translations[data-v-f2779853],.menu+.appearance[data-v-f2779853],.translations+.appearance[data-v-f2779853]{margin-top:24px}.menu+.social-links[data-v-f2779853]{margin-top:16px}.appearance+.social-links[data-v-f2779853]{margin-top:16px}.VPNav[data-v-ae24b3ad]{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-ae24b3ad]{position:fixed}}.VPSidebarItem.level-0[data-v-b3fd67f8]{padding-bottom:24px}.VPSidebarItem.collapsed.level-0[data-v-b3fd67f8]{padding-bottom:10px}.item[data-v-b3fd67f8]{position:relative;display:flex;width:100%}.VPSidebarItem.collapsible>.item[data-v-b3fd67f8]{cursor:pointer}.indicator[data-v-b3fd67f8]{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-b3fd67f8],.VPSidebarItem.level-3.is-active>.item>.indicator[data-v-b3fd67f8],.VPSidebarItem.level-4.is-active>.item>.indicator[data-v-b3fd67f8],.VPSidebarItem.level-5.is-active>.item>.indicator[data-v-b3fd67f8]{background-color:var(--vp-c-brand-1)}.link[data-v-b3fd67f8]{display:flex;align-items:center;flex-grow:1}.text[data-v-b3fd67f8]{flex-grow:1;padding:4px 0;line-height:24px;font-size:14px;transition:color .25s}.VPSidebarItem.level-0 .text[data-v-b3fd67f8]{font-weight:700;color:var(--vp-c-text-1)}.VPSidebarItem.level-1 .text[data-v-b3fd67f8],.VPSidebarItem.level-2 .text[data-v-b3fd67f8],.VPSidebarItem.level-3 .text[data-v-b3fd67f8],.VPSidebarItem.level-4 .text[data-v-b3fd67f8],.VPSidebarItem.level-5 .text[data-v-b3fd67f8]{font-weight:500;color:var(--vp-c-text-2)}.VPSidebarItem.level-0.is-link>.item>.link:hover .text[data-v-b3fd67f8],.VPSidebarItem.level-1.is-link>.item>.link:hover .text[data-v-b3fd67f8],.VPSidebarItem.level-2.is-link>.item>.link:hover .text[data-v-b3fd67f8],.VPSidebarItem.level-3.is-link>.item>.link:hover .text[data-v-b3fd67f8],.VPSidebarItem.level-4.is-link>.item>.link:hover .text[data-v-b3fd67f8],.VPSidebarItem.level-5.is-link>.item>.link:hover .text[data-v-b3fd67f8]{color:var(--vp-c-brand-1)}.VPSidebarItem.level-0.has-active>.item>.text[data-v-b3fd67f8],.VPSidebarItem.level-1.has-active>.item>.text[data-v-b3fd67f8],.VPSidebarItem.level-2.has-active>.item>.text[data-v-b3fd67f8],.VPSidebarItem.level-3.has-active>.item>.text[data-v-b3fd67f8],.VPSidebarItem.level-4.has-active>.item>.text[data-v-b3fd67f8],.VPSidebarItem.level-5.has-active>.item>.text[data-v-b3fd67f8],.VPSidebarItem.level-0.has-active>.item>.link>.text[data-v-b3fd67f8],.VPSidebarItem.level-1.has-active>.item>.link>.text[data-v-b3fd67f8],.VPSidebarItem.level-2.has-active>.item>.link>.text[data-v-b3fd67f8],.VPSidebarItem.level-3.has-active>.item>.link>.text[data-v-b3fd67f8],.VPSidebarItem.level-4.has-active>.item>.link>.text[data-v-b3fd67f8],.VPSidebarItem.level-5.has-active>.item>.link>.text[data-v-b3fd67f8]{color:var(--vp-c-text-1)}.VPSidebarItem.level-0.is-active>.item .link>.text[data-v-b3fd67f8],.VPSidebarItem.level-1.is-active>.item .link>.text[data-v-b3fd67f8],.VPSidebarItem.level-2.is-active>.item .link>.text[data-v-b3fd67f8],.VPSidebarItem.level-3.is-active>.item .link>.text[data-v-b3fd67f8],.VPSidebarItem.level-4.is-active>.item .link>.text[data-v-b3fd67f8],.VPSidebarItem.level-5.is-active>.item .link>.text[data-v-b3fd67f8]{color:var(--vp-c-brand-1)}.caret[data-v-b3fd67f8]{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-b3fd67f8]{color:var(--vp-c-text-2)}.item:hover .caret[data-v-b3fd67f8]:hover{color:var(--vp-c-text-1)}.caret-icon[data-v-b3fd67f8]{font-size:18px;transform:rotate(90deg);transition:transform .25s}.VPSidebarItem.collapsed .caret-icon[data-v-b3fd67f8]{transform:rotate(0)}.VPSidebarItem.level-1 .items[data-v-b3fd67f8],.VPSidebarItem.level-2 .items[data-v-b3fd67f8],.VPSidebarItem.level-3 .items[data-v-b3fd67f8],.VPSidebarItem.level-4 .items[data-v-b3fd67f8],.VPSidebarItem.level-5 .items[data-v-b3fd67f8]{border-left:1px solid var(--vp-c-divider);padding-left:16px}.VPSidebarItem.collapsed .items[data-v-b3fd67f8]{display:none}.no-transition[data-v-c40bc020] .caret-icon{transition:none}.group+.group[data-v-c40bc020]{border-top:1px solid var(--vp-c-divider);padding-top:10px}@media (min-width: 960px){.group[data-v-c40bc020]{padding-top:10px;width:calc(var(--vp-sidebar-width) - 64px)}}.VPSidebar[data-v-319d5ca6]{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-319d5ca6]{opacity:1;visibility:visible;transform:translate(0);transition:opacity .25s,transform .5s cubic-bezier(.19,1,.22,1)}.dark .VPSidebar[data-v-319d5ca6]{box-shadow:var(--vp-shadow-1)}@media (min-width: 960px){.VPSidebar[data-v-319d5ca6]{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-319d5ca6]{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-319d5ca6]{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-319d5ca6]{outline:0}.VPSkipLink[data-v-0b0ada53]{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-0b0ada53]:focus{height:auto;width:auto;clip:auto;clip-path:none}@media (min-width: 1280px){.VPSkipLink[data-v-0b0ada53]{top:14px;left:16px}}.Layout[data-v-5d98c3a5]{display:flex;flex-direction:column;min-height:100vh}.VPHomeSponsors[data-v-3d121b4a]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPHomeSponsors[data-v-3d121b4a]{margin:96px 0}@media (min-width: 768px){.VPHomeSponsors[data-v-3d121b4a]{margin:128px 0}}.VPHomeSponsors[data-v-3d121b4a]{padding:0 24px}@media (min-width: 768px){.VPHomeSponsors[data-v-3d121b4a]{padding:0 48px}}@media (min-width: 960px){.VPHomeSponsors[data-v-3d121b4a]{padding:0 64px}}.container[data-v-3d121b4a]{margin:0 auto;max-width:1152px}.love[data-v-3d121b4a]{margin:0 auto;width:fit-content;font-size:28px;color:var(--vp-c-text-3)}.icon[data-v-3d121b4a]{display:inline-block}.message[data-v-3d121b4a]{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-3d121b4a]{padding-top:32px}.action[data-v-3d121b4a]{padding-top:40px;text-align:center}.VPTeamMembersItem[data-v-f3fa364a]{display:flex;flex-direction:column;gap:2px;border-radius:12px;width:100%;height:100%;overflow:hidden}.VPTeamMembersItem.small .profile[data-v-f3fa364a]{padding:32px}.VPTeamMembersItem.small .data[data-v-f3fa364a]{padding-top:20px}.VPTeamMembersItem.small .avatar[data-v-f3fa364a]{width:64px;height:64px}.VPTeamMembersItem.small .name[data-v-f3fa364a]{line-height:24px;font-size:16px}.VPTeamMembersItem.small .affiliation[data-v-f3fa364a]{padding-top:4px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .desc[data-v-f3fa364a]{padding-top:12px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .links[data-v-f3fa364a]{margin:0 -16px -20px;padding:10px 0 0}.VPTeamMembersItem.medium .profile[data-v-f3fa364a]{padding:48px 32px}.VPTeamMembersItem.medium .data[data-v-f3fa364a]{padding-top:24px;text-align:center}.VPTeamMembersItem.medium .avatar[data-v-f3fa364a]{width:96px;height:96px}.VPTeamMembersItem.medium .name[data-v-f3fa364a]{letter-spacing:.15px;line-height:28px;font-size:20px}.VPTeamMembersItem.medium .affiliation[data-v-f3fa364a]{padding-top:4px;font-size:16px}.VPTeamMembersItem.medium .desc[data-v-f3fa364a]{padding-top:16px;max-width:288px;font-size:16px}.VPTeamMembersItem.medium .links[data-v-f3fa364a]{margin:0 -16px -12px;padding:16px 12px 0}.profile[data-v-f3fa364a]{flex-grow:1;background-color:var(--vp-c-bg-soft)}.data[data-v-f3fa364a]{text-align:center}.avatar[data-v-f3fa364a]{position:relative;flex-shrink:0;margin:0 auto;border-radius:50%;box-shadow:var(--vp-shadow-3)}.avatar-img[data-v-f3fa364a]{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;object-fit:cover}.name[data-v-f3fa364a]{margin:0;font-weight:600}.affiliation[data-v-f3fa364a]{margin:0;font-weight:500;color:var(--vp-c-text-2)}.org.link[data-v-f3fa364a]{color:var(--vp-c-text-2);transition:color .25s}.org.link[data-v-f3fa364a]:hover{color:var(--vp-c-brand-1)}.desc[data-v-f3fa364a]{margin:0 auto}.desc[data-v-f3fa364a] a{font-weight:500;color:var(--vp-c-brand-1);text-decoration-style:dotted;transition:color .25s}.links[data-v-f3fa364a]{display:flex;justify-content:center;height:56px}.sp-link[data-v-f3fa364a]{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-f3fa364a]:hover,.sp .sp-link.link[data-v-f3fa364a]:focus{outline:none;color:var(--vp-c-white);background-color:var(--vp-c-sponsor)}.sp-icon[data-v-f3fa364a]{margin-right:8px;font-size:16px}.VPTeamMembers.small .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(224px,1fr))}.VPTeamMembers.small.count-1 .container[data-v-6cb0dbc4]{max-width:276px}.VPTeamMembers.small.count-2 .container[data-v-6cb0dbc4]{max-width:576px}.VPTeamMembers.small.count-3 .container[data-v-6cb0dbc4]{max-width:876px}.VPTeamMembers.medium .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(256px,1fr))}@media (min-width: 375px){.VPTeamMembers.medium .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(288px,1fr))}}.VPTeamMembers.medium.count-1 .container[data-v-6cb0dbc4]{max-width:368px}.VPTeamMembers.medium.count-2 .container[data-v-6cb0dbc4]{max-width:760px}.container[data-v-6cb0dbc4]{display:grid;gap:24px;margin:0 auto;max-width:1152px}.VPTeamPage[data-v-7c57f839]{margin:96px 0}@media (min-width: 768px){.VPTeamPage[data-v-7c57f839]{margin:128px 0}}.VPHome .VPTeamPageTitle[data-v-7c57f839-s]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPTeamPageSection+.VPTeamPageSection[data-v-7c57f839-s],.VPTeamMembers+.VPTeamPageSection[data-v-7c57f839-s]{margin-top:64px}.VPTeamMembers+.VPTeamMembers[data-v-7c57f839-s]{margin-top:24px}@media (min-width: 768px){.VPTeamPageTitle+.VPTeamPageSection[data-v-7c57f839-s]{margin-top:16px}.VPTeamPageSection+.VPTeamPageSection[data-v-7c57f839-s],.VPTeamMembers+.VPTeamPageSection[data-v-7c57f839-s]{margin-top:96px}}.VPTeamMembers[data-v-7c57f839-s]{padding:0 24px}@media (min-width: 768px){.VPTeamMembers[data-v-7c57f839-s]{padding:0 48px}}@media (min-width: 960px){.VPTeamMembers[data-v-7c57f839-s]{padding:0 64px}}.VPTeamPageSection[data-v-b1a88750]{padding:0 32px}@media (min-width: 768px){.VPTeamPageSection[data-v-b1a88750]{padding:0 48px}}@media (min-width: 960px){.VPTeamPageSection[data-v-b1a88750]{padding:0 64px}}.title[data-v-b1a88750]{position:relative;margin:0 auto;max-width:1152px;text-align:center;color:var(--vp-c-text-2)}.title-line[data-v-b1a88750]{position:absolute;top:16px;left:0;width:100%;height:1px;background-color:var(--vp-c-divider)}.title-text[data-v-b1a88750]{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-b1a88750]{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-b1a88750]{padding-top:40px}.VPTeamPageTitle[data-v-bf2cbdac]{padding:48px 32px;text-align:center}@media (min-width: 768px){.VPTeamPageTitle[data-v-bf2cbdac]{padding:64px 48px 48px}}@media (min-width: 960px){.VPTeamPageTitle[data-v-bf2cbdac]{padding:80px 64px 48px}}.title[data-v-bf2cbdac]{letter-spacing:0;line-height:44px;font-size:36px;font-weight:500}@media (min-width: 768px){.title[data-v-bf2cbdac]{letter-spacing:-.5px;line-height:56px;font-size:48px}}.lead[data-v-bf2cbdac]{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-bf2cbdac]{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)} diff --git a/docs/.vitepress/dist/assets/vite_plugin.md.CCc_E7Wh.js b/docs/.vitepress/dist/assets/vite_plugin.md.CCc_E7Wh.js new file mode 100644 index 0000000..d865024 --- /dev/null +++ b/docs/.vitepress/dist/assets/vite_plugin.md.CCc_E7Wh.js @@ -0,0 +1,207 @@ +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/vite';
+
+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

javascript
// main.js
+import { $, html } from 'sigpro';
+import { routes } from 'virtual:sigpro-routes';
+
+// Use the generated routes directly
+const router = $.router(routes);
+document.body.appendChild(router);

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!

`,65)])])}const o=i(l,[["render",p]]);export{E as __pageData,o as default}; diff --git a/docs/.vitepress/dist/assets/vite_plugin.md.CCc_E7Wh.lean.js b/docs/.vitepress/dist/assets/vite_plugin.md.CCc_E7Wh.lean.js new file mode 100644 index 0000000..e77b754 --- /dev/null +++ b/docs/.vitepress/dist/assets/vite_plugin.md.CCc_E7Wh.lean.js @@ -0,0 +1 @@ +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("",65)])])}const o=i(l,[["render",p]]);export{E as __pageData,o as default}; diff --git a/docs/.vitepress/dist/guide/getting-started.html b/docs/.vitepress/dist/guide/getting-started.html new file mode 100644 index 0000000..e9a1599 --- /dev/null +++ b/docs/.vitepress/dist/guide/getting-started.html @@ -0,0 +1,196 @@ + + + + + + Getting Started with SigPro 🚀 | 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
+
+# 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! 🎉

+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/guide/why.html b/docs/.vitepress/dist/guide/why.html new file mode 100644 index 0000000..92b9faa --- /dev/null +++ b/docs/.vitepress/dist/guide/why.html @@ -0,0 +1,47 @@ + + + + + + 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';
+
+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

+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/hashmap.json b/docs/.vitepress/dist/hashmap.json new file mode 100644 index 0000000..d0dc26d --- /dev/null +++ b/docs/.vitepress/dist/hashmap.json @@ -0,0 +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":"dTY448ug","vite_plugin.md":"CCc_E7Wh"} diff --git a/docs/.vitepress/dist/index.html b/docs/.vitepress/dist/index.html new file mode 100644 index 0000000..df67725 --- /dev/null +++ b/docs/.vitepress/dist/index.html @@ -0,0 +1,25 @@ + + + + + + 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."

+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/vite/plugin.html b/docs/.vitepress/dist/vite/plugin.html new file mode 100644 index 0000000..ed289dc --- /dev/null +++ b/docs/.vitepress/dist/vite/plugin.html @@ -0,0 +1,231 @@ + + + + + + Vite Plugin: Automatic 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/vite';
+
+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

javascript
// main.js
+import { $, html } from 'sigpro';
+import { routes } from 'virtual:sigpro-routes';
+
+// Use the generated routes directly
+const router = $.router(routes);
+document.body.appendChild(router);

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!

+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/vp-icons.css b/docs/.vitepress/dist/vp-icons.css new file mode 100644 index 0000000..ddc5bd8 --- /dev/null +++ b/docs/.vitepress/dist/vp-icons.css @@ -0,0 +1 @@ +.vpi-social-github{--icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")} \ No newline at end of file diff --git a/docs/vite/plugin.md b/docs/vite/plugin.md new file mode 100644 index 0000000..9ff68e0 --- /dev/null +++ b/docs/vite/plugin.md @@ -0,0 +1,395 @@ +# 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/vite'; + +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 + +```javascript +// main.js +import { $, html } from 'sigpro'; +import { routes } from 'virtual:sigpro-routes'; + +// Use the generated routes directly +const router = $.router(routes); +document.body.appendChild(router); +``` + +### 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!