New 1.1.3

This commit is contained in:
2026-03-26 00:39:30 +01:00
parent 5d799608a3
commit 58f6876261
121 changed files with 3038 additions and 4707 deletions

View File

@@ -1,355 +0,0 @@
```txt
# SigPro Core Complete Reference for AI
SigPro is a reactive UI library built on signals, with no virtual DOM and no compiler. It provides fine-grained reactivity, automatic cleanup, and direct DOM manipulation. This document describes the core API as exposed by the `$` global.
---
## The `$` Function
The `$` function creates **signals**, **computed values**, **effects**, and **persistent state**. Its behavior depends on the argument type.
### 1. Primitive Signal
```js
const count = $(0); // create with initial value
count(); // read → 0
count(5); // write → 5
count(v => v + 1); // update (increment)
```
### 2. Reactive Object (Proxy)
If the argument is a plain object or array, a reactive proxy is returned. All properties become reactive, including nested objects.
```js
const user = $({ name: 'Juan', age: 30 });
user().name; // read property → 'Juan'
user.name = 'Carlos'; // write property → triggers updates
user.age = 31; // also reactive
```
Nested objects are automatically wrapped in proxies when accessed.
### 3. Computed Signal
If the argument is a **function**, it creates a computed signal. The function is executed lazily and caches its result. Dependencies are automatically tracked.
```js
const count = $(0);
const double = $(() => count() * 2);
double(); // → 0
count(5);
double(); // → 10 (automatically updated)
```
Computed signals are readonly.
### 4. Effect
If you call `$` with a function **inside a component or runtime**, it becomes an effect that runs immediately and reruns whenever any of its dependencies change. The effect can return a cleanup function.
```js
$(() => {
console.log('Count changed:', count());
});
$(() => {
const timer = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(timer); // cleanup on each rerun or unmount
});
```
Effects are automatically stopped when the component is unmounted (see `$.mount`).
### 5. Persistent Signal
If you pass a second argument (a string key), the signal is synchronized with `localStorage`. The initial value is loaded from storage, and every write is saved.
```js
const theme = $('light', 'theme-key');
const settings = $({ volume: 50 }, 'app-settings');
```
---
## DOM Creation
### `$.html(tag, props, children)`
Creates a DOM element with the given tag, properties, and children.
```js
const el = $.html('div', { class: 'container' }, 'Hello');
```
If the second argument is not an object, it is treated as children:
```js
$.html('div', 'Hello'); // no props
$.html('div', [h1('Title'), p('Text')]); // children array
```
### Tag Shortcuts
Common tags are directly available as functions:
```js
div(props, children)
span(props, children)
p()
h1(), h2(), h3()
ul(), li()
button()
input()
label()
form()
section()
a()
img()
nav()
hr()
```
All tag functions call `$.html` internally.
---
## Props
Props are objects passed to `$.html` or tag functions.
### Static Attributes
Standard HTML attributes are set as strings or booleans.
```js
div({ class: 'btn', id: 'submit', disabled: true }, 'Click');
```
### Reactive Attributes (prefixed with `$`)
Attributes starting with `$` are reactive. Their value can be a static value, a signal, or a function returning a value. The attribute is updated automatically when the value changes.
```js
const isActive = $(false);
div({ $class: () => isActive() ? 'active' : 'inactive' });
```
Special reactive attributes:
- `$value` twoway binding for `<input>`, `<textarea>`, etc.
- `$checked` twoway binding for checkboxes/radios.
When `$value` or `$checked` is a function (signal), the elements `value`/`checked` property is updated, and the elements `input`/`change` event will update the signal.
```js
const name = $('');
input({ $value: name }); // autoupdates name when user types
```
### Event Handlers (prefixed with `on`)
Events are attached with `onEventName`. The event name is caseinsensitive and can be followed by modifiers separated by dots.
```js
button({ onclick: () => console.log('clicked') }, 'Click');
```
Modifiers:
- `.prevent` calls `e.preventDefault()`
- `.stop` calls `e.stopPropagation()`
- `.once` removes listener after first call
Example:
```js
button({ onClick.prevent: handleSubmit }, 'Submit');
div({ onClick.stop: handleDivClick }, 'Click');
```
---
## Children
Children can be:
- **String** inserted as text node.
- **DOM Node** inserted directly.
- **Array** each item is processed recursively.
- **Function** treated as reactive content. The function is called every time its dependencies change, and the result is rendered in place.
```js
const count = $(0);
div([
'Static text',
() => `Count: ${count()}`, // updates automatically
button({ onclick: () => count(count()+1) }, '+')
]);
```
When a function child returns an array, each element is inserted.
---
## Routing
### `$.router(routes)`
Creates a router outlet that displays the component matching the current hash (`#/`). Returns a DOM element (a `div` with class `router-outlet`) that can be inserted anywhere.
`routes` is an array of objects with `path` and `component` properties.
- `path`: string like `/`, `/users`, `/users/:id`. Dynamic parameters are prefixed with `:`.
- `component`: a function that receives an object of params and returns a DOM node / runtime.
A `*` path serves as a 404 catchall.
```js
const routes = [
{ path: '/', component: () => Home() },
{ path: '/users/:id', component: (params) => UserDetail(params.id) },
{ path: '*', component: () => NotFound() }
];
const outlet = $.router(routes);
document.body.appendChild(outlet);
```
### `$.router.go(path)`
Programmatically navigates to a new hash path.
```js
$.router.go('/users/123');
```
---
## Mounting
### `$.mount(component, target)`
Mounts a component into a DOM element. The component can be a function (called to produce a runtime) or a static DOM node.
- `target`: a CSS selector string or a DOM element.
- Returns the runtime instance (with a `destroy` method).
If the target already has a mounted SigPro instance, it is automatically destroyed before the new one is mounted.
```js
$.mount(() => div('Hello'), '#app');
```
### Component Functions
A component function receives an object with `onCleanup` which can be used to register cleanup callbacks.
```js
const MyComponent = ({ onCleanup }) => {
const timer = setInterval(() => {}, 1000);
onCleanup(() => clearInterval(timer));
return div('Component');
};
```
Components can return:
- a DOM node
- an array of nodes
- a runtime object (like one returned from another `$.mount` call, but typically you return a node)
---
## Memory Management
SigPro automatically cleans up resources to prevent leaks:
- **WeakMaps** for proxies and mounted nodes allow garbage collection.
- Every effect, event listener, or reactive attribute created through SigPro is stored in `_cleanups` on the element and removed when the element is swept.
- The `sweep` function recursively cleans all child nodes and their attached effects.
- When a component is unmounted (by calling `destroy()` or via router), all its effects and event listeners are removed.
Effects that return a cleanup function have that function called before the next run and on unmount.
---
## Examples
### Counter with localStorage persistence
```js
const Counter = () => {
const count = $(0, 'counter'); // persistent signal
return div([
span(() => `Count: ${count()}`),
button({ onclick: () => count(count() + 1) }, '+'),
button({ onclick: () => count(count() - 1) }, '-')
]);
};
$.mount(Counter, '#app');
```
### Form with twoway binding
```js
const Form = () => {
const name = $('');
const email = $('');
return form({ onsubmit: (e) => e.preventDefault() }, [
label('Name:'),
input({ $value: name, placeholder: 'Your name' }),
label('Email:'),
input({ $value: email, type: 'email' }),
p(() => `Hello, ${name() || 'stranger'}`)
]);
};
```
### Router with parameters
```js
const routes = [
{ path: '/', component: () => div('Home') },
{ path: '/user/:id', component: (p) => div(`User ${p.id}`) }
];
const App = () => div([
nav([
a({ href: '#/' }, 'Home'),
a({ href: '#/user/42' }, 'User 42')
]),
$.router(routes)
]);
$.mount(App, '#app');
```
### Effect with cleanup
```js
const Clock = () => {
const time = $(new Date().toLocaleTimeString());
$(() => {
const interval = setInterval(() => {
time(new Date().toLocaleTimeString());
}, 1000);
return () => clearInterval(interval);
});
return span(() => time());
};
```
---
## Notes for AI
- SigPro is entirely selfcontained. No external libraries are needed.
- All reactivity is based on signals and proxies.
- To update the DOM, just change a signal the UI updates automatically.
- Use `$` for state, `$.html` (or tag shortcuts) for DOM.
- The router uses hash navigation only; it does not use the History API.
- There is no builtin `_if` or `_for`; those are part of optional UI plugins. For conditional rendering, use a function child that returns different content based on a signal.
- For lists, a common pattern is to create a function child that maps over an array and returns an array of elements. However, the core does not provide keyed diffing; that is left to the user or plugin.
For any further details, refer to the source code of `$.html`, `$.router`, and the reactive internals. The API is stable and minimal.
```

387
sigpro/sigpro.d.ts vendored
View File

@@ -1,387 +0,0 @@
/**
* SigPro - Atomic Unified Reactive Engine
* A lightweight, fine-grained reactivity system with built-in routing and plugin support.
*
* @example
* ```typescript
* // Create a reactive signal
* const count = $(0);
*
* // Create a computed value
* const double = $(() => count() * 2);
*
* // Create reactive HTML
* const app = div({ $class: () => count() > 5 ? 'high' : 'low' }, [
* h1("Counter Demo"),
* p("Count: ", () => count()),
* p("Double: ", () => double()),
* button({ onclick: () => count(count() + 1) }, "Increment")
* ]);
*
* // Mount to DOM
* $.mount(app);
* ```
*/
declare global {
interface Window {
/** Main SigPro instance */
$: SigPro;
/** HTML element creators (auto-generated from tags list) */
div: typeof html;
span: typeof html;
p: typeof html;
button: typeof html;
h1: typeof html;
h2: typeof html;
h3: typeof html;
ul: typeof html;
ol: typeof html;
li: typeof html;
a: typeof html;
label: typeof html;
section: typeof html;
nav: typeof html;
main: typeof html;
header: typeof html;
footer: typeof html;
input: typeof html;
form: typeof html;
img: typeof html;
select: typeof html;
option: typeof html;
table: typeof html;
thead: typeof html;
tbody: typeof html;
tr: typeof html;
th: typeof html;
td: typeof html;
canvas: typeof html;
video: typeof html;
audio: typeof html;
}
/**
* Reactive Signal - A reactive state container
* @template T Type of the stored value
*/
interface Signal<T> {
/** Get the current value */
(): T;
/** Set a new value */
(value: T): T;
/** Update value based on previous value */
(updater: (prev: T) => T): T;
}
/**
* Computed Signal - A reactive derived value
* @template T Type of the computed value
*/
interface Computed<T> {
/** Get the current computed value */
(): T;
}
/**
* Reactive Effect - A function that re-runs when dependencies change
*/
type Effect = () => void;
/**
* HTML Content Types
*/
type HtmlContent =
| string
| number
| boolean
| null
| undefined
| Node
| HtmlContent[]
| (() => string | number | Node | null | undefined);
/**
* HTML Attributes and Event Handlers
*/
interface HtmlProps extends Record<string, any> {
/** Two-way binding for input values */
$value?: Signal<any> | ((val: any) => void);
/** Two-way binding for checkbox/radio checked state */
$checked?: Signal<boolean> | ((val: boolean) => void);
/** Reactive class binding */
$class?: string | (() => string);
/** Reactive style binding */
$style?: string | object | (() => string | object);
/** Reactive attribute binding (any attribute can be prefixed with $) */
[key: `$${string}`]: any;
/** Standard event handlers */
onclick?: (event: MouseEvent) => void;
oninput?: (event: Event) => void;
onchange?: (event: Event) => void;
onsubmit?: (event: Event) => void;
onkeydown?: (event: KeyboardEvent) => void;
onkeyup?: (event: KeyboardEvent) => void;
onfocus?: (event: FocusEvent) => void;
onblur?: (event: FocusEvent) => void;
onmouseover?: (event: MouseEvent) => void;
onmouseout?: (event: MouseEvent) => void;
[key: `on${string}`]: ((event: any) => void) | undefined;
}
/**
* Route Configuration
*/
interface Route {
/** URL path pattern (supports :params and * wildcard) */
path: string;
/** Component to render (can be sync, async, or Promise) */
component: ((params: Record<string, string>) => HTMLElement | Promise<HTMLElement>) | Promise<any> | HTMLElement;
}
/**
* Router Instance
*/
interface Router {
/** Router container element */
(routes: Route[]): HTMLElement;
/** Programmatic navigation */
go: (path: string) => void;
}
/**
* Plugin System
*/
interface Plugin {
/**
* Extend SigPro with custom functionality or load external scripts
* @param source - Plugin function, script URL, or array of URLs
* @returns SigPro instance (sync) or Promise (async loading)
*/
(source: ((sigpro: SigPro) => void) | string | string[]): SigPro | Promise<SigPro>;
}
/**
* Main SigPro Interface
*/
interface SigPro {
/**
* Create a reactive Signal or Computed value
* @template T Type of the value
* @param initial - Initial value or computed function
* @param key - Optional localStorage key for persistence
* @returns Reactive signal or computed function
*
* @example
* ```typescript
* // Signal with localStorage persistence
* const count = $(0, 'app.count');
*
* // Computed value
* const double = $(() => count() * 2);
*
* // Reactive effect (runs automatically)
* $(() => console.log('Count changed:', count()));
* ```
*/
<T>(initial: T, key?: string): Signal<T>;
<T>(computed: () => T, key?: string): Computed<T>;
/**
* Create reactive HTML elements with hyperscript syntax
* @param tag - HTML tag name
* @param props - Attributes, events, and reactive bindings
* @param content - Child nodes or content
* @returns Live DOM element with reactivity
*
* @example
* ```typescript
* const name = $('World');
*
* const element = $.html('div',
* { class: 'greeting', $class: () => name().length > 5 ? 'long' : '' },
* [
* $.html('h1', 'Hello'),
* $.html('p', () => `Hello, ${name()}!`),
* $.html('input', {
* $value: name,
* placeholder: 'Enter your name'
* })
* ]
* );
* ```
*/
html: typeof html;
/**
* Mount a component to the DOM
* @param node - Component or element to mount
* @param target - Target element or CSS selector (default: document.body)
*
* @example
* ```typescript
* // Mount to body
* $.mount(app);
*
* // Mount to specific element
* $.mount(app, '#app');
*
* // Mount with component function
* $.mount(() => div("Dynamic component"));
* ```
*/
mount: typeof mount;
/**
* Initialize a hash-based router
* @param routes - Array of route configurations
* @returns Router container element
*
* @example
* ```typescript
* const routes = [
* { path: '/', component: Home },
* { path: '/user/:id', component: UserProfile },
* { path: '*', component: NotFound }
* ];
*
* const router = $.router(routes);
* $.mount(router);
*
* // Navigate programmatically
* $.router.go('/user/42');
* ```
*/
router: Router;
/**
* Extend SigPro with plugins or load external scripts
* @param source - Plugin function, script URL, or array of URLs
* @returns SigPro instance or Promise
*
* @example
* ```typescript
* // Load external library
* await $.plugin('https://cdn.jsdelivr.net/npm/lodash/lodash.min.js');
*
* // Register plugin
* $.plugin(($) => {
* $.customMethod = () => console.log('Custom method');
* });
*
* // Load multiple scripts
* await $.plugin(['lib1.js', 'lib2.js']);
* ```
*/
plugin: Plugin;
}
/**
* Creates a reactive HTML element
* @param tag - HTML tag name
* @param props - Attributes and event handlers
* @param content - Child content
* @returns Live DOM element
*/
function html(tag: string, props?: HtmlProps | HtmlContent, content?: HtmlContent): HTMLElement;
/**
* Mount a component to the DOM
* @param node - Component or element to mount
* @param target - Target element or CSS selector
*/
function mount(node: HTMLElement | (() => HTMLElement), target?: HTMLElement | string): void;
/**
* Type-safe HTML element creators for common tags
*
* @example
* ```typescript
* // Using tag functions directly
* const myDiv = div({ class: 'container' }, [
* h1("Title"),
* p("Paragraph text"),
* button({ onclick: () => alert('Clicked!') }, "Click me")
* ]);
* ```
*/
interface HtmlTagCreator {
/**
* Create HTML element with props and content
* @param props - HTML attributes and event handlers
* @param content - Child nodes or content
* @returns HTMLElement
*/
(props?: HtmlProps | HtmlContent, content?: HtmlContent): HTMLElement;
}
// Type-safe tag creators
const div: HtmlTagCreator;
const span: HtmlTagCreator;
const p: HtmlTagCreator;
const button: HtmlTagCreator;
const h1: HtmlTagCreator;
const h2: HtmlTagCreator;
const h3: HtmlTagCreator;
const ul: HtmlTagCreator;
const ol: HtmlTagCreator;
const li: HtmlTagCreator;
const a: HtmlTagCreator;
const label: HtmlTagCreator;
const section: HtmlTagCreator;
const nav: HtmlTagCreator;
const main: HtmlTagCreator;
const header: HtmlTagCreator;
const footer: HtmlTagCreator;
const input: HtmlTagCreator;
const form: HtmlTagCreator;
const img: HtmlTagCreator;
const select: HtmlTagCreator;
const option: HtmlTagCreator;
const table: HtmlTagCreator;
const thead: HtmlTagCreator;
const tbody: HtmlTagCreator;
const tr: HtmlTagCreator;
const th: HtmlTagCreator;
const td: HtmlTagCreator;
const canvas: HtmlTagCreator;
const video: HtmlTagCreator;
const audio: HtmlTagCreator;
}
/**
* Helper types for common use cases
*/
export namespace SigProTypes {
/**
* Extract the value type from a Signal
*/
type SignalValue<T> = T extends Signal<infer U> ? U : never;
/**
* Extract the return type from a Computed
*/
type ComputedValue<T> = T extends Computed<infer U> ? U : never;
/**
* Props for a component function
*/
interface ComponentProps {
children?: HtmlContent;
[key: string]: any;
}
/**
* Component function type
*/
type Component<P extends ComponentProps = ComponentProps> = (props: P) => HTMLElement;
/**
* Async component type (for lazy loading)
*/
type AsyncComponent = () => Promise<{ default: Component }>;
}
export {};
// Make sure $ is available globally
declare const $: SigPro;

View File

@@ -1,5 +1,5 @@
/**
* SigPro
* SigPro Core
*/
(() => {
let activeEffect = null;
@@ -19,7 +19,7 @@
isFlushing = false;
};
const track = subs => {
const track = (subs) => {
if (activeEffect && !activeEffect._deleted) {
subs.add(activeEffect);
activeEffect._deps.add(subs);
@@ -39,64 +39,20 @@
if (!isFlushing) queueMicrotask(flush);
};
const isObj = v => v && typeof v === 'object' && !(v instanceof Node);
const PROXIES = new WeakMap();
const RAW_SUBS = new WeakMap();
const getPropSubs = (target, prop) => {
let props = RAW_SUBS.get(target);
if (!props) RAW_SUBS.set(target, (props = new Map()));
let subs = props.get(prop);
if (!subs) props.set(prop, (subs = new Set()));
return subs;
};
const $ = (initial, key) => {
if (isObj(initial) && !key && typeof initial !== 'function') {
if (PROXIES.has(initial)) return PROXIES.get(initial);
const proxy = new Proxy(initial, {
get(t, p, r) {
track(getPropSubs(t, p));
const val = Reflect.get(t, p, r);
return isObj(val) ? $(val) : val;
},
set(t, p, v, r) {
const old = Reflect.get(t, p, r);
if (Object.is(old, v)) return true;
const res = Reflect.set(t, p, v, r);
trigger(getPropSubs(t, p));
if (Array.isArray(t) && p !== 'length') trigger(getPropSubs(t, 'length'));
return res;
},
deleteProperty(t, p) {
const res = Reflect.deleteProperty(t, p);
trigger(getPropSubs(t, p));
return res;
}
});
PROXIES.set(initial, proxy);
return proxy;
}
if (typeof initial === 'function') {
const $ = (initial, key = null) => {
if (typeof initial === "function") {
const subs = new Set();
let cached, dirty = true;
let cached;
let dirty = true;
const effect = () => {
if (effect._deleted) return;
effect._cleanups.forEach(c => c());
effect._cleanups.clear();
effect._deps.forEach(s => s.delete(effect));
effect._deps.forEach((s) => s.delete(effect));
effect._deps.clear();
const prev = activeEffect;
activeEffect = effect;
try {
let maxD = 0;
effect._deps.forEach(s => { if (s._d > maxD) maxD = s._d; });
effect.depth = maxD + 1;
subs._d = effect.depth;
const val = initial();
if (!Object.is(cached, val) || dirty) {
cached = val;
@@ -108,25 +64,20 @@
}
};
effect._isComputed = true;
effect._deps = new Set();
effect._cleanups = new Set();
effect._isComputed = true;
effect._subs = subs;
effect.markDirty = () => dirty = true;
effect._deleted = false;
effect.markDirty = () => (dirty = true);
effect.stop = () => {
effect._deleted = true;
effectQueue.delete(effect);
effect._cleanups.forEach(c => c());
effect._deps.forEach(s => s.delete(effect));
effect._deps.forEach((s) => s.delete(effect));
effect._deps.clear();
subs.clear();
};
if (currentOwner) {
currentOwner.cleanups.add(effect.stop);
effect._isComputed = false;
effect();
return () => { };
}
if (currentOwner) currentOwner.cleanups.add(effect.stop);
return () => {
if (dirty) effect();
@@ -135,159 +86,274 @@
};
}
const subs = new Set();
subs._d = 0;
let value = initial;
if (key) {
try { const s = localStorage.getItem(key); if (s !== null) initial = JSON.parse(s); } catch (e) { }
const saved = localStorage.getItem(key);
if (saved !== null) {
try {
value = JSON.parse(saved);
} catch {
value = saved;
}
}
}
const subs = new Set();
return (...args) => {
if (args.length) {
const next = typeof args[0] === 'function' ? args[0](initial) : args[0];
if (!Object.is(initial, next)) {
initial = next;
if (key) try { localStorage.setItem(key, JSON.stringify(initial)); } catch (e) { }
const next = typeof args[0] === "function" ? args[0](value) : args[0];
if (!Object.is(value, next)) {
value = next;
if (key) {
localStorage.setItem(key, JSON.stringify(value));
}
trigger(subs);
}
}
track(subs);
return initial;
return value;
};
};
const sweep = node => {
if (node._cleanups) { node._cleanups.forEach(f => f()); node._cleanups.clear(); }
$.effect = (fn) => {
const owner = currentOwner;
const effect = () => {
if (effect._deleted) return;
effect._deps.forEach((s) => s.delete(effect));
effect._deps.clear();
effect._cleanups.forEach((c) => c());
effect._cleanups.clear();
const prevEffect = activeEffect;
const prevOwner = currentOwner;
activeEffect = effect;
currentOwner = { cleanups: effect._cleanups };
effect.depth = prevEffect ? prevEffect.depth + 1 : 0;
try {
fn();
} finally {
activeEffect = prevEffect;
currentOwner = prevOwner;
}
};
effect._deps = new Set();
effect._cleanups = new Set();
effect._deleted = false;
effect.stop = () => {
if (effect._deleted) return;
effect._deleted = true;
effectQueue.delete(effect);
effect._deps.forEach((s) => s.delete(effect));
effect._deps.clear();
effect._cleanups.forEach((c) => c());
effect._cleanups.clear();
if (owner) {
owner.cleanups.delete(effect.stop);
}
};
if (owner) owner.cleanups.add(effect.stop);
effect();
return effect.stop;
};
const sweep = (node) => {
if (node._cleanups) {
node._cleanups.forEach((f) => f());
node._cleanups.clear();
}
node.childNodes?.forEach(sweep);
};
const createRuntime = fn => {
$.view = (fn) => {
const cleanups = new Set();
const prev = currentOwner;
const container = document.createElement("div");
container.style.display = "contents";
currentOwner = { cleanups };
const container = $.html('div', { style: 'display:contents' });
try {
const res = fn({ onCleanup: f => cleanups.add(f) });
const process = n => {
const res = fn({ onCleanup: (f) => cleanups.add(f) });
const process = (n) => {
if (!n) return;
if (n._isRuntime) { cleanups.add(n.destroy); container.appendChild(n.container); }
else if (Array.isArray(n)) n.forEach(process);
if (n._isRuntime) {
cleanups.add(n.destroy);
container.appendChild(n.container);
} else if (Array.isArray(n)) n.forEach(process);
else container.appendChild(n instanceof Node ? n : document.createTextNode(String(n)));
};
process(res);
} finally { currentOwner = prev; }
} finally {
currentOwner = prev;
}
return {
_isRuntime: true,
container,
destroy: () => {
cleanups.forEach(f => f());
cleanups.forEach((f) => f());
sweep(container);
container.remove();
}
},
};
};
$.html = (tag, props = {}, content = []) => {
if (props instanceof Node || Array.isArray(props) || typeof props !== "object") {
content = props; props = {};
content = props;
props = {};
}
const el = document.createElement(tag);
el._cleanups = new Set();
for (let [k, v] of Object.entries(props)) {
if (k.startsWith('on')) {
const name = k.slice(2).toLowerCase().split('.')[0];
const mods = k.slice(2).toLowerCase().split('.').slice(1);
const handler = e => { if (mods.includes('prevent')) e.preventDefault(); if (mods.includes('stop')) e.stopPropagation(); v(e); };
el.addEventListener(name, handler, { once: mods.includes('once') });
if (k.startsWith("on")) {
const name = k.slice(2).toLowerCase().split(".")[0];
const mods = k.slice(2).toLowerCase().split(".").slice(1);
const handler = (e) => {
if (mods.includes("prevent")) e.preventDefault();
if (mods.includes("stop")) e.stopPropagation();
v(e);
};
el.addEventListener(name, handler, { once: mods.includes("once") });
el._cleanups.add(() => el.removeEventListener(name, handler));
} else if (k.startsWith('$')) {
} else if (k.startsWith("$")) {
const attr = k.slice(1);
const stopAttr = $(() => {
const val = typeof v === 'function' ? v() : v;
if (attr === 'value' || attr === 'checked') el[attr] = val;
else if (typeof val === 'boolean') el.toggleAttribute(attr, val);
const stopAttr = $.effect(() => {
const val = typeof v === "function" ? v() : v;
if (el[attr] === val) return;
if (attr === "value" || attr === "checked") el[attr] = val;
else if (typeof val === "boolean") el.toggleAttribute(attr, val);
else val == null ? el.removeAttribute(attr) : el.setAttribute(attr, val);
});
el._cleanups.add(stopAttr);
if ((attr === 'value' || attr === 'checked') && typeof v === 'function') {
const evt = attr === 'checked' ? 'change' : 'input';
const h = e => v(e.target[attr]);
el.addEventListener(evt, h);
el._cleanups.add(() => el.removeEventListener(evt, h));
if (typeof v === "function") {
const evt = attr === "checked" ? "change" : "input";
const handler = (e) => v(e.target[attr]);
el.addEventListener(evt, handler);
el._cleanups.add(() => el.removeEventListener(evt, handler));
}
} else el.setAttribute(k, v);
} else if (typeof v === "function") {
const stopAttr = $.effect(() => {
const val = v();
if (k === "class" || k === "className") el.className = val || "";
else if (typeof val === "boolean") el.toggleAttribute(k, val);
else val == null ? el.removeAttribute(k) : el.setAttribute(k, val);
});
el._cleanups.add(stopAttr);
} else {
if (k === "class" || k === "className") el.className = v || "";
else if (typeof v === "boolean") el.toggleAttribute(k, v);
else v == null ? el.removeAttribute(k) : el.setAttribute(k, v);
}
}
const append = c => {
if (Array.isArray(c)) c.forEach(append);
else if (typeof c === 'function') {
const marker = document.createTextNode('');
const append = (c) => {
if (Array.isArray(c)) return c.forEach(append);
if (typeof c === "function") {
const marker = document.createTextNode("");
el.appendChild(marker);
let nodes = [marker];
const stopList = $(() => {
let nodes = [];
const stopList = $.effect(() => {
const res = c();
const next = (Array.isArray(res) ? res : [res]).map(i => i?.container || (i instanceof Node ? i : document.createTextNode(i ?? '')));
if (marker.parentNode) {
next.forEach(n => marker.parentNode.insertBefore(n, marker));
nodes.forEach(n => { if (n !== marker) { sweep(n); n.remove(); } });
nodes = [...next, marker];
}
const next = (Array.isArray(res) ? res : [res]).map((i) =>
i?._isRuntime ? i.container : i instanceof Node ? i : document.createTextNode(i ?? ""),
);
nodes.forEach((n) => {
sweep(n);
n.remove();
});
next.forEach((n) => marker.parentNode?.insertBefore(n, marker));
nodes = next;
});
el._cleanups.add(stopList);
} else el.appendChild(c instanceof Node ? c : document.createTextNode(c ?? ''));
} else el.appendChild(c instanceof Node ? c : document.createTextNode(c ?? ""));
};
append(content);
return el;
};
const tags = ['div', 'span', 'p', 'h1', 'h2', 'h3', 'ul', 'li', 'button', 'input', 'label', 'form', 'section', 'a', 'img', 'nav', 'hr'];
window.$ = new Proxy($, { get: (t, p) => t[p] || (tags.includes(p) ? (pr, c) => t.html(p, pr, c) : undefined) });
tags.forEach(t => window[t] = (p, c) => $.html(t, p, c));
$.ignore = (fn) => {
const prev = activeEffect;
activeEffect = null;
try {
return fn();
} finally {
activeEffect = prev;
}
};
$.router = routes => {
const sPath = $(window.location.hash.replace(/^#/, '') || '/');
const handler = () => sPath(window.location.hash.replace(/^#/, '') || '/');
window.addEventListener('hashchange', handler);
const outlet = $.html('div', { class: 'router-outlet' });
$.router = (routes) => {
const sPath = $(window.location.hash.replace(/^#/, "") || "/");
window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/"));
const outlet = Div({ class: "router-outlet" });
let current = null;
if (currentOwner) currentOwner.cleanups.add(() => {
window.removeEventListener('hashchange', handler);
$.effect(() => {
const path = sPath();
if (current) current.destroy();
outlet.innerHTML = "";
const parts = path.split("/").filter(Boolean);
const route =
routes.find((r) => {
const rp = r.path.split("/").filter(Boolean);
return rp.length === parts.length && rp.every((p, i) => p.startsWith(":") || p === parts[i]);
}) || routes.find((r) => r.path === "*");
if (route) {
const params = {};
route.path
.split("/")
.filter(Boolean)
.forEach((p, i) => {
if (p.startsWith(":")) params[p.slice(1)] = parts[i];
});
current = $.ignore(() =>
$.view(() => {
const res = route.component(params);
return typeof res === "function" ? res() : res;
}),
);
outlet.appendChild(current.container);
}
});
$(() => {
const path = sPath(), parts = path.split('/').filter(Boolean);
const route = routes.find(r => {
const rp = r.path.split('/').filter(Boolean);
return rp.length === parts.length && rp.every((p, i) => p.startsWith(':') || p === parts[i]);
}) || routes.find(r => r.path === '*');
if (current) current.destroy();
if (!route) return outlet.replaceChildren($.html('h1', '404'));
const params = {};
route.path.split('/').filter(Boolean).forEach((p, i) => { if (p.startsWith(':')) params[p.slice(1)] = parts[i]; });
current = createRuntime(() => route.component(params));
outlet.replaceChildren(current.container);
});
return outlet;
};
$.router.go = p => window.location.hash = p.replace(/^#?\/?/, '#/');
$.go = (p) => (window.location.hash = p.replace(/^#?\/?/, "#/"));
$.mount = (component, target) => {
const el = typeof target === 'string' ? document.querySelector(target) : target;
const el = typeof target === "string" ? document.querySelector(target) : target;
if (!el) return;
if (MOUNTED_NODES.has(el)) {
MOUNTED_NODES.get(el).destroy();
}
const instance = createRuntime(typeof component === 'function' ? component : () => component);
if (MOUNTED_NODES.has(el)) MOUNTED_NODES.get(el).destroy();
const instance = $.view(typeof component === "function" ? component : () => component);
el.replaceChildren(instance.container);
MOUNTED_NODES.set(el, instance);
return instance;
};
const tags =
`div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption img video audio canvas svg iframe picture source progress meter`.split(
/\s+/,
);
tags.forEach((t) => {
window[t.charAt(0).toUpperCase() + t.slice(1)] = (p, c) => $.html(t, p, c);
});
window.$ = $;
})();
export const {$} = window;
export const { $ } = window;

1
sigpro/sigpro.min.js vendored Normal file

File diff suppressed because one or more lines are too long