Compare commits
296 Commits
5533cfe100
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| eb1c81ec26 | |||
| 8481e339cc | |||
| d83aff6229 | |||
| 8df06d9c12 | |||
| 72bfc2b5c1 | |||
| 1db7b81eb7 | |||
| 1607b41ebc | |||
| 27d9474610 | |||
| 3dea037697 | |||
| 9fc4eaebbb | |||
| c86c37aec2 | |||
| 26464d2161 | |||
| dc9af3181f | |||
| 00ad6d7f9f | |||
| 5efb9e0f96 | |||
| 651d9587c2 | |||
| 3fe05d40e6 | |||
| 0b3eb0159f | |||
| 1349d431e9 | |||
| 6f538b8613 | |||
| 7d8db0192a | |||
| 1d71340552 | |||
| 06c7763b34 | |||
| d48241a9d9 | |||
| 2a482f2340 | |||
| 1800b16940 | |||
| 8b2e67b3b0 | |||
| 0a790de054 | |||
| c01b41d892 | |||
| 5a2cefa115 | |||
| a0701422f5 | |||
| 645f9b42b0 | |||
| 8796b9f94d | |||
| afa2817118 | |||
| 369a35d92a | |||
| 610c9a9586 | |||
| cd58b97d09 | |||
| e4b08a0aad | |||
| 439809b1e7 | |||
| ab0e6e0697 | |||
| 39a67b94fc | |||
| 820d55b012 | |||
| f3fb26354c | |||
| 8a9805b79a | |||
| bef6c20231 | |||
| b1fa97afc3 | |||
| a35ea1e38e | |||
| 69e277d726 | |||
| 9da5bd74f9 | |||
| 7251573e28 | |||
| 1ca67dd4a0 | |||
| db9bd39679 | |||
| 2b303fc55c | |||
| f98cb19ee1 | |||
| f28594348e | |||
| 7d4340a987 | |||
| f11dd340ff | |||
| 6d7ac2d2e9 | |||
| 0df4b3912d | |||
| 771f4a9f83 | |||
| d46c5ca3af | |||
| 4526726b1b | |||
| 2a0ce8c68f | |||
| 995f1557bf | |||
| 6a33b7df07 | |||
| 99780e8399 | |||
| b931434edc | |||
| dc2f6f8736 | |||
| 7cce0e5e59 | |||
| 03da2b7cd3 | |||
| 496ad150ce | |||
| a65219759d | |||
| 25975eb89a | |||
| 04052ef7b4 | |||
| 3faf1fe5a6 | |||
| 76a97fe2a2 | |||
| fb7ebe5fec | |||
| 9b9284d3d1 | |||
| 83c5279ab9 | |||
| ab36557c8c | |||
| 1c45dc5466 | |||
| ee5e6e5207 | |||
| 29dda4c07e | |||
| ab1413ca5a | |||
| a5ecc17166 | |||
| fdffac2a72 | |||
| 5b0cfad9b8 | |||
| ccdbeb1b16 | |||
| 2f1cfae0b2 | |||
| 6e0c21eddc | |||
| 20f7242e83 | |||
| 73d5c12f13 | |||
| c653d361d6 | |||
| 1006a42284 | |||
| ba6d731377 | |||
| 91225e185d | |||
| c28b1860e7 | |||
| 7287e9c094 | |||
| 14f06c3a88 | |||
| 5bf1ecd4f0 | |||
| 2d97d7d117 | |||
| 0d59518a80 | |||
| 4690aa5013 | |||
| 0837030da8 | |||
| e659aa940f | |||
| fb1ac8c9c3 | |||
| 69b2dd723c | |||
| 60ff7f4e99 | |||
| becc4b8227 | |||
| af5bd1a537 | |||
| a792e72b63 | |||
| 13f7fba03c | |||
| 6b447b67a4 | |||
| a0711b3294 | |||
| f4654a938a | |||
| de4b09324d | |||
| a59d26f18a | |||
| 482ff19adb | |||
| f05511dad8 | |||
| 3d3de01aae | |||
| d166209ce6 | |||
| 2b786b290f | |||
| 3dde70d177 | |||
| df69877203 | |||
| 1c788f1dd1 | |||
| 99bf97a8d3 | |||
| b718fe20e4 | |||
| 8b45c84e67 | |||
| 2b86fec0ee | |||
| 7ba14217fb | |||
| 60682b3c60 | |||
| 4c2ef02e95 | |||
| 1a78879197 | |||
| d7bdfccb03 | |||
| dad4fc0aca | |||
| 8bdc86faf3 | |||
| 728abe0aa3 | |||
| 7d01ff13be | |||
| 49029b5eae | |||
| 91c72b6b9f | |||
| b472cb8921 | |||
| 5dbff9d499 | |||
| 1aee6a297d | |||
| 77b73cceef | |||
| 50d0d1319e | |||
| e203168ac1 | |||
| b21af833ac | |||
| b9207b1ddc | |||
| 51abd36724 | |||
| ef45d5f057 | |||
| f3f774fda1 | |||
| ac9527f9a3 | |||
| 9856eefa64 | |||
| 70e3c33bc8 | |||
| e06d5da447 | |||
| 9db278ea9b | |||
| 0aa57fb944 | |||
| 2e2a0b56f0 | |||
| bedd4ae225 | |||
| 1e470179ef | |||
| 4826892582 | |||
| c654c6ff1a | |||
| dc0dd14fd5 | |||
| f3b536e00e | |||
| 0fb71372ac | |||
| 4a8461903c | |||
| 9dd231fbc3 | |||
| 4d4f423ed7 | |||
| b835cd1992 | |||
| 9e469c975d | |||
| 873b3624c6 | |||
| f8fd9b4ae7 | |||
| 3ee77d4e9b | |||
| 32ab220ade | |||
| f50823c3cd | |||
| 71f2b2340b | |||
| 26d33c94bf | |||
| 5d3ab87940 | |||
| 5d3a9cdea4 | |||
| 80fa361da3 | |||
| 34c0b16595 | |||
| abeab4a8b6 | |||
| 5a0bec8837 | |||
| 0b124871fb | |||
| 0d119bcc35 | |||
| 8a71b86895 | |||
| c8e352ea96 | |||
| 73e019a582 | |||
| f14346cf39 | |||
| 14e4955acd | |||
| 23451f04df | |||
| 9f36e5e3fc | |||
| de607e3dda | |||
| 3000212961 | |||
| 541a3526a1 | |||
| 25188e7473 | |||
| 876d4c01f0 | |||
| 3c04a9433f | |||
| 5e90f62168 | |||
| 4a8959162c | |||
| 5a10974d0b | |||
| b8b42652d9 | |||
| 6f03422d64 | |||
| dc8568ae3a | |||
| f5bb657018 | |||
| 52ec5f46c3 | |||
| 64ead15875 | |||
| 43b8f4034c | |||
| a443d099ac | |||
| 8e4f071f52 | |||
| fb9b752cce | |||
| bc272e3d3c | |||
| 9adaeb5cae | |||
| 8b310805c3 | |||
| e40d39a26c | |||
| 59bf869686 | |||
| 1a8a33dd47 | |||
| 0ae2d56c37 | |||
| 45b34c9668 | |||
| 6d6c4ce703 | |||
| a43b624e00 | |||
| b7dbbd7add | |||
| 6d1539cf20 | |||
| e6b1f65055 | |||
| 205e5f5f06 | |||
| 1018c0bf9f | |||
| 0729f00fe2 | |||
| 1d09e8b382 | |||
| 7aac307af5 | |||
| bc23716280 | |||
| cf0e5b2913 | |||
| 627cfcd5d5 | |||
| d8ab8c75d8 | |||
| dcc669d6fe | |||
| f4032777a1 | |||
| 6ea0dcd3d7 | |||
| 5593c2e701 | |||
| 6813283c45 | |||
| f411c2e9bb | |||
| f38b3b4b46 | |||
| 9f524feb98 | |||
| 2e5142f073 | |||
| 8903be50ea | |||
| 3243f67431 | |||
| e9e0828206 | |||
| fbd9ddba47 | |||
| 03fe2a1b80 | |||
| b168761127 | |||
| 53fdaaa212 | |||
| 02ee84f8e5 | |||
| 9b9b345e48 | |||
| 9588bcbce8 | |||
| 52af01e128 | |||
| adafb8eb11 | |||
| 689febdbf4 | |||
| 98789b438b | |||
| 7a9c138b65 | |||
| d0c6663112 | |||
| ad96380e6f | |||
| 38b833e5c2 | |||
| 272c086e4f | |||
| 7350633a63 | |||
| d586af52b6 | |||
| d9cbbae40d | |||
| c4929e26e2 | |||
| 7879aa463b | |||
| d053ba39a1 | |||
| 215a3375b5 | |||
| 9c0acb83f8 | |||
| 557bd1428a | |||
| 4b5243052b | |||
| b2ba771b9c | |||
| 925a673775 | |||
| 2ac429c3f8 | |||
| 20022a361c | |||
| 150cad34d5 | |||
| fc148fdbcc | |||
| b75d1175a5 | |||
| 1e8b3ad803 | |||
| bfd79f8491 | |||
| a39d85ff89 | |||
| 9e36bb8041 | |||
| 75520a8499 | |||
| e9ca8d397b | |||
| 35bfbde6a7 | |||
| c56bdd4ba9 | |||
| 5f34c79fca | |||
| d15251c808 | |||
| 80ab4baf87 | |||
| 4e59bcb460 | |||
| d508a99290 | |||
| b2c6b8d398 | |||
| 4a9707819d | |||
| a0bebd41e5 | |||
| c1beda24ca | |||
| 59f464a70f |
28
.github/workflows/docs.yaml
vendored
Normal file
28
.github/workflows/docs.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Deploy Docs to Synology
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:20-bullseye
|
||||
options: >-
|
||||
--dns 192.168.1.1
|
||||
--add-host git.natxocc.com:host-gateway
|
||||
--add-host gitea:host-gateway
|
||||
-v /volume1/webdocs/sigpro:/mnt/nas_docs
|
||||
|
||||
steps:
|
||||
- name: Checkout código
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
env:
|
||||
GIT_CONFIG_PARAMETERS: "'url.https://git.natxocc.com/.insteadOf=http://gitea:3000/'"
|
||||
|
||||
- name: Copiar archivos
|
||||
run: |
|
||||
cp -r docs/. /mnt/nas_docs/
|
||||
ls -la /mnt/nas_docs
|
||||
43
.github/workflows/publish-gitea.yml
vendored
Normal file
43
.github/workflows/publish-gitea.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Publish to SigPro (NPM)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:20-bullseye
|
||||
options: >-
|
||||
--dns 192.168.1.1
|
||||
--add-host git.natxocc.com:host-gateway
|
||||
--add-host gitea:host-gateway
|
||||
|
||||
steps:
|
||||
- name: Checkout código
|
||||
uses: actions/checkout@v4
|
||||
env:
|
||||
GIT_CONFIG_PARAMETERS: "'url.https://git.natxocc.com/.insteadOf=http://gitea:3000/'"
|
||||
|
||||
- name: Instalar Bun
|
||||
run: |
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
echo "$HOME/.bun/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Instalar y Build
|
||||
run: |
|
||||
export PATH="$HOME/.bun/bin:$PATH"
|
||||
bun install
|
||||
bun run build
|
||||
|
||||
- name: Configurar Registro y Publicar
|
||||
run: |
|
||||
# 1. Definimos la URL del registro
|
||||
REGISTRY="git.natxocc.com/api/packages/natxocc/npm/"
|
||||
|
||||
echo "//${REGISTRY}:_authToken=${{ secrets.PACK_TOKEN }}" > ~/.npmrc
|
||||
|
||||
npm publish --registry "https://${REGISTRY}" --userconfig ~/.npmrc
|
||||
38
.github/workflows/publish-npm.yml
vendored
Normal file
38
.github/workflows/publish-npm.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Publish to NPM (Manual)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Versión a publicar (ej: 1.2.20)'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:20-bullseye
|
||||
options: >-
|
||||
--dns 192.168.1.1
|
||||
--add-host git.natxocc.com:host-gateway
|
||||
--add-host gitea:host-gateway
|
||||
|
||||
steps:
|
||||
- name: Clone source from Gitea
|
||||
run: |
|
||||
git clone https://git.natxocc.com/natxocc/sigpro.git source
|
||||
cd source
|
||||
git checkout main
|
||||
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install and publish
|
||||
run: |
|
||||
cd source
|
||||
bun install
|
||||
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
|
||||
npm publish --access public
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
25
.github/workflows/publish.yml
vendored
25
.github/workflows/publish.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: Publish to NPM
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published] # Se dispara cuando creas una "Release" en GitHub
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Usamos Bun ya que tu proyecto lo usa
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Authenticate with NPM
|
||||
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
|
||||
|
||||
- name: Publish to NPM
|
||||
run: npm publish --access public
|
||||
22
.github/workflows/unpublish-npm.yml
vendored
Normal file
22
.github/workflows/unpublish-npm.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Delete npm version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Versión a eliminar'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
delete:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Unpublish version
|
||||
run: npm unpublish sigpro@${{ github.event.inputs.version }} --force
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
53
Readme.md
53
Readme.md
@@ -1,15 +1,12 @@
|
||||
# `SigPro` ⚛️
|
||||
Blazing fast, zero-overhead, vanilla JS renderer with atomic reactivity.
|
||||
|
||||
# `SigPro`
|
||||
|
||||
[](https://www.npmjs.com/package/sigpro)
|
||||
[](https://bundlephobia.com/package/sigpro)
|
||||
[](https://github.com/natxocc/sigpro)
|
||||

|
||||
[](https://github.com/natxocc/sigpro/blob/main/LICENSE)
|
||||
|
||||
### **The Atomic Reactivity Engine for the Modern Web.**
|
||||
|
||||
**SigPro** is an ultra-lightweight rendering engine designed for extreme performance. By eliminating the Virtual DOM and heavy compilers, it achieves **surgical reactivity** in less than 2KB.
|
||||
|
||||
[**Explore the Docs →**](https://natxocc.github.io/sigpro/) | [**View on GitHub**](https://github.com/natxocc/sigpro)
|
||||
[**Explore the Docs →**](https://sigpro.natxocc.com/#/)
|
||||
|
||||
---
|
||||
|
||||
@@ -19,23 +16,23 @@ After years of building within closed ecosystems like **React, Vue, or Svelte**
|
||||
|
||||
That extra development time and the cognitive load of "learning the framework" instead of "learning the language" is exactly what **SigPro** eliminates. If the final destination is always JS, why not use a Pure JS-based system that drastically simplifies coding with a readable, vanilla, and remarkably fast architecture?
|
||||
|
||||
* 🎯 **Atomic Precision:** Powered by a *Signal-based* architecture. State is bound directly to DOM nodes—when a value changes, **only that specific node updates**.
|
||||
* 🚀 **Zero-Hydration Bottlenecks:** No 100KB bundles or complex build steps. SigPro is pure, optimized JavaScript tailored for the browser's native engine.
|
||||
* 🍦 **Pure Vanilla JS Performance:** High-octane performance without the need for transpilers or heavy transformations. It runs natively in the browser just as well as it does in complex build pipelines.
|
||||
* 🛠️ **Build-Tool Agnostic:** Total freedom. Use it with **Vite, Webpack, or Rollup** for enterprise projects, or simply import it via a **`<script>` tag** for rapid prototyping. No tooling required.
|
||||
* 🚀 **Vite-Powered DX:** First-class Vite support with **file-based routing** out of the box. The official `sigpro/vite` plugin automatically scans your `src/pages` directory and generates reactive routes—no manual route configuration needed.
|
||||
* 📈 **Zero-Scale Bloat:** Unlike other frameworks where the bundle grows exponentially, SigPro's footprint remains **flat and predictable**. You only pay for the code you write.
|
||||
* 💎 **Premium DX (Developer Experience):** Forget boilerplate imports. SigPro injects an elegant, functional syntax (`Div()`, `Button()`, `Span()`) directly into your scope for a **"Zero-Import"** workflow.
|
||||
* 📦 **Fully Loaded:** Built-in Hash Routing, native **`localStorage` persistence**, and automatic lifecycle management (cleanups) included in less than 2KB.
|
||||
* 🌳 **Tree-Shakable:** Optimized for modern bundlers. Import only what you use, or load the full engine for rapid prototyping.
|
||||
* **Atomic Precision:** Powered by a *Signal-based* architecture. State is bound directly to DOM nodes—when a value changes, **only that specific node updates**.
|
||||
* **Zero-Hydration Bottlenecks:** No 100KB bundles or complex build steps. SigPro is pure, optimized JavaScript tailored for the browser's native engine.
|
||||
* **Pure Vanilla JS Performance:** High-octane performance without the need for transpilers or heavy transformations. It runs natively in the browser just as well as it does in complex build pipelines.
|
||||
* **Build-Tool Agnostic:** Total freedom. Use it with **Vite, Webpack, or Rollup** for enterprise projects, or simply import it via a **`<script>` tag** for rapid prototyping. No tooling required.
|
||||
* **Vite-Powered DX:** First-class Vite support with **file-based routing** out of the box. The official `sigpro/vite` plugin automatically scans your `src/pages` directory and generates reactive routes—no manual route configuration needed.
|
||||
* **Zero-Scale Bloat:** Unlike other frameworks where the bundle grows exponentially, SigPro's footprint remains **flat and predictable**. You only pay for the code you write.
|
||||
* **Premium DX (Developer Experience):** Forget boilerplate imports. SigPro injects an elegant, functional syntax (`div()`, `button()`, `span()`) directly into your scope for a **"Zero-Import"** workflow.
|
||||
* **Fully Loaded:** Built-in Hash Routing, native **`localStorage` persistence**, and automatic lifecycle management (cleanups) included in less than 2KB.
|
||||
* **Tree-Shakable:** Optimized for modern bundlers. Import only what you use, or load the full engine for rapid prototyping.
|
||||
|
||||
-----
|
||||
|
||||
## ⚡ Real-World Benchmarks
|
||||
## Real-World Benchmarks
|
||||
|
||||
SigPro isn't just "fast on paper." In the industry-standard **JS Framework Benchmark**, it consistently outperforms the most popular libraries by operating at near-native speeds with almost zero memory overhead.
|
||||
|
||||
### 🚀 Execution Speed (CPU)
|
||||
### Execution Speed (CPU)
|
||||
*Lower is better. Measured in milliseconds (ms).*
|
||||
|
||||
| Benchmark Test | **SigPro** | SolidJS | Vue 3 | React 18 |
|
||||
@@ -44,7 +41,7 @@ SigPro isn't just "fast on paper." In the industry-standard **JS Framework Bench
|
||||
| **Direct Selection** (on click) | **17.5ms** | ~18ms | ~32ms | ~65ms |
|
||||
| **Initial Render** (1k rows) | **~35ms** | ~32ms | ~45ms | ~70ms |
|
||||
|
||||
### 🧠 Memory Footprint
|
||||
### Memory Footprint
|
||||
*Lower is better. Measured in Megabytes (MB) after 1k rows.*
|
||||
|
||||
| Metric | **SigPro** | Vanilla JS | Svelte | React |
|
||||
@@ -65,6 +62,8 @@ Create reactive, persistent components with a syntax that feels like Vanilla JS,
|
||||
```
|
||||
|
||||
```javascript
|
||||
import { $, mount } from "sigpro";
|
||||
|
||||
const Counter = () => {
|
||||
// Simple signal
|
||||
const value = $(100);
|
||||
@@ -74,14 +73,14 @@ const Counter = () => {
|
||||
const doubleValue = $(()=> value() * count());
|
||||
|
||||
// Create fast HTML with pure JS
|
||||
return Div({ class: "card" }, [
|
||||
H1(`Count: ${count()}, Reference: ${value()}, Double x Ref: ${doubleValue()}`),
|
||||
P("Atomic updates. Zero re-renders of the parent tree."),
|
||||
Button({ onclick: () => count(c => c + 1)}, "Increment +1")
|
||||
return div({ class: "card" }, [
|
||||
h1(() => `Count: ${count()}, Reference: ${value()}, Double x Ref: ${doubleValue()}`),
|
||||
p("Atomic updates. Zero re-renders of the parent tree."),
|
||||
button({ onclick: () => count(c => c + 1)}, "Increment +1")
|
||||
]);
|
||||
};
|
||||
|
||||
Mount(Counter, "#app");
|
||||
mount(Counter, "#app");
|
||||
```
|
||||
|
||||
-----
|
||||
@@ -90,7 +89,7 @@ Mount(Counter, "#app");
|
||||
|
||||
| Feature | **SigPro** | React / Vue | Svelte |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Payload (Gzipped)** | **< 1.8KB** | ~30KB - 50KB | ~5KB (Compiled Runtime) |
|
||||
| **Payload (Gzipped)** | **<3KB** | ~30KB - 50KB | ~5KB (Compiled Runtime) |
|
||||
| **State Logic** | **Atomic Signals** | Virtual DOM Diffing | Compiler Dirty Bits |
|
||||
| **Update Speed** | **Direct Node Access** | Component Re-render | Block Reconciliation |
|
||||
| **Native Persistence** | **Included ($)** | Requires Plugins | Manual |
|
||||
@@ -123,7 +122,7 @@ src/
|
||||
```javascript
|
||||
// vite.config.js
|
||||
import { defineConfig } from 'vite';
|
||||
import { sigproRouter } from 'sigpro/vite';
|
||||
import { sigproRouter } from 'sigpro/router';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sigproRouter()]
|
||||
|
||||
1
dist/sigpro.db.js
vendored
Normal file
1
dist/sigpro.db.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
var o=async(e,n={},t=null)=>{if(t)t(!0);try{let r=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n),credentials:"include"});if(!r.ok){let s=await r.text();throw Error(`Error ${r.status}: ${s}`)}return await r.json()}finally{if(t)t(!1)}};export{o as db};
|
||||
1
dist/sigpro.editor.js
vendored
Normal file
1
dist/sigpro.editor.js
vendored
Normal file
File diff suppressed because one or more lines are too long
439
dist/sigpro.esm.js
vendored
439
dist/sigpro.esm.js
vendored
@@ -1,439 +0,0 @@
|
||||
// sigpro.js
|
||||
var activeEffect = null;
|
||||
var currentOwner = null;
|
||||
var effectQueue = new Set;
|
||||
var isFlushing = false;
|
||||
var MOUNTED_NODES = new WeakMap;
|
||||
var doc = document;
|
||||
var isArr = Array.isArray;
|
||||
var assign = Object.assign;
|
||||
var createEl = (t) => doc.createElement(t);
|
||||
var createText = (t) => doc.createTextNode(String(t ?? ""));
|
||||
var isFunc = (f) => typeof f === "function";
|
||||
var isObj = (o) => typeof o === "object" && o !== null;
|
||||
var runWithContext = (effect, callback) => {
|
||||
const previousEffect = activeEffect;
|
||||
activeEffect = effect;
|
||||
try {
|
||||
return callback();
|
||||
} finally {
|
||||
activeEffect = previousEffect;
|
||||
}
|
||||
};
|
||||
var cleanupNode = (node) => {
|
||||
if (node._cleanups) {
|
||||
node._cleanups.forEach((dispose) => dispose());
|
||||
node._cleanups.clear();
|
||||
}
|
||||
node.childNodes?.forEach(cleanupNode);
|
||||
};
|
||||
var flushEffects = () => {
|
||||
if (isFlushing)
|
||||
return;
|
||||
isFlushing = true;
|
||||
while (effectQueue.size > 0) {
|
||||
const sortedEffects = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
|
||||
effectQueue.clear();
|
||||
for (const effect of sortedEffects) {
|
||||
if (!effect._deleted)
|
||||
effect();
|
||||
}
|
||||
}
|
||||
isFlushing = false;
|
||||
};
|
||||
var trackSubscription = (subscribers) => {
|
||||
if (activeEffect && !activeEffect._deleted) {
|
||||
subscribers.add(activeEffect);
|
||||
activeEffect._deps.add(subscribers);
|
||||
}
|
||||
};
|
||||
var triggerUpdate = (subscribers) => {
|
||||
subscribers.forEach((effect) => {
|
||||
if (effect === activeEffect || effect._deleted)
|
||||
return;
|
||||
if (effect._isComputed) {
|
||||
effect.markDirty();
|
||||
if (effect._subs)
|
||||
triggerUpdate(effect._subs);
|
||||
} else {
|
||||
effectQueue.add(effect);
|
||||
}
|
||||
});
|
||||
if (!isFlushing)
|
||||
queueMicrotask(flushEffects);
|
||||
};
|
||||
var Render = (renderFn) => {
|
||||
const cleanups = new Set;
|
||||
const previousOwner = currentOwner;
|
||||
const container = createEl("div");
|
||||
container.style.display = "contents";
|
||||
currentOwner = { cleanups };
|
||||
const processResult = (result) => {
|
||||
if (!result)
|
||||
return;
|
||||
if (result._isRuntime) {
|
||||
cleanups.add(result.destroy);
|
||||
container.appendChild(result.container);
|
||||
} else if (isArr(result)) {
|
||||
result.forEach(processResult);
|
||||
} else {
|
||||
container.appendChild(result instanceof Node ? result : createText(result));
|
||||
}
|
||||
};
|
||||
try {
|
||||
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
|
||||
} finally {
|
||||
currentOwner = previousOwner;
|
||||
}
|
||||
return {
|
||||
_isRuntime: true,
|
||||
container,
|
||||
destroy: () => {
|
||||
cleanups.forEach((fn) => fn());
|
||||
cleanupNode(container);
|
||||
container.remove();
|
||||
}
|
||||
};
|
||||
};
|
||||
var $ = (initialValue, storageKey = null) => {
|
||||
const subscribers = new Set;
|
||||
if (isFunc(initialValue)) {
|
||||
let cachedValue, isDirty = true;
|
||||
const effect = () => {
|
||||
if (effect._deleted)
|
||||
return;
|
||||
effect._deps.forEach((dep) => dep.delete(effect));
|
||||
effect._deps.clear();
|
||||
runWithContext(effect, () => {
|
||||
const newValue = initialValue();
|
||||
if (!Object.is(cachedValue, newValue) || isDirty) {
|
||||
cachedValue = newValue;
|
||||
isDirty = false;
|
||||
triggerUpdate(subscribers);
|
||||
}
|
||||
});
|
||||
};
|
||||
assign(effect, {
|
||||
_deps: new Set,
|
||||
_isComputed: true,
|
||||
_subs: subscribers,
|
||||
_deleted: false,
|
||||
markDirty: () => isDirty = true,
|
||||
stop: () => {
|
||||
effect._deleted = true;
|
||||
effect._deps.forEach((dep) => dep.delete(effect));
|
||||
subscribers.clear();
|
||||
}
|
||||
});
|
||||
if (currentOwner)
|
||||
currentOwner.cleanups.add(effect.stop);
|
||||
return () => {
|
||||
if (isDirty)
|
||||
effect();
|
||||
trackSubscription(subscribers);
|
||||
return cachedValue;
|
||||
};
|
||||
}
|
||||
let value = initialValue;
|
||||
if (storageKey) {
|
||||
try {
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (saved !== null)
|
||||
value = JSON.parse(saved);
|
||||
} catch (e) {
|
||||
console.warn("SigPro Storage Lock", e);
|
||||
}
|
||||
}
|
||||
return (...args) => {
|
||||
if (args.length) {
|
||||
const nextValue = isFunc(args[0]) ? args[0](value) : args[0];
|
||||
if (!Object.is(value, nextValue)) {
|
||||
value = nextValue;
|
||||
if (storageKey)
|
||||
localStorage.setItem(storageKey, JSON.stringify(value));
|
||||
triggerUpdate(subscribers);
|
||||
}
|
||||
}
|
||||
trackSubscription(subscribers);
|
||||
return value;
|
||||
};
|
||||
};
|
||||
var $$ = (object, cache = new WeakMap) => {
|
||||
if (!isObj(object))
|
||||
return object;
|
||||
if (cache.has(object))
|
||||
return cache.get(object);
|
||||
const keySubscribers = {};
|
||||
const proxy = new Proxy(object, {
|
||||
get(target, key) {
|
||||
if (activeEffect)
|
||||
trackSubscription(keySubscribers[key] ??= new Set);
|
||||
const value = Reflect.get(target, key);
|
||||
return isObj(value) ? $$(value, cache) : value;
|
||||
},
|
||||
set(target, key, value) {
|
||||
if (Object.is(target[key], value))
|
||||
return true;
|
||||
const success = Reflect.set(target, key, value);
|
||||
if (keySubscribers[key])
|
||||
triggerUpdate(keySubscribers[key]);
|
||||
return success;
|
||||
}
|
||||
});
|
||||
cache.set(object, proxy);
|
||||
return proxy;
|
||||
};
|
||||
var Watch = (target, callbackFn) => {
|
||||
const isExplicit = isArr(target);
|
||||
const callback = isExplicit ? callbackFn : target;
|
||||
if (!isFunc(callback))
|
||||
return () => {};
|
||||
const owner = currentOwner;
|
||||
const runner = () => {
|
||||
if (runner._deleted)
|
||||
return;
|
||||
runner._deps.forEach((dep) => dep.delete(runner));
|
||||
runner._deps.clear();
|
||||
runner._cleanups.forEach((cleanup) => cleanup());
|
||||
runner._cleanups.clear();
|
||||
const previousOwner = currentOwner;
|
||||
runner.depth = activeEffect ? activeEffect.depth + 1 : 0;
|
||||
runWithContext(runner, () => {
|
||||
currentOwner = { cleanups: runner._cleanups };
|
||||
if (isExplicit) {
|
||||
runWithContext(null, callback);
|
||||
target.forEach((dep) => isFunc(dep) && dep());
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
currentOwner = previousOwner;
|
||||
});
|
||||
};
|
||||
assign(runner, {
|
||||
_deps: new Set,
|
||||
_cleanups: new Set,
|
||||
_deleted: false,
|
||||
stop: () => {
|
||||
if (runner._deleted)
|
||||
return;
|
||||
runner._deleted = true;
|
||||
effectQueue.delete(runner);
|
||||
runner._deps.forEach((dep) => dep.delete(runner));
|
||||
runner._cleanups.forEach((cleanup) => cleanup());
|
||||
if (owner)
|
||||
owner.cleanups.delete(runner.stop);
|
||||
}
|
||||
});
|
||||
if (owner)
|
||||
owner.cleanups.add(runner.stop);
|
||||
runner();
|
||||
return runner.stop;
|
||||
};
|
||||
var Tag = (tag, props = {}, children = []) => {
|
||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||
children = props;
|
||||
props = {};
|
||||
}
|
||||
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
|
||||
const element = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : createEl(tag);
|
||||
element._cleanups = new Set;
|
||||
element.onUnmount = (fn) => element._cleanups.add(fn);
|
||||
const booleanAttributes = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
|
||||
const updateAttribute = (name, value) => {
|
||||
const sanitized = (name === "src" || name === "href") && String(value).toLowerCase().includes("javascript:") ? "#" : value;
|
||||
if (booleanAttributes.includes(name)) {
|
||||
element[name] = !!sanitized;
|
||||
sanitized ? element.setAttribute(name, "") : element.removeAttribute(name);
|
||||
} else {
|
||||
sanitized == null ? element.removeAttribute(name) : element.setAttribute(name, sanitized);
|
||||
}
|
||||
};
|
||||
for (let [key, value] of Object.entries(props)) {
|
||||
if (key === "ref") {
|
||||
isFunc(value) ? value(element) : value.current = element;
|
||||
continue;
|
||||
}
|
||||
const isSignal = isFunc(value);
|
||||
if (key.startsWith("on")) {
|
||||
const eventName = key.slice(2).toLowerCase().split(".")[0];
|
||||
element.addEventListener(eventName, value);
|
||||
element._cleanups.add(() => element.removeEventListener(eventName, value));
|
||||
} else if (isSignal) {
|
||||
element._cleanups.add(Watch(() => {
|
||||
const currentVal = value();
|
||||
key === "class" ? element.className = currentVal || "" : updateAttribute(key, currentVal);
|
||||
}));
|
||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(element.tagName) && (key === "value" || key === "checked")) {
|
||||
const event = key === "checked" ? "change" : "input";
|
||||
const handler = (e) => value(e.target[key]);
|
||||
element.addEventListener(event, handler);
|
||||
element._cleanups.add(() => element.removeEventListener(event, handler));
|
||||
}
|
||||
} else {
|
||||
updateAttribute(key, value);
|
||||
}
|
||||
}
|
||||
const appendChildNode = (child) => {
|
||||
if (isArr(child))
|
||||
return child.forEach(appendChildNode);
|
||||
if (isFunc(child)) {
|
||||
const marker = createText("");
|
||||
element.appendChild(marker);
|
||||
let currentNodes = [];
|
||||
element._cleanups.add(Watch(() => {
|
||||
const result = child();
|
||||
const nextNodes = (isArr(result) ? result : [result]).map((node) => node?._isRuntime ? node.container : node instanceof Node ? node : createText(node));
|
||||
currentNodes.forEach((node) => {
|
||||
cleanupNode(node);
|
||||
node.remove();
|
||||
});
|
||||
nextNodes.forEach((node) => marker.parentNode?.insertBefore(node, marker));
|
||||
currentNodes = nextNodes;
|
||||
}));
|
||||
} else {
|
||||
element.appendChild(child instanceof Node ? child : createText(child));
|
||||
}
|
||||
};
|
||||
appendChildNode(children);
|
||||
return element;
|
||||
};
|
||||
var If = (condition, thenVal, otherwiseVal = null, transition = null) => {
|
||||
const marker = createText("");
|
||||
const container = Tag("div", { style: "display:contents" }, [marker]);
|
||||
let currentView = null, lastState = null;
|
||||
Watch(() => {
|
||||
const state = !!(isFunc(condition) ? condition() : condition);
|
||||
if (state === lastState)
|
||||
return;
|
||||
lastState = state;
|
||||
const dispose = () => {
|
||||
if (currentView)
|
||||
currentView.destroy();
|
||||
currentView = null;
|
||||
};
|
||||
if (currentView && !state && transition?.out) {
|
||||
transition.out(currentView.container, dispose);
|
||||
} else {
|
||||
dispose();
|
||||
}
|
||||
const branch = state ? thenVal : otherwiseVal;
|
||||
if (branch) {
|
||||
currentView = Render(() => isFunc(branch) ? branch() : branch);
|
||||
container.insertBefore(currentView.container, marker);
|
||||
if (state && transition?.in)
|
||||
transition.in(currentView.container);
|
||||
}
|
||||
});
|
||||
return container;
|
||||
};
|
||||
var For = (source, renderFn, keyFn, tag = "div", props = { style: "display:contents" }) => {
|
||||
const marker = createText("");
|
||||
const container = Tag(tag, props, [marker]);
|
||||
let viewCache = new Map;
|
||||
Watch(() => {
|
||||
const items = (isFunc(source) ? source() : source) || [];
|
||||
const nextCache = new Map;
|
||||
const order = [];
|
||||
for (let i = 0;i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const key = keyFn ? keyFn(item, i) : i;
|
||||
let view = viewCache.get(key);
|
||||
if (!view) {
|
||||
const result = renderFn(item, i);
|
||||
view = result instanceof Node ? { container: result, destroy: () => {
|
||||
cleanupNode(result);
|
||||
result.remove();
|
||||
} } : Render(() => result);
|
||||
}
|
||||
viewCache.delete(key);
|
||||
nextCache.set(key, view);
|
||||
order.push(key);
|
||||
}
|
||||
viewCache.forEach((v) => v.destroy());
|
||||
let anchor = marker;
|
||||
for (let i = order.length - 1;i >= 0; i--) {
|
||||
const view = nextCache.get(order[i]);
|
||||
if (view.container.nextSibling !== anchor) {
|
||||
container.insertBefore(view.container, anchor);
|
||||
}
|
||||
anchor = view.container;
|
||||
}
|
||||
viewCache = nextCache;
|
||||
});
|
||||
return container;
|
||||
};
|
||||
var Router = (routes) => {
|
||||
const currentPath = $(window.location.hash.replace(/^#/, "") || "/");
|
||||
window.addEventListener("hashchange", () => currentPath(window.location.hash.replace(/^#/, "") || "/"));
|
||||
const outlet = Tag("div", { class: "router-transition" });
|
||||
let currentView = null;
|
||||
Watch([currentPath], async () => {
|
||||
const path = currentPath();
|
||||
const route = routes.find((r) => {
|
||||
const routeParts = r.path.split("/").filter(Boolean);
|
||||
const pathParts = path.split("/").filter(Boolean);
|
||||
return routeParts.length === pathParts.length && routeParts.every((part, i) => part.startsWith(":") || part === pathParts[i]);
|
||||
}) || routes.find((r) => r.path === "*");
|
||||
if (route) {
|
||||
let component = route.component;
|
||||
if (isFunc(component) && component.toString().includes("import")) {
|
||||
component = (await component()).default || await component();
|
||||
}
|
||||
const params = {};
|
||||
route.path.split("/").filter(Boolean).forEach((part, i) => {
|
||||
if (part.startsWith(":"))
|
||||
params[part.slice(1)] = path.split("/").filter(Boolean)[i];
|
||||
});
|
||||
if (currentView)
|
||||
currentView.destroy();
|
||||
if (Router.params)
|
||||
Router.params(params);
|
||||
currentView = Render(() => {
|
||||
try {
|
||||
return isFunc(component) ? component(params) : component;
|
||||
} catch (e) {
|
||||
return Tag("div", { class: "p-4 text-error" }, "Error loading view");
|
||||
}
|
||||
});
|
||||
outlet.appendChild(currentView.container);
|
||||
}
|
||||
});
|
||||
return outlet;
|
||||
};
|
||||
Router.params = $({});
|
||||
Router.to = (path) => window.location.hash = path.replace(/^#?\/?/, "#/");
|
||||
Router.back = () => window.history.back();
|
||||
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||
var Mount = (component, target) => {
|
||||
const targetEl = typeof target === "string" ? doc.querySelector(target) : target;
|
||||
if (!targetEl)
|
||||
return;
|
||||
if (MOUNTED_NODES.has(targetEl))
|
||||
MOUNTED_NODES.get(targetEl).destroy();
|
||||
const instance = Render(isFunc(component) ? component : () => component);
|
||||
targetEl.replaceChildren(instance.container);
|
||||
MOUNTED_NODES.set(targetEl, instance);
|
||||
return instance;
|
||||
};
|
||||
var SigPro = { $, $$, Render, Watch, Tag, If, For, Router, Mount };
|
||||
if (typeof window !== "undefined") {
|
||||
assign(window, SigPro);
|
||||
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(" ");
|
||||
tags.forEach((tag) => {
|
||||
const helper = tag[0].toUpperCase() + tag.slice(1);
|
||||
if (!(helper in window))
|
||||
window[helper] = (p, c) => Tag(tag, p, c);
|
||||
});
|
||||
window.SigPro = Object.freeze(SigPro);
|
||||
}
|
||||
export {
|
||||
Watch,
|
||||
Tag,
|
||||
Router,
|
||||
Render,
|
||||
Mount,
|
||||
If,
|
||||
For,
|
||||
$$,
|
||||
$
|
||||
};
|
||||
1
dist/sigpro.esm.min.js
vendored
1
dist/sigpro.esm.min.js
vendored
File diff suppressed because one or more lines are too long
78
dist/sigpro.grid.js
vendored
Normal file
78
dist/sigpro.grid.js
vendored
Normal file
File diff suppressed because one or more lines are too long
473
dist/sigpro.js
vendored
473
dist/sigpro.js
vendored
File diff suppressed because one or more lines are too long
1
dist/sigpro.locale.js
vendored
Normal file
1
dist/sigpro.locale.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
var{$:c}=window.SigPro,s=c("en"),n={},e=(t)=>{for(let o of Object.keys(t)){if(!n[o])n[o]={};Object.assign(n[o],t[o])}},r=(t)=>{if(t&&n[t])s(t)},a=(t)=>{return()=>n[s()]?.[t]??t};export{a as t,r as setLocale,e as addLang};
|
||||
1
dist/sigpro.min.js
vendored
1
dist/sigpro.min.js
vendored
File diff suppressed because one or more lines are too long
1
dist/sigpro.router.js
vendored
Normal file
1
dist/sigpro.router.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
var{$:p,h:u,watch:m,render:g,isF:y}=window.SigPro,d=()=>window.location.hash.slice(1)||"/",s=p(d());window.addEventListener("hashchange",()=>s(d()));var w=p({}),c=(n)=>{let i=u("div",{class:"router-hook"}),r=null;return m([s],()=>{let l=s(),t=n.find((o)=>{let e=o.path.split("/").filter(Boolean),a=l.split("/").filter(Boolean);return e.length===a.length&&e.every((h,f)=>h[0]===":"||h===a[f])})||n.find((o)=>o.path==="*");if(t){r?.destroy();let o={};t.path.split("/").filter(Boolean).forEach((e,a)=>{if(e[0]===":")o[e.slice(1)]=l.split("/").filter(Boolean)[a]}),w(o),r=g(()=>y(t.component)?t.component(o):t.component),i.replaceChildren(r.container)}}),i.destroy=()=>{r?.destroy()},i};c.params=w;c.to=(n)=>window.location.hash=n.replace(/^#?\/?/,"#/");c.back=()=>window.history.back();c.path=()=>s();export{w as routerParams,c as router};
|
||||
2
dist/sigpro.ui.css
vendored
Normal file
2
dist/sigpro.ui.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/sigpro.ui.js
vendored
Normal file
1
dist/sigpro.ui.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/sigpro.vite.js
vendored
Normal file
4
dist/sigpro.vite.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
function g(){let u="\x00virtual:sigpro-routes",i=(e)=>{if(!fs.existsSync(e))return[];return fs.readdirSync(e,{recursive:!0}).filter((r)=>/\.(js|jsx)$/.test(r)&&!path.basename(r).startsWith("_")).map((r)=>path.resolve(e,r))},l=(e,r)=>{return("/"+path.relative(e,r).replace(/\\/g,"/").replace(/\.(js|jsx)$/,"").replace(/\/index$/,"").replace(/^index$/,"")).replace(/\/+/g,"/").replace(/\[\.\.\.([^\]]+)\]/g,"*").replace(/\[([^\]]+)\]/g,":$1").replace(/\/$/,"")||"/"};return{name:"sigpro-router",resolveId(e){if(e==="virtual:sigpro-routes")return u},load(e){if(e!==u)return;let r=process.cwd(),t=path.resolve(r,"src/pages"),p=i(t).sort((n,a)=>{let o=l(t,n),c=l(t,a);if(o.includes(":")&&!c.includes(":"))return 1;if(!o.includes(":")&&c.includes(":"))return-1;return c.length-o.length}),s="";if(p.forEach((n)=>{let a=l(t,n),o="./"+path.relative(r,n).replace(/\\/g,"/");s+=` { path: '${a}', component: () => import('/${o}') },
|
||||
`}),!s.includes("path: '*'"))s+=` { path: '*', component: () => ({ default: () => document.createTextNode('404 - Not Found') }) },
|
||||
`;return`export const routes = [
|
||||
${s}];`}}}export{g as sigproRouter};
|
||||
@@ -1,42 +1,38 @@
|
||||
<div class="w-full -mt-10"><section class="relative py-20 overflow-hidden border-b border-base-200/30 text-center flex flex-col items-center"><div class="relative z-10 max-w-5xl mx-auto px-6 flex flex-col items-center"><div class="flex justify-center mb-10"><img src="logo.svg" alt="SigPro Logo" class="w-48 h-48 md:w-64 md:h-64 object-contain drop-shadow-2xl"></div><h1 class="text-7xl md:text-9xl font-black tracking-tighter mb-4 bg-clip-text text-transparent bg-linear-to-r from-primary via-secondary to-accent !text-center w-full">SigPro</h1><div class="text-2xl md:text-3xl font-bold text-base-content/90 mb-4 !text-center w-full">Atomic Unified Reactive Engine</div><div class="text-xl text-base-content/60 max-w-3xl mx-auto mb-10 leading-relaxed italic text-balance font-light !text-center w-full">"The efficiency of direct DOM manipulation with the elegance of functional reactivity."</div><div class="flex flex-wrap justify-center gap-4 w-full"><a href="#/install" class="btn btn-primary btn-lg shadow-xl shadow-primary/20 group px-10 border-none">Get Started <span class="group-hover:translate-x-1 transition-transform inline-block">→</span></a><button onclick="window.open('https://github.com/natxocc/sigpro')" class="btn btn-outline btn-lg border-base-300 hover:bg-base-300 hover:text-base-content">GitHub</button></div></div><div class="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full -z-0 opacity-10 pointer-events-none"><div class="absolute top-10 left-1/4 w-96 h-96 bg-primary filter blur-3xl rounded-full animate-pulse"></div><div class="absolute bottom-10 right-1/4 w-96 h-96 bg-accent filter blur-3xl rounded-full animate-pulse" style="animation-delay: 2.5s"></div></div></section></div>
|
||||
<div class="w-full -mt-10"><section class="relative py-20 overflow-hidden border-b border-base-200/30 text-center flex flex-col items-center"><div class="relative z-10 max-w-5xl mx-auto px-6 flex flex-col items-center"><div class="flex justify-center mb-10"><img src="logo.svg" alt="SigPro Logo" class="w-48 h-48 md:w-64 md:h-64 object-contain drop-shadow-2xl"></div><h1 class="text-7xl md:text-9xl font-black tracking-tighter mb-4 bg-clip-text text-transparent bg-linear-to-r from-primary via-secondary to-accent !text-center w-full">SigPro</h1><div class="text-2xl md:text-3xl font-bold text-base-content/90 mb-4 !text-center w-full">Atomic Unified Reactive Engine</div><div class="text-xl text-base-content/60 max-w-3xl mx-auto mb-10 leading-relaxed italic text-balance font-light !text-center w-full">"The efficiency of direct DOM manipulation with the elegance of functional reactivity."</div><div class="flex flex-wrap justify-center gap-4 w-full"><a href="#/install" class="btn btn-primary btn-lg shadow-xl shadow-primary/20 group px-10 border-none">Get Started <span class="group-hover:translate-x-1 transition-transform inline-block">→</span></a><button onclick="window.open('https://git.natxocc.com/natxocc/sigpro')" class="btn btn-outline btn-lg border-base-300 hover:bg-base-300 hover:text-base-content">Gitea</button></div></div><div class="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full -z-0 opacity-10 pointer-events-none"><div class="absolute top-10 left-1/4 w-96 h-96 bg-primary filter blur-3xl rounded-full animate-pulse"></div><div class="absolute bottom-10 right-1/4 w-96 h-96 bg-accent filter blur-3xl rounded-full animate-pulse" style="animation-delay: 2.5s"></div></div></section></div>
|
||||
|
||||
<section class="max-w-6xl mx-auto px-6 py-16"><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 items-stretch"><div class="card bg-base-200/30 border border-base-300 hover:border-primary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-primary italic">FUNCTIONAL</h3><p class="text-sm opacity-70">No strings. No templates. Pure JS function calls for instant DOM mounting.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-secondary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-secondary italic">ATOMIC</h3><p class="text-sm opacity-70">Fine-grained signals update exactly what changes. No V-DOM diffing overhead.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-accent/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-accent italic">ULTRA-THIN</h3><p class="text-sm opacity-70">Sub-2KB runtime. Infinitely smaller bundle than React, Vue or even Svelte.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-base-content/20 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black italic text-base-content">COMPILER-FREE</h3><p class="text-sm opacity-70">Standard Vanilla JS. What you write is what the browser executes. Period.</p></div></div></div></section></div>
|
||||
<section class="max-w-6xl mx-auto px-6 py-16"><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 items-stretch"><div class="card bg-base-200/30 border border-base-300 hover:border-primary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-primary italic">FUNCTIONAL</h3><p class="text-sm opacity-70">No strings. No templates. Pure JS function calls for instant DOM mounting.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-secondary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-secondary italic">ATOMIC</h3><p class="text-sm opacity-70">Fine‑grained signals update exactly what changes. No V‑DOM diffing overhead.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-accent/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-accent italic">ULTRA‑THIN</h3><p class="text-sm opacity-70">less than 3KB runtime. Infinitely smaller bundle than React, Vue or even Svelte.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-base-content/20 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black italic text-base-content">COMPILER‑FREE</h3><p class="text-sm opacity-70">Standard Vanilla JS. What you write is what the browser executes. Period.</p></div></div></div></section>
|
||||
|
||||
## Functional DOM Construction
|
||||
<div class="max-w-6xl mx-auto px-6 py-8"><h2 class="text-4xl font-black mb-6">Functional DOM Construction</h2><p class="text-lg opacity-80 mb-6">SigPro replaces slow "Template Parsing" with <strong>High‑Efficiency Function Calls</strong>. While other frameworks force the browser to parse strings of HTML or execute complex JSX transformations, SigPro uses a direct functional approach.</p>
|
||||
|
||||
SigPro replaces the slow "Template Parsing" with **High-Efficiency Function Calls**.
|
||||
<table class="table w-full mb-12">
|
||||
<thead><tr><th>Feature</th><th>Standard HTML / JSX</th><th>SigPro Functional</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Syntax</td><td><code><div>Hello</div></code></td><td><code>div("Hello")</code> (or <code>h('div', "Hello")</code>)</td></tr>
|
||||
<tr><td>Processing</td><td>Parse → Diff → Patch</td><td>Direct API Call</td></tr>
|
||||
<tr><td>Overhead</td><td>High (V‑DOM / Parser)</td><td><strong>Zero</strong></td></tr>
|
||||
<tr><td>Reactivity</td><td>Component‑wide</td><td><strong>Atomic (Node‑level)</strong></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
While other frameworks force the browser to parse strings of HTML or execute complex JSX transformations, SigPro uses a direct functional approach:
|
||||
<h3 class="text-2xl font-bold mt-8 mb-4">Less Code, More Power</h3>
|
||||
<p class="mb-4">By sharing a minuscule runtime, your final application bundle is <strong>infinitely smaller</strong>.</p>
|
||||
<ul class="list-disc pl-6 space-y-2 mb-8">
|
||||
<li><strong>React/Vue:</strong> You ship a heavy orchestrator (~30‑90KB) just to say "Hello World".</li>
|
||||
<li><strong>Solid/Svelte:</strong> You still depend on a compilation step that generates extra boilerplate.</li>
|
||||
<li><strong>SigPro:</strong> You ship <strong>Pure Vanilla JS</strong>. The runtime is so small that it often costs less than a single high‑res icon.</li>
|
||||
</ul>
|
||||
|
||||
| Feature | Standard HTML / JSX | SigPro Functional |
|
||||
| :--- | :--- | :--- |
|
||||
| **Syntax** | `<div>Hello</div>` | `Div("Hello")` |
|
||||
| **Processing** | Parse → Diff → Patch | Direct API Call |
|
||||
| **Overhead** | High (V-DOM / Parser) | **Zero** |
|
||||
| **Reactivity** | Component-wide | **Atomic (Node-level)** |
|
||||
<h3 class="text-2xl font-bold mt-10 mb-4">Precision Engineering</h3>
|
||||
<h4 class="text-xl font-semibold mt-6 mb-2">1. Functional Efficiency</h4>
|
||||
<p><code>div()</code>, <code>button()</code>, <code>span()</code>… These aren't just wrappers; they are pre‑optimized constructors. When you call <code>div({ class: 'btn' }, "Click")</code>, SigPro creates the element and attaches its reactive listeners in a single, surgical operation.</p>
|
||||
|
||||
### Less Code, More Power
|
||||
By sharing a miniscule runtime, your final application bundle is **infinitely smaller**.
|
||||
<h4 class="text-xl font-semibold mt-6 mb-2">2. The "No‑Bundle" Bundle</h4>
|
||||
<p>Because SigPro is so small, it is the only engine where <strong>the more code you write, the more the efficiency gap grows</strong>. While others grow linearly with components and framework overhead, SigPro stays flat, leveraging the native power of modern browser engines.</p>
|
||||
|
||||
* **React/Vue:** You ship a heavy orchestrator (~30-90KB) just to say "Hello World".
|
||||
* **Solid/Svelte:** You still depend on a compilation step that generates extra boilerplate.
|
||||
* **SigPro:** You ship **Pure Vanilla JS**. The runtime is so small that it often costs less than a single high-res icon.
|
||||
<h4 class="text-xl font-semibold mt-6 mb-2">3. Shared Runtime</h4>
|
||||
<p>All components share the same atomic engine. One signal can update a single character in a paragraph across 100 components without ever re‑evaluating the component functions themselves.</p>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Precision Engineering
|
||||
|
||||
### 1. Functional Efficiency
|
||||
`Div()`, `Button()`, `Span()`... These aren't just wrappers; they are pre-optimized constructors. When you call `Div({ class: 'btn' }, "Click")`, SigPro creates the element and attaches its reactive listeners in a single, surgical operation.
|
||||
|
||||
### 2. The "No-Bundle" Bundle
|
||||
Because SigPro is so small, it is the only engine where **the more code you write, the more the efficiency gap grows**. While others grow linearly with components and framework overhead, SigPro stays flat, leveraging the native power of modern browser engines.
|
||||
|
||||
### 3. Shared Runtime
|
||||
All components share the same atomic engine. One signal can update a single character in a paragraph across 100 components without ever re-evaluating the component functions themselves.
|
||||
|
||||
---
|
||||
|
||||
<div class="bg-base-200/50 rounded-3xl p-10 my-16 border border-base-300 shadow-inner"><div class="grid grid-cols-1 lg:grid-cols-3 gap-8 items-center"><div class="lg:col-span-2"><h2 class="text-4xl font-black mb-4 mt-0 tracking-tight italic text-primary">Kill the Bloat.</h2><p class="text-xl opacity-80 leading-relaxed">Stop shipping 100KB of "Framework" for 2KB of business logic. SigPro gives you the tools to build ultra-fast, modern apps with <strong>True Vanilla Performance</strong>.</p></div></div></div>
|
||||
<div class="bg-base-200/50 rounded-3xl p-10 my-16 border border-base-300 shadow-inner max-w-6xl mx-auto"><div class="grid grid-cols-1 lg:grid-cols-3 gap-8 items-center"><div class="lg:col-span-2"><h2 class="text-4xl font-black mb-4 mt-0 tracking-tight italic text-primary">Kill the Bloat.</h2><p class="text-xl opacity-80 leading-relaxed">Stop shipping 100KB of "Framework" for 2KB of business logic. SigPro gives you the tools to build ultra‑fast, modern apps with <strong>True Vanilla Performance</strong>.</p></div></div></div>
|
||||
|
||||
<div class="text-center py-10 opacity-30 font-mono text-xs tracking-widest uppercase">Precision Reactive Engine — NatxoCC</div>
|
||||
|
||||
@@ -2,21 +2,23 @@
|
||||
|
||||
* **Introduction**
|
||||
* [Installation](install.md)
|
||||
* [Examples](examples.md)
|
||||
* [Vite Plugin](vite/plugin.md)
|
||||
* [Router](router.md)
|
||||
|
||||
* **API Reference**
|
||||
* [Quick Start](api/quick.md)
|
||||
* [Signals & Proxies](api/signal.md)
|
||||
* [Watch](api/watch.md)
|
||||
* [If](api/if.md)
|
||||
* [For](api/for.md)
|
||||
* [Router](api/router.md)
|
||||
* [Mount](api/mount.md)
|
||||
* [Tag](api/html.md)
|
||||
* [$ignal](api/signal.md)
|
||||
* [watch](api/watch.md)
|
||||
* [when](api/when.md)
|
||||
* [each](api/each.md)
|
||||
* [mount](api/mount.md)
|
||||
* [h](api/h.md)
|
||||
|
||||
* **Concepts**
|
||||
* [Tags](api/tags.md)
|
||||
* [Global Store](api/global.md)
|
||||
* [JSX Style](api/jsx.md)
|
||||
* [HTML converter](convert.md)
|
||||
* [UI](ui.md)
|
||||
|
||||
* **UI Components**
|
||||
* [Quick Start](ui/quick.md)
|
||||
* **UI**
|
||||
* [WIP]
|
||||
173
docs/api/each.md
Normal file
173
docs/api/each.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Reactive Lists: `each( )`
|
||||
|
||||
The `each` function is a high‑performance keyed list renderer. It maps a reactive array to DOM nodes and surgically updates only the items that changed (added, removed, or reordered). Unlike a simple `.map()`, `each` reuses DOM nodes and preserves internal state.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
each(
|
||||
source: Signal<any[]> | (() => any[]) | any[],
|
||||
itemFn: (item: any, index: number) => Node | (() => Node),
|
||||
keyField?: string
|
||||
): HTMLElement
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`source`** | `Signal`, `() => any[]`, or `any[]` | Yes | The reactive array to iterate over. |
|
||||
| **`itemFn`** | `(item, index) => Node` | Yes | Returns a DOM node (or a function that returns a node) for each item. |
|
||||
| **`keyField`** | `string` | No | Name of the property to use as unique key (e.g., `"id"`). Default: `item?.id ?? index`. |
|
||||
|
||||
**Returns:** A `div` with `style="display: contents"` that contains the live list.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Basic Keyed List (Recommended)
|
||||
|
||||
Pass the name of the property that contains the unique identifier (e.g., `"id"`). This allows SigPro to reuse DOM nodes when the list is reordered or filtered.
|
||||
|
||||
```javascript
|
||||
const users = $([
|
||||
{ id: 1, name: "Alice" },
|
||||
{ id: 2, name: "Bob" }
|
||||
]);
|
||||
|
||||
ul({ class: "list" }, [
|
||||
each(users,
|
||||
(user) => li({ class: "p-2" }, user.name),
|
||||
"id" // ← use property "id" as stable key
|
||||
)
|
||||
]);
|
||||
```
|
||||
|
||||
### 2. Automatic Key (Simple Lists)
|
||||
|
||||
If you omit the `keyField`, `each` defaults to `item?.id ?? index`. For primitive arrays or objects without an `id`, the index is used.
|
||||
|
||||
```javascript
|
||||
const tags = $(["Tech", "JS", "Web"]);
|
||||
|
||||
div({ class: "flex gap-1" }, [
|
||||
each(tags, (tag) => span({ class: "badge" }, tag))
|
||||
// key defaults to index (0,1,2) – fine for static order
|
||||
]);
|
||||
```
|
||||
|
||||
### 3. Using a Different Property Name
|
||||
|
||||
If your unique identifier is not called `id` (e.g., `_id`, `userId`, `slug`), just pass the property name as the third parameter:
|
||||
|
||||
```javascript
|
||||
const products = $([
|
||||
{ _id: 101, name: "Laptop" },
|
||||
{ _id: 102, name: "Mouse" }
|
||||
]);
|
||||
|
||||
each(products, (item) => li(item.name), "_id");
|
||||
```
|
||||
|
||||
### 4. Dynamic Content Using Functions
|
||||
|
||||
If your `itemFn` returns a **function**, that function is re‑executed every time the item’s data changes (but the node is reused).
|
||||
|
||||
```javascript
|
||||
const todos = $([
|
||||
{ id: 1, text: "Learn SigPro", done: false }
|
||||
]);
|
||||
|
||||
each(todos,
|
||||
(todo) => div([
|
||||
input({ type: "checkbox", checked: () => todo.done, onInput: e => todo.done = e.target.checked }),
|
||||
span(() => todo.done ? s(todo.text) : todo.text)
|
||||
]),
|
||||
"id"
|
||||
);
|
||||
```
|
||||
|
||||
### 5. Source as a Plain Array or Function
|
||||
|
||||
`source` can be a plain array (non‑reactive) or a function that returns an array – it will still react to changes if signals are read inside the function.
|
||||
|
||||
```javascript
|
||||
const filter = $("all");
|
||||
|
||||
const filteredTodos = () => {
|
||||
const all = todos();
|
||||
if (filter() === "active") return all.filter(t => !t.done);
|
||||
return all;
|
||||
};
|
||||
|
||||
each(filteredTodos, (todo) => li(todo.text), "id");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How It Works (Reconciliation)
|
||||
|
||||
When the `source` changes, `each`:
|
||||
|
||||
1. **Compares keys** between the old and new items using the specified `keyField` (or `item.id` / index).
|
||||
2. **Reuses existing DOM nodes** for keys that stay the same.
|
||||
3. **Moves nodes** if order changed (no recreation).
|
||||
4. **Creates new nodes** for new keys.
|
||||
5. **Destroys nodes** for removed keys – cleans up all effects, event listeners, and child components.
|
||||
|
||||
> This is much more efficient than destroying and rebuilding the whole list on every update.
|
||||
|
||||
---
|
||||
|
||||
## Performance Tips
|
||||
|
||||
- **Stable keys** – Use a property that never changes (like a database primary key). Avoid `Math.random()` or array `index` for lists that can be reordered.
|
||||
- **State preservation** – If a list item contains an input or local state, using a stable key ensures that state is preserved even when the list is filtered or sorted.
|
||||
- **Lazy item functions** – If an item is expensive to render, wrap it in a function: `() => ExpensiveComponent(item)`. The component is only created when the item actually appears in the DOM.
|
||||
|
||||
---
|
||||
|
||||
## Summary Comparison
|
||||
|
||||
| Feature | Standard `Array.map` | SigPro `each` |
|
||||
| :--- | :--- | :--- |
|
||||
| **Re‑renders on change** | Re‑creates entire list | Only adds/removes/moves changed items |
|
||||
| **DOM nodes** | New nodes every time | **Reused via keys** |
|
||||
| **Memory cleanup** | Manual (or leak) | **Automatic** (destroy on removal) |
|
||||
| **Internal state per item** | Lost on every update | **Preserved** (if key stable) |
|
||||
| **Reactivity** | None (manual re‑render) | Built‑in, fine‑grained |
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
const items = $([
|
||||
{ id: 1, name: "Apple", price: 1.2 },
|
||||
{ id: 2, name: "Banana", price: 0.8 }
|
||||
]);
|
||||
|
||||
const addItem = () => {
|
||||
const newId = Date.now();
|
||||
items([...items(), { id: newId, name: `Item ${newId}`, price: 1.0 }]);
|
||||
};
|
||||
|
||||
const removeItem = (id) => {
|
||||
items(items().filter(i => i.id !== id));
|
||||
};
|
||||
|
||||
const App = () =>
|
||||
div([
|
||||
button({ onClick: addItem }, "Add item"),
|
||||
ul(
|
||||
each(items,
|
||||
(item) => li([
|
||||
span(`${item.name} – $${item.price}`),
|
||||
button({ onClick: () => removeItem(item.id) }, "X")
|
||||
]),
|
||||
"id"
|
||||
)
|
||||
)
|
||||
]);
|
||||
|
||||
mount(App, '#app');
|
||||
```
|
||||
@@ -1,83 +0,0 @@
|
||||
# Reactive Lists: `For( )`
|
||||
|
||||
The `For` function is a high-performance list renderer. It maps an array (or a Signal containing an array) to DOM nodes. Unlike a simple `.map()`, `For` is **keyed**, meaning it only updates, moves, or deletes the specific items that changed.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
For(
|
||||
source: Signal<any[]> | Function | any[],
|
||||
render: (item: any, index: number) => HTMLElement,
|
||||
keyFn?: (item: any, index: number) => string | number
|
||||
): HTMLElement
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`source`** | `Signal` | Yes | The reactive array to iterate over. |
|
||||
| **`render`** | `Function` | Yes | A function that returns a component or Node for each item. |
|
||||
| **`keyFn`** | `Function` | **No** | A function to extract a **unique ID**. If omitted, it defaults to the `index`. |
|
||||
|
||||
**Returns:** A `div` element with `display: contents` containing the live list.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Basic Keyed List (Recommended)
|
||||
Always use a unique property (like an `id`) as a key to ensure SigPro doesn't recreate nodes unnecessarily when reordering or filtering.
|
||||
|
||||
```javascript
|
||||
const users = $([
|
||||
{ id: 1, name: "Alice" },
|
||||
{ id: 2, name: "Bob" }
|
||||
]);
|
||||
|
||||
Ul({ class: "list" }, [
|
||||
For(users,
|
||||
(user) => Li({ class: "p-2" }, user.name),
|
||||
(user) => user.id // Stable and unique key
|
||||
)
|
||||
]);
|
||||
```
|
||||
|
||||
### 2. Simplified Usage (Automatic Key)
|
||||
If you omit the third parameter, `For` will automatically use the array index as the key. This is ideal for simple lists that don't change order frequently.
|
||||
|
||||
```javascript
|
||||
const tags = $(["Tech", "JS", "Web"]);
|
||||
|
||||
// No need to pass keyFn if the index is sufficient
|
||||
Div({ class: "flex gap-1" }, [
|
||||
For(tags, (tag) => Badge(tag))
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How it Works (The Reconciliation)
|
||||
|
||||
When the `source` signal changes, `For` performs the following steps:
|
||||
|
||||
1. **Key Diffing**: It compares the new keys with the previous ones stored in an internal `Map`.
|
||||
2. **Node Reuse**: If a key already exists, the DOM node is **reused** and moved to its new position. No new elements are created.
|
||||
3. **Physical Cleanup**: If a key disappears from the list, SigPro calls `.destroy()` to stop reactivity and physically removes the node from the DOM to prevent memory leaks.
|
||||
|
||||
---
|
||||
|
||||
## Performance Tips
|
||||
|
||||
* **Stable Keys**: Never use `Math.random()` as a key. This will force SigPro to destroy and recreate the entire list on every update, killing performance.
|
||||
* **State Preservation**: If your list items have internal state (like an input with text), `For` ensures that state is preserved even if the list is reordered, as long as the key (`id`) remains the same.
|
||||
|
||||
---
|
||||
|
||||
## Summary Comparison
|
||||
|
||||
| Feature | Standard `Array.map` | SigPro `For` |
|
||||
| :--- | :--- | :--- |
|
||||
| **Re-render** | Re-renders everything | Only updates changes |
|
||||
| **DOM Nodes** | Re-created every time | **Reused via Keys** |
|
||||
| **Memory** | Potential leaks | **Automatic Cleanup** |
|
||||
| **State** | Lost on re-render | **Preserved per item** |
|
||||
| **Ease of Use** | Manual logic required | **Optional (fallback to index)** |
|
||||
@@ -1,64 +1,69 @@
|
||||
# Global State Management: Atomic & Modular
|
||||
|
||||
SigPro leverages the native power and efficiency of **Signals** to create robust global stores with **0% complexity**. While other frameworks force you into heavy libraries and rigid boilerplate (Redux, Pinia, or Svelte Stores), SigPro treats "The Store" as a simple architectural choice: **defining a Signal outside of a component.**
|
||||
SigPro leverages the native power and efficiency of **signals** to create robust global stores with **zero complexity**. While other frameworks force you into heavy libraries and rigid boilerplate (Redux, Pinia, or Svelte stores), SigPro treats “the store” as a simple architectural choice: **defining a signal outside of a component.**
|
||||
|
||||
> **Availability:** `$` (and other core functions) are exported from the SigPro module. In **ESM** you must import them (`import { $ } from 'sigpro'`). In the **IIFE** classic script, `$` is automatically available on `window`. The examples below assume `$` is already in scope (via import or global).
|
||||
|
||||
## Modular Organization (Zero Constraints)
|
||||
|
||||
You are not restricted to a single `store.js`. You can organize your state by **feature**, **domain**, or **page**. Since a SigPro store is just a standard JavaScript module exporting Signals, you can name your files whatever you like (`auth.js`, `cart.js`, `settings.js`) to keep your logic clean.
|
||||
You are not restricted to a single `store.js`. You can organize your state by **feature**, **domain**, or **page**. Since a SigPro store is just a standard JavaScript module exporting signals, you can name your files whatever you like (`auth.js`, `cart.js`, `settings.js`) to keep your logic clean.
|
||||
|
||||
### 1. File-Based Stores (`<any-name>.js`)
|
||||
Creating a dedicated file allows you to export only what you need. This modularity ensures **Tree Shaking** works perfectly—you never load state that isn't imported.
|
||||
### 1. File‑Based Stores (`<any-name>.js`)
|
||||
|
||||
Creating a dedicated file allows you to export only what you need. This modularity ensures **tree shaking** works perfectly – you never load state that isn’t imported.
|
||||
|
||||
```javascript
|
||||
// auth.js
|
||||
import SigPro from "sigpro";
|
||||
import { $ } from 'sigpro';
|
||||
|
||||
// A simple global signal
|
||||
export const user = $({ name: "Guest", loggedIn: false });
|
||||
|
||||
// A persistent global signal (auto-syncs with localStorage via native key)
|
||||
// A persistent global signal (auto‑syncs with localStorage)
|
||||
export const theme = $("light", "app-theme-pref");
|
||||
|
||||
// A computed global signal that reacts to the 'user' signal
|
||||
export const welcomeMessage = $(() => `Welcome back, ${user().name}!`);
|
||||
```
|
||||
|
||||
### 2. Cross-Component Consumption
|
||||
Once exported, these signals act as a **Single Source of Truth**. SigPro ensures that if a signal changes in one file, every component importing it across the entire app updates **atomically** without a full re-render.
|
||||
### 2. Cross‑Component Consumption
|
||||
|
||||
Once exported, these signals act as a **single source of truth**. SigPro ensures that if a signal changes in one file, every component importing it across the entire app updates **atomically** without a full re‑render.
|
||||
|
||||
```javascript
|
||||
// Profile.js
|
||||
import { user } from "./auth.js";
|
||||
|
||||
const Profile = () => Div([
|
||||
H2(user().name),
|
||||
Button({ onclick: () => user({ name: "John Doe", loggedIn: true }) }, "Log In")
|
||||
const Profile = () => div([
|
||||
h2(() => user().name),
|
||||
button({ onclick: () => user({ name: "John Doe", loggedIn: true }) }, "Log In")
|
||||
]);
|
||||
|
||||
// Navbar.js
|
||||
import { welcomeMessage, theme } from "./auth.js";
|
||||
|
||||
const Navbar = () => Nav({ class: theme }, [
|
||||
Span(welcomeMessage)
|
||||
const Navbar = () => nav({ class: () => theme() }, [
|
||||
span(() => welcomeMessage())
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why SigPro Stores are Superior
|
||||
## Why SigPro Stores Are Superior
|
||||
|
||||
| Feature | SigPro | Redux / Pinia / Svelte |
|
||||
| :--- | :--- | :--- |
|
||||
| **Boilerplate** | **0%** (Just a variable) | High (Actions, Reducers, Wrappers) |
|
||||
| **Organization** | **Unlimited** (Any filename) | Often strictly "Store" or "Actions" |
|
||||
| **Persistence** | **Native** (Just add a key) | Requires Middleware / Plugins |
|
||||
| **Learning Curve** | **Instant** | Steep / Complex |
|
||||
| **Bundle Size** | **0KB** (Part of the core) | 10KB - 30KB+ |
|
||||
| :-------------------- | :---------------------------- | :------------------------------ |
|
||||
| **Boilerplate** | **0%** (just a variable) | High (actions, reducers, stores)|
|
||||
| **Organization** | **Unlimited** (any filename) | Often strictly “store” files |
|
||||
| **Persistence** | **Native** (just add a key) | Requires middleware / plugins |
|
||||
| **Learning Curve** | **Instant** | Steep / complex |
|
||||
| **Bundle Size** | **0KB** (part of core) | 10KB – 30KB+ |
|
||||
|
||||
---
|
||||
|
||||
## The "Persistence" Advantage
|
||||
The magic of SigPro’s `$(value, "key")` is that it works identically for local and global states. By simply adding a second argument, your Modular Store survives browser refreshes automatically. No manual `localStorage.getItem` or `JSON.parse` logic is ever required.
|
||||
## The Persistence Advantage
|
||||
|
||||
The magic of SigPro’s `$(value, "key")` is that it works identically for local and global states. By simply adding a second argument, your modular store survives browser refreshes automatically. No manual `localStorage.getItem` or `JSON.parse` logic is ever required.
|
||||
|
||||
```javascript
|
||||
// This single line creates a global, reactive,
|
||||
@@ -69,7 +74,69 @@ export const cart = $([], "session-cart");
|
||||
---
|
||||
|
||||
## Summary of Scopes
|
||||
* **Local Scope:** Signal defined **inside** a component. Unique to every instance created.
|
||||
* **Module Scope:** Signal defined **outside** a component (same file). Shared by all instances within that specific file.
|
||||
* **Global Scope:** Signal defined in a **separate file**. Shared across the entire application by any importing module.
|
||||
* **Persistent Scope:** Any Signal defined with a **key**. Shared globally and remembered after a page reload.
|
||||
|
||||
| Scope | Definition | Behaviour |
|
||||
| :-------------- | :-------------------------------------------------------------- | :-------------------------------------------- |
|
||||
| **Local** | Signal defined **inside** a component | Unique to every component instance |
|
||||
| **Module** | Signal defined **outside** a component (same file) | Shared by all instances within that file |
|
||||
| **Global** | Signal defined in a **separate file** and imported | Shared across the entire application |
|
||||
| **Persistent** | Any Signal defined with a **key** (e.g., `$([], "cart")`) | Shared globally and persisted in `localStorage` |
|
||||
|
||||
---
|
||||
|
||||
## Complete Example – Todo Store
|
||||
|
||||
```javascript
|
||||
// store/todos.js
|
||||
import { $ } from 'sigpro';
|
||||
|
||||
export const todos = $([], "todos");
|
||||
export const filter = $("all");
|
||||
|
||||
export const addTodo = (text) => {
|
||||
todos([...todos(), { id: Date.now(), text, done: false }]);
|
||||
};
|
||||
|
||||
export const toggleTodo = (id) => {
|
||||
todos(todos().map(t => t.id === id ? { ...t, done: !t.done } : t));
|
||||
};
|
||||
|
||||
export const filteredTodos = $(() => {
|
||||
const all = todos();
|
||||
if (filter() === "active") return all.filter(t => !t.done);
|
||||
if (filter() === "completed") return all.filter(t => t.done);
|
||||
return all;
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
// components/TodoApp.js
|
||||
import 'sigpro';
|
||||
import { todos, filter, addTodo, toggleTodo, filteredTodos } from "../store/todos.js";
|
||||
|
||||
const TodoApp = () =>
|
||||
div({ class: "todo-app" }, [
|
||||
input({ placeholder: "Add todo...", onKeyDown: (e) => {
|
||||
if (e.key === "Enter" && e.target.value) {
|
||||
addTodo(e.target.value);
|
||||
e.target.value = "";
|
||||
}
|
||||
}}),
|
||||
div({ class: "filters" }, [
|
||||
button({ onClick: () => filter("all") }, "All"),
|
||||
button({ onClick: () => filter("active") }, "Active"),
|
||||
button({ onClick: () => filter("completed") }, "Completed")
|
||||
]),
|
||||
ul(
|
||||
each(filteredTodos,
|
||||
(todo) => li([
|
||||
input({ type: "checkbox", checked: () => todo.done, onInput: () => toggleTodo(todo.id) }),
|
||||
span(() => todo.done ? s(todo.text) : todo.text)
|
||||
]),
|
||||
(todo) => todo.id
|
||||
)
|
||||
)
|
||||
]);
|
||||
|
||||
mount(TodoApp, "#app");
|
||||
```
|
||||
161
docs/api/h.md
Normal file
161
docs/api/h.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Hyperscript Function: `h( )`
|
||||
|
||||
The `h` function is the **core DOM builder** of SigPro. It creates DOM elements from a tag name, props, and children. While the global tag helpers (`div()`, `button()`, etc.) are built on top of `h`, you may need `h` directly for dynamic tag names or when you prefer an explicit function style.
|
||||
|
||||
> **Availability:** `h` and all tag helpers (`div`, `button`, etc.) are exported from the SigPro module. In **ESM** you must import them (`import { h, div, button } from 'sigpro'`). In the **IIFE** classic script, `h` and all tag helpers are automatically available on `window`. The examples below assume the functions are already in scope.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
h(
|
||||
tag: string | Function,
|
||||
props?: object | Node | any[],
|
||||
children?: any
|
||||
): Node
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **`tag`** | `string` or `Function` | HTML tag name (e.g., `"div"`) or a component function. |
|
||||
| **`props`** | `object` | Optional. Attributes, event handlers, refs, etc. If not an object, it becomes `children`. |
|
||||
| **`children`** | `any` | Optional. Text, nodes, arrays, or reactive functions. |
|
||||
|
||||
**Returns:** A DOM node (or an array of nodes when the tag is a component that returns an array).
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Basic Element Creation
|
||||
|
||||
```javascript
|
||||
// Simple div with text
|
||||
h('div', {}, 'Hello world');
|
||||
|
||||
// With attributes
|
||||
h('button', { class: 'btn', onclick: () => alert('clicked') }, 'Click me');
|
||||
```
|
||||
|
||||
### 2. Nested Children
|
||||
|
||||
Children can be a single node, an array, or a function.
|
||||
|
||||
```javascript
|
||||
h('div', { class: 'container' }, [
|
||||
h('h1', {}, 'Title'),
|
||||
h('p', {}, 'Paragraph text')
|
||||
]);
|
||||
```
|
||||
|
||||
### 3. Reactive Children
|
||||
|
||||
Pass a **function** as a child – it will be re‑evaluated whenever any signal inside changes, and the DOM will be patched surgically.
|
||||
|
||||
```javascript
|
||||
const count = $(0);
|
||||
|
||||
h('div', {}, [
|
||||
h('p', {}, () => `Count: ${count()}`),
|
||||
h('button', { onclick: () => count(count() + 1) }, '+1')
|
||||
]);
|
||||
```
|
||||
|
||||
### 4. Reactive Attributes
|
||||
|
||||
Pass a function as an attribute value to keep it dynamic.
|
||||
|
||||
```javascript
|
||||
const theme = $('dark');
|
||||
|
||||
h('div', { class: () => `box ${theme()}` }, 'Themed box');
|
||||
```
|
||||
|
||||
### 5. Two‑Way Binding
|
||||
|
||||
Assign a signal directly to `value` or `checked` on form elements – SigPro automatically syncs both ways.
|
||||
|
||||
```javascript
|
||||
const name = $('');
|
||||
|
||||
h('input', {
|
||||
type: 'text',
|
||||
value: name, // two-way binding
|
||||
placeholder: 'Your name'
|
||||
});
|
||||
h('p', {}, () => `Hello, ${name()}`);
|
||||
```
|
||||
|
||||
### 6. Component Functions as `tag`
|
||||
|
||||
You can pass a component function directly to `h`. SigPro will execute it with the provided props and an `emit` helper for custom events.
|
||||
|
||||
```javascript
|
||||
const Button = (props, { children }) =>
|
||||
h('button', { class: 'btn', onclick: props.onClick }, children);
|
||||
|
||||
const App = () =>
|
||||
h('div', {}, [
|
||||
h(Button, { onClick: () => alert('clicked') }, 'Custom button')
|
||||
]);
|
||||
```
|
||||
|
||||
### 7. SVG Elements
|
||||
|
||||
Use `h` with SVG tag names – SigPro automatically applies the correct namespace.
|
||||
|
||||
```javascript
|
||||
h('svg', { width: 100, height: 100 }, [
|
||||
h('circle', { cx: 50, cy: 50, r: 40, fill: 'red' })
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Special Props
|
||||
|
||||
| Prop | Behaviour |
|
||||
| :--- | :--- |
|
||||
| **`ref`** | `ref: (el) => ...` or `ref: { current: null }` – provides direct access to the DOM node after creation. |
|
||||
| **`onEvent`** | Any prop starting with `on` (e.g., `onClick`, `onInput`) is treated as an event listener. Automatically removed on cleanup. |
|
||||
| **`value` / `checked`** | When a signal is passed, creates two‑way binding for inputs, textareas, and selects. |
|
||||
| **`class`** | You can use `class` (not `className`). Accepts a string or a reactive function. |
|
||||
|
||||
---
|
||||
|
||||
## `h` vs Tag Helpers
|
||||
|
||||
| Feature | `h('div', ...)` | `div(...)` (tag helper) |
|
||||
| :--- | :--- | :--- |
|
||||
| **Dynamic tag names** | ✅ `h(tagName, ...)` | ❌ Must know tag name at write time |
|
||||
| **Explicit style** | More verbose | Cleaner, DSL‑like |
|
||||
| **Availability** | Import or global | Import or global (same) |
|
||||
| **Performance** | Identical | Identical (helpers call `h` internally) |
|
||||
|
||||
> **Recommendation:** Use tag helpers (`div()`, `button()`, etc.) for most cases – they are shorter and more readable. Use `h` directly only when the tag name is dynamic (e.g., `h(props.tag, ...)`).
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
import 'sigpro';
|
||||
|
||||
const dynamicTag = $('h1');
|
||||
|
||||
const App = () =>
|
||||
h('div', { class: 'demo' }, [
|
||||
h(dynamicTag(), {}, () => `Current tag: ${dynamicTag()}`),
|
||||
h('button', { onclick: () => dynamicTag(dynamicTag() === 'h1' ? 'h2' : 'h1') }, 'Toggle heading size')
|
||||
]);
|
||||
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
- `h` is the low‑level DOM builder used internally by all tag helpers.
|
||||
- It supports reactive attributes, reactive children, two‑way binding, event listeners, and SVG.
|
||||
- Use `h` directly when you need a **dynamic tag name**; otherwise, prefer the convenient tag helpers (import them or inject globally).
|
||||
- Components written with `h` are fully reactive and automatically cleaned up.
|
||||
104
docs/api/html.md
104
docs/api/html.md
@@ -1,104 +0,0 @@
|
||||
# The DOM Factory: `Tag( )`
|
||||
|
||||
`Tag` is the internal engine that creates, attributes, and attaches reactivity to DOM elements. It uses `Watch` to maintain a live, high-performance link between your Signals and the Document Object Model.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
Tag(tagName: string, props?: Object, children?: any[] | any): HTMLElement
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`tagName`** | `string` | Yes | Valid HTML tag name (e.g., `"div"`, `"button"`). |
|
||||
| **`props`** | `Object` | No | Attributes, Events, Two-way bindings, and **Refs**. |
|
||||
| **`children`** | `any` | No | Nested elements, text strings, or reactive functions. |
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Manual DOM Access: `ref`
|
||||
The `ref` property allows you to capture the underlying `HTMLElement` as soon as it is created. This is essential for integrating 3rd-party libraries (like AG Grid or Chart.js) or managing focus and measurements.
|
||||
|
||||
* **Callback Ref**: A function that receives the `HTMLElement` immediately.
|
||||
* **Object Ref**: An object with a `.current` property assigned to the `HTMLElement`.
|
||||
|
||||
```javascript
|
||||
// Auto-focus on mount
|
||||
Input({ ref: (el) => el.focus() });
|
||||
|
||||
// Capturing a node for an external library
|
||||
const gridDiv = { current: null };
|
||||
Div({ ref: gridDiv, class: "ag-theme-quartz" });
|
||||
```
|
||||
|
||||
### 2. Attribute Handling
|
||||
SigPro intelligently decides how to apply each property:
|
||||
* **Standard Props**: Applied via `setAttribute` or direct property assignment.
|
||||
* **Class Names**: Supports `class` or `className` interchangeably.
|
||||
* **Boolean Props**: Automatic handling for `checked`, `disabled`, `hidden`, etc.
|
||||
* **Note**: The `ref` property is intercepted and **never** rendered as an attribute in the HTML.
|
||||
|
||||
### 3. Event Listeners
|
||||
Events are defined by the `on` prefix. SigPro automatically registers the listener and ensures it is cleaned up when the element is destroyed.
|
||||
|
||||
```javascript
|
||||
Button({
|
||||
onclick: (e) => console.log("Clicked!", e),
|
||||
}, "Click Me");
|
||||
```
|
||||
|
||||
### 4. Reactive Attributes (One-Way)
|
||||
If an attribute value is a **function** (like a Signal), `Tag` creates an internal **`Watch`** to keep the DOM in sync with the state.
|
||||
|
||||
```javascript
|
||||
Div({
|
||||
// Updates the class whenever 'theme()' changes
|
||||
class: () => theme() === "dark" ? "bg-black" : "bg-white"
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Smart Two-Way Binding (Automatic)
|
||||
SigPro automatically enables **bidirectional synchronization** when it detects a **Signal** assigned to a form-capable attribute (`value` or `checked`) on an input element (`input`, `textarea`, `select`).
|
||||
|
||||
```javascript
|
||||
// Syncs input value <-> signal automatically
|
||||
Input({
|
||||
type: "text",
|
||||
value: username // No special symbols needed!
|
||||
})
|
||||
```
|
||||
> **Note:** To use a Signal as **read-only** in an input, wrap it in an anonymous function: `value: () => username()`.
|
||||
|
||||
### 6. Reactive Children
|
||||
Children can be static or dynamic. When a child is a function, SigPro creates a reactive boundary using `Watch` for that specific part of the DOM.
|
||||
|
||||
```javascript
|
||||
Div({}, [
|
||||
H1("Static Title"),
|
||||
// Only this text node re-renders when 'count' changes
|
||||
() => `Current count: ${count()}`
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Management (Internal)
|
||||
Every element created with `Tag` is "self-aware" regarding its reactive dependencies.
|
||||
* **`._cleanups`**: A hidden `Set` attached to the element that stores all `stop()` functions from its internal `Watch` calls and event listeners.
|
||||
* **Lifecycle**: When an element is removed by a Controller (`If`, `For`, or `Router`), SigPro performs a recursive **"sweep"** to execute these cleanups, ensuring **zero memory leaks**.
|
||||
|
||||
---
|
||||
|
||||
## Tag Constructors (The Shortcuts)
|
||||
|
||||
Instead of writing `Tag("div", ...)` every time, SigPro provides PascalCase global functions for all standard HTML tags. These are direct mappings to the `Tag` factory.
|
||||
|
||||
```javascript
|
||||
// This:
|
||||
Div({ class: "wrapper" }, [ Span("Hello") ])
|
||||
|
||||
// Is exactly equivalent to:
|
||||
Tag("div", { class: "wrapper" }, [ Tag("span", {}, "Hello") ])
|
||||
```
|
||||
180
docs/api/if.md
180
docs/api/if.md
@@ -1,180 +0,0 @@
|
||||
# Reactive Branching: `If( )`
|
||||
|
||||
The `If` function is a reactive control flow operator. It manages the conditional rendering of components with optional smooth transitions, ensuring that only the active branch exists in the DOM and in memory.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
If(
|
||||
condition: Signal<boolean> | Function,
|
||||
thenVal: Component | Node,
|
||||
otherwiseVal?: Component | Node,
|
||||
transition?: Transition
|
||||
): HTMLElement
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`condition`** | `Signal` | Yes | A reactive source that determines which branch to render. |
|
||||
| **`thenVal`** | `any` | Yes | The content to show when the condition is **truthy**. |
|
||||
| **`otherwiseVal`** | `any` | No | The content to show when the condition is **falsy** (defaults to null). |
|
||||
| **`transition`** | `Transition` | No | Optional animation hooks for enter/exit transitions. |
|
||||
|
||||
**Returns:** A `div` element with `display: contents` that acts as a reactive portal for the branches.
|
||||
|
||||
---
|
||||
|
||||
## Transition Interface
|
||||
|
||||
```typescript
|
||||
interface Transition {
|
||||
/** Called when branch enters. Use for fade-in, slide-in, etc. */
|
||||
in: (el: HTMLElement) => void;
|
||||
/** Called when branch leaves. Call `done()` when animation completes. */
|
||||
out: (el: HTMLElement, done: () => void) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Fade Transition
|
||||
|
||||
```javascript
|
||||
const fade = {
|
||||
in: (el) => {
|
||||
el.style.opacity = "0";
|
||||
el.style.transition = "opacity 0.3s";
|
||||
requestAnimationFrame(() => {
|
||||
el.style.opacity = "1";
|
||||
});
|
||||
},
|
||||
out: (el, done) => {
|
||||
el.style.transition = "opacity 0.3s";
|
||||
el.style.opacity = "0";
|
||||
setTimeout(done, 300);
|
||||
}
|
||||
};
|
||||
|
||||
If(show, Modal, null, fade);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Simple Toggle
|
||||
|
||||
```javascript
|
||||
const isVisible = $(false);
|
||||
|
||||
Div([
|
||||
Button({ onclick: () => isVisible(!isVisible()) }, "Toggle Message"),
|
||||
|
||||
If(isVisible,
|
||||
P("Now you see me!"),
|
||||
P("Now you don't...")
|
||||
)
|
||||
]);
|
||||
```
|
||||
|
||||
### 2. With Smooth Animation
|
||||
|
||||
```javascript
|
||||
const showModal = $(false);
|
||||
|
||||
Div([
|
||||
Button({ onclick: () => showModal(true) }, "Open Modal"),
|
||||
|
||||
If(showModal,
|
||||
() => Modal({ onClose: () => showModal(false) }),
|
||||
null,
|
||||
fade // ← Smooth enter/exit animation
|
||||
)
|
||||
]);
|
||||
```
|
||||
|
||||
### 3. Lazy Component Loading
|
||||
|
||||
Unlike CSS `display: none`, `If` is **lazy**. The inactive branch is never created, saving memory.
|
||||
|
||||
```javascript
|
||||
If(() => user.isLogged(),
|
||||
() => Dashboard(), // Only executed if logged in
|
||||
() => LoginGate() // Only executed if guest
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Complex Conditions
|
||||
|
||||
```javascript
|
||||
If(() => count() > 10 && status() === 'ready',
|
||||
Span("Threshold reached!")
|
||||
)
|
||||
```
|
||||
|
||||
### 5. If.not Helper
|
||||
|
||||
```javascript
|
||||
If.not(loading,
|
||||
() => Content(), // Shows when loading is FALSE
|
||||
() => Spinner() // Shows when loading is TRUE
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automatic Cleanup
|
||||
|
||||
One of the core strengths of `If` is its integrated **Cleanup** logic. SigPro ensures that when a branch is swapped out, it is completely purged.
|
||||
|
||||
1. **Stop Watchers**: All `Watch` calls inside the inactive branch are permanently stopped.
|
||||
2. **Unbind Events**: Event listeners attached via `Tag` are removed.
|
||||
3. **Recursive Sweep**: SigPro performs a deep "sweep" of the removed branch.
|
||||
4. **Transition Respect**: When using transitions, destruction only happens AFTER the `out` animation completes.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Function Wrappers**: For heavy components, use `() => MyComponent()` to prevent initialization until needed.
|
||||
- **Reusable Transitions**: Define common transitions (fade, slide, scale) in a shared module.
|
||||
- **Cleanup**: No manual cleanup needed. SigPro handles everything automatically.
|
||||
|
||||
---
|
||||
|
||||
## Technical Comparison
|
||||
|
||||
| Feature | Standard CSS `hidden` | SigPro `If` |
|
||||
| :--- | :--- | :--- |
|
||||
| **DOM Presence** | Always present | Only if active |
|
||||
| **Reactivity** | Still processing | **Paused/Destroyed** |
|
||||
| **Memory usage** | Higher | **Optimized** |
|
||||
| **Cleanup** | Manual | **Automatic** |
|
||||
| **Smooth Transitions** | Manual | **Built-in hook** |
|
||||
| **Animation Timing** | You manage | **Respected by core** |
|
||||
|
||||
---
|
||||
|
||||
## Complete Transition Examples
|
||||
|
||||
### Fade
|
||||
```javascript
|
||||
const fade = {
|
||||
in: (el) => { el.style.opacity = "0"; requestAnimationFrame(() => { el.style.transition = "opacity 0.3s"; el.style.opacity = "1"; }); },
|
||||
out: (el, done) => { el.style.transition = "opacity 0.3s"; el.style.opacity = "0"; setTimeout(done, 300); }
|
||||
};
|
||||
```
|
||||
|
||||
### Slide
|
||||
```javascript
|
||||
const slide = {
|
||||
in: (el) => { el.style.transform = "translateX(-100%)"; requestAnimationFrame(() => { el.style.transition = "transform 0.3s"; el.style.transform = "translateX(0)"; }); },
|
||||
out: (el, done) => { el.style.transition = "transform 0.3s"; el.style.transform = "translateX(-100%)"; setTimeout(done, 300); }
|
||||
};
|
||||
```
|
||||
|
||||
### Scale
|
||||
```javascript
|
||||
const scale = {
|
||||
in: (el) => { el.style.transform = "scale(0)"; requestAnimationFrame(() => { el.style.transition = "transform 0.2s"; el.style.transform = "scale(1)"; }); },
|
||||
out: (el, done) => { el.style.transition = "transform 0.2s"; el.style.transform = "scale(0)"; setTimeout(done, 200); }
|
||||
};
|
||||
```
|
||||
119
docs/api/jsx.md
119
docs/api/jsx.md
@@ -1,23 +1,34 @@
|
||||
# JSX with SigPro
|
||||
# Hyperscript & Tag Helpers & JSX Style
|
||||
|
||||
SigPro works seamlessly with JSX.
|
||||
SigPro provides two complementary ways to create DOM elements:
|
||||
|
||||
## Configuration
|
||||
1. **The `h` function** – the low‑level DOM builder.
|
||||
2. **Global Tag Helpers** (e.g., `div()`, `button()`, `span()`) – a convenient DSL built on top of `h`.
|
||||
|
||||
### TypeScript
|
||||
Both are reactive, auto‑cleanup, and support SVG, events, two‑way binding, and dynamic children.
|
||||
|
||||
---
|
||||
|
||||
## JSX with SigPro
|
||||
|
||||
SigPro works seamlessly with JSX. You can use JSX as a compile‑time syntax sugar for `h` calls.
|
||||
|
||||
### Configuration
|
||||
|
||||
#### TypeScript
|
||||
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"jsxFactory": "Tag",
|
||||
"jsxFactory": "h",
|
||||
"jsxFragmentFactory": "Fragment"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vite
|
||||
#### Vite
|
||||
|
||||
```js
|
||||
// vite.config.js
|
||||
@@ -25,31 +36,33 @@ import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
esbuild: {
|
||||
jsxFactory: 'Tag',
|
||||
jsxFactory: 'h',
|
||||
jsxFragmentFactory: 'Fragment'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Babel
|
||||
#### Babel
|
||||
|
||||
```js
|
||||
// babel.config.js
|
||||
export default {
|
||||
plugins: [
|
||||
['@babel/plugin-transform-react-jsx', {
|
||||
pragma: 'Tag',
|
||||
pragma: 'h',
|
||||
pragmaFrag: 'Fragment'
|
||||
}]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
> **Note:** You need to import `h` and `Fragment` from SigPro in every JSX file, or make them global.
|
||||
|
||||
### JSX Example
|
||||
|
||||
```jsx
|
||||
// App.jsx
|
||||
import { $, Mount, Fragment } from 'sigpro';
|
||||
import { $, h, Fragment, mount } from 'sigpro';
|
||||
|
||||
const Button = ({ onClick, children }) => (
|
||||
<button class="btn btn-primary" onclick={onClick}>
|
||||
@@ -76,10 +89,10 @@ const App = () => {
|
||||
);
|
||||
};
|
||||
|
||||
Mount(App, '#app');
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
## What Gets Compiled
|
||||
### What Gets Compiled
|
||||
|
||||
Your JSX:
|
||||
```jsx
|
||||
@@ -89,59 +102,53 @@ Your JSX:
|
||||
```
|
||||
|
||||
Compiles to:
|
||||
```javascript
|
||||
Tag('div', { class: "container" },
|
||||
Tag(Button, {}, "Click")
|
||||
```js
|
||||
h('div', { class: "container" },
|
||||
h(Button, {}, "Click")
|
||||
)
|
||||
```
|
||||
|
||||
## Without Build Step (CDN)
|
||||
---
|
||||
|
||||
SigPro automatically injects `Div()`, `Button()`, `Span()`, and all other HTML tag helpers globally when loaded via CDN. `Fragment` is also available.
|
||||
## Without a Build Step (CDN + Tag Helpers)
|
||||
|
||||
If you don’t want to configure a JSX compiler, you can use the global tag helpers directly. They are available after loading SigPro via CDN.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://unpkg.com/sigpro"></script>
|
||||
<script type="module">
|
||||
import 'https://cdn.jsdelivr.net/npm/sigpro@1.2.18/+esm';
|
||||
// Now $, $$, watch, h, mount, div, button, etc. are global
|
||||
|
||||
const count = $(0);
|
||||
const App = () =>
|
||||
div({ class: 'container' }, [
|
||||
h1(() => `Count: ${count()}`),
|
||||
button({ onClick: () => count(count() + 1) }, 'Increment')
|
||||
]);
|
||||
|
||||
mount(App, '#app');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script>
|
||||
const { $, Mount, Fragment } = SigPro;
|
||||
|
||||
const App = () => {
|
||||
const count = $(0);
|
||||
|
||||
return Div({ class: "container p-8" }, [
|
||||
H1({ class: "text-2xl font-bold" }, "SigPro Demo"),
|
||||
Button({
|
||||
class: "btn-primary",
|
||||
onclick: () => count(count() + 1)
|
||||
}, () => `Clicks: ${count()}`),
|
||||
Fragment({}, [
|
||||
P({}, "Multiple elements"),
|
||||
P({}, "Without wrapper")
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
Mount(App, '#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Template Literals Alternative (htm)
|
||||
---
|
||||
|
||||
For a JSX-like syntax without a build step, use `htm`:
|
||||
## Template Literals Alternative (`htm`)
|
||||
|
||||
```javascript
|
||||
import { $, Mount } from 'https://unpkg.com/sigpro';
|
||||
For a JSX‑like syntax without a build step, you can combine SigPro with [`htm`](https://github.com/developit/htm).
|
||||
|
||||
```js
|
||||
import { $, h, mount } from 'https://cdn.jsdelivr.net/npm/sigpro@1.2.18/+esm';
|
||||
import htm from 'https://esm.sh/htm';
|
||||
|
||||
const html = htm.bind(Tag);
|
||||
const html = htm.bind(h); // bind to SigPro's h function
|
||||
|
||||
const App = () => {
|
||||
const count = $(0);
|
||||
@@ -156,16 +163,18 @@ const App = () => {
|
||||
`;
|
||||
};
|
||||
|
||||
Mount(App, '#app');
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
## Summary
|
||||
---
|
||||
|
||||
| Method | Build Step | Syntax |
|
||||
|--------|------------|--------|
|
||||
| JSX | Required | `<div>...</div>` |
|
||||
| CDN (Tag Helpers) | Optional | `Div({}, ...)` |
|
||||
| htm | Optional | `` html`<div>...</div>` `` |
|
||||
## Summary Comparison
|
||||
|
||||
> [!TIP]
|
||||
> **Recommendation:** Use JSX for large projects, CDN tag helpers (`Div()`, `Button()`) for simple projects, or htm for buildless projects that want HTML-like syntax.
|
||||
| Method | Build Step | Syntax | Recommended for |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`h` function** | Optional | `h('div', ...)` | Dynamic tag names, low‑level control |
|
||||
| **Tag Helpers** | Optional | `div(...)` | Most cases – clean, simple, no build step |
|
||||
| **JSX** | Required | `<div>...</div>` | Large projects, teams familiar with React syntax |
|
||||
| **`htm`** | Optional | `` html`<div>...</div>` `` | Buildless but HTML‑like syntax |
|
||||
|
||||
> **Tip:** All approaches are fully reactive, support two‑way binding, events, SVG, and automatic cleanup. Choose the one that fits your workflow.
|
||||
|
||||
@@ -1,85 +1,151 @@
|
||||
# Application Mounter: `Mount( )`
|
||||
# Application Mounter: `mount( )`
|
||||
|
||||
The `Mount` function is the entry point of your reactive world. It bridges the gap between your SigPro logic and the browser's Real DOM by injecting a component into the document and initializing its reactive lifecycle.
|
||||
The `mount` function is the entry point of your reactive world. It bridges the gap between your SigPro logic and the browser's real DOM by rendering a component into a target element and managing its full reactive lifecycle.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
Mount(node: Function | HTMLElement, target?: string | HTMLElement): RuntimeObject
|
||||
mount(component: Function | Node, target: string | HTMLElement): RuntimeObject
|
||||
```
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`node`** | `Function` or `Node` | **Required** | The component function or DOM element to render. |
|
||||
| **`target`** | `string` or `Node` | `document.body` | CSS selector or DOM element where the app will live. |
|
||||
| **`component`** | `Function` or `Node` | Yes | A component function (returns a Node) or a direct DOM node. |
|
||||
| **`target`** | `string` or `HTMLElement` | Yes | CSS selector (e.g., `"#app"`) or DOM element where the app will be mounted. |
|
||||
|
||||
**Returns:** A `Runtime` object containing the `container` and a `destroy()` method to wipe all reactivity and DOM nodes.
|
||||
**Returns:** A `Runtime` object with:
|
||||
- `container`: The actual DOM element created by the renderer.
|
||||
- `destroy()`: A method to completely unmount and clean up the application.
|
||||
|
||||
> **Availability:** `mount` is exported from the SigPro module. In **ESM** you must import it (`import { mount } from 'sigpro'`). In the **IIFE** classic script, it is automatically available on `window`. The examples below assume the function is already in scope.
|
||||
|
||||
---
|
||||
|
||||
## Common Usage Scenarios
|
||||
## Usage Patterns
|
||||
|
||||
### 1. The SPA Entry Point
|
||||
In a Single Page Application, you typically mount your main component to the body or a root div. SigPro manages the entire view from that point.
|
||||
### 1. Main Application Entry Point
|
||||
|
||||
```javascript
|
||||
import SigPro from 'sigpro';
|
||||
import App from './App.js';
|
||||
import { mount } from 'sigpro';
|
||||
|
||||
// Mounts your main App component
|
||||
Mount(App, '#app-root');
|
||||
const App = () => div({ class: "app" }, [
|
||||
h1("Hello SigPro"),
|
||||
button("Click me")
|
||||
]);
|
||||
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
### 2. Reactive "Islands"
|
||||
SigPro is perfect for adding reactivity to static pages. You can mount small widgets into specific parts of an existing HTML layout.
|
||||
### 2. Reactive Widget (Island Architecture)
|
||||
|
||||
Mount small reactive components into static HTML pages.
|
||||
|
||||
```javascript
|
||||
const Counter = () => {
|
||||
const count = $(0);
|
||||
return Button({ onclick: () => count(c => c + 1) }, [
|
||||
"Clicks: ", count
|
||||
]);
|
||||
return button({ onclick: () => count(count() + 1) }, () => `Clicks: ${count()}`);
|
||||
};
|
||||
|
||||
// Mount only the counter into a specific sidebar div
|
||||
Mount(Counter, '#sidebar-widget');
|
||||
mount(Counter, '#sidebar-widget');
|
||||
```
|
||||
|
||||
### 3. Direct Node Mounting
|
||||
|
||||
You can also mount an already existing DOM node.
|
||||
|
||||
```javascript
|
||||
const myDiv = div("I am already a node");
|
||||
mount(myDiv, '#container');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How it Works (Lifecycle & Cleanup)
|
||||
## How It Works (Lifecycle & Cleanup)
|
||||
|
||||
When `Mount` is executed, it performs these critical steps to ensure a leak-free environment:
|
||||
When you call `mount`, SigPro performs these steps:
|
||||
|
||||
1. **Duplicate Detection**: If you call `Mount` on a target that already has a SigPro instance, it automatically calls `.destroy()` on the previous instance. This prevents "Zombie Effects" from stacking in memory.
|
||||
2. **Internal Scoping**: It executes the component function inside an internal **Reactive Owner**. This captures every `Watch` and event listener created during the render.
|
||||
3. **Target Injection**: It clears the target using `replaceChildren()` and appends the new component.
|
||||
4. **Runtime Creation**: It returns a control object:
|
||||
* `container`: The actual DOM element created.
|
||||
* `destroy()`: The "kill switch" that runs all cleanups, stops all watchers, and removes the element from the DOM.
|
||||
1. **Duplicate Detection**
|
||||
SigPro keeps a `WeakMap` (`MOUNTED_NODES`) that tracks which DOM target already has a mounted runtime. If you mount a new component to the same target, the previous instance is **automatically destroyed** before the new one is rendered. This prevents memory leaks and “zombie effects”.
|
||||
|
||||
2. **Render Phase**
|
||||
The `render` function creates a **cleanup container** (a `div` with `style="display: contents"`), and executes the component inside a fresh reactive owner. All effects (`watch`), event listeners, and child components created during this render are captured.
|
||||
|
||||
3. **DOM Injection**
|
||||
The target element is cleared using `replaceChildren()`, and the container (which holds the rendered content) is appended.
|
||||
|
||||
4. **Runtime Object**
|
||||
Returns an object `{ _isRuntime: true, container, destroy }`. The `destroy` function recursively disposes all effects, cleans up DOM nodes, and removes the container from the parent.
|
||||
|
||||
---
|
||||
|
||||
## Manual Unmounting
|
||||
|
||||
While SigPro handles most cleanups automatically (via `If`, `For`, and `Router`), you can manually destroy any mounted instance. This is vital for imperatively managed UI like **Toasts** or **Modals**.
|
||||
You can call `destroy()` at any time to tear down the application. This is essential for imperatively managed UI like **modals**, **toasts**, or **dynamic panels**.
|
||||
|
||||
```javascript
|
||||
const instance = Mount(MyToast, '#toast-container');
|
||||
const widget = mount(MyToast, '#toast-container');
|
||||
|
||||
// Later, to remove the toast and kill its reactivity:
|
||||
instance.destroy();
|
||||
// Later, remove it completely:
|
||||
widget.destroy();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automatic Re‑mount on Same Target
|
||||
|
||||
If you call `mount` a second time on the same target, SigPro automatically destroys the previous instance and replaces it with the new one. No manual cleanup required.
|
||||
|
||||
```javascript
|
||||
mount(LoginScreen, '#app');
|
||||
// ... later, after login
|
||||
mount(Dashboard, '#app'); // LoginScreen is destroyed automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What is Automatically Cleaned Up
|
||||
|
||||
When `destroy()` is called (or when a new mount replaces an old one), everything is purged:
|
||||
|
||||
- All `watch` effects
|
||||
- All event listeners added via SigPro (`onClick`, `onInput`, etc.)
|
||||
- All child components created with `when`, `each`, or nested `mount` calls
|
||||
- Any custom cleanups registered with `onUnmount`
|
||||
|
||||
> **You only need manual cleanup** for external resources not managed by SigPro (e.g., `setInterval`, third‑party libraries, WebSocket connections). Use `onUnmount` for that.
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
import { $, mount } from 'sigpro';
|
||||
|
||||
|
||||
const App = () => {
|
||||
const count = $(0);
|
||||
return div({ class: "demo" }, [
|
||||
h1(() => `Count: ${count()}`),
|
||||
button({ onClick: () => count(count() + 1) }, "Increment")
|
||||
]);
|
||||
};
|
||||
|
||||
const runtime = mount(App, '#app');
|
||||
|
||||
// Destroy after 10 seconds
|
||||
setTimeout(() => runtime.destroy(), 10000);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary Cheat Sheet
|
||||
|
||||
| Goal | Code Pattern |
|
||||
| Goal | Code |
|
||||
| :--- | :--- |
|
||||
| **Mount to body** | `Mount(App)` |
|
||||
| **Mount to CSS Selector** | `Mount(App, '#root')` |
|
||||
| **Mount to DOM Node** | `Mount(App, myElement)` |
|
||||
| **Clean & Re-mount** | Calling `Mount` again on the same target |
|
||||
| **Total Cleanup** | `const app = Mount(App); app.destroy();` |
|
||||
| Mount to a CSS selector | `mount(App, '#root')` |
|
||||
| Mount to a DOM element | `mount(App, document.getElementById('root'))` |
|
||||
| Mount a static node | `mount(div("Hello"), '#target')` |
|
||||
| Manual destruction | `const app = mount(App, '#app'); app.destroy();` |
|
||||
| Auto‑replace on same target | Just call `mount` again – SigPro handles cleanup. |
|
||||
|
||||
> **Note:** The target must exist in the DOM at the time of mounting.
|
||||
@@ -1,238 +1,177 @@
|
||||
# ⚡ Quick API Reference
|
||||
# SigPro – Complete API Reference
|
||||
|
||||
SigPro is a high-performance micro-framework that updates the **Real DOM** surgically. No Virtual DOM, no unnecessary re-renders, and built-in **Cleanup** (memory cleanup).
|
||||
## Core Reactivity
|
||||
|
||||
<div class="text-center my-8">
|
||||
<div class="flex justify-center gap-2 flex-wrap mb-4">
|
||||
<span class="badge badge-primary badge-lg font-mono text-lg">$-$$</span>
|
||||
<span class="badge badge-secondary badge-lg font-mono text-lg">Watch</span>
|
||||
<span class="badge badge-accent badge-lg font-mono text-lg">Tag</span>
|
||||
<span class="badge badge-info badge-lg font-mono text-lg">If</span>
|
||||
<span class="badge badge-success badge-lg font-mono text-lg">For</span>
|
||||
<span class="badge badge-warning badge-lg font-mono text-lg">Router</span>
|
||||
<span class="badge badge-error badge-lg font-mono text-lg">Mount</span>
|
||||
</div>
|
||||
<h1 class="text-5xl font-black bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent">
|
||||
⚡ All the power! ⚡
|
||||
</h1>
|
||||
</div>
|
||||
### `$(value, localStorageKey?)` – Signal & Computed
|
||||
|
||||
## Core Functions
|
||||
Creates a reactive signal. If a function is passed, it becomes a **computed** signal that caches its result until dependencies change.
|
||||
|
||||
Explore the reactive building blocks of SigPro.
|
||||
| Usage | Description |
|
||||
|-------|-------------|
|
||||
| `const count = $(0)` | Basic signal, returns a getter/setter: `count()` reads, `count(5)` writes. |
|
||||
| `const double = $( () => count() * 2 )` | Computed signal – updates automatically when `count` changes. |
|
||||
| `const stored = $('hello', 'myKey')` | Persisted signal – reads/writes to `localStorage`. |
|
||||
|
||||
<div class="overflow-x-auto my-8 border border-base-300 rounded-xl shadow-sm">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead class="bg-base-200 text-base-content">
|
||||
<tr>
|
||||
<th>Function</th>
|
||||
<th>Signature</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code class="text-primary font-bold">$(val, key?)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(any, string?) => Signal</td>
|
||||
<td>Creates a <b>Signal</b>. If <code>key</code> is provided, it persists in <code>localStorage</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="text-primary font-bold">$(fn)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(function) => Computed</td>
|
||||
<td>Creates a <b>Computed Signal</b> that auto-updates when dependencies change.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="text-primary font-bold">$$(obj)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(object) => Proxy</td>
|
||||
<td>Creates a <b>Deep Reactive Proxy</b>. Track nested property access automatically. No need for manual signals.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="text-secondary font-bold">Watch(fn)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(function) => stopFn</td>
|
||||
<td><b>Auto Mode:</b> Tracks any signal touched inside. Returns a stop function.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="text-secondary font-bold">Watch(deps, fn)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(Array, function) => stopFn</td>
|
||||
<td><b>Explicit Mode:</b> Only runs when signals in <code>deps</code> change.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="text-accent font-bold">If(cond, then, else?)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(Signal|bool, fn, fn?) => Node</td>
|
||||
<td>Reactive conditional. Automatically destroys "else" branch memory.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="text-accent font-bold">For(src, render, key)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(Signal, fn, fn) => Node</td>
|
||||
<td><b>Keyed Loop:</b> Optimized list renderer. Uses the key function for surgical DOM moves.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="text-info font-bold">Router(routes)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(Array) => Node</td>
|
||||
<td><b>SPA Router:</b> Hash-based routing with dynamic params (<code>:id</code>) and auto-cleanup.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="font-bold">Mount(node, target)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(any, string|Node) => Runtime</td>
|
||||
<td>Entry point. Cleans the target and mounts the app with full lifecycle management.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
**Example**
|
||||
```javascript
|
||||
const count = $(0)
|
||||
const double = $( () => count() * 2 )
|
||||
|
||||
watch(() => {
|
||||
console.log(`count = ${count()}, double = ${double()}`)
|
||||
}) // logs on every change
|
||||
|
||||
count(5) // triggers log: count=5, double=10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Element Constructors (Tags)
|
||||
### `watch(source, callback?)` – Reactive Effect
|
||||
|
||||
SigPro provides **PascalCase** wrappers for all standard HTML5 tags (e.g., `Div`, `Span`, `Button`).
|
||||
Two modes:
|
||||
|
||||
### Syntax Pattern
|
||||
1. **Auto‑track mode** – pass a function: `watch(() => { /* reads signals */ })`
|
||||
Automatically re‑runs whenever any signal read inside changes.
|
||||
|
||||
<div class="mockup-code bg-base-300 text-base-content">
|
||||
<pre data-prefix=""><code>Tag({ attributes }, [children])</code></pre>
|
||||
</div>
|
||||
2. **Explicit mode** – pass an array of signals and a callback:
|
||||
`watch([count, double], () => { ... })`
|
||||
Runs the callback when any of the listed signals change. The callback receives the new values.
|
||||
|
||||
### Special Attributes & Routing
|
||||
Both modes return a `stop` function that disposes the effect.
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 my-10">
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="text-xs font-black uppercase tracking-widest opacity-60">Two-way Binding</h3>
|
||||
<code class="text-primary font-bold text-sm bg-base-300/50 p-2 rounded-lg">value: mySignal</code>
|
||||
<p class="text-xs mt-3 leading-relaxed">Automatic sync for <code>Input</code>, <code>Textarea</code>, and <code>Select</code>. Updates the signal on 'input' or 'change'.</p>
|
||||
</div>
|
||||
</div>
|
||||
```javascript
|
||||
// auto mode
|
||||
const stop = watch(() => console.log(count()))
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="text-xs font-black uppercase tracking-widest opacity-60">Dynamic Routing</h3>
|
||||
<code class="text-info font-bold text-sm bg-base-300/50 p-2 rounded-lg">Router.to('/user/1')</code>
|
||||
<p class="text-xs mt-3 leading-relaxed">Navigate programmatically. Access params via <code>Router.params().id</code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
// explicit mode
|
||||
watch([count, double], ([newCount, newDouble]) => {
|
||||
console.log(newCount, newDouble)
|
||||
})
|
||||
```
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="text-xs font-black uppercase tracking-widest opacity-60">Refs & DOM</h3>
|
||||
<code class="text-accent font-bold text-sm bg-base-300/50 p-2 rounded-lg">ref: (el) => ...</code>
|
||||
<p class="text-xs mt-3 leading-relaxed">Get direct access to the DOM node once it is created.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="text-xs font-black uppercase tracking-widest opacity-60">Event Handling</h3>
|
||||
<code class="text-secondary font-bold text-sm bg-base-300/50 p-2 rounded-lg">onClick: (e) => ...</code>
|
||||
<p class="text-xs mt-3 leading-relaxed">Standard events with automatic <code>removeEventListener</code> on destruction.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
> **Important**: Effects are depth‑aware – they run in topological order, parents before children.
|
||||
|
||||
---
|
||||
|
||||
## Custom API (Bring Your Own Syntax)
|
||||
## Components & DOM (Hyperscript)
|
||||
|
||||
SigPro's core functions are intentionally simple and can be easily renamed in **one line** to match your preferred coding style.
|
||||
### `h(tag, props, children)` – Create DOM Nodes
|
||||
|
||||
### One-Line Renaming
|
||||
The universal builder. `props` can be omitted. Children can be strings, numbers, nodes, arrays, or **dynamic functions**.
|
||||
|
||||
| Feature | Example |
|
||||
|---------|---------|
|
||||
| Standard attributes | `h('div', { class: 'box', id: 'main' })` |
|
||||
| Events | `onClick: (e) => ...` (automatically cleaned up) |
|
||||
| Reactive attributes | `class: () => count() > 0 ? 'positive' : 'negative'` |
|
||||
| Two‑way binding | `value: mySignal` (works on `input`, `textarea`, `select`) |
|
||||
| Refs | `ref: (el) => ...` or `ref: { current: null }` |
|
||||
| SVG support | tag names like `svg`, `circle`, `path` – sets correct namespace |
|
||||
| Dangerous URL sanitising | `href` / `src` with `javascript:` or `data:` are blocked → `'#'` (when XSS shield is active) |
|
||||
|
||||
**Dynamic children** – pass a function as a child, it will be re‑executed and the DOM patched automatically:
|
||||
|
||||
```javascript
|
||||
import { $ as signal, Mount as render, Tag as tag, If as when, For as each, Watch as effect } from 'sigpro';
|
||||
|
||||
// Now use your custom names
|
||||
const count = signal(0);
|
||||
effect(() => console.log(count()));
|
||||
|
||||
render(() =>
|
||||
tag('div', {}, [
|
||||
when(count,
|
||||
() => tag('span', {}, 'Positive'),
|
||||
() => tag('span', {}, 'Zero or negative')
|
||||
)
|
||||
]),
|
||||
'#app'
|
||||
);
|
||||
h('div', {}, [
|
||||
() => count() > 0 ? h('span', {}, 'positive') : h('span', {}, 'zero or negative')
|
||||
])
|
||||
```
|
||||
|
||||
### Create React-like Hooks
|
||||
### Tag shortcuts
|
||||
|
||||
Tag helpers **are exported** from the core.
|
||||
|
||||
Available tags: `a`, `abbr`, `article`, `aside`, `audio`, `b`, `blockquote`, `br`, `button`, `canvas`, `caption`, `cite`, `code`, `col`, `colgroup`, `datalist`, `dd`, `del`, `details`, `dfn`, `dialog`, `div`, `dl`, `dt`, `em`, `embed`, `fieldset`, `figcaption`, `figure`, `footer`, `form`, `h1`…`h6`, `header`, `hr`, `i`, `iframe`, `img`, `input`, `ins`, `kbd`, `label`, `legend`, `li`, `main`, `mark`, `meter`, `nav`, `object`, `ol`, `optgroup`, `option`, `output`, `p`, `picture`, `pre`, `progress`, `section`, `select`, `slot`, `small`, `source`, `span`, `strong`, `sub`, `summary`, `sup`, `svg`, `table`, `tbody`, `td`, `template`, `textarea`, `tfoot`, `th`, `thead`, `time`, `tr`, `u`, `ul`, `video`.
|
||||
|
||||
---
|
||||
|
||||
## Flow Control Components
|
||||
|
||||
### `when(condition, thenComponent, elseComponent?)`
|
||||
|
||||
Reactive conditional rendering. `condition` can be a boolean, a signal, or any function that returns a boolean. Both branches can be `Node`, `() => Node`, or `null`. Automatically disposes the unmounted branch.
|
||||
|
||||
```javascript
|
||||
import * as SigPro from 'sigpro';
|
||||
|
||||
const useState = (initial) => {
|
||||
const signal = SigPro.$(initial);
|
||||
return [signal, (value) => signal(value)];
|
||||
};
|
||||
|
||||
const useEffect = (fn, deps) => {
|
||||
deps ? SigPro.Watch(deps, fn) : SigPro.Watch(fn);
|
||||
};
|
||||
|
||||
// Usage
|
||||
const Counter = () => {
|
||||
const [count, setCount] = useState(0);
|
||||
useEffect(() => console.log(count()), [count]);
|
||||
return SigPro.Tag('button', { onClick: () => setCount(count() + 1) }, count);
|
||||
};
|
||||
when(
|
||||
() => user.loggedIn(),
|
||||
() => div({}, 'Welcome back!'),
|
||||
() => button({ onClick: () => login() }, 'Login')
|
||||
)
|
||||
```
|
||||
|
||||
### Create Vue-like API
|
||||
---
|
||||
|
||||
### `each(source, itemRenderer, keyFn)`
|
||||
|
||||
Optimised keyed list rendering. `source` can be an array or a signal/function returning an array. `itemRenderer(item, index)` returns a Node (or a function that returns Nodes). `keyFn(item, index)` returns a unique identifier – **required** for efficient DOM reuse.
|
||||
|
||||
```javascript
|
||||
import { $ as ref, Watch as watch, Mount as mount } from 'sigpro';
|
||||
const items = $([{ id: 1, text: 'a' }, { id: 2, text: 'b' }])
|
||||
|
||||
const computed = (fn) => ref(fn);
|
||||
const createApp = (component) => ({ mount: (selector) => mount(component, selector) });
|
||||
|
||||
// Usage
|
||||
const count = ref(0);
|
||||
const double = computed(() => count() * 2);
|
||||
watch([count], () => console.log(count()));
|
||||
each(items,
|
||||
(item) => Li({}, item.text),
|
||||
(item) => item.id
|
||||
)
|
||||
```
|
||||
|
||||
### Global Custom API with sigpro.config.js
|
||||
When the array changes, elements are added, removed, or reordered with minimal DOM operations.
|
||||
|
||||
Create a central configuration file to reuse your custom naming across the entire project:
|
||||
---
|
||||
|
||||
## Batch
|
||||
|
||||
### `batch(fn)`
|
||||
|
||||
Batch multiple reactive updates into a single flush, improving performance.
|
||||
|
||||
```javascript
|
||||
// config/sigpro.config.js
|
||||
import { $ as signal, Mount as render, Tag as tag, If as when, For as each, Watch as effect } from 'sigpro';
|
||||
|
||||
// Re-export everything with your custom names
|
||||
export { signal, render, tag, when, each, effect };
|
||||
|
||||
// Also re-export the original functions if needed
|
||||
export * from 'sigpro';
|
||||
batch(() => {
|
||||
count(1)
|
||||
name('John')
|
||||
// effects run only once after the batch ends
|
||||
})
|
||||
```
|
||||
|
||||
## Mounting – `mount(component, target)`
|
||||
|
||||
Clears the target element and mounts the application. Returns the runtime instance (which has a `.destroy()` method).
|
||||
|
||||
```javascript
|
||||
// app.js - Import your custom API globally
|
||||
import { signal, render, tag, when, each, effect } from './config/sigpro.config.js';
|
||||
mount(() => App(), '#app')
|
||||
// or
|
||||
mount(App, document.body)
|
||||
```
|
||||
|
||||
// Use your preferred syntax everywhere
|
||||
const count = signal(0);
|
||||
const double = signal(() => count() * 2);
|
||||
If you mount again on the same target, the previous instance is automatically destroyed.
|
||||
|
||||
effect(() => console.log(`Count: ${count()}, Double: ${double()}`));
|
||||
---
|
||||
|
||||
## Global Cleanup & Memory
|
||||
|
||||
SigPro tracks every effect, DOM event listener, and nested component. When a component is unmounted:
|
||||
- All its effects are disposed.
|
||||
- All DOM event listeners are removed.
|
||||
- All `onUnmount` callbacks run.
|
||||
- Child components are recursively destroyed.
|
||||
|
||||
You never need to manually clean up – just write reactive code.
|
||||
|
||||
---
|
||||
|
||||
## Full Example – Counter with Persistence
|
||||
|
||||
```javascript
|
||||
import { $, mount } from 'sigpro';
|
||||
|
||||
const count = $(0, 'counter') // persists in localStorage
|
||||
|
||||
const App = () =>
|
||||
tag('div', { class: 'p-4' }, [
|
||||
tag('h1', {}, () => `Count: ${count()}`),
|
||||
tag('button', { onclick: () => count(count() + 1) }, 'Increment')
|
||||
]);
|
||||
div({ class: 'counter' }, [
|
||||
h1({}, () => `Count: ${count()}`),
|
||||
button({ onClick: () => count(count() + 1) }, '+'),
|
||||
button({ onClick: () => count(count() - 1) }, '-'),
|
||||
button({ onClick: () => count(0) }, 'Reset')
|
||||
])
|
||||
|
||||
render(App, '#app');
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> **Why rename?** Team preferences, framework migration, or just personal taste. SigPro adapts to you, not the other way around.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Performance Hint:** For lists (`For`), always provide a unique key function `(item) => item.id` to prevent unnecessary node creation and enable reordering.
|
||||
|
||||
> [!TIP]
|
||||
> **Pro Tip:** Use `$$()` for complex nested state objects instead of multiple `$()` signals. It's cleaner and automatically tracks deep properties.
|
||||
|
||||
> [!TIP]
|
||||
> **Performance Hint:** Always use functions `() => signal()` for dynamic children to ensure SigPro only updates the specific node and not the whole container.
|
||||
mount(App, '#app')
|
||||
```
|
||||
@@ -1,96 +0,0 @@
|
||||
# Routing: `Router()` & Utilities
|
||||
|
||||
SigPro includes a built-in, lightweight **Hash Router** to create Single Page Applications (SPA). It manages the URL state, matches components to paths, and handles the lifecycle of your pages automatically.
|
||||
|
||||
## Router Signature
|
||||
|
||||
```typescript
|
||||
Router(routes: Route[]): HTMLElement
|
||||
```
|
||||
|
||||
### Route Object
|
||||
| Property | Type | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **`path`** | `string` | The URL fragment (e.g., `"/"`, `"/user/:id"`, or `"*"`). |
|
||||
| **`component`** | `Function` | A function that returns a Node, a String, or a reactive View. |
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Defining Routes
|
||||
The `Router` returns a `div` element with the class `.router-transition`. When the hash changes, the router destroys the previous view and mounts the new one inside this container.
|
||||
|
||||
```javascript
|
||||
const App = () => Div({ class: "app-layout" }, [
|
||||
Navbar(),
|
||||
// The router outlet is placed here
|
||||
Router([
|
||||
{ path: "/", component: Home },
|
||||
{ path: "/profile/:id", component: (params) => UserProfile(params.id) },
|
||||
{ path: "*", component: () => H1("404 Not Found") }
|
||||
])
|
||||
]);
|
||||
```
|
||||
|
||||
### 2. Dynamic Segments (`:id`)
|
||||
The router automatically parses URL parameters (like `:id`) and passes them as an object to the component function. You can also access them globally via `Router.params()`.
|
||||
|
||||
```javascript
|
||||
// If the URL is #/profile/42
|
||||
const UserProfile = (params) => {
|
||||
return H1(`User ID is: ${params.id}`); // Displays "User ID is: 42"
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Navigation Utilities
|
||||
|
||||
SigPro provides a set of programmatic methods to control the history and read the state.
|
||||
|
||||
### `Router.to(path)`
|
||||
Navigates to a specific path. It automatically formats the hash (e.g., `/home` becomes `#/home`).
|
||||
```javascript
|
||||
Button({ onclick: () => Router.to("/dashboard") }, "Go to Dashboard")
|
||||
```
|
||||
|
||||
### `Router.back()`
|
||||
Goes back to the previous page in the browser history.
|
||||
```javascript
|
||||
Button({ onclick: () => Router.back() }, "Back")
|
||||
```
|
||||
|
||||
### `Router.path()`
|
||||
Returns the current path (without the `#`).
|
||||
```javascript
|
||||
Watch(() => {
|
||||
console.log("Current path is:", Router.path());
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Technical Behavior
|
||||
|
||||
* **Automatic Cleanup**: Every time you navigate, the router calls `.destroy()` on the previous view. This ensures that all **signals, effects, and event listeners** from the old page are purged from memory.
|
||||
* **Reactive Params**: `Router.params` is a signal (`$`). You can react to parameter changes without re-mounting the entire router outlet.
|
||||
* **Hash-Based**: By using `window.location.hash`, SigPro works out-of-the-box on any static hosting (like GitHub Pages or Vercel) without needing server-side redirects.
|
||||
|
||||
---
|
||||
|
||||
## Styling the transition of Router
|
||||
The router returns a standard `div` with the `.router-transition` class. You can easily style it or add transitions:
|
||||
|
||||
```css
|
||||
.router-transition {
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
# The Signal Function: `$( )`
|
||||
# The Signal Function: `$()`
|
||||
|
||||
The `$( )` function is the core constructor of SigPro. It defines how data is stored, computed, and persisted.
|
||||
The `$()` function is the **only** reactive primitive in SigPro. It defines how data is stored, computed, and persisted. For complex nested objects, you compose signals naturally.
|
||||
|
||||
## Function Signature
|
||||
|
||||
@@ -25,21 +25,34 @@ $(computation: Function): ComputedSignal
|
||||
**`$(value)`**
|
||||
Creates a writable signal. It returns a function that acts as both **getter** and **setter**.
|
||||
|
||||
```javascript
|
||||
const count = $(0);
|
||||
<div id="demo-signal-simple"></div>
|
||||
|
||||
count(); // Read (0)
|
||||
count(10); // Write (10)
|
||||
```javascript
|
||||
{
|
||||
const count = $(0);
|
||||
const App = () => div({ class: "example" }, [
|
||||
p(() => `Count: ${count()}`),
|
||||
button({ onClick: () => count(count() + 1) }, "+1")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-signal-simple'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Persistent State
|
||||
**`$(value, key)`**
|
||||
Creates a writable signal that syncs with the browser's storage.
|
||||
|
||||
```javascript
|
||||
const theme = $("light", "app-theme");
|
||||
<div id="demo-signal-persist"></div>
|
||||
|
||||
theme("dark"); // Automatically calls localStorage.setItem("app-theme", '"dark"')
|
||||
```javascript
|
||||
{
|
||||
const theme = $("light", "theme-persist-demo");
|
||||
const App = () => div([
|
||||
p(() => `Current theme: ${theme()}`),
|
||||
button({ onClick: () => theme(theme() === "light" ? "dark" : "light") }, "Toggle theme")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-signal-persist'), 50);
|
||||
}
|
||||
```
|
||||
*Note: On page load, SigPro will prioritize the value found in `localStorage` over the `initialValue`.*
|
||||
|
||||
@@ -47,12 +60,23 @@ theme("dark"); // Automatically calls localStorage.setItem("app-theme", '"dark"'
|
||||
**`$(function)`**
|
||||
Creates a read-only signal that updates automatically when any signal used inside it changes.
|
||||
|
||||
```javascript
|
||||
const price = $(100);
|
||||
const tax = $(0.21);
|
||||
<div id="demo-signal-computed"></div>
|
||||
|
||||
// This tracks both 'price' and 'tax' automatically
|
||||
const total = $(() => price() * (1 + tax()));
|
||||
```javascript
|
||||
{
|
||||
const price = $(100);
|
||||
const tax = $(0.21);
|
||||
const total = $(() => price() * (1 + tax()));
|
||||
|
||||
const App = () => div([
|
||||
p(() => `Price: €${price()}`),
|
||||
p(() => `Tax rate: ${tax() * 100}%`),
|
||||
p(() => `Total: €${total().toFixed(2)}`),
|
||||
button({ onClick: () => price(price() + 10) }, "+€10"),
|
||||
button({ onClick: () => price(price() - 10) }, "-€10")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-signal-computed'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -60,156 +84,196 @@ const total = $(() => price() * (1 + tax()));
|
||||
## Updating with Logic
|
||||
When calling the setter, you can pass an **updater function** to access the current value safely.
|
||||
|
||||
<div id="demo-signal-updater"></div>
|
||||
|
||||
```javascript
|
||||
const list = $(["A", "B"]);
|
||||
|
||||
// Adds "C" using the previous state
|
||||
list(prev => [...prev, "C"]);
|
||||
{
|
||||
const list = $(["A", "B"]);
|
||||
const App = () => div([
|
||||
ul(() => list().map(item => li(item))),
|
||||
button({ onClick: () => list(prev => [...prev, "C"]) }, "Add C")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-signal-updater'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# The Reactive Object: `$$( )`
|
||||
## Composing Signals for Complex State
|
||||
|
||||
The `$$( )` function creates a reactive proxy for complex nested objects. Unlike `$()`, which tracks a single value, `$$()` tracks **every property access** automatically.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
$$<T extends object>(obj: T): T
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`obj`** | `object` | Yes | The object to make reactive. Properties are tracked recursively. |
|
||||
|
||||
**Returns:** A reactive proxy that behaves like the original object but triggers updates when any property changes.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
For nested objects, **compose signals** instead of using magic proxies. This gives you explicit control over reactivity and memory.
|
||||
|
||||
### 1. Simple Object
|
||||
|
||||
<div id="demo-compose-simple"></div>
|
||||
|
||||
```javascript
|
||||
const state = $$({ count: 0, name: "Juan" });
|
||||
{
|
||||
const count = $(0);
|
||||
const name = $("Juan");
|
||||
|
||||
Watch(() => state.count, () => {
|
||||
console.log(`Count is now ${state.count}`);
|
||||
});
|
||||
// Optionally create a derived combined state
|
||||
const state = $(() => ({ count: count(), name: name() }));
|
||||
|
||||
state.count++; // ✅ Triggers update
|
||||
state.name = "Ana"; // ✅ Also reactive
|
||||
const App = () => div([
|
||||
p(() => `Count: ${count()}, Name: ${name()}`),
|
||||
button({ onClick: () => count(count() + 1) }, "Increment count"),
|
||||
button({ onClick: () => name(name() === "Juan" ? "Ana" : "Juan") }, "Toggle name")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-compose-simple'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Deep Reactivity
|
||||
### 2. Deeply Nested State
|
||||
|
||||
Unlike `$()`, `$$()` tracks nested properties automatically.
|
||||
<div id="demo-compose-deep"></div>
|
||||
|
||||
```javascript
|
||||
const user = $$({
|
||||
profile: {
|
||||
name: "Juan",
|
||||
address: {
|
||||
city: "Madrid",
|
||||
zip: "28001"
|
||||
}
|
||||
}
|
||||
});
|
||||
{
|
||||
const profileName = $("Juan");
|
||||
const profileCity = $("Madrid");
|
||||
const profileZip = $("28001");
|
||||
|
||||
// This works! Tracks deep property access
|
||||
Watch(() => user.profile.address.city, () => {
|
||||
console.log("City changed");
|
||||
});
|
||||
// Computed derived values
|
||||
const fullAddress = $(() => `${profileCity()}, ${profileZip()}`);
|
||||
|
||||
user.profile.address.city = "Barcelona"; // ✅ Triggers update
|
||||
watch(profileCity, () => console.log("City changed to:", profileCity()));
|
||||
|
||||
const App = () => div([
|
||||
p(() => `Name: ${profileName()}`),
|
||||
p(() => `City: ${profileCity()}`),
|
||||
p(() => `Full address: ${fullAddress()}`),
|
||||
button({ onClick: () => profileCity("Barcelona") }, "Change to Barcelona")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-compose-deep'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Arrays
|
||||
|
||||
`$$()` works with arrays and array methods.
|
||||
<div id="demo-compose-array"></div>
|
||||
|
||||
```javascript
|
||||
const todos = $$([
|
||||
{
|
||||
const todos = $([
|
||||
{ id: 1, text: "Learn SigPro", done: false },
|
||||
{ id: 2, text: "Build an app", done: false }
|
||||
]);
|
||||
]);
|
||||
|
||||
Watch(() => todos.length, () => {
|
||||
console.log(`You have ${todos.length} todos`);
|
||||
});
|
||||
const todoCount = $(() => todos().length);
|
||||
|
||||
// Array methods are reactive
|
||||
todos.push({ id: 3, text: "Deploy", done: false }); // ✅ Triggers
|
||||
todos[0].done = true; // ✅ Deep reactivity works
|
||||
todos.splice(1, 1); // ✅ Triggers
|
||||
watch(todoCount, () => console.log(`You have ${todoCount()} todos`));
|
||||
|
||||
const App = () => div([
|
||||
ul(() => todos().map(todo => li(todo.text + (todo.done ? " ✓" : "")))),
|
||||
button({ onClick: () => todos(prev => [...prev, { id: Date.now(), text: "New todo", done: false }]) }, "Add todo"),
|
||||
button({ onClick: () => {
|
||||
const updated = [...todos()];
|
||||
updated[0] = { ...updated[0], done: !updated[0].done };
|
||||
todos(updated);
|
||||
}}, "Toggle first todo")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-compose-array'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Mixed with Signals
|
||||
### 4. Complete Form Example
|
||||
|
||||
`$$()` works seamlessly with `$()` signals.
|
||||
<div id="demo-compose-form"></div>
|
||||
|
||||
```javascript
|
||||
const form = $$({
|
||||
fields: {
|
||||
email: "",
|
||||
password: ""
|
||||
},
|
||||
isValid: $(false) // Signal inside reactive object
|
||||
});
|
||||
{
|
||||
const email = $("");
|
||||
const password = $("");
|
||||
const isValid = $(() => email().includes("@") && password().length > 6);
|
||||
|
||||
// Computed using both
|
||||
const canSubmit = $(() =>
|
||||
form.fields.email.includes("@") &&
|
||||
form.fields.password.length > 6
|
||||
);
|
||||
watch(isValid, valid => console.log("Form valid:", valid));
|
||||
|
||||
Watch(canSubmit, (valid) => {
|
||||
form.isValid(valid); // Update signal inside reactive object
|
||||
});
|
||||
const App = () => div([
|
||||
input({
|
||||
type: "email",
|
||||
placeholder: "Email",
|
||||
value: email,
|
||||
onInput: e => email(e.target.value)
|
||||
}),
|
||||
input({
|
||||
type: "password",
|
||||
placeholder: "Password",
|
||||
value: password,
|
||||
onInput: e => password(e.target.value)
|
||||
}),
|
||||
p(() => `Form valid: ${isValid() ? "Yes" : "No"}`)
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-compose-form'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Differences: `$()` vs `$$()`
|
||||
## Best Practices for Complex State
|
||||
|
||||
| Feature | `$()` Signal | `$$()` Reactive |
|
||||
| :--- | :--- | :--- |
|
||||
| **Primitives** | ✅ Works directly | ❌ Needs wrapper object |
|
||||
| **Objects** | Manual tracking | ✅ Automatic deep tracking |
|
||||
| **Nested properties** | ❌ Not reactive | ✅ Fully reactive |
|
||||
| **Arrays** | Requires reassignment | ✅ Methods (push, pop, etc.) work |
|
||||
| **Syntax** | `count()` / `count(5)` | `state.count = 5` |
|
||||
| **LocalStorage** | ✅ Built-in | ❌ (use `$()` for persistence) |
|
||||
| **Performance** | Lighter | Slightly heavier (Proxy) |
|
||||
| **Destructuring** | ✅ Safe | ❌ Breaks reactivity |
|
||||
|
||||
---
|
||||
|
||||
## When to Use Each
|
||||
|
||||
### Use `$()` when:
|
||||
- Working with primitives (numbers, strings, booleans)
|
||||
- Need localStorage persistence
|
||||
- Creating computed values
|
||||
- Want explicit control over updates
|
||||
### ✅ DO: Compose signals explicitly
|
||||
|
||||
```javascript
|
||||
const count = $(0);
|
||||
const user = $(null);
|
||||
const fullName = $(() => `${firstName()} ${lastName()}`);
|
||||
// Clear, predictable, and memory-safe
|
||||
const user = {
|
||||
name: $("Juan"),
|
||||
email: $("juan@example.com"),
|
||||
preferences: {
|
||||
theme: $("dark"),
|
||||
notifications: $(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Computed values derived from composition
|
||||
const userDisplay = $(() => `${user.name()} <${user.email()}>`)
|
||||
```
|
||||
|
||||
### Use `$$()` when:
|
||||
- Working with complex nested objects
|
||||
- Managing forms with multiple fields
|
||||
- Using arrays with mutations (push, pop, splice)
|
||||
- Want natural object syntax (no function calls)
|
||||
### ✅ DO: Create store patterns
|
||||
|
||||
```javascript
|
||||
const form = $$({ email: "", password: "" });
|
||||
const settings = $$({ theme: "dark", notifications: true });
|
||||
const store = $$({ users: [], filters: {}, pagination: { page: 1 } });
|
||||
const createUserStore = () => {
|
||||
const name = $("")
|
||||
const email = $("")
|
||||
|
||||
const isValid = $(() => name().length > 0 && email().includes("@"))
|
||||
|
||||
const actions = {
|
||||
setName: (value) => name(value),
|
||||
setEmail: (value) => email(value),
|
||||
reset: () => {
|
||||
name("")
|
||||
email("")
|
||||
}
|
||||
}
|
||||
|
||||
return { name, email, isValid, ...actions }
|
||||
}
|
||||
|
||||
const userStore = createUserStore()
|
||||
```
|
||||
|
||||
### ❌ DON'T: Try to wrap objects with signals
|
||||
|
||||
```javascript
|
||||
// Wrong - loses reactivity on nested properties
|
||||
const user = $({ name: "Juan", email: "..." })
|
||||
user().name = "Ana" // ❌ Not reactive!
|
||||
|
||||
// Correct - each property its own signal
|
||||
const userName = $("Juan")
|
||||
const userEmail = $("...")
|
||||
```
|
||||
|
||||
### ❌ DON'T: Destructure signals in reactive contexts
|
||||
|
||||
```javascript
|
||||
// Wrong - breaks tracking
|
||||
const { name, email } = user
|
||||
watch(() => name(), ...) // ❌ 'name' is not tracked properly
|
||||
|
||||
// Correct - use the original signal
|
||||
watch(() => user.name(), ...) // ✅
|
||||
```
|
||||
|
||||
---
|
||||
@@ -218,124 +282,121 @@ const store = $$({ users: [], filters: {}, pagination: { page: 1 } });
|
||||
|
||||
### ✅ DO:
|
||||
```javascript
|
||||
// Access properties directly
|
||||
state.count = 10;
|
||||
state.user.name = "Ana";
|
||||
todos.push(newItem);
|
||||
// Update by recreating objects for arrays
|
||||
todos(prev => [...prev, newTodo])
|
||||
|
||||
// Track in effects
|
||||
Watch(() => state.count, () => {});
|
||||
Watch(() => state.user.name, () => {});
|
||||
// Update objects immutably
|
||||
const current = user()
|
||||
user({ ...current, name: "Ana" })
|
||||
|
||||
// Track individual signals
|
||||
watch(() => user.name(), () => {})
|
||||
watch(() => user.email(), () => {})
|
||||
```
|
||||
|
||||
### ❌ DON'T:
|
||||
```javascript
|
||||
// Destructuring breaks reactivity
|
||||
const { count, user } = state; // ❌ count and user are not reactive
|
||||
// Mutate objects directly
|
||||
user().name = "Ana" // ❌ Not reactive
|
||||
|
||||
// Reassigning the whole object
|
||||
state = { count: 10 }; // ❌ Loses reactivity
|
||||
// Mutate arrays in place
|
||||
todos().push(newTodo) // ❌ Not reactive
|
||||
|
||||
// Using primitive directly
|
||||
const count = $$(0); // ❌ Doesn't work (use $() instead)
|
||||
// Destructure in component bodies
|
||||
const { name, email } = user // ❌ Breaks reactivity
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automatic Cleanup
|
||||
|
||||
Like all SigPro reactive primitives, `$$()` integrates with the cleanup system:
|
||||
All signals integrate with the cleanup system:
|
||||
|
||||
- Effects tracking reactive properties are automatically disposed
|
||||
- No manual cleanup needed
|
||||
- Works with `Watch`, `If`, and `For`
|
||||
```javascript
|
||||
// Effects are automatically disposed when components unmount
|
||||
const name = $("Juan")
|
||||
watch(name, () => console.log("Name changed"))
|
||||
|
||||
---
|
||||
|
||||
## Technical Comparison
|
||||
|
||||
| Aspect | `$()` | `$$()` |
|
||||
| :--- | :--- | :--- |
|
||||
| **Implementation** | Closure with Set | Proxy with WeakMap |
|
||||
| **Tracking** | Explicit (function call) | Implicit (property access) |
|
||||
| **Memory** | Minimal | Slightly more (WeakMap cache) |
|
||||
| **Use Case** | Simple state | Complex state |
|
||||
| **Learning Curve** | Low | Low (feels like plain JS) |
|
||||
// Manual cleanup if needed
|
||||
const stop = watch(name, callback)
|
||||
stop() // Clean up manually
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
<div id="demo-complete-final"></div>
|
||||
|
||||
```javascript
|
||||
// Combining both approaches
|
||||
const app = {
|
||||
// Simple primitives with persistence
|
||||
theme: $("dark", "theme"),
|
||||
sidebarOpen: $(true),
|
||||
{
|
||||
// All state as explicit signals
|
||||
const theme = $("dark", "theme_complete")
|
||||
const sidebarOpen = $(true)
|
||||
const userName = $("")
|
||||
const userEmail = $("")
|
||||
const notifications = $(true)
|
||||
const language = $("es")
|
||||
|
||||
// Complex state with $$()
|
||||
user: $$({
|
||||
name: "",
|
||||
email: "",
|
||||
preferences: {
|
||||
notifications: true,
|
||||
language: "es"
|
||||
// Computed signals
|
||||
const isLoggedIn = $(() => !!userName() && !!userEmail())
|
||||
|
||||
// Actions as plain functions
|
||||
const login = (name, email) => {
|
||||
userName(name)
|
||||
userEmail(email)
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userName("")
|
||||
userEmail("")
|
||||
notifications(true) // Reset on logout
|
||||
}
|
||||
|
||||
// Components using signals directly
|
||||
const LoginForm = () => div([
|
||||
input({
|
||||
placeholder: "Name",
|
||||
onInput: e => userName(e.target.value)
|
||||
}),
|
||||
input({
|
||||
placeholder: "Email",
|
||||
onInput: e => userEmail(e.target.value)
|
||||
}),
|
||||
button({
|
||||
onClick: () => login(userName(), userEmail())
|
||||
}, "Login")
|
||||
])
|
||||
|
||||
// Computed values
|
||||
isLoggedIn: $(() => !!app.user.name),
|
||||
const UserProfile = () => div([
|
||||
h2(() => `Welcome ${userName()}`),
|
||||
p(() => `Email: ${userEmail()}`),
|
||||
p(() => `Notifications: ${notifications() ? "ON" : "OFF"}`),
|
||||
p(() => `Language: ${language()}`),
|
||||
button({
|
||||
onClick: () => notifications(!notifications())
|
||||
}, "Toggle Notifications"),
|
||||
button({ onClick: logout }, "Logout")
|
||||
])
|
||||
|
||||
// Actions
|
||||
login(name, email) {
|
||||
app.user.name = name;
|
||||
app.user.email = email;
|
||||
},
|
||||
const App = () => div({ class: "complete-example" }, [
|
||||
when(() => isLoggedIn(), () => UserProfile(), () => LoginForm())
|
||||
])
|
||||
|
||||
logout() {
|
||||
app.user.name = "";
|
||||
app.user.email = "";
|
||||
app.user.preferences.notifications = true;
|
||||
}
|
||||
};
|
||||
|
||||
// UI component
|
||||
const UserProfile = () => {
|
||||
return Div({}, [
|
||||
If(() => app.isLoggedIn(),
|
||||
() => Div({}, [
|
||||
H2(`Welcome ${app.user.name}`),
|
||||
P(`Email: ${app.user.email}`),
|
||||
P(`Notifications: ${app.user.preferences.notifications ? "ON" : "OFF"}`),
|
||||
Button({ onclick: () => app.user.preferences.notifications = !app.user.preferences.notifications },
|
||||
"Toggle Notifications"
|
||||
),
|
||||
Button({ onclick: app.logout }, "Logout")
|
||||
]),
|
||||
() => LoginForm()
|
||||
)
|
||||
]);
|
||||
};
|
||||
setTimeout(() => mount(App, '#demo-complete-final'), 50)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration from `$()` to `$$()`
|
||||
## Summary
|
||||
|
||||
If you have code using nested signals:
|
||||
With **only `$()`** as your reactive primitive:
|
||||
|
||||
```javascript
|
||||
// Before - Manual nesting
|
||||
const user = $({
|
||||
name: $(""),
|
||||
email: $("")
|
||||
});
|
||||
user().name("Juan"); // Need to call inner signal
|
||||
- ✅ **Explicit** - You know exactly what's reactive
|
||||
- ✅ **Memory safe** - No hidden proxies or WeakMap caches
|
||||
- ✅ **Predictable** - No magic, just signals
|
||||
- ✅ **Performant** - Minimal overhead
|
||||
- ✅ **Debuggable** - Clear data flow
|
||||
|
||||
// After - Automatic nesting
|
||||
const user = $$({
|
||||
name: "",
|
||||
email: ""
|
||||
});
|
||||
user.name = "Juan"; // Direct assignment
|
||||
```
|
||||
Complex state is built by **composing signals**, not by wrapping objects. This gives you the same power as reactive proxies but with better control and fewer surprises.
|
||||
185
docs/api/tags.md
185
docs/api/tags.md
@@ -1,189 +1,100 @@
|
||||
# Global Tag Helpers
|
||||
|
||||
In **SigPro**, you don't need to manually type `Tag('div', ...)` for every element. To keep your code declarative and readable, the engine automatically generates **Global Helper Functions** for all standard HTML5 tags upon initialization.
|
||||
In **SigPro**, you don't need to manually type `h('div', ...)` for every element. To keep your code declarative and readable, the engine provides **helper functions** for all standard HTML5 tags.
|
||||
|
||||
## 1. How it Works
|
||||
|
||||
SigPro iterates through a manifest of standard HTML tags and attaches a wrapper function for each one directly to the `window` object. This creates a specialized **DSL** (Domain Specific Language) that looks like a template engine but is **100% standard JavaScript**.
|
||||
SigPro creates a wrapper function for each standard HTML tag.
|
||||
- **Under the hood:** `h('button', { onclick: ... }, 'Click')`
|
||||
- **SigPro Style:** `button({ onclick: ... }, 'Click')`
|
||||
|
||||
* **Under the hood:** `Tag('button', { onclick: ... }, 'Click')`
|
||||
* **SigPro Style:** `Button({ onclick: ... }, 'Click')`
|
||||
> **Note:** All tag helpers are **lowercase** (e.g., `div`, `span`, `button`) and can be used directly once globally enabled.
|
||||
|
||||
> If you prefer to avoid globals, you can always use `h('div', ...)` directly—it’s perfectly fine.
|
||||
|
||||
> **Auto‑cleanup:** All tag helpers and `h` automatically dispose effects, event listeners, and nested components when removed from the DOM.
|
||||
|
||||
---
|
||||
|
||||
## 2. The Complete Global Registry
|
||||
## 2. The Complete List of Tag Helpers
|
||||
|
||||
The following functions are injected into the global scope using **PascalCase** to prevent naming collisions with common JS variables:
|
||||
All helpers are **lowercase** and follow HTML5 tag names.
|
||||
|
||||
| Category | Available Global Functions |
|
||||
| Category | Available functions |
|
||||
| :--- | :--- |
|
||||
| **Structure** | `Div`, `Span`, `P`, `Section`, `Nav`, `Main`, `Header`, `Footer`, `Article`, `Aside` |
|
||||
| **Typography** | `H1` to `H6`, `Ul`, `Ol`, `Li`, `Dl`, `Dt`, `Dd`, `Strong`, `Em`, `Code`, `Pre`, `Small`, `B`, `U`, `Mark` |
|
||||
| **Interactive** | `Button`, `A`, `Label`, `Br`, `Hr`, `Details`, `Summary`, `Dialog` |
|
||||
| **Forms** | `Form`, `Input`, `Select`, `Option`, `Textarea`, `Fieldset`, `Legend` |
|
||||
| **Tables** | `Table`, `Thead`, `Tbody`, `Tr`, `Th`, `Td`, `Tfoot`, `Caption` |
|
||||
| **Media** | `Img`, `Canvas`, `Video`, `Audio`, `Svg`, `Iframe`, `Picture`, `Source` |
|
||||
| **Structure** | `div`, `span`, `p`, `section`, `nav`, `main`, `header`, `footer`, `article`, `aside` |
|
||||
| **Typography** | `h1`…`h6`, `ul`, `ol`, `li`, `dl`, `dt`, `dd`, `strong`, `em`, `code`, `pre`, `small`, `b`, `u`, `mark` |
|
||||
| **Interactive** | `button`, `a`, `label`, `br`, `hr`, `details`, `summary`, `dialog` |
|
||||
| **Forms** | `form`, `input`, `select`, `option`, `textarea`, `fieldset`, `legend` |
|
||||
| **Tables** | `table`, `thead`, `tbody`, `tr`, `th`, `td`, `tfoot`, `caption` |
|
||||
| **Media** | `img`, `canvas`, `video`, `audio`, `svg`, `iframe`, `picture`, `source` |
|
||||
|
||||
Full list: `a`, `abbr`, `article`, `aside`, `audio`, `b`, `blockquote`, `br`, `button`, `canvas`, `caption`, `cite`, `code`, `col`, `colgroup`, `datalist`, `dd`, `del`, `details`, `dfn`, `dialog`, `div`, `dl`, `dt`, `em`, `embed`, `fieldset`, `figcaption`, `figure`, `footer`, `form`, `h1`…`h6`, `header`, `hr`, `i`, `iframe`, `img`, `input`, `ins`, `kbd`, `label`, `legend`, `li`, `main`, `mark`, `meter`, `nav`, `object`, `ol`, `optgroup`, `option`, `output`, `p`, `picture`, `pre`, `progress`, `section`, `select`, `slot`, `small`, `source`, `span`, `strong`, `sub`, `summary`, `sup`, `svg`, `table`, `tbody`, `td`, `template`, `textarea`, `tfoot`, `th`, `thead`, `time`, `tr`, `u`, `ul`, `video`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Usage Patterns (Smart Arguments)
|
||||
|
||||
SigPro tag helpers are flexible. They automatically detect if you are passing attributes, children, or both.
|
||||
## 3. Usage Patterns
|
||||
|
||||
### A. Attributes + Children
|
||||
|
||||
```javascript
|
||||
Div({ class: 'container', id: 'main' }, [
|
||||
H1("Welcome to SigPro"),
|
||||
P("The zero-VDOM framework.")
|
||||
div({ class: 'container', id: 'main' }, [
|
||||
h1("Welcome to SigPro"),
|
||||
p("The zero‑VDOM framework.")
|
||||
]);
|
||||
```
|
||||
|
||||
### B. Children Only (The "Skipper")
|
||||
If you don't need attributes, you can pass the content directly as the first argument.
|
||||
### B. Children Only
|
||||
|
||||
If you don't need attributes, pass the content directly as the first argument.
|
||||
|
||||
```javascript
|
||||
Section([
|
||||
H2("Clean Syntax"),
|
||||
Button("I have no props!")
|
||||
section([
|
||||
h2("Clean Syntax"),
|
||||
button("I have no props!")
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Reactive Power
|
||||
## 4. Custom Components with `h()` or Tag Helpers
|
||||
|
||||
These helpers are natively wired into SigPro's **`Watch`** engine.
|
||||
While the tag helpers cover all standard HTML tags, you can create reusable components using them directly.
|
||||
|
||||
### Reactive Attributes (One-Way)
|
||||
Simply pass a Signal (function) to any attribute. SigPro creates an internal `Watch` to keep the DOM in sync.
|
||||
```javascript
|
||||
const theme = $("light");
|
||||
|
||||
Div({
|
||||
class: () => `app-box ${theme()}`
|
||||
}, "Themeable Box");
|
||||
```
|
||||
|
||||
### Smart Two-Way Binding (Automatic)
|
||||
SigPro automatically bridges the **Signal** and the **Input** element bi-directionally when you assign a Signal to `value` or `checked`. No special operators are required.
|
||||
|
||||
```javascript
|
||||
const search = $("");
|
||||
|
||||
// UI updates Signal AND Signal updates UI automatically
|
||||
Input({
|
||||
type: "text",
|
||||
placeholder: "Search...",
|
||||
value: search
|
||||
});
|
||||
```
|
||||
|
||||
> **Pro Tip:** If you want an input to be **read-only** but still reactive, wrap the signal in an anonymous function: `value: () => search()`. This prevents the "backwards" synchronization.
|
||||
|
||||
### Dynamic Flow & Cleanup
|
||||
Combine tags with Core controllers. SigPro automatically cleans up the `Watch` instances and event listeners when nodes are removed from the DOM.
|
||||
```javascript
|
||||
const items = $(["Apple", "Banana", "Cherry"]);
|
||||
|
||||
Ul({ class: "list-disc" }, [
|
||||
For(items, (item) => Li(item), (item) => item)
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<div class="alert alert-error shadow-lg my-8 border-l-8 border-error bg-error/10 text-error-content">
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg">Important: Naming Conventions</h3>
|
||||
<div class="text-sm opacity-90">
|
||||
<ol class="list-decimal ml-4 mt-2 space-y-2">
|
||||
<li><b>Avoid Shadowing:</b> Don't name your local variables like the tags (e.g., <code>const Div = ...</code>). This will "hide" the global SigPro helper.</li>
|
||||
<li><b>Custom Components:</b> Always use <b>PascalCase</b> for your own component functions (e.g., <code>UserCard</code>, <code>NavMenu</code>) to distinguish them from built-in Tag Helpers.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 5. Logic to UI Comparison
|
||||
|
||||
Here is how a dynamic **User Status** component translates from SigPro logic to the final DOM structure.
|
||||
|
||||
```javascript
|
||||
const UserStatus = (name, online) => (
|
||||
Div({ class: 'flex items-center gap-2' }, [
|
||||
Span({
|
||||
hidden: () => !online(),
|
||||
class: 'w-3 h-3 bg-green-500 rounded-full'
|
||||
}),
|
||||
P({
|
||||
class: () => online() ? "text-bold" : "text-gray-400"
|
||||
}, name)
|
||||
])
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Custom Tags with `Tag`
|
||||
|
||||
Create reusable components using `Tag`. All reactivity auto-cleans itself.
|
||||
|
||||
### Basic Example
|
||||
### Basic Component
|
||||
|
||||
```javascript
|
||||
const UserCard = (props, children) =>
|
||||
Tag('div', { class: 'card p-4', 'data-id': props.id }, children);
|
||||
div({ class: 'card p-4', 'data-id': props.id }, children);
|
||||
|
||||
UserCard({ id: 123 }, [H3("John Doe"), P("john@example.com")]);
|
||||
UserCard({ id: 123 }, [h3("John Doe"), p("john@example.com")]);
|
||||
```
|
||||
|
||||
### Reactive Component (Auto-Cleaned)
|
||||
### Reactive Component
|
||||
|
||||
```javascript
|
||||
const Counter = (initial = 0) => {
|
||||
const count = $(initial);
|
||||
return Tag('div', { class: 'flex gap-2' }, [
|
||||
Button({ onclick: () => count(count() - 1) }, '-'),
|
||||
Span(() => count()),
|
||||
Button({ onclick: () => count(count() + 1) }, '+')
|
||||
const Counter = () => {
|
||||
const count = $(0);
|
||||
return div({ class: 'flex gap-2' }, [
|
||||
button({ onClick: () => count(count() - 1) }, '-'),
|
||||
span(() => count()),
|
||||
button({ onClick: () => count(count() + 1) }, '+')
|
||||
]);
|
||||
};
|
||||
```
|
||||
|
||||
### When Manual Cleanup is Needed
|
||||
### Manual Cleanup for External Resources
|
||||
|
||||
Only for external resources (intervals, sockets, third-party libs):
|
||||
Only needed for intervals, sockets, third‑party libraries:
|
||||
|
||||
```javascript
|
||||
const Timer = () => {
|
||||
const time = $(new Date().toLocaleTimeString());
|
||||
const el = Tag('span', {}, () => time());
|
||||
const el = span(() => time());
|
||||
|
||||
const interval = setInterval(() => time(new Date().toLocaleTimeString()), 1000);
|
||||
el._cleanups.add(() => clearInterval(interval)); // Manual cleanup
|
||||
onUnmount(() => clearInterval(interval));
|
||||
|
||||
return el;
|
||||
};
|
||||
```
|
||||
|
||||
### `Tag` vs Tag Helpers
|
||||
|
||||
| Use | Recommendation |
|
||||
|:---|:---|
|
||||
| Standard tags (`div`, `span`) | `Div()`, `Span()` helpers |
|
||||
| Reusable components | Function returning `Tag` |
|
||||
| Dynamic tag names | `Tag(tagName, props, children)` |
|
||||
|
||||
> **Auto-cleanup**: `Tag` automatically destroys watchers, events, and nested components. Only add to `_cleanups` for external resources.
|
||||
|
||||
---
|
||||
|
||||
| State (`online`) | Rendered HTML | Memory Management |
|
||||
| :--- | :--- | :--- |
|
||||
| **`true`** | `<div class="flex..."><span class="w-3..."></span><p class="text-bold">John</p></div>` | Watcher active |
|
||||
| **`false`** | `<div class="flex..."><span hidden class="w-3..."></span><p class="text-gray-400">John</p></div>` | Attribute synced |
|
||||
|
||||
|
||||
@@ -1,91 +1,124 @@
|
||||
# Reactivity Control: `Watch( )`
|
||||
# Reactivity Control: `watch( )`
|
||||
|
||||
The `Watch` function is the reactive engine of SigPro. It allows you to execute code automatically when signals change. `Watch` is **polymorphic**: it can track dependencies automatically or follow an explicit list.
|
||||
The `watch` function is the reactive engine of SigPro. It allows you to execute code automatically when signals change. `watch` is **polymorphic**: it can track dependencies automatically or follow an explicit list.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
// Automatic Mode (Magic Tracking)
|
||||
Watch(callback: Function): StopFunction
|
||||
watch(callback: Function): StopFunction
|
||||
|
||||
// Explicit Mode (Isolated Dependencies)
|
||||
Watch(deps: Signal[], callback: Function): StopFunction
|
||||
watch(deps: Signal[], callback: (values: any[]) => void): StopFunction
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`target / deps`** | `Function` | `Array` | Yes | Either the code to run (Auto) or an array of signals to watch (Explicit). |
|
||||
| **`callback`** | `Function` | Only in Explicit | The code that will run when the `deps` change. |
|
||||
| **`callback`** (auto mode) | `Function` | Yes | The code to run. Any signal accessed inside becomes a dependency. |
|
||||
| **`deps`** (explicit mode) | `Signal[]` | Yes | An array of signals to watch explicitly. |
|
||||
| **`callback`** (explicit mode) | `Function` | Yes | Runs when any of the `deps` change. Receives an array of their current values. |
|
||||
|
||||
**Returns:** A `StopFunction` that, when called, destroys the watcher and releases memory.
|
||||
|
||||
> **Availability:** `watch` is exported from the SigPro module. In **ESM** you must import it (`import { watch } from 'sigpro'`). In the **IIFE** classic script, it is automatically available on `window`. The examples below assume the function is already in scope.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Automatic Mode (Default)
|
||||
Any signal you "touch" inside the callback becomes a dependency. SigPro tracks them behind the scenes.
|
||||
Any signal you **touch** inside the callback becomes a dependency. SigPro tracks them behind the scenes.
|
||||
|
||||
```javascript
|
||||
const count = $(0);
|
||||
|
||||
Watch(() => {
|
||||
// Re-runs every time 'count' changes
|
||||
watch(() => {
|
||||
// Re‑runs every time 'count' changes
|
||||
console.log(`Count is: ${count()}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Explicit Mode (Advanced Cleanup)
|
||||
This mode **isolates** execution. The callback only triggers when the signals in the array change. Any other signal accessed *inside* the callback will NOT trigger a re-run. This is the "gold standard" for Routers and heavy components.
|
||||
### 2. Explicit Mode (Isolated)
|
||||
This mode **isolates** execution. The callback only triggers when the signals in the array change. Any other signal accessed *inside* the callback will **not** trigger a re‑run. This is ideal for routers or performance‑critical components.
|
||||
|
||||
```javascript
|
||||
const sPath = $("/home");
|
||||
const path = $("/home");
|
||||
const user = $("Admin");
|
||||
|
||||
Watch([sPath], () => {
|
||||
// Only triggers when 'sPath' changes.
|
||||
// Changes to 'user' will NOT trigger this, preventing accidental re-renders.
|
||||
console.log(`Navigating to ${sPath()} as ${user()}`);
|
||||
watch([path], ([newPath]) => {
|
||||
// Only triggers when 'path' changes.
|
||||
// Changes to 'user' will NOT trigger this.
|
||||
console.log(`Navigating to ${newPath} as ${user()}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Automatic Cleanup
|
||||
If your logic creates timers, event listeners, or other reactive effects, SigPro tracks them as "children" of the current watch. When the watcher re-runs or stops, it kills everything inside automatically.
|
||||
In explicit mode, the callback receives an array of current values corresponding to the `deps` order.
|
||||
|
||||
### 3. Stopping a Watcher
|
||||
Call the returned function to kill the watcher manually.
|
||||
|
||||
```javascript
|
||||
Watch(() => {
|
||||
const timer = setInterval(() => console.log("Tick"), 1000);
|
||||
|
||||
// Register a manual cleanup if needed
|
||||
// Or simply rely on SigPro to kill nested Watch() calls
|
||||
return () => clearInterval(timer);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stopping a Watcher
|
||||
Call the returned function to manually kill the watcher. This is essential for manual DOM injections (like Toasts) or long-lived background processes.
|
||||
|
||||
```javascript
|
||||
const stop = Watch(() => console.log(count()));
|
||||
|
||||
const stop = watch(() => console.log(count()));
|
||||
// Later...
|
||||
stop(); // The link between the signal and this code is physically severed.
|
||||
stop(); // Disconnects the watcher completely.
|
||||
```
|
||||
|
||||
### 4. Automatic Cleanup Inside Effects
|
||||
If your watcher creates timers, event listeners, or nested effects, SigPro tracks them as children and cleans them up automatically before re‑running or when stopped.
|
||||
|
||||
```javascript
|
||||
watch(() => {
|
||||
const timer = setInterval(() => console.log("tick"), 1000);
|
||||
// No need to manually clear – SigPro will dispose it when the watcher re‑runs or stops.
|
||||
// (But you can also return a cleanup function if needed)
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pro Tip: The Microtask Queue
|
||||
SigPro batches updates. If you update multiple signals in the same execution block, the watcher will only fire **once** at the end of the task.
|
||||
## Batching & Microtask Queue
|
||||
|
||||
SigPro batches reactive updates. If you modify several signals in the same synchronous block, the watcher will fire **only once**, after the task completes.
|
||||
|
||||
```javascript
|
||||
const a = $(0);
|
||||
const b = $(0);
|
||||
|
||||
Watch(() => console.log(a(), b()));
|
||||
watch(() => console.log(a(), b()));
|
||||
|
||||
// This triggers only ONE re-run.
|
||||
// Triggers only ONE log: "1 2"
|
||||
a(1);
|
||||
b(2);
|
||||
```
|
||||
|
||||
This is achieved via `queueMicrotask`, ensuring optimal performance.
|
||||
|
||||
---
|
||||
|
||||
## Key Points
|
||||
|
||||
- **Function name:** `watch` (lowercase) – exported from SigPro and also available globally (depending on environment).
|
||||
- **Auto mode:** `watch(fn)` – automatically tracks any signals read inside `fn`.
|
||||
- **Explicit mode:** `watch([sig1, sig2], (values) => {...})` – only re‑runs when listed signals change; callback receives an array of their new values.
|
||||
- **Stop function:** returned by both modes; call it to dispose the effect and its children.
|
||||
- **Batching:** multiple signal writes in one event loop tick trigger a single execution (microtask).
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
const count = $(0);
|
||||
const step = $(1);
|
||||
|
||||
watch(() => {
|
||||
console.log(`Count changed to ${count()}`);
|
||||
});
|
||||
|
||||
watch([count, step], ([newCount, newStep]) => {
|
||||
console.log(`Count=${newCount}, step=${newStep} (explicit)`);
|
||||
});
|
||||
|
||||
count(5); // logs: auto + explicit
|
||||
step(2); // logs: explicit only (auto does not track step)
|
||||
```
|
||||
132
docs/api/when.md
Normal file
132
docs/api/when.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Reactive Branching: `when( )`
|
||||
|
||||
The `when` function is the reactive control flow operator in SigPro. It conditionally renders one of two branches (or nothing) based on a reactive condition. The inactive branch is completely removed from the DOM and its effects are destroyed, saving memory and CPU.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
when(
|
||||
condition: boolean | Signal<boolean> | (() => boolean),
|
||||
thenBranch: Node | (() => Node),
|
||||
elseBranch?: Node | (() => Node) | null
|
||||
): HTMLElement
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`condition`** | `boolean` / `Signal` / `() => boolean` | Yes | A value or reactive expression that determines which branch to show. |
|
||||
| **`thenBranch`** | `Node` / `() => Node` | Yes | Content rendered when the condition is **truthy**. |
|
||||
| **`elseBranch`** | `Node` / `() => Node` | No | Content rendered when the condition is **falsy**. Defaults to nothing. |
|
||||
|
||||
**Returns:** A `div` with `style="display:contents"` that acts as an anchor for the dynamic content. This element is part of the DOM and will be replaced/updated automatically.
|
||||
|
||||
> **Availability:** `when` is exported from the SigPro module. In **ESM** you must import it (`import { when } from 'sigpro'`). In the **IIFE** classic script, it is automatically available on `window`. The examples below assume the function is already in scope.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Simple Toggle
|
||||
|
||||
```javascript
|
||||
const isVisible = $(false);
|
||||
|
||||
div([
|
||||
button({ onclick: () => isVisible(!isVisible()) }, "Toggle"),
|
||||
when(isVisible,
|
||||
p("Now you see me!"),
|
||||
p("Now you don't...")
|
||||
)
|
||||
]);
|
||||
```
|
||||
|
||||
### 2. With Functions (Lazy Initialization)
|
||||
|
||||
To avoid creating heavy components until they are actually needed, pass a function that returns the branch.
|
||||
|
||||
```javascript
|
||||
when(() => user.isLoggedIn(),
|
||||
() => DashboardComponent(), // Only created when logged in
|
||||
() => LoginForm() // Only created when guest
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Complex Conditions
|
||||
|
||||
`condition` can be any expression that returns a boolean – it can read signals, computed values, or plain booleans.
|
||||
|
||||
```javascript
|
||||
when(() => count() > 10 && status() === 'ready',
|
||||
span("Threshold reached!")
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Without an `else` branch
|
||||
|
||||
If no `elseBranch` is provided, nothing is rendered when the condition is falsy.
|
||||
|
||||
```javascript
|
||||
when(loading,
|
||||
div({ class: "spinner" }, "Loading...")
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automatic Cleanup
|
||||
|
||||
`when` automatically manages the lifecycle of each branch:
|
||||
|
||||
- When the condition changes, the current branch is destroyed.
|
||||
- All effects (`watch`), event listeners, and child `when`/`each` inside the destroyed branch are recursively disposed.
|
||||
- The new branch is created and mounted.
|
||||
- Memory leaks are prevented without any manual intervention.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Use functions for expensive branches** – `() => Component()` ensures the component is only created when the branch becomes active.
|
||||
- **Avoid inline complex logic** – keep conditions readable; extract to computed signals if needed.
|
||||
- **No manual cleanup required** – SigPro handles everything.
|
||||
|
||||
---
|
||||
|
||||
## Technical Comparison
|
||||
|
||||
| Feature | CSS `display: none` | `when` |
|
||||
| :--- | :--- | :--- |
|
||||
| **DOM presence** | Always present | Only active branch exists |
|
||||
| **Event listeners** | Still attached | Removed |
|
||||
| **Effects (`watch`)** | Still running | Destroyed |
|
||||
| **Memory usage** | Higher | Optimised (only one branch alive) |
|
||||
| **Cleanup** | Manual | Automatic |
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
const loggedIn = $(false);
|
||||
const username = $("Guest");
|
||||
|
||||
const Profile = () => div([
|
||||
h2(`Welcome, ${username()}`),
|
||||
button({ onclick: () => loggedIn(false) }, "Logout")
|
||||
]);
|
||||
|
||||
const LoginForm = () => div([
|
||||
input({ placeholder: "Name", onInput: e => username(e.target.value) }),
|
||||
button({ onclick: () => loggedIn(true) }, "Login")
|
||||
]);
|
||||
|
||||
const App = () =>
|
||||
div([
|
||||
when(loggedIn,
|
||||
() => Profile(),
|
||||
() => LoginForm()
|
||||
)
|
||||
]);
|
||||
|
||||
mount(App, '#app');
|
||||
```
|
||||
121
docs/convert.js
Normal file
121
docs/convert.js
Normal file
@@ -0,0 +1,121 @@
|
||||
// src/convert.js
|
||||
import { $ } from "./sigpro.js";
|
||||
function html2sigpro(h, advanced = false) {
|
||||
const B = new Set(["allowfullscreen", "async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "formnovalidate", "hidden", "ismap", "itemscope", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected", "truespeed"]), esc = (v) => v.replace(/"/g, "\\\""), bP = (el) => {
|
||||
let a = [...el.attributes].map(({ name: n, value: v }) => /^on/i.test(n) ? `${n}: (e) => { ${v.replace(/\s+/g, " ").trim()} }` : B.has(n.toLowerCase()) && (!v || v == n) ? `${n}: true` : `${n}: "${esc(v)}"`);
|
||||
return a.length ? `{ ${a.join(", ")} }` : "";
|
||||
}, cN = (n, d = 0) => {
|
||||
let s = " ".repeat(d);
|
||||
if (n.nodeType == 3) {
|
||||
let t = n.textContent;
|
||||
return t.trim() ? `${s}"${esc(t)}"` : "";
|
||||
}
|
||||
if (n.nodeType == 1) {
|
||||
let t = n.tagName.toLowerCase();
|
||||
let classes = [];
|
||||
let otherAttrs = [];
|
||||
if (advanced) {
|
||||
const classAttribute = Array.from(n.attributes).find((attr) => attr.name === "class");
|
||||
if (classAttribute) {
|
||||
classes = classAttribute.value.trim().split(/\s+/).filter((c2) => c2);
|
||||
}
|
||||
otherAttrs = [...n.attributes].filter((attr) => attr.name !== "class");
|
||||
}
|
||||
let p = "";
|
||||
if (advanced && classes.length > 0) {
|
||||
const classChain = classes.map((c2) => `.${c2.replace(/-/g, "_")}`).join("");
|
||||
if (otherAttrs.length > 0) {
|
||||
const otherProps = otherAttrs.map(({ name: n2, value: v }) => /^on/i.test(n2) ? `${n2}: (e) => { ${v.replace(/\s+/g, " ").trim()} }` : B.has(n2.toLowerCase()) && (!v || v == n2) ? `${n2}: true` : `${n2}: "${esc(v)}"`);
|
||||
p = `${classChain}({ ${otherProps.join(", ")} })`;
|
||||
} else {
|
||||
p = classChain;
|
||||
}
|
||||
} else {
|
||||
p = bP(n);
|
||||
}
|
||||
let c = [...n.childNodes].map((i) => cN(i, d + 1)).filter(Boolean);
|
||||
if (!c.length) {
|
||||
if (advanced && classes.length > 0 && otherAttrs.length === 0) {
|
||||
return `${s}${t}${p}`;
|
||||
}
|
||||
return `${s}${t}(${p})`;
|
||||
}
|
||||
if (c.length == 1 && !c[0].includes(`
|
||||
`)) {
|
||||
if (advanced && classes.length > 0 && otherAttrs.length === 0 && !p.includes("{")) {
|
||||
return `${s}${t}${p}(${c[0].trim()})`;
|
||||
}
|
||||
return `${s}${t}(${p ? p + ", " : ""}${c[0].trim()})`;
|
||||
}
|
||||
if (advanced && classes.length > 0 && otherAttrs.length === 0 && !p.includes("{")) {
|
||||
return `${s}${t}${p}([
|
||||
${c.join(`,
|
||||
`)}
|
||||
${s}])`;
|
||||
}
|
||||
return `${s}${t}(${p ? p + ", " : ""}[
|
||||
${c.join(`,
|
||||
`)}
|
||||
${s}])`;
|
||||
}
|
||||
return "";
|
||||
}, r = [...new DOMParser().parseFromString(h, "text/html").body.childNodes].map((n) => cN(n)).filter(Boolean);
|
||||
return r.length == 1 ? r[0].trim() : `[
|
||||
${r.join(`,
|
||||
`)}
|
||||
]`;
|
||||
}
|
||||
var converter = () => {
|
||||
const inH = $("");
|
||||
const setInH = (v) => inH(v);
|
||||
const outS = $("");
|
||||
const setOutS = (v) => outS(v);
|
||||
const advanced = $(false);
|
||||
const setAdvanced = (v) => advanced(v);
|
||||
const cnv = () => {
|
||||
try {
|
||||
setOutS(html2sigpro(inH(), advanced()));
|
||||
} catch (e) {
|
||||
setOutS("Error: " + e.message);
|
||||
}
|
||||
};
|
||||
const txS = "width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical", btS = "padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px";
|
||||
return div({ style: "max-width:900px;margin:20px auto;font-family:sans-serif" }, [
|
||||
h1("HTML → SigPro"),
|
||||
label({ style: "display:block;font-weight:700" }, "HTML:"),
|
||||
textarea({
|
||||
style: txS,
|
||||
placeholder: "HTML...",
|
||||
value: inH,
|
||||
oninput: (e) => {
|
||||
setInH(e.target.value);
|
||||
cnv();
|
||||
}
|
||||
}),
|
||||
div({ style: "margin:10px 0;display:flex;align-items:center;gap:10px" }, [
|
||||
button({ style: btS + ";background:#3b82f6;color:#fff", onclick: cnv }, "Convert"),
|
||||
button({ style: btS + ";background:#d1d5db", onclick: () => {
|
||||
setInH("");
|
||||
setOutS("");
|
||||
setAdvanced(false);
|
||||
} }, "Clear"),
|
||||
label({ style: "display:flex;align-items:center;gap:5px;cursor:pointer" }, [
|
||||
input({ type: "checkbox", checked: advanced, onchange: (e) => {
|
||||
setAdvanced(e.target.checked);
|
||||
cnv();
|
||||
} }),
|
||||
span("Advanced (dot notation for classes)")
|
||||
])
|
||||
]),
|
||||
div({ style: "display:flex;justify-content:space-between;align-items:center;margin-bottom:5px" }, [
|
||||
span({ style: "font-weight:700" }, "Out:"),
|
||||
button({ style: btS + ";background:#10b981;color:#fff", onclick: () => {
|
||||
navigator.clipboard.writeText(outS());
|
||||
alert("Copied!");
|
||||
} }, "Copy")
|
||||
]),
|
||||
textarea({ style: txS + ";background:#f9fafb", readonly: true, value: outS, placeholder: "Result..." })
|
||||
]);
|
||||
};
|
||||
window.html2sigpro = html2sigpro;
|
||||
window.converter = converter;
|
||||
11
docs/convert.md
Normal file
11
docs/convert.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# HTML to SigPro Converter
|
||||
|
||||
Convert your existing HTML markup directly into SigPro Tag Helper syntax. Paste your HTML in the left panel and get clean, ready-to-use SigPro code on the right.
|
||||
|
||||
## Usage
|
||||
|
||||
<div id="sigpro-converter"></div>
|
||||
|
||||
```js
|
||||
mount(window.converter, '#sigpro-converter');
|
||||
```
|
||||
227
docs/examples.md
227
docs/examples.md
@@ -1,227 +0,0 @@
|
||||
# Interactive Examples
|
||||
|
||||
Explore the power of SigPro through practical patterns. From basic reactivity to advanced composition.
|
||||
NOTE: Here we use DaisyUI for styles.
|
||||
|
||||
---
|
||||
|
||||
## 1. Basic Reactivity
|
||||
The classic counter. Notice how we use `$(0)` to create a signal and the `Button` tag helper to update it.
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
|
||||
<div id="demo-counter" class="bg-base-100 p-6 rounded-xl border border-base-300 flex items-center justify-center"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
```javascript
|
||||
const Counter = () => {
|
||||
const $count = $(0);
|
||||
return Div({ class: 'flex gap-4 items-center' }, [
|
||||
Button({ class: 'btn btn-circle btn-outline', onclick: () => $count(c => c - 1) }, "-"),
|
||||
Span({ class: 'text-2xl font-mono w-12 text-center' }, $count),
|
||||
Button({ class: 'btn btn-circle btn-primary', onclick: () => $count(c => c + 1) }, "+")
|
||||
]);
|
||||
};
|
||||
Mount(Counter, '#demo-counter');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Derived State (Computed)
|
||||
Signals can depend on other signals. The "Double" value updates automatically.
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
|
||||
<div id="demo-computed" class="bg-base-100 p-6 rounded-xl border border-base-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
```javascript
|
||||
const ComputedDemo = () => {
|
||||
const $count = $(10);
|
||||
const $double = $(() => $count() * 2);
|
||||
return Div({ class: 'space-y-4' }, [
|
||||
Input({ type: 'range', min: 1, max: 100, class: 'range range-primary', value: $count }),
|
||||
P({ class: 'text-center' }, ["Base: ", $count, " ⮕ ", Span({class: 'text-primary font-bold'}, ["Double: ", $double])])
|
||||
]);
|
||||
};
|
||||
Mount(ComputedDemo, '#demo-computed');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Lists and Loops (For)
|
||||
SigPro handles lists efficiently, only updating specific DOM nodes.
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
|
||||
<div id="demo-list" class="bg-base-100 p-6 rounded-xl border border-base-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
```javascript
|
||||
const ListDemo = () => {
|
||||
const $todos = $(['Learn SigPro', 'Build an App']);
|
||||
const $input = $("");
|
||||
const addTodo = () => {
|
||||
if ($input()) {
|
||||
$todos(prev => [...prev, $input()]);
|
||||
$input("");
|
||||
}
|
||||
};
|
||||
return Div([
|
||||
Div({ class: 'flex gap-2 mb-4' }, [
|
||||
Input({ class: 'input input-bordered flex-1', value: $input, placeholder: 'New task...' }),
|
||||
Button({ class: 'btn btn-primary', onclick: addTodo }, "Add")
|
||||
]),
|
||||
Ul({ class: 'menu bg-base-200 rounded-box p-2' },
|
||||
For($todos, (item) => Li([A(item)]), (item) => item)
|
||||
)
|
||||
]);
|
||||
};
|
||||
Mount(ListDemo, '#demo-list');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Conditional Rendering (If)
|
||||
Toggle visibility without re-rendering the entire parent.
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
|
||||
<div id="demo-if" class="bg-base-100 p-6 rounded-xl border border-base-300 flex justify-center"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
```javascript
|
||||
const ConditionalDemo = () => {
|
||||
const $isVisible = $(false);
|
||||
return Div({ class: 'text-center w-full' }, [
|
||||
Button({ class: 'btn btn-outline mb-4', onclick: () => $isVisible(! $isVisible()) }, "Toggle Secret"),
|
||||
If($isVisible,
|
||||
() => Div({ class: 'p-4 bg-warning text-warning-content rounded-lg shadow-inner' }, "🤫 SigPro is Awesome!"),
|
||||
() => Div({ class: 'p-4 opacity-50' }, "Nothing to see here...")
|
||||
)
|
||||
]);
|
||||
};
|
||||
Mount(ConditionalDemo, '#demo-if');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Persistent State
|
||||
Pass a string as a second argument to `$(val, key)` to sync with `localStorage`.
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
|
||||
<div id="demo-persist" class="bg-base-100 p-6 rounded-xl border border-base-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
```javascript
|
||||
const PersistDemo = () => {
|
||||
const $name = $("Guest", "sigpro-demo-name");
|
||||
return Div({ class: 'flex flex-col gap-2' }, [
|
||||
H3({ class: 'text-lg font-bold' }, ["Hello, ", $name]),
|
||||
Input({ class: 'input input-bordered', value: $name, placeholder: 'Type your name...' }),
|
||||
P({ class: 'text-xs opacity-50' }, "Refresh the page to see magic!")
|
||||
]);
|
||||
};
|
||||
Mount(PersistDemo, '#demo-persist');
|
||||
```
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const initExamples = () => {
|
||||
|
||||
const counterTarget = document.querySelector('#demo-counter');
|
||||
if (counterTarget && !counterTarget.hasChildNodes()) {
|
||||
const Counter = () => {
|
||||
const $count = $(0);
|
||||
return Div({ class: 'flex gap-4 items-center' }, [
|
||||
Button({ class: 'btn btn-circle btn-outline', onclick: () => $count(c => c - 1) }, "-"),
|
||||
Span({ class: 'text-2xl font-mono w-12 text-center' }, $count),
|
||||
Button({ class: 'btn btn-circle btn-primary', onclick: () => $count(c => c + 1) }, "+")
|
||||
]);
|
||||
};
|
||||
Mount(Counter, counterTarget);
|
||||
}
|
||||
|
||||
// 2. Computed
|
||||
const computedTarget = document.querySelector('#demo-computed');
|
||||
if (computedTarget && !computedTarget.hasChildNodes()) {
|
||||
const ComputedDemo = () => {
|
||||
const $count = $(10);
|
||||
const $double = $(() => $count() * 2);
|
||||
return Div({ class: 'space-y-4 w-full' }, [
|
||||
Input({ type: 'range', min: 1, max: 100, class: 'range range-primary', value: $count }),
|
||||
P({ class: 'text-center' }, ["Base: ", $count, " ⮕ ", Span({class: 'text-primary font-bold'}, ["Double: ", $double])])
|
||||
]);
|
||||
};
|
||||
Mount(ComputedDemo, computedTarget);
|
||||
}
|
||||
|
||||
// 3. List
|
||||
const listTarget = document.querySelector('#demo-list');
|
||||
if (listTarget && !listTarget.hasChildNodes()) {
|
||||
const ListDemo = () => {
|
||||
const $todos = $(['Learn SigPro', 'Build an App']);
|
||||
const $input = $("");
|
||||
const addTodo = () => { if ($input()) { $todos(prev => [...prev, $input()]); $input(""); } };
|
||||
return Div([
|
||||
Div({ class: 'flex gap-2 mb-4' }, [
|
||||
Input({ class: 'input input-bordered flex-1', value: $input, placeholder: 'New task...' }),
|
||||
Button({ class: 'btn btn-primary', onclick: addTodo }, "Add")
|
||||
]),
|
||||
Ul({ class: 'menu bg-base-200 rounded-box p-2' }, For($todos, (item) => Li([A(item)]), (item) => item))
|
||||
]);
|
||||
};
|
||||
Mount(ListDemo, listTarget);
|
||||
}
|
||||
|
||||
// 4. If
|
||||
const ifTarget = document.querySelector('#demo-if');
|
||||
if (ifTarget && !ifTarget.hasChildNodes()) {
|
||||
const ConditionalDemo = () => {
|
||||
const $isVisible = $(false);
|
||||
return Div({ class: 'text-center w-full' }, [
|
||||
Button({ class: 'btn btn-outline mb-4', onclick: () => $isVisible(! $isVisible()) }, "Toggle Secret"),
|
||||
If($isVisible,
|
||||
() => Div({ class: 'p-4 bg-warning text-warning-content rounded-lg' }, "🤫 SigPro is Awesome!"),
|
||||
() => Div({ class: 'p-4 opacity-50' }, "Nothing to see here...")
|
||||
)
|
||||
]);
|
||||
};
|
||||
Mount(ConditionalDemo, ifTarget);
|
||||
}
|
||||
|
||||
// 5. Persist
|
||||
const persistTarget = document.querySelector('#demo-persist');
|
||||
if (persistTarget && !persistTarget.hasChildNodes()) {
|
||||
const PersistDemo = () => {
|
||||
const $name = $("Guest", "sigpro-demo-name");
|
||||
return Div({ class: 'flex flex-col gap-2' }, [
|
||||
H3({ class: 'text-lg font-bold' }, ["Hello, ", $name]),
|
||||
Input({ class: 'input input-bordered', value: $name }),
|
||||
P({ class: 'text-xs opacity-50' }, "Refresh the page!")
|
||||
]);
|
||||
};
|
||||
Mount(PersistDemo, persistTarget);
|
||||
}
|
||||
};
|
||||
|
||||
// Ejecutar inmediatamente y también en cada navegación de Docsify
|
||||
initExamples();
|
||||
if (window.$docsify) {
|
||||
window.$docsify.plugins = [].concat(window.$docsify.plugins || [], (hook) => {
|
||||
hook.doneEach(initExamples);
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -1,38 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<!doctype html>
|
||||
<html lang="es" data-theme="splight">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>SigPro Docs</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify/lib/themes/vue.css">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdn.jsdelivr.net/npm/docsify/lib/themes/vue.css"
|
||||
/>
|
||||
<link href="./sigpro.ui.css" rel="stylesheet" type="text/css" />
|
||||
<!-- <link
|
||||
href="https://cdn.jsdelivr.net/npm/daisyui@5"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/> -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
</head>
|
||||
<body>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script>
|
||||
window.$docsify = {
|
||||
name: 'SigPro',
|
||||
repo: '',
|
||||
name: "SigPro",
|
||||
repo: "",
|
||||
loadSidebar: true,
|
||||
subMaxLevel: 0,
|
||||
sidebarDisplayLevel: 1,
|
||||
executeScript: true,
|
||||
copyCode: {
|
||||
buttonText: '<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>',
|
||||
errorText: 'Error',
|
||||
successText: '<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><polyline points="20 6 9 17 4 12"></polyline></svg>'
|
||||
}
|
||||
buttonText:
|
||||
'<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>',
|
||||
errorText: "Error",
|
||||
successText:
|
||||
'<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><polyline points="20 6 9 17 4 12"></polyline></svg>',
|
||||
},
|
||||
search: {
|
||||
placeholder: "Type to search",
|
||||
noData: "No Results!",
|
||||
depth: 3,
|
||||
hideOtherSidebarContent: true,
|
||||
},
|
||||
plugins: [
|
||||
function (hook, vm) {
|
||||
hook.doneEach(function () {
|
||||
const codeBlocks = document.querySelectorAll(
|
||||
'pre[data-lang="js"] code',
|
||||
);
|
||||
codeBlocks.forEach((code) => {
|
||||
try {
|
||||
const scriptContent = `(function() { ${code.innerText} }).call(window);`;
|
||||
const runDemo = new Function(scriptContent);
|
||||
runDemo();
|
||||
} catch (err) {
|
||||
console.error("Error ejecutando demo de SigPro:", err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
|
||||
<script type="module">
|
||||
import * as SigPro from "./sigpro.js";
|
||||
Object.assign(window, SigPro);
|
||||
import "./sigpro.tags.js";
|
||||
import "./sigpro.ui.js";
|
||||
|
||||
<script src="./sigpro.js"></script>
|
||||
import("./sigpro.convert.js").then(() => {
|
||||
console.log("SigPro y Convert cargados correctamente.");
|
||||
});
|
||||
// document.documentElement.setAttribute("data-theme", "splight");
|
||||
</script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify/lib/docsify.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
150
docs/install.md
150
docs/install.md
@@ -12,106 +12,62 @@ Choose the method that best fits your workflow:
|
||||
|
||||
```bash
|
||||
npm install sigpro
|
||||
```
|
||||
|
||||
</div>
|
||||
or
|
||||
|
||||
<input type="radio" name="install_method" class="tab border-base-300" aria-label="pnpm" />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-box p-6">
|
||||
|
||||
```bash
|
||||
pnpm add sigpro
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
<input type="radio" name="install_method" class="tab border-base-300" aria-label="yarn" />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-box p-6">
|
||||
|
||||
```bash
|
||||
yarn add sigpro
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
<input type="radio" name="install_method" class="tab border-base-300" aria-label="bun" />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-box p-6">
|
||||
|
||||
```bash
|
||||
bun add sigpro
|
||||
```
|
||||
|
||||
</div>
|
||||
<input type="radio" name="install_method" class="tab border-base-300 whitespace-nowrap" aria-label="CDN (ESM)" />
|
||||
|
||||
<input type="radio" name="install_method" class="tab border-base-300 whitespace-nowrap" aria-label="CDN" />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-box p-6">
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
// Import the core and UI components
|
||||
import SigPro from "[https://cdn.jsdelivr.net/npm/sigpro@latest/+esm](https://cdn.jsdelivr.net/npm/sigpro@latest/+esm)";
|
||||
import { UI } from "[https://cdn.jsdelivr.net/npm/sigpro@latest/ui/+esm](https://cdn.jsdelivr.net/npm/sigpro@latest/ui/+esm)";
|
||||
|
||||
// Initialize UI components globally
|
||||
UI($);
|
||||
import { $, h, mount } from "https://cdn.jsdelivr.net/npm/sigpro@latest/dist/sigpro.esm.min.js";
|
||||
|
||||
const count = $(0);
|
||||
mount(() => h("h1", {}, () => `Count: ${count()}`), "#app");
|
||||
|
||||
</script>
|
||||
```
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="radio" name="install_method" class="tab border-base-300 whitespace-nowrap" aria-label="CDN (ESM)" />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-box p-6">
|
||||
<pre class="bg-base-200 p-4 rounded-lg"><code class="language-html"><script type="module">
|
||||
// Import the core and UI components
|
||||
import SigPro from 'https://cdn.jsdelivr.net/npm/sigpro@latest/+esm';
|
||||
import { UI } from 'https://cdn.jsdelivr.net/npm/sigpro@latest/ui/+esm';
|
||||
|
||||
// Initialize UI components globally
|
||||
UI($);
|
||||
</script></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 2. Quick Start Examples
|
||||
|
||||
SigPro uses **PascalCase** for Tag Helpers (e.g., `Div`, `Button`) to provide a clean, component-like syntax without needing JSX.
|
||||
SigPro uses **lowercase** Tag Helpers (e.g., `div`, `button`) to keep the syntax close to raw HTML, while still being pure JavaScript functions.
|
||||
|
||||
<div class="tabs tabs-box w-full mt-8 mb-12 bg-base-200/50 p-2 rounded-xl border border-base-300">
|
||||
<input type="radio" name="quick_start_tabs" class="tab !rounded-lg" aria-label="Mainstream (Bundlers)" checked />
|
||||
<input type="radio" name="quick_start_tabs" class="tab !rounded-lg" aria-label="ESM" checked />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
|
||||
|
||||
```javascript
|
||||
// File: App.js
|
||||
import "sigpro";
|
||||
// App.js – Use named imports for the core, activate helpers if needed
|
||||
import { $, mount } from "sigpro";
|
||||
|
||||
export const App = () => {
|
||||
const $count = $(0);
|
||||
|
||||
// Tag Helpers like Div, H1, Button are available globally
|
||||
return Div({ class: "card p-4" }, [
|
||||
H1(["Count is: ", $count]),
|
||||
Button(
|
||||
{
|
||||
class: "btn btn-primary",
|
||||
onclick: () => $count((c) => c + 1),
|
||||
},
|
||||
const App = () => {
|
||||
const count = $(0);
|
||||
return div({ class: "card p-4" }, [
|
||||
h1(() => `Count is: ${count()}`),
|
||||
button(
|
||||
{ class: "btn btn-primary", onclick: () => count(count() + 1) },
|
||||
"Increment",
|
||||
),
|
||||
]);
|
||||
};
|
||||
|
||||
// File: main.js
|
||||
import "sigpro";
|
||||
import { App } from "./App.js";
|
||||
|
||||
Mount(App, "#app");
|
||||
mount(App, "#app");
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
<input type="radio" name="quick_start_tabs" class="tab !rounded-lg" aria-label="Classic (Direct CDN)" />
|
||||
<input type="radio" name="quick_start_tabs" class="tab !rounded-lg" aria-label="CDN" />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
|
||||
|
||||
```html
|
||||
@@ -121,23 +77,20 @@ Mount(App, "#app");
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module">
|
||||
import SigPro from "https://cdn.jsdelivr.net/npm/sigpro@latest/+esm";
|
||||
|
||||
const $name = $("Developer");
|
||||
|
||||
// No need to import Div, Section, H2, Input... they are global!
|
||||
// Import the core
|
||||
import { $, h, mount } from "https://cdn.jsdelivr.net/npm/sigpro@latest/dist/sigpro.esm.min.js";
|
||||
const name = $("Developer");
|
||||
const App = () =>
|
||||
Section({ class: "container" }, [
|
||||
H2(["Welcome, ", $name]),
|
||||
Input({
|
||||
section({ class: "container" }, [
|
||||
h2(() => `Welcome, ${name()}`),
|
||||
input({
|
||||
type: "text",
|
||||
class: "input input-bordered",
|
||||
$value: $name, // Automatic two-way binding
|
||||
value: name,
|
||||
placeholder: "Type your name...",
|
||||
}),
|
||||
]);
|
||||
|
||||
Mount(App, "#app");
|
||||
mount(App, "#app");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -148,56 +101,25 @@ Mount(App, "#app");
|
||||
|
||||
---
|
||||
|
||||
## 3. Global by Design
|
||||
|
||||
One of SigPro's core strengths is its **Global API**, which eliminates "Import Hell" while remaining ESM-compatible.
|
||||
|
||||
- **The "Zero-Config" Import:** By simply adding `import SigPro from "sigpro"`, the framework automatically "hydrates" the global `window` object.
|
||||
- **Core Functions:** You get immediate access to `$`, `Watch`, `Tag`, `If`, `For`, and `Router` anywhere in your scripts without using the `SigPro.` prefix.
|
||||
- **Auto-Installation:** This happens instantly upon import thanks to its built-in `install()` routine, making it "Plug & Play" for both local projects and CDN usage.
|
||||
|
||||
- **PascalCase Tag Helpers:** Standard HTML tags are pre-registered as global functions (`Div`, `Span`, `Button`, `Section`, etc.).
|
||||
- **Clean UI Syntax:** This allows you to write UI structures that look like HTML but are pure, reactive JavaScript: `Div({ class: "card" }, [ H1("Title") ])`.
|
||||
|
||||
- **Hybrid Tree Shaking:** \* For **Maximum Speed**, use `import SigPro from "sigpro"`.
|
||||
- For **Maximum Optimization**, you can still use Named Imports: `import { $, Tag } from "sigpro"`. This allows modern bundlers like Vite to prune unused code while keeping your core reactive.
|
||||
|
||||
- **Custom Components:** We recommend using **PascalCase** for your own components (e.g., `UserCard()`) to maintain visual consistency with the built-in Tag Helpers and distinguish them from standard logic.
|
||||
|
||||
---
|
||||
|
||||
## 4. Why no build step?
|
||||
## 3. Why no build step?
|
||||
|
||||
Because SigPro uses **native ES Modules** and standard JavaScript functions to generate the DOM, you don't actually _need_ a compiler like Babel or a transformer for JSX.
|
||||
|
||||
- **Development:** Just save and refresh. Pure JS, no "transpilation" required.
|
||||
- **Performance:** Extremely lightweight. Use any modern bundler (Vite, esbuild) only when you are ready to minify and tree-shake for production.
|
||||
|
||||
## 5. Why SigPro? (The Competitive Edge)
|
||||
## 4. Key Advantages
|
||||
|
||||
SigPro stands out by removing the "Build Step" tax and the "Virtual DOM" overhead. It is the closest you can get to writing raw HTML/JS while maintaining modern reactivity.
|
||||
|
||||
| Feature | **SigPro** | **SolidJS** | **Svelte** | **React** | **Vue** |
|
||||
| :----------------- | :--------------- | :----------- | :----------- | :---------- | :---------- |
|
||||
| **Bundle Size** | **~2KB** | ~7KB | ~4KB | ~40KB+ | ~30KB |
|
||||
| **DOM Strategy** | **Direct DOM** | Direct DOM | Compiled DOM | Virtual DOM | Virtual DOM |
|
||||
| **Reactivity** | **Fine-grained** | Fine-grained | Compiled | Re-renders | Proxies |
|
||||
| **Build Step** | **Optional** | Required | Required | Required | Optional |
|
||||
| **Learning Curve** | **Minimal** | Medium | Low | High | Medium |
|
||||
| **Initialization** | **Ultra-Fast** | Very Fast | Fast | Slow | Medium |
|
||||
|
||||
---
|
||||
|
||||
## 6. Key Advantages
|
||||
|
||||
- **Extreme Performance**: No Virtual DOM reconciliation. SigPro updates the specific node or attribute instantly when a Signal changes.
|
||||
- **Extreme Performance**: No Virtual DOM reconciliation. SigPro updates the specific node or attribute instantly when a signal changes.
|
||||
- **Fine-Grained Reactivity**: State changes only trigger updates where the data is actually used, not on the entire component.
|
||||
- **Native Web Standards**: Everything is a standard JS function. No custom template syntax to learn.
|
||||
- **Zero Magic**: No hidden compilers. What you write is what runs in the browser.
|
||||
- **Global by Design**: Tag Helpers and the `$` function are available globally to eliminate "Import Hell" and keep your code clean.
|
||||
- **Global by Design** (with control): Tag helpers and core functions can be globally available (IIFE) or imported on demand (ESM) – you choose.
|
||||
|
||||
## 7. Summary
|
||||
---
|
||||
|
||||
SigPro isn't just another framework; it's a bridge to the native web. By using standard ES Modules and functional DOM generation, you gain the benefits of a modern library with the weight of a utility script.
|
||||
## 5. Summary
|
||||
|
||||
SigPro isn't just another framework; it's a bridge to the native web. By using standard ES Modules and functional DOM generation, you get the benefits of a modern reactive library with the weight of a utility script.
|
||||
|
||||
**Because, in the end... why fight the web when we can embrace it?**
|
||||
|
||||
345
docs/router.md
Normal file
345
docs/router.md
Normal file
@@ -0,0 +1,345 @@
|
||||
# Routing: `router( )` & Utilities
|
||||
|
||||
SigPro includes a built‑in, lightweight **hash router** to create single‑page applications (SPA). It manages the URL hash, matches components to routes with dynamic segments (`:id`), and automatically cleans up each page when you navigate away.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
router(routes: Route[]): HTMLElement
|
||||
```
|
||||
|
||||
### Route Object
|
||||
|
||||
| Property | Type | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **`path`** | `string` | The URL fragment pattern (e.g. `"/"`, `"/user/:id"`, or `"*"` for catch‑all). |
|
||||
| **`component`** | `Function` | A function that returns a Node, a string, or a reactive view. Receives `params` object as argument. |
|
||||
|
||||
**Returns:** A `div` element (with class `"router-hook"`) that acts as the router outlet. The router automatically destroys the previous view and mounts the matched component when the hash changes.
|
||||
|
||||
> You must import them (`import { router } from 'sigpro/router'`). The examples below assume the functions are already in scope.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Defining Routes
|
||||
|
||||
Place the `router` element where you want the page content to appear. Inside the routes array, define your routes.
|
||||
|
||||
```javascript
|
||||
// remember import router
|
||||
import { router } from 'sigpro/router'
|
||||
|
||||
const Home = () => h1("Home Page");
|
||||
const UserProfile = (params) => h1(`User ID: ${params.id}`);
|
||||
const NotFound = () => h1("404 – Page not found");
|
||||
|
||||
const App = () =>
|
||||
div({ class: "app-layout" }, [
|
||||
nav([
|
||||
a({ href: "#/" }, "Home"),
|
||||
a({ href: "#/user/42" }, "User 42")
|
||||
]),
|
||||
router([
|
||||
{ path: "/", component: Home },
|
||||
{ path: "/user/:id", component: UserProfile },
|
||||
{ path: "*", component: NotFound }
|
||||
])
|
||||
]);
|
||||
|
||||
mount(App, "#app");
|
||||
```
|
||||
|
||||
### 2. Dynamic Segments (`:id`)
|
||||
|
||||
When a route contains a colon‑prefixed segment (like `:id`), the router extracts the corresponding value from the current hash and passes it as a property inside the `params` object to the component function.
|
||||
|
||||
```javascript
|
||||
// If the hash is #/user/42
|
||||
const UserProfile = (params) => {
|
||||
console.log(params.id); // "42"
|
||||
return div(`User ${params.id}`);
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Accessing Route Parameters Anywhere
|
||||
|
||||
The router maintains a reactive signal `router.params` that always holds the parameters of the currently matched route. You can read it anywhere in your app.
|
||||
|
||||
```javascript
|
||||
watch(() => {
|
||||
const params = router.params();
|
||||
console.log("Current route params:", params);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Navigation Utilities
|
||||
|
||||
SigPro provides several helper functions to control navigation and read the router state.
|
||||
|
||||
### `router.to(path)`
|
||||
|
||||
Navigates to the given path. It automatically formats the hash (e.g. `"/dashboard"` becomes `"#/dashboard"`). You can pass either a full hash string or a path without the `#`.
|
||||
|
||||
```javascript
|
||||
button({ onclick: () => router.to("/dashboard") }, "Go to Dashboard")
|
||||
```
|
||||
|
||||
### `router.back()`
|
||||
|
||||
Goes back one step in the browser’s history, just like calling `history.back()`.
|
||||
|
||||
```javascript
|
||||
button({ onclick: () => router.back() }, "← Back")
|
||||
```
|
||||
|
||||
### `router.path()`
|
||||
|
||||
Returns the current route path **without the leading `#`**. This is a plain string, not a signal.
|
||||
|
||||
```javascript
|
||||
console.log(router.path()); // e.g. "/user/42"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automatic Cleanup
|
||||
|
||||
Every time you navigate to a new route, the router calls `.destroy()` on the previous view. This recursively disposes of:
|
||||
|
||||
- All `watch` effects created inside that page
|
||||
- All event listeners attached via SigPro’s event binding
|
||||
- Any nested `when`, `each`, or `router` instances
|
||||
|
||||
**No manual cleanup is required** – memory leaks are prevented automatically.
|
||||
|
||||
---
|
||||
|
||||
## Reactive Route Parameters
|
||||
|
||||
`router.params` is a **reactive signal** (created with `$({})`). You can watch it to react to parameter changes without re‑mounting the whole router outlet.
|
||||
|
||||
```javascript
|
||||
watch(() => router.params(), (params) => {
|
||||
console.log("Params changed:", params);
|
||||
// e.g. fetch new data when the :id changes
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Styling the Router Outlet
|
||||
|
||||
The router returns a `div` with the class `"router-hook"`. You can style it just like any other element:
|
||||
|
||||
```css
|
||||
.router-hook {
|
||||
display: block;
|
||||
min-height: 60vh;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
```
|
||||
|
||||
If you want the router outlet to have no layout impact, you can set `display: contents` on it.
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
import { mount } from 'sigpro';
|
||||
|
||||
const Home = () => div("Welcome home");
|
||||
const About = () => div("About us");
|
||||
const User = (params) => div(`User profile: ${params.id}`);
|
||||
|
||||
const App = () =>
|
||||
div([
|
||||
nav([
|
||||
a({ href: "#/" }, "Home"),
|
||||
a({ href: "#/about" }, "About"),
|
||||
a({ href: "#/user/5" }, "User 5")
|
||||
]),
|
||||
router([
|
||||
{ path: "/", component: Home },
|
||||
{ path: "/about", component: About },
|
||||
{ path: "/user/:id", component: User },
|
||||
{ path: "*", component: () => div("404 – Not found") }
|
||||
])
|
||||
]);
|
||||
|
||||
mount(App, "#app");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Function | Description |
|
||||
| :--- | :--- |
|
||||
| `router(routes)` | Creates a router outlet. |
|
||||
| `router.to(path)` | Navigates to a new hash route. |
|
||||
| `router.back()` | Goes back in history. |
|
||||
| `router.path()` | Returns the current path without `#`. |
|
||||
| `router.params()` | Reactive signal of the current route parameters. |
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
# Vite Plugin: File-based Routing
|
||||
|
||||
The `sigproRouter` plugin for Vite automates route generation by scanning your `pages` directory. It creates a **virtual module** that you can import directly into your code, eliminating the need to maintain a manual routes array.
|
||||
|
||||
## 1. Project Structure
|
||||
|
||||
To use the plugin, organize your files within the `src/pages` directory. The folder hierarchy directly determines your application's URL structure. SigPro uses brackets `[param]` for dynamic segments.
|
||||
|
||||
<div class="mockup-code bg-base-300 text-base-content shadow-xl my-8">
|
||||
<pre><code>my-sigpro-app/
|
||||
├── src/
|
||||
│ ├── pages/
|
||||
│ │ ├── index.js → #/
|
||||
│ │ ├── about.js → #/about
|
||||
│ │ ├── users/
|
||||
│ │ │ └── [id].js → #/users/:id
|
||||
│ │ └── blog/
|
||||
│ │ ├── index.js → #/blog
|
||||
│ │ └── [slug].js → #/blog/:slug
|
||||
│ ├── App.js (Main Layout)
|
||||
│ └── main.js (Entry Point)
|
||||
├── vite.config.js
|
||||
└── package.json</code></pre>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 2. Setup & Configuration
|
||||
|
||||
Add the plugin to your `vite.config.js`. It works out of the box with zero configuration.
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
import { defineConfig } from 'vite';
|
||||
import { sigproRouter } from 'sigpro/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sigproRouter()]
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation
|
||||
|
||||
Thanks to **SigPro's synchronous initialization**, you no longer need to wrap your mounting logic in `.then()` blocks.
|
||||
|
||||
<div class="tabs tabs-box w-full mt-8 mb-12 bg-base-200/50 p-2 border border-base-300">
|
||||
<input type="radio" name="route_impl" class="tab" aria-label="Option A: Direct in main.js" checked />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
|
||||
|
||||
```javascript
|
||||
// src/main.js
|
||||
import { mount } from 'sigpro';
|
||||
import { router } from 'sigpro/utils';
|
||||
import { routes } from 'virtual:sigpro-routes';
|
||||
|
||||
// The Core already has Router ready
|
||||
mount(router(routes), '#app');
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
<input type="radio" name="route_impl" class="tab" aria-label="Option B: Inside App.js (Persistent Layout)" />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
|
||||
|
||||
```javascript
|
||||
// src/App.js
|
||||
import { routes } from 'virtual:sigpro-routes';
|
||||
|
||||
export default () => div({ class: 'layout' }, [
|
||||
header([
|
||||
h1("SigPro App"),
|
||||
nav([
|
||||
button({ onclick: () => Router.go('/') }, "Home"),
|
||||
button({ onclick: () => Router.go('/blog') }, "Blog")
|
||||
])
|
||||
]),
|
||||
// Only the content inside <main> will be swapped reactively
|
||||
main(Router(routes))
|
||||
]);
|
||||
```
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 4. Route Mapping Reference
|
||||
|
||||
The plugin follows a simple convention to transform your file system into a routing map.
|
||||
|
||||
<div class="overflow-x-auto my-8">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead class="bg-base-200">
|
||||
<tr>
|
||||
<th>File Path</th>
|
||||
<th>Generated Path</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>index.js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/</td>
|
||||
<td>The application root.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>about.js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/about</td>
|
||||
<td>A static page.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>[id].js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/:id</td>
|
||||
<td>Dynamic parameter (passed to the component).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>blog/index.js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/blog</td>
|
||||
<td>Folder index page.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>_utils.js</code></td>
|
||||
<td class="italic opacity-50 text-error">Ignored</td>
|
||||
<td>Files starting with <code>_</code> are excluded from routing.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 5. How it Works (Vite Virtual Module)
|
||||
|
||||
The plugin generates a virtual module named `virtual:sigpro-routes`. This module exports an array of objects compatible with `Router()`:
|
||||
|
||||
```javascript
|
||||
// Internal representation generated by the plugin
|
||||
export const routes = [
|
||||
{ path: '/', component: () => import('/src/pages/index.js') },
|
||||
{ path: '/users/:id', component: () => import('/src/pages/users/[id].js') },
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
Because it uses dynamic `import()`, Vite automatically performs **Code Splitting**, meaning each page is its own small JS file that only loads when the user navigates to it.
|
||||
34
docs/sigpro.convert.js
Normal file
34
docs/sigpro.convert.js
Normal file
@@ -0,0 +1,34 @@
|
||||
var{$:x}=window.SigPro;function f(s,l="tags"){let a=new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected","truespeed"]),p=(o)=>o.replace(/"/g,"\\\""),c=(o)=>{let t=[...o.attributes].map(({name:e,value:n})=>/^on/i.test(e)?`${e}: (e) => { ${n.replace(/\s+/g," ").trim()} }`:a.has(e.toLowerCase())&&(!n||n==e)?`${e}: true`:`${e}: "${p(n)}"`);return t.length?`{ ${t.join(", ")} }`:""},m=(o,t=0)=>{let e=" ".repeat(t);if(o.nodeType==3){let n=o.textContent;return n.trim()?`${e}"${p(n)}"`:""}if(o.nodeType==1){let n=o.tagName.toLowerCase(),d=c(o),i=l==="core"?`h('${n}'`:n,r=[...o.childNodes].map((h)=>m(h,t+1)).filter(Boolean),g=!!d;if(l==="core"){if(!r.length)return g?`${e}${i}, ${d})`:`${e}${i})`;if(r.length===1&&!r[0].includes(`
|
||||
`))return g?`${e}${i}, ${d}, ${r[0].trim()})`:`${e}${i}, ${r[0].trim()})`;return g?`${e}${i}, ${d}, [
|
||||
${r.join(`,
|
||||
`)}
|
||||
${e}])`:`${e}${i}, [
|
||||
${r.join(`,
|
||||
`)}
|
||||
${e}])`}else{if(!r.length)return g?`${e}${i}(${d})`:`${e}${i}`;if(r.length===1&&!r[0].includes(`
|
||||
`))return g?`${e}${i}(${d}, ${r[0].trim()})`:`${e}${i}(${r[0].trim()})`;return g?`${e}${i}(${d}, [
|
||||
${r.join(`,
|
||||
`)}
|
||||
${e}])`:`${e}${i}([
|
||||
${r.join(`,
|
||||
`)}
|
||||
${e}])`}}return""},u=[...new DOMParser().parseFromString(s,"text/html").body.childNodes].map((o)=>m(o)).filter(Boolean);return u.length==1?u[0].trim():`[
|
||||
${u.join(`,
|
||||
`)}
|
||||
]`}var b=()=>{let s=x(""),l=x(""),a=x("tags"),p=x(""),c=()=>{try{l(f(s(),a()))}catch(t){l("Error: "+t.message)}p(s())},m=()=>{s(""),l(""),a("tags"),p("")},u="width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical",o="padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px";return div({style:"margin:20px auto;font-family:sans-serif"},[h1("HTML → SigPro"),div({style:"margin-bottom:10px"},[div({style:"display:flex;gap:20px;flex-wrap:wrap;margin-top:5px"},[label({style:"display:flex;align-items:center;gap:6px"},["Core",input({type:"radio",name:"mode",value:"core",checked:a()==="core",onchange:(t)=>{if(t.target.checked)a("core"),c()}}),span("core — h('tag', props, ...)")]),label({style:"display:flex;align-items:center;gap:6px"},["Tags",input({type:"radio",name:"mode",value:"tags",checked:a()==="tags",onchange:(t)=>{if(t.target.checked)a("tags"),c()}}),span("tags — tag({ props }, ...)")])])]),div({style:"margin-top:15px;display:flex;gap:10px"},[button({style:"padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px;background:#3b82f6;color:#fff",onclick:c},"Convert"),button({style:"padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px;background:#d1d5db",onclick:m},"Clear")]),div({style:"display:grid;grid-template-columns:1fr;gap:15px;margin-top:15px;width:100%"},[div({style:"border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column"},[label({style:"font-weight:bold;margin-bottom:8px"},"HTML Input"),textarea({style:"width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical",placeholder:"Paste your HTML here...",value:s,oninput:(t)=>{s(t.target.value),c()}})]),div({style:"border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column"},[div({style:"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px"},[span({style:"font-weight:bold"},"SigPro Output"),button({style:"padding:4px 8px;background:#10b981;color:white;border:none;border-radius:4px;cursor:pointer;font-size:12px",onclick:()=>{navigator.clipboard.writeText(l()),alert("Copied!")}},"Copy")]),textarea({style:"width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical;background:#f9fafb",readonly:!0,value:l,placeholder:"Converted code will appear here..."})]),div({style:"border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column"},[label({style:"font-weight:bold;margin-bottom:8px"},"Live Preview"),iframe({style:"width:100%;height:200px;border:1px solid #e2e8f0;border-radius:4px;background:white;",srcdoc:()=>{return`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<style>
|
||||
body { padding: 10px; margin: 0; font-family: sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${p()||""}
|
||||
</body>
|
||||
</html>
|
||||
`},sandbox:"allow-same-origin allow-scripts allow-popups allow-forms allow-modals"})])])])};window.html2sigpro=f;window.converter=b;
|
||||
1
docs/sigpro.db.js
Normal file
1
docs/sigpro.db.js
Normal file
@@ -0,0 +1 @@
|
||||
var o=async(e,n={},t=null)=>{if(t)t(!0);try{let r=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n),credentials:"include"});if(!r.ok){let s=await r.text();throw Error(`Error ${r.status}: ${s}`)}return await r.json()}finally{if(t)t(!1)}};export{o as db};
|
||||
1
docs/sigpro.editor.js
Normal file
1
docs/sigpro.editor.js
Normal file
File diff suppressed because one or more lines are too long
78
docs/sigpro.grid.js
Normal file
78
docs/sigpro.grid.js
Normal file
File diff suppressed because one or more lines are too long
473
docs/sigpro.js
473
docs/sigpro.js
File diff suppressed because one or more lines are too long
1
docs/sigpro.locale.js
Normal file
1
docs/sigpro.locale.js
Normal file
@@ -0,0 +1 @@
|
||||
var{$:c}=window.SigPro,s=c("en"),n={},e=(t)=>{for(let o of Object.keys(t)){if(!n[o])n[o]={};Object.assign(n[o],t[o])}},r=(t)=>{if(t&&n[t])s(t)},a=(t)=>{return()=>n[s()]?.[t]??t};export{a as t,r as setLocale,e as addLang};
|
||||
1
docs/sigpro.router.js
Normal file
1
docs/sigpro.router.js
Normal file
@@ -0,0 +1 @@
|
||||
var{$:p,h:u,watch:m,render:g,isF:y}=window.SigPro,d=()=>window.location.hash.slice(1)||"/",s=p(d());window.addEventListener("hashchange",()=>s(d()));var w=p({}),c=(n)=>{let i=u("div",{class:"router-hook"}),r=null;return m([s],()=>{let l=s(),t=n.find((o)=>{let e=o.path.split("/").filter(Boolean),a=l.split("/").filter(Boolean);return e.length===a.length&&e.every((h,f)=>h[0]===":"||h===a[f])})||n.find((o)=>o.path==="*");if(t){r?.destroy();let o={};t.path.split("/").filter(Boolean).forEach((e,a)=>{if(e[0]===":")o[e.slice(1)]=l.split("/").filter(Boolean)[a]}),w(o),r=g(()=>y(t.component)?t.component(o):t.component),i.replaceChildren(r.container)}}),i.destroy=()=>{r?.destroy()},i};c.params=w;c.to=(n)=>window.location.hash=n.replace(/^#?\/?/,"#/");c.back=()=>window.history.back();c.path=()=>s();export{w as routerParams,c as router};
|
||||
1
docs/sigpro.tags.js
Normal file
1
docs/sigpro.tags.js
Normal file
@@ -0,0 +1 @@
|
||||
var{h:a}=window.SigPro;if(typeof window<"u")"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((e)=>{window[e]=(t,s)=>a(e,t,s)});
|
||||
2
docs/sigpro.ui.css
Normal file
2
docs/sigpro.ui.css
Normal file
File diff suppressed because one or more lines are too long
1
docs/sigpro.ui.js
Normal file
1
docs/sigpro.ui.js
Normal file
File diff suppressed because one or more lines are too long
1
docs/sigpro.utils.js
Normal file
1
docs/sigpro.utils.js
Normal file
@@ -0,0 +1 @@
|
||||
var{$:d,h:m,watch:g,render:x,isF:b}=window.SigPro,l=(t)=>{let e=()=>window.location.hash.slice(1)||"/",o=d(e()),n=()=>o(e());window.addEventListener("hashchange",n);let s=m("div",{class:"router-hook"}),h=null;return g([o],()=>{let f=o(),a=t.find((r)=>{let c=r.path.split("/").filter(Boolean),p=f.split("/").filter(Boolean);return c.length===p.length&&c.every((w,y)=>w[0]===":"||w===p[y])})||t.find((r)=>r.path==="*");if(a){h?.destroy();let r={};a.path.split("/").filter(Boolean).forEach((c,p)=>{if(c[0]===":")r[c.slice(1)]=f.split("/").filter(Boolean)[p]}),l.params(r),h=x(()=>b(a.component)?a.component(r):a.component),s.replaceChildren(h.container)}}),s.destroy=()=>{window.removeEventListener("hashchange",n),h?.destroy()},s};l.params=d({});l.to=(t)=>window.location.hash=t.replace(/^#?\/?/,"#/");l.back=()=>window.history.back();l.path=()=>window.location.hash.replace(/^#/,"")||"/";var k=async(t,e={},o=null)=>{if(o)o(!0);try{let n=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e),credentials:"include"});if(!n.ok){let s=await n.text();throw Error(`Error ${n.status}: ${s}`)}return await n.json()}finally{if(o)o(!1)}},u=d("en"),i={},v=(t)=>{for(let e of Object.keys(t)){if(!i[e])i[e]={};Object.assign(i[e],t[e])}},E=(t)=>{if(t&&i[t])u(t)},L=(t)=>{return()=>i[u()]?.[t]??t};export{L as t,E as setLocale,l as router,k as db,v as addLang};
|
||||
4
docs/sigpro.vite.js
Normal file
4
docs/sigpro.vite.js
Normal file
@@ -0,0 +1,4 @@
|
||||
function g(){let u="\x00virtual:sigpro-routes",i=(e)=>{if(!fs.existsSync(e))return[];return fs.readdirSync(e,{recursive:!0}).filter((r)=>/\.(js|jsx)$/.test(r)&&!path.basename(r).startsWith("_")).map((r)=>path.resolve(e,r))},l=(e,r)=>{return("/"+path.relative(e,r).replace(/\\/g,"/").replace(/\.(js|jsx)$/,"").replace(/\/index$/,"").replace(/^index$/,"")).replace(/\/+/g,"/").replace(/\[\.\.\.([^\]]+)\]/g,"*").replace(/\[([^\]]+)\]/g,":$1").replace(/\/$/,"")||"/"};return{name:"sigpro-router",resolveId(e){if(e==="virtual:sigpro-routes")return u},load(e){if(e!==u)return;let r=process.cwd(),t=path.resolve(r,"src/pages"),p=i(t).sort((n,a)=>{let o=l(t,n),c=l(t,a);if(o.includes(":")&&!c.includes(":"))return 1;if(!o.includes(":")&&c.includes(":"))return-1;return c.length-o.length}),s="";if(p.forEach((n)=>{let a=l(t,n),o="./"+path.relative(r,n).replace(/\\/g,"/");s+=` { path: '${a}', component: () => import('/${o}') },
|
||||
`}),!s.includes("path: '*'"))s+=` { path: '*', component: () => ({ default: () => document.createTextNode('404 - Not Found') }) },
|
||||
`;return`export const routes = [
|
||||
${s}];`}}}export{g as sigproRouter};
|
||||
388
docs/ui.md
Normal file
388
docs/ui.md
Normal file
@@ -0,0 +1,388 @@
|
||||
<div id="ui"></div>
|
||||
<div id="tab"></div>
|
||||
<div id="file"></div>
|
||||
<div id="demo-toast"></div>
|
||||
|
||||
```js
|
||||
const fruta = $("");
|
||||
const color = $("#3b82f6");
|
||||
const fecha = $("");
|
||||
const rango = $({ start: null, end: null });
|
||||
const pais = $("");
|
||||
|
||||
const frutas = [
|
||||
"Manzana",
|
||||
"Pera",
|
||||
"Plátano",
|
||||
"Fresa",
|
||||
"Mango",
|
||||
"Sandía",
|
||||
"Melón",
|
||||
"Uva",
|
||||
];
|
||||
const paises = [
|
||||
{ label: "🇪🇸 España", value: "ES" },
|
||||
{ label: "🇲🇽 México", value: "MX" },
|
||||
{ label: "🇦🇷 Argentina", value: "AR" },
|
||||
{ label: "🇨🇴 Colombia", value: "CO" },
|
||||
{ label: "🇨🇱 Chile", value: "CL" },
|
||||
];
|
||||
|
||||
mount(
|
||||
() =>
|
||||
div({ class: "p-8 max-w-md mx-auto flex flex-col gap-4" }, [
|
||||
h1({ class: "text-2xl font-bold" }, "Field Components"),
|
||||
|
||||
|
||||
ui.autocomplete({
|
||||
label: "Fruta favorita",
|
||||
items: frutas,
|
||||
value: fruta,
|
||||
placeholder: "Buscar fruta...",
|
||||
}),
|
||||
|
||||
|
||||
ui.autocomplete({
|
||||
label: "País",
|
||||
items: paises,
|
||||
value: pais,
|
||||
placeholder: "Elige un país...",
|
||||
}),
|
||||
|
||||
|
||||
ui.datepicker({
|
||||
label: "Fecha de nacimiento",
|
||||
value: fecha,
|
||||
placeholder: "Selecciona fecha...",
|
||||
}),
|
||||
|
||||
|
||||
ui.datepicker({
|
||||
label: "Estancia",
|
||||
range: true,
|
||||
value: rango,
|
||||
placeholder: "Check-in → Check-out",
|
||||
}),
|
||||
|
||||
|
||||
ui.colorpicker({
|
||||
label: "Color favorito",
|
||||
value: color,
|
||||
placeholder: "Elige un color...",
|
||||
}),
|
||||
|
||||
ui.password({}),
|
||||
ui.theme(),
|
||||
|
||||
div(
|
||||
{ class: "bg-base-200 rounded-box p-4 flex flex-col gap-2 text-sm" },
|
||||
[
|
||||
div({}, () => `🍎 Fruta: ${val(fruta) || "—"}`),
|
||||
div({}, () => `🌍 País: ${val(pais) || "—"}`),
|
||||
div({}, () => `📅 Fecha: ${val(fecha) || "—"}`),
|
||||
div({}, () => {
|
||||
const r = val(rango);
|
||||
return r.start && r.end
|
||||
? `🏨 Estancia: ${r.start} → ${r.end}`
|
||||
: "🏨 Estancia: —";
|
||||
}),
|
||||
div({ class: "flex items-center gap-2" }, [
|
||||
span({}, "🎨 Color:"),
|
||||
div({
|
||||
class: "w-6 h-6 rounded border border-base-300",
|
||||
style: () => `background:${val(color)}`,
|
||||
}),
|
||||
]),
|
||||
],
|
||||
),
|
||||
]),
|
||||
"#ui",
|
||||
);
|
||||
|
||||
const archivos = $([]);
|
||||
const drag = $(false);
|
||||
const error = $("");
|
||||
const subiendo = $(false);
|
||||
const progreso = $(0);
|
||||
|
||||
const MAX_SIZE = 5 * 1024 * 1024;
|
||||
const MAX_FILES = 3;
|
||||
|
||||
const handleFiles = (files) => {
|
||||
const arr = Array.from(files);
|
||||
if (arr.length > MAX_FILES) {
|
||||
error(`Máximo ${MAX_FILES} archivos`);
|
||||
return;
|
||||
}
|
||||
const big = arr.find(f => f.size > MAX_SIZE);
|
||||
if (big) {
|
||||
error(`"${big.name}" supera los 5MB`);
|
||||
return;
|
||||
}
|
||||
error("");
|
||||
archivos(arr);
|
||||
};
|
||||
|
||||
const subirArchivos = async () => {
|
||||
const files = archivos();
|
||||
if (!files.length) return toast("Selecciona archivos primero", "alert-warning");
|
||||
|
||||
subiendo(true);
|
||||
progreso(0);
|
||||
|
||||
const formData = new FormData();
|
||||
files.forEach(f => formData.append('files', f));
|
||||
formData.append('carpeta', 'demo');
|
||||
|
||||
try {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
xhr.upload.onprogress = (e) => {
|
||||
if (e.lengthComputable) {
|
||||
progreso(Math.round((e.loaded / e.total) * 100));
|
||||
}
|
||||
};
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve(JSON.parse(xhr.responseText));
|
||||
} else {
|
||||
reject(new Error(`Error ${xhr.status}`));
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => reject(new Error('Error de conexión'));
|
||||
});
|
||||
|
||||
xhr.open('POST', '/api/upload');
|
||||
xhr.send(formData);
|
||||
|
||||
await promise;
|
||||
toast(`✅ ${files.length} archivo(s) subidos`, "alert-success");
|
||||
archivos([]);
|
||||
progreso(0);
|
||||
} catch (err) {
|
||||
toast(`❌ ${err.message}`, "alert-error");
|
||||
} finally {
|
||||
subiendo(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fileInputProps = {
|
||||
type: "file",
|
||||
class: "hidden",
|
||||
multiple: true,
|
||||
accept: "image/*,.pdf,.doc,.docx",
|
||||
onchange: (e) => { handleFiles(e.target.files); e.target.value = ''; }
|
||||
};
|
||||
|
||||
mount(
|
||||
() => div({ class: "p-8 max-w-md mx-auto flex flex-col gap-4" }, [
|
||||
h1({ class: "text-2xl font-bold" }, "📁 Upload Files"),
|
||||
|
||||
// Zona drag & drop
|
||||
h("label", {
|
||||
class: () => `relative flex items-center justify-between h-14 px-4 border-2 border-dashed rounded-lg cursor-pointer transition-all ${
|
||||
drag() ? 'border-primary bg-primary/10' : 'border-base-content/20 bg-base-100'
|
||||
} ${subiendo() ? 'pointer-events-none opacity-50' : ''}`,
|
||||
ondragover: (e) => { e.preventDefault(); if (!subiendo()) drag(true); },
|
||||
ondragleave: () => drag(false),
|
||||
ondrop: (e) => {
|
||||
e.preventDefault();
|
||||
drag(false);
|
||||
if (subiendo()) return;
|
||||
handleFiles(e.dataTransfer.files);
|
||||
}
|
||||
}, [
|
||||
h("div", { class: "flex items-center gap-3" }, [
|
||||
h("span", { class: "icon-[lucide--upload] w-5 h-5 text-base-content/60" }),
|
||||
h("div", {}, [
|
||||
h("div", { class: "text-sm font-medium" }, "Arrastra archivos aquí"),
|
||||
h("div", { class: "text-xs text-base-content/50" }, `Máx ${MAX_FILES} archivos · ${MAX_SIZE / 1024 / 1024}MB c/u`),
|
||||
]),
|
||||
]),
|
||||
h("span", { class: "text-xs text-base-content/40" }, "o haz clic"),
|
||||
h("input", fileInputProps),
|
||||
]),
|
||||
|
||||
|
||||
() => error() ? ui.fileError({ message: error() }) : null,
|
||||
|
||||
|
||||
() => archivos().length > 0 ? h("div", { class: "space-y-3" }, [
|
||||
h("div", { class: "flex flex-wrap gap-2" },
|
||||
archivos().map((f, i) => {
|
||||
const isImage = f.type?.startsWith('image/');
|
||||
const url = isImage ? URL.createObjectURL(f) : null;
|
||||
|
||||
return h("div", {
|
||||
class: "relative group rounded-lg overflow-hidden border border-base-300 bg-base-200 w-20"
|
||||
}, [
|
||||
isImage ? h("img", {
|
||||
src: url,
|
||||
class: "w-20 h-20 object-cover",
|
||||
onload: () => url && URL.revokeObjectURL(url)
|
||||
}) : h("div", {
|
||||
class: "w-20 h-20 flex flex-col items-center justify-center gap-1"
|
||||
}, [
|
||||
h("span", { class: "text-2xl" }, f.type?.includes('pdf') ? "📕" : "📄"),
|
||||
h("span", { class: "text-[8px] uppercase opacity-50" }, f.name?.split('.').pop()),
|
||||
]),
|
||||
h("div", { class: "p-1" }, [
|
||||
h("div", { class: "text-[9px] truncate font-medium leading-tight" }, f.name),
|
||||
h("div", { class: "text-[8px] opacity-50" }, `${~~(f.size / 1024)} KB`),
|
||||
]),
|
||||
!subiendo() ? h("button", {
|
||||
class: "absolute top-0.5 right-0.5 btn btn-circle btn-ghost btn-xs opacity-0 group-hover:opacity-100 bg-base-100/80",
|
||||
onclick: () => archivos(archivos().filter((_, idx) => idx !== i))
|
||||
}, h("span", { class: "icon-[lucide--x] w-3 h-3" })) : null,
|
||||
])
|
||||
})
|
||||
),
|
||||
|
||||
|
||||
() => subiendo() ? h("div", { class: "space-y-1" }, [
|
||||
h("div", { class: "flex justify-between text-xs" }, [
|
||||
h("span", {}, "Subiendo..."),
|
||||
h("span", {}, () => `${progreso()}%`),
|
||||
]),
|
||||
h("progress", { class: "progress progress-primary w-full", value: progreso, max: "100" }),
|
||||
]) : null,
|
||||
|
||||
|
||||
h("div", { class: "flex gap-2" }, [
|
||||
h("button", {
|
||||
class: "btn btn-ghost btn-sm",
|
||||
onclick: () => { archivos([]); error(""); }
|
||||
}, "🗑 Limpiar"),
|
||||
h("button", {
|
||||
class: "btn btn-primary btn-sm",
|
||||
disabled: subiendo,
|
||||
onclick: subirArchivos
|
||||
}, () => subiendo() ? "⏳ Subiendo..." : "☁️ Subir al servidor"),
|
||||
]),
|
||||
]) : null,
|
||||
]),
|
||||
"#file",
|
||||
);
|
||||
|
||||
|
||||
const tabsSignal = $([
|
||||
{ id: "a", label: "Tab A", content: "Content of tab A", open: true },
|
||||
{ id: "b", label: "Tab B", content: "Content of tab B", closable: true },
|
||||
{ id: "c", label: "Tab C", content: "Content of tab C" },
|
||||
]);
|
||||
|
||||
mount(
|
||||
() => ui.tabs(
|
||||
{ class: "tabs-box" },
|
||||
() => tabsSignal().flatMap((tab, i) =>
|
||||
ui.tab({
|
||||
name: "demo-tabs",
|
||||
classContent: "bg-base-100 border-base-300 p-6",
|
||||
label: tab.label,
|
||||
content: tab.content,
|
||||
checked: tab.open || false,
|
||||
tabs: tabsSignal,
|
||||
index: i,
|
||||
closable: tab.closable || false,
|
||||
})
|
||||
)
|
||||
),
|
||||
"#tab",
|
||||
);
|
||||
|
||||
mount(
|
||||
div({ class: "flex flex-wrap gap-2" }, [
|
||||
button({ class: "btn", onclick: () => toast("File saved!") }, "Simple"),
|
||||
button(
|
||||
{ class: "btn", onclick: () => toast("Error!", "alert-error", 5000) },
|
||||
"Error (5s)",
|
||||
),
|
||||
button(
|
||||
{
|
||||
class: "btn",
|
||||
onclick: () =>
|
||||
toast(
|
||||
div({ class: "flex items-center gap-2" }, [
|
||||
span({ class: "icon-[lucide--check] text-lg" }),
|
||||
span({}, "Report generated"),
|
||||
]),
|
||||
"alert-success",
|
||||
),
|
||||
},
|
||||
"With icon",
|
||||
),
|
||||
button(
|
||||
{
|
||||
class: "btn",
|
||||
onclick: () =>
|
||||
toast(
|
||||
div({ class: "flex flex-col" }, [
|
||||
strong({}, "ATTENTION!"),
|
||||
span({}, "Error saving!"),
|
||||
button(
|
||||
{
|
||||
class: "btn btn-xs mt-1",
|
||||
onclick: () => console.log("Retry"),
|
||||
},
|
||||
"Retry",
|
||||
),
|
||||
]),
|
||||
"alert-warning",
|
||||
7000,
|
||||
),
|
||||
},
|
||||
"Complex",
|
||||
),
|
||||
]),
|
||||
"#toast",
|
||||
);
|
||||
|
||||
mount(
|
||||
div({ class: "flex flex-wrap gap-2" }, [
|
||||
button({ class: "btn", onclick: () => toast("File saved!") }, "Simple"),
|
||||
button(
|
||||
{ class: "btn", onclick: () => toast("Error!", "alert-error", 5000) },
|
||||
"Error (5s)",
|
||||
),
|
||||
button(
|
||||
{
|
||||
class: "btn",
|
||||
onclick: () =>
|
||||
toast(
|
||||
div({ class: "flex items-center gap-2" }, [
|
||||
ui.icon("icon-[lucide--check]"),
|
||||
span({}, "Report generated"),
|
||||
]),
|
||||
"alert-success",
|
||||
),
|
||||
},
|
||||
"With icon",
|
||||
),
|
||||
button(
|
||||
{
|
||||
class: "btn",
|
||||
onclick: () =>
|
||||
toast(
|
||||
div({ class: "flex flex-col" }, [
|
||||
strong({}, "ATTENTION!"),
|
||||
span({}, "Error saving!"),
|
||||
button(
|
||||
{
|
||||
class: "btn btn-xs mt-1",
|
||||
onclick: () => console.log("Retry"),
|
||||
},
|
||||
"Retry",
|
||||
),
|
||||
]),
|
||||
"alert-warning",
|
||||
7000,
|
||||
),
|
||||
},
|
||||
"Complex",
|
||||
),
|
||||
]),
|
||||
"#demo-toast",
|
||||
);
|
||||
|
||||
```
|
||||
148
docs/ui/quick.md
148
docs/ui/quick.md
@@ -1,148 +0,0 @@
|
||||
# UI Components `(WIP)`
|
||||
|
||||
> **Status: Work In Progress.**
|
||||
> SigPro UI is a complete component environment built with SigPro, Tailwind CSS and DaisyUI. It provides smart components that handle their own internal logic, reactivity, and professional styling.
|
||||
|
||||
<div class="alert alert-info shadow-md my-10 border-l-8 border-info bg-info/10 text-info-content">
|
||||
<div class="flex flex-col md:flex-row items-start gap-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6 mt-1"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<div>
|
||||
<h2 class="font-black text-xl tracking-tight mb-2">📢 Official Documentation</h2>
|
||||
<p class="text-sm opacity-90 leading-relaxed mb-2">
|
||||
All official documentation, interactive examples, and usage guides are available at:
|
||||
</p>
|
||||
<div class="bg-base-100 p-3 rounded-lg inline-block">
|
||||
<a href="https://natxocc.github.io/sigpro-ui/#/" target="_blank" class="text-info font-mono text-base font-bold hover:underline">
|
||||
https://natxocc.github.io/sigpro-ui/#/
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-xs mt-4 opacity-70 italic">
|
||||
* Documentation is actively being updated as components are released.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 1. What are UI Components?
|
||||
|
||||
Unlike **Tag Helpers** (which are just functional mirrors of HTML tags), SigPro UI Components are smart abstractions:
|
||||
|
||||
* **Stateful**: They manage complex internal states (like date ranges, search filtering, or API lifecycles).
|
||||
* **Reactive**: Attributes prefixed with `$` are automatically tracked via `Watch`.
|
||||
* **Self-Sane**: They automatically use `._cleanups` to destroy observers or event listeners when removed from the DOM.
|
||||
* **Themed**: Fully compatible with DaisyUI v5 theme system and Tailwind v4 utility classes.
|
||||
|
||||
---
|
||||
|
||||
## 2. Prerequisites
|
||||
|
||||
<div class="alert alert-warning shadow-md my-6 border-l-8 border-warning bg-warning/10">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-5 h-5 mt-0.5"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
|
||||
<div>
|
||||
<p class="text-sm font-semibold">To ensure all components render correctly, your project must have:</p>
|
||||
<ul class="flex flex-wrap gap-3 mt-2">
|
||||
<li class="badge badge-warning badge-md">Tailwind CSS v4+</li>
|
||||
<li class="badge badge-warning badge-md">DaisyUI v5+</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 3. The UI Registry (Available Now)
|
||||
|
||||
| Category | Components |
|
||||
| :--- | :--- |
|
||||
| **Forms & Inputs** | `Button`, `Input`, `Select`, `Autocomplete`, `Datepicker`, `Colorpicker`, `CheckBox`, `Radio`, `Range`, `Rating`, `Swap` |
|
||||
| **Feedback** | `Alert`, `Toast`, `Modal`, `Loading`, `Badge`, `Tooltip`, `Indicator` |
|
||||
| **Navigation** | `Navbar`, `Menu`, `Drawer`, `Tabs`, `Accordion`, `Dropdown` |
|
||||
| **Data & Layout** | `Request`, `Response`, `List`, `Stack`, `Timeline`, `Stat`, `Fieldset`, `Fab` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Examples with "Superpowers"
|
||||
|
||||
### A. The Declarative API Flow (`Request` & `Response`)
|
||||
Instead of manually managing `loading` and `error` flags, use these together to handle data fetching elegantly.
|
||||
|
||||
```javascript
|
||||
// 1. Define the request (it tracks dependencies automatically)
|
||||
const userProfile = Request(
|
||||
() => `https://api.example.com/user/${userId()}`
|
||||
);
|
||||
|
||||
// 2. Render the UI based on the request state
|
||||
Div({ class: "p-4" }, [
|
||||
Response(userProfile, (data) =>
|
||||
Div([
|
||||
H1(data.name),
|
||||
P(data.email)
|
||||
])
|
||||
)
|
||||
]);
|
||||
```
|
||||
|
||||
### B. Smart Inputs & Autocomplete
|
||||
SigPro UI inputs handle labels, icons, password toggles, and validation states out of the box using DaisyUI v5 classes.
|
||||
|
||||
```javascript
|
||||
const searchQuery = $("");
|
||||
|
||||
Autocomplete({
|
||||
label: "Find a Country",
|
||||
placeholder: "Start typing...",
|
||||
options: ["Spain", "France", "Germany", "Italy", "Portugal"],
|
||||
$value: searchQuery,
|
||||
onSelect: (val) => console.log("Selected:", val)
|
||||
});
|
||||
```
|
||||
|
||||
### C. The Reactive Datepicker
|
||||
Handles single dates or ranges with a clean, reactive interface that automatically syncs with your signals.
|
||||
|
||||
```javascript
|
||||
const myDate = $(""); // or { start: "", end: "" } for range
|
||||
|
||||
Datepicker({
|
||||
label: "Select Expiry Date",
|
||||
$value: myDate,
|
||||
range: false
|
||||
});
|
||||
```
|
||||
|
||||
### D. Imperative Toasts & Modals
|
||||
Trigger complex UI elements from your logic. These components use `Mount` internally to ensure they are properly cleaned up from memory after they close.
|
||||
|
||||
```javascript
|
||||
// Show a notification (Self-destroying after 3s)
|
||||
Toast("Settings saved successfully!", "alert-success", 3000);
|
||||
|
||||
// Control a modal with a simple signal
|
||||
const isModalOpen = $(false);
|
||||
|
||||
Modal({
|
||||
$open: isModalOpen,
|
||||
title: "Delete Account",
|
||||
buttons: [
|
||||
Button({ class: "btn-error", onclick: doDelete }, "Confirm")
|
||||
]
|
||||
}, "This action cannot be undone.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Internationalization (i18n)
|
||||
|
||||
The UI library comes with a built-in locale system.
|
||||
|
||||
```javascript
|
||||
// Set the global UI language
|
||||
Locale("en");
|
||||
|
||||
// Access translated strings (Returns a signal that tracks the current locale)
|
||||
const t = tt("confirm");
|
||||
```
|
||||
@@ -1,146 +0,0 @@
|
||||
# Vite Plugin: File-based Routing
|
||||
|
||||
The `sigproRouter` plugin for Vite automates route generation by scanning your `pages` directory. It creates a **virtual module** that you can import directly into your code, eliminating the need to maintain a manual routes array.
|
||||
|
||||
## 1. Project Structure
|
||||
|
||||
To use the plugin, organize your files within the `src/pages` directory. The folder hierarchy directly determines your application's URL structure. SigPro uses brackets `[param]` for dynamic segments.
|
||||
|
||||
<div class="mockup-code bg-base-300 text-base-content shadow-xl my-8">
|
||||
<pre><code>my-sigpro-app/
|
||||
├── src/
|
||||
│ ├── pages/
|
||||
│ │ ├── index.js → #/
|
||||
│ │ ├── about.js → #/about
|
||||
│ │ ├── users/
|
||||
│ │ │ └── [id].js → #/users/:id
|
||||
│ │ └── blog/
|
||||
│ │ ├── index.js → #/blog
|
||||
│ │ └── [slug].js → #/blog/:slug
|
||||
│ ├── App.js (Main Layout)
|
||||
│ └── main.js (Entry Point)
|
||||
├── vite.config.js
|
||||
└── package.json</code></pre>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 2. Setup & Configuration
|
||||
|
||||
Add the plugin to your `vite.config.js`. It works out of the box with zero configuration.
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
import { defineConfig } from 'vite';
|
||||
import { sigproRouter } from 'sigpro/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sigproRouter()]
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation
|
||||
|
||||
Thanks to **SigPro's synchronous initialization**, you no longer need to wrap your mounting logic in `.then()` blocks.
|
||||
|
||||
<div class="tabs tabs-box w-full mt-8 mb-12 bg-base-200/50 p-2 border border-base-300">
|
||||
<input type="radio" name="route_impl" class="tab" aria-label="Option A: Direct in main.js" checked />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
|
||||
|
||||
```javascript
|
||||
// src/main.js
|
||||
import SigPro from 'sigpro';
|
||||
import { routes } from 'virtual:sigpro-routes';
|
||||
|
||||
// The Core already has Router ready
|
||||
Mount(Router(routes), '#app');
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
<input type="radio" name="route_impl" class="tab" aria-label="Option B: Inside App.js (Persistent Layout)" />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
|
||||
|
||||
```javascript
|
||||
// src/App.js
|
||||
import { routes } from 'virtual:sigpro-routes';
|
||||
|
||||
export default () => div({ class: 'layout' }, [
|
||||
header([
|
||||
h1("SigPro App"),
|
||||
nav([
|
||||
button({ onclick: () => Router.go('/') }, "Home"),
|
||||
button({ onclick: () => Router.go('/blog') }, "Blog")
|
||||
])
|
||||
]),
|
||||
// Only the content inside <main> will be swapped reactively
|
||||
main(Router(routes))
|
||||
]);
|
||||
```
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 4. Route Mapping Reference
|
||||
|
||||
The plugin follows a simple convention to transform your file system into a routing map.
|
||||
|
||||
<div class="overflow-x-auto my-8">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead class="bg-base-200">
|
||||
<tr>
|
||||
<th>File Path</th>
|
||||
<th>Generated Path</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>index.js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/</td>
|
||||
<td>The application root.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>about.js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/about</td>
|
||||
<td>A static page.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>[id].js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/:id</td>
|
||||
<td>Dynamic parameter (passed to the component).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>blog/index.js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/blog</td>
|
||||
<td>Folder index page.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>_utils.js</code></td>
|
||||
<td class="italic opacity-50 text-error">Ignored</td>
|
||||
<td>Files starting with <code>_</code> are excluded from routing.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 5. How it Works (Vite Virtual Module)
|
||||
|
||||
The plugin generates a virtual module named `virtual:sigpro-routes`. This module exports an array of objects compatible with `Router()`:
|
||||
|
||||
```javascript
|
||||
// Internal representation generated by the plugin
|
||||
export const routes = [
|
||||
{ path: '/', component: () => import('/src/pages/index.js') },
|
||||
{ path: '/users/:id', component: () => import('/src/pages/users/[id].js') },
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
Because it uses dynamic `import()`, Vite automatically performs **Code Splitting**, meaning each page is its own small JS file that only loads when the user navigates to it.
|
||||
117
package.json
117
package.json
@@ -1,57 +1,112 @@
|
||||
{
|
||||
"name": "sigpro",
|
||||
"version": "1.1.22",
|
||||
"version": "1.2.40",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"main": "./dist/sigpro.esm.min.js",
|
||||
"module": "./dist/sigpro.esm.min.js",
|
||||
"unpkg": "./dist/sigpro.min.js",
|
||||
"jsdelivr": "./dist/sigpro.min.js",
|
||||
"author": {
|
||||
"name": "NatxoCC",
|
||||
"email": "sigpro@natxocc.com",
|
||||
"url": "https://sigpro.natxocc.com"
|
||||
},
|
||||
"main": "./dist/sigpro.js",
|
||||
"module": "./dist/sigpro.js",
|
||||
"unpkg": "./dist/sigpro.js",
|
||||
"jsdelivr": "./dist/sigpro.js",
|
||||
"types": "./sigpro.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/sigpro.esm.min.js",
|
||||
"script": "./dist/sigpro.js",
|
||||
"types": "./sigpro.d.ts"
|
||||
"types": "./sigpro.d.ts",
|
||||
"import": "./dist/sigpro.js",
|
||||
"default": "./dist/sigpro.js"
|
||||
},
|
||||
"./vite": "./vite/index.js",
|
||||
"./vite/*": "./vite/*.js"
|
||||
"./db": {
|
||||
"types": "./sigpro.d.ts",
|
||||
"import": "./dist/sigpro.db.js",
|
||||
"default": "./dist/sigpro.db.js"
|
||||
},
|
||||
"./router": {
|
||||
"types": "./sigpro.d.ts",
|
||||
"import": "./dist/sigpro.router.js",
|
||||
"default": "./dist/sigpro.router.js"
|
||||
},
|
||||
"./locale": {
|
||||
"types": "./sigpro.d.ts",
|
||||
"import": "./dist/sigpro.locale.js",
|
||||
"default": "./dist/sigpro.locale.js"
|
||||
},
|
||||
"./grid": {
|
||||
"import": "./dist/sigpro.grid.js",
|
||||
"default": "./dist/sigpro.grid.js"
|
||||
},
|
||||
"./editor": {
|
||||
"import": "./dist/sigpro.editor.js",
|
||||
"default": "./dist/sigpro.editor.js"
|
||||
},
|
||||
"./vite": {
|
||||
"import": "./dist/sigpro.vite.js",
|
||||
"default": "./dist/sigpro.vite.js"
|
||||
},
|
||||
"./css": "./dist/sigpro.ui.css",
|
||||
"./ui": {
|
||||
"types": "./sigpro.ui.d.ts",
|
||||
"import": "./dist/sigpro.ui.js",
|
||||
"default": "./dist/sigpro.ui.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"dist",
|
||||
"sigpro",
|
||||
"vite",
|
||||
"dist/",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
"LICENSE",
|
||||
"sigpro.d.ts",
|
||||
"sigpro.ui.d.ts"
|
||||
],
|
||||
"homepage": "https://natxocc.github.io/sigpro/",
|
||||
"homepage": "https://sigpro.natxocc.com/#/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/natxocc/sigpro.git"
|
||||
"url": "https://github.com/natxocc/sigpro"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/natxocc/sigpro/issues"
|
||||
"url": "https://github.com/natxocc/sigpro/issues",
|
||||
"email": "sigpro@natxocc.com"
|
||||
},
|
||||
"scripts": {
|
||||
"del": "bun pm cache rm && rm -f bun.lockb && rm -f bun.lock",
|
||||
"clean": "rm -rf dist",
|
||||
"prebuild": "npm run clean",
|
||||
"build:iife": "bun build ./index.js --bundle --outfile=./dist/sigpro.js --format=iife --global-name=SigPro",
|
||||
"build:docs": "bun build ./index.js --bundle --outfile=./docs/sigpro.js --format=iife --global-name=SigPro",
|
||||
"build:iife:min": "bun build ./index.js --bundle --outfile=./dist/sigpro.min.js --format=iife --global-name=SigPro --minify",
|
||||
"build:esm": "bun build ./index.js --bundle --outfile=./dist/sigpro.esm.js --format=esm",
|
||||
"build:esm:min": "bun build ./index.js --bundle --outfile=./dist/sigpro.esm.min.js --format=esm --minify",
|
||||
"build": "bun run build:iife && bun run build:iife:min && bun run build:esm && bun run build:esm:min && bun run build:docs",
|
||||
"docs": "bun x serve docs",
|
||||
"prepublishOnly": "npm run build"
|
||||
"build:core": "bun build ./src/sigpro.js --bundle --outfile=./dist/sigpro.js --format=esm --minify",
|
||||
"build:db": "bun build ./src/sigpro.db.js --bundle --outfile=./dist/sigpro.db.js --format=esm --minify",
|
||||
"build:router": "bun build ./src/sigpro.router.js --bundle --outfile=./dist/sigpro.router.js --format=esm --external ./src/sigpro.js --minify",
|
||||
"build:locale": "bun build ./src/sigpro.locale.js --bundle --outfile=./dist/sigpro.locale.js --format=esm --external ./src/sigpro.js --minify",
|
||||
"build:ui": "bun build ./src/sigpro.ui.js --bundle --outfile=./dist/sigpro.ui.js --format=esm --external ./src/sigpro.js --minify",
|
||||
"build:grid": "bun build ./src/sigpro.grid.js --bundle --external sigpro --outfile=./dist/sigpro.grid.js --format=esm --minify",
|
||||
"build:editor": "bun build ./src/sigpro.editor.js --bundle --external sigpro --outfile=./dist/sigpro.editor.js --format=esm --minify",
|
||||
"build:vite": "bun build ./src/sigpro.vite.js --bundle --outfile=./dist/sigpro.vite.js --format=esm --external fs --external path --minify",
|
||||
"build:css": "tailwindcss -i ./src/sigpro.ui.css -o ./dist/sigpro.ui.css --minify --content './src/tailwind' && du -h ./dist/sigpro.ui.css",
|
||||
"build:convert": "bun build ./src/sigpro.convert.js --bundle --outfile=./docs/sigpro.convert.js --format=esm --external ./src/sigpro.js --minify",
|
||||
"build": "bun run build:core && bun run build:db && bun run build:router && bun run build:locale && bun run build:ui && bun run build:grid && bun run build:editor && bun run build:vite && bun run build:css && bun run build:convert && cp ./dist/* ./docs",
|
||||
"docs": "bun x serve docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^2.2.473",
|
||||
"@iconify/tailwind4": "^1.2.3",
|
||||
"@tailwindcss/cli": "^4.3.0",
|
||||
"daisyui": "^5.5.19",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"ag-grid-community": "^35.3.0"
|
||||
},
|
||||
"keywords": [
|
||||
"signals",
|
||||
"reactivity",
|
||||
"reactive",
|
||||
"web-components",
|
||||
"vanilla-js",
|
||||
"reactive-programming",
|
||||
"signals-library",
|
||||
"fine-grained-reactivity"
|
||||
"pure",
|
||||
"vanilla",
|
||||
"js",
|
||||
"ui",
|
||||
"dom",
|
||||
"state",
|
||||
"frontend",
|
||||
"spa",
|
||||
"lightweight",
|
||||
"sigpro"
|
||||
]
|
||||
}
|
||||
21
sigpro.code-workspace
Normal file
21
sigpro.code-workspace
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".",
|
||||
},
|
||||
{
|
||||
"path": "../sigpro-ui",
|
||||
},
|
||||
{
|
||||
"path": "../sigpro",
|
||||
},
|
||||
],
|
||||
"settings": {
|
||||
"files.associations": {
|
||||
"*.js": "javascript",
|
||||
},
|
||||
"emmet.includeLanguages": {
|
||||
"javascript": "html",
|
||||
},
|
||||
},
|
||||
}
|
||||
537
sigpro.d.ts
vendored
537
sigpro.d.ts
vendored
@@ -1,188 +1,395 @@
|
||||
// sigpro.d.ts
|
||||
/**
|
||||
* SigPro
|
||||
* A minimalistic reactive library with fine-grained reactivity,
|
||||
* direct DOM updates, and built-in component helpers.
|
||||
*/
|
||||
|
||||
declare const SIG_BRAND: unique symbol;
|
||||
// ============================================================================
|
||||
// Core Reactivity
|
||||
// ============================================================================
|
||||
|
||||
export interface Signal<T = any> {
|
||||
readonly [SIG_BRAND]: true;
|
||||
/**
|
||||
* Creates a reactive signal. When a function is passed, it becomes a computed signal.
|
||||
* If a `localStorageKey` is provided, the value persists.
|
||||
*
|
||||
* @param value - Initial value or computation function
|
||||
* @param localStorageKey - Optional key for persistence
|
||||
* @returns A getter/setter function
|
||||
*/
|
||||
export function $<T>(value: T, localStorageKey?: string): Signal<T>;
|
||||
export function $<T>(computation: () => T): Signal<T>;
|
||||
|
||||
export interface Signal<T> {
|
||||
(): T;
|
||||
(value: T): T;
|
||||
(updater: (prev: T) => T): T;
|
||||
(value: T | ((prev: T) => T)): void;
|
||||
}
|
||||
|
||||
export interface Computed<T = any> {
|
||||
readonly [SIG_BRAND]: true;
|
||||
(): T;
|
||||
/**
|
||||
* Creates a deep reactive proxy for objects and arrays.
|
||||
* Tracks property access and mutations automatically.
|
||||
*
|
||||
* @param target - Object or array to make reactive
|
||||
* @returns A reactive proxy
|
||||
*/
|
||||
export function $$<T extends object>(target: T): DeepReactive<T>;
|
||||
|
||||
export type DeepReactive<T> = T extends object
|
||||
? {
|
||||
[K in keyof T]: T[K] extends object ? DeepReactive<T[K]> : T[K];
|
||||
}
|
||||
: T;
|
||||
|
||||
/**
|
||||
* Watches reactive sources and runs a callback.
|
||||
*
|
||||
* @example
|
||||
* // Auto-track mode
|
||||
* watch(() => {
|
||||
* console.log(count());
|
||||
* });
|
||||
*
|
||||
* @example
|
||||
* // Explicit sources
|
||||
* watch([count, name], ([c, n]) => {
|
||||
* console.log(c, n);
|
||||
* });
|
||||
*
|
||||
* @returns A function to stop the watcher.
|
||||
*/
|
||||
export function watch(fn: () => void): () => void;
|
||||
export function watch<T>(
|
||||
sources: Array<Signal<any>> | (() => T),
|
||||
callback: (values: T | any[]) => void
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Batches multiple reactive updates into a single flush.
|
||||
*/
|
||||
export function batch<T>(fn: () => T): T;
|
||||
|
||||
// ============================================================================
|
||||
// DOM Creation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hyperscript function to create DOM elements or components.
|
||||
*
|
||||
* @param tag - HTML/SVG tag name or component function
|
||||
* @param props - Optional properties/attributes
|
||||
* @param children - Child nodes or reactive functions
|
||||
* @returns DOM node or array of nodes
|
||||
*/
|
||||
export function h(
|
||||
tag: string | ((props: any, ctx: ComponentContext) => any),
|
||||
props?: any,
|
||||
children?: any
|
||||
): Node;
|
||||
|
||||
export interface ComponentContext {
|
||||
children: any;
|
||||
emit: (event: string, ...args: any[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally renders content.
|
||||
*
|
||||
* @param condition - Boolean, signal, or function returning boolean
|
||||
* @param thenBranch - Content when truthy (Node or function)
|
||||
* @param elseBranch - Optional content when falsy
|
||||
* @returns A placeholder element that updates reactively
|
||||
*/
|
||||
export function when(
|
||||
condition: boolean | (() => boolean) | Signal<boolean>,
|
||||
thenBranch: any | (() => any),
|
||||
elseBranch?: any | (() => any)
|
||||
): HTMLElement;
|
||||
|
||||
/**
|
||||
* Keyed list renderer. Uses `item?.id` by default or a custom key field.
|
||||
*
|
||||
* @param src - Array, signal, or function returning array
|
||||
* @param itemFn - Render function (item, index) => Node
|
||||
* @param keyField - Optional property name for unique key (e.g., "id")
|
||||
* @returns A container element with reactive list
|
||||
*/
|
||||
export function each<T>(
|
||||
src: T[] | (() => T[]) | Signal<T[]>,
|
||||
itemFn: (item: T, index: number) => any,
|
||||
keyField?: keyof T
|
||||
): HTMLElement;
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Router
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hash-based router.
|
||||
*
|
||||
* @param routes - Array of route definitions
|
||||
* @returns A container that renders the current route
|
||||
*/
|
||||
export function router(routes: RouteDefinition[]): HTMLElement;
|
||||
|
||||
export interface RouteDefinition {
|
||||
path: string; // e.g., "/", "/user/:id", "*"
|
||||
component: any | ((params: Record<string, string>) => any);
|
||||
}
|
||||
|
||||
export namespace router {
|
||||
/** Reactive params signal */
|
||||
export const params: Signal<Record<string, string>>;
|
||||
|
||||
/** Navigate to path */
|
||||
export function to(path: string): void;
|
||||
|
||||
/** Go back in history */
|
||||
export function back(): void;
|
||||
|
||||
/** Current path without hash */
|
||||
export function path(): string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mount API
|
||||
// ============================================================================
|
||||
|
||||
export interface RuntimeInstance {
|
||||
readonly _isRuntime: true;
|
||||
readonly container: HTMLElement;
|
||||
destroy(): void;
|
||||
_isRuntime: true;
|
||||
container: HTMLElement;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export interface TransitionOptions {
|
||||
on?: (el: HTMLElement) => void;
|
||||
off?: (el: HTMLElement, destroy: () => void) => void;
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
path: string;
|
||||
component: (params: Record<string, string>) => any;
|
||||
}
|
||||
|
||||
export interface RenderContext {
|
||||
onCleanup: (fn: () => void) => void;
|
||||
}
|
||||
|
||||
export interface TagProps extends Record<string, any> {
|
||||
ref?: ((el: HTMLElement) => void) | { current: HTMLElement | null };
|
||||
class?: string | (() => string);
|
||||
style?: string | (() => string);
|
||||
}
|
||||
|
||||
export type ReactiveObject<T extends object> = {
|
||||
[K in keyof T]: T[K] extends object ? ReactiveObject<T[K]> : T[K];
|
||||
};
|
||||
|
||||
export function $<T>(val: T, key?: string | null): Signal<T>;
|
||||
export function $<T>(val: () => T): Computed<T>;
|
||||
|
||||
export function $$<T>(fn: () => T): Computed<T>;
|
||||
|
||||
export function $_<T extends object>(obj: T): ReactiveObject<T>;
|
||||
|
||||
export function untrack<T>(fn: () => T): T;
|
||||
|
||||
export function Watch(cb: () => void): () => void;
|
||||
export function Watch(cb: () => () => void): () => void;
|
||||
|
||||
export function Render<T>(fn: (ctx: RenderContext) => T): RuntimeInstance;
|
||||
|
||||
export function Tag(tag: string, props?: TagProps | null, children?: any[]): HTMLElement;
|
||||
export function Tag(tag: string, children?: any[]): HTMLElement;
|
||||
|
||||
export function If(
|
||||
cond: boolean | (() => boolean),
|
||||
a: any | (() => any),
|
||||
b?: any | (() => any) | null,
|
||||
options?: TransitionOptions
|
||||
): HTMLElement;
|
||||
|
||||
export function For<T>(
|
||||
source: T[] | (() => T[]),
|
||||
renderFn: (item: T, index: number) => any,
|
||||
keyFn?: (item: T, index: number) => any,
|
||||
tag?: string,
|
||||
props?: TagProps
|
||||
): HTMLElement;
|
||||
|
||||
export function Router(routes: Route[]): HTMLElement;
|
||||
|
||||
export namespace Router {
|
||||
const params: Signal<Record<string, string>>;
|
||||
function to(path: string): void;
|
||||
function back(): void;
|
||||
function path(): string;
|
||||
}
|
||||
|
||||
export function Mount(
|
||||
component: (() => any) | any,
|
||||
/**
|
||||
* Mounts a component to a DOM target.
|
||||
*
|
||||
* @param component - Component function or node
|
||||
* @param target - CSS selector or DOM element
|
||||
* @returns Runtime instance
|
||||
*/
|
||||
export function mount(
|
||||
component: (() => any) | Node,
|
||||
target: string | HTMLElement
|
||||
): RuntimeInstance;
|
||||
): RuntimeInstance | undefined;
|
||||
|
||||
export function Share<T>(key: string, value: T): void;
|
||||
// ============================================================================
|
||||
// Tag Helpers (globally available, lowercase)
|
||||
// ============================================================================
|
||||
|
||||
export function Use<T>(key: string, defaultValue?: T): T | undefined;
|
||||
// All standard HTML tags are available as global functions.
|
||||
// They follow the same signature as `h` but with predefined tag names.
|
||||
// Examples:
|
||||
export const a: TagHelper;
|
||||
export const abbr: TagHelper;
|
||||
export const article: TagHelper;
|
||||
export const aside: TagHelper;
|
||||
export const audio: TagHelper;
|
||||
export const b: TagHelper;
|
||||
export const blockquote: TagHelper;
|
||||
export const br: TagHelper;
|
||||
export const button: TagHelper;
|
||||
export const canvas: TagHelper;
|
||||
export const caption: TagHelper;
|
||||
export const cite: TagHelper;
|
||||
export const code: TagHelper;
|
||||
export const col: TagHelper;
|
||||
export const colgroup: TagHelper;
|
||||
export const datalist: TagHelper;
|
||||
export const dd: TagHelper;
|
||||
export const del: TagHelper;
|
||||
export const details: TagHelper;
|
||||
export const dfn: TagHelper;
|
||||
export const dialog: TagHelper;
|
||||
export const div: TagHelper;
|
||||
export const dl: TagHelper;
|
||||
export const dt: TagHelper;
|
||||
export const em: TagHelper;
|
||||
export const embed: TagHelper;
|
||||
export const fieldset: TagHelper;
|
||||
export const figcaption: TagHelper;
|
||||
export const figure: TagHelper;
|
||||
export const footer: TagHelper;
|
||||
export const form: TagHelper;
|
||||
export const h1: TagHelper;
|
||||
export const h2: TagHelper;
|
||||
export const h3: TagHelper;
|
||||
export const h4: TagHelper;
|
||||
export const h5: TagHelper;
|
||||
export const h6: TagHelper;
|
||||
export const header: TagHelper;
|
||||
export const hr: TagHelper;
|
||||
export const i: TagHelper;
|
||||
export const iframe: TagHelper;
|
||||
export const img: TagHelper;
|
||||
export const input: TagHelper;
|
||||
export const ins: TagHelper;
|
||||
export const kbd: TagHelper;
|
||||
export const label: TagHelper;
|
||||
export const legend: TagHelper;
|
||||
export const li: TagHelper;
|
||||
export const main: TagHelper;
|
||||
export const mark: TagHelper;
|
||||
export const meter: TagHelper;
|
||||
export const nav: TagHelper;
|
||||
export const object: TagHelper;
|
||||
export const ol: TagHelper;
|
||||
export const optgroup: TagHelper;
|
||||
export const option: TagHelper;
|
||||
export const output: TagHelper;
|
||||
export const p: TagHelper;
|
||||
export const picture: TagHelper;
|
||||
export const pre: TagHelper;
|
||||
export const progress: TagHelper;
|
||||
export const section: TagHelper;
|
||||
export const select: TagHelper;
|
||||
export const slot: TagHelper;
|
||||
export const small: TagHelper;
|
||||
export const source: TagHelper;
|
||||
export const span: TagHelper;
|
||||
export const strong: TagHelper;
|
||||
export const sub: TagHelper;
|
||||
export const summary: TagHelper;
|
||||
export const sup: TagHelper;
|
||||
export const svg: TagHelper;
|
||||
export const table: TagHelper;
|
||||
export const tbody: TagHelper;
|
||||
export const td: TagHelper;
|
||||
export const template: TagHelper;
|
||||
export const textarea: TagHelper;
|
||||
export const tfoot: TagHelper;
|
||||
export const th: TagHelper;
|
||||
export const thead: TagHelper;
|
||||
export const time: TagHelper;
|
||||
export const tr: TagHelper;
|
||||
export const u: TagHelper;
|
||||
export const ul: TagHelper;
|
||||
export const video: TagHelper;
|
||||
|
||||
// Funciones JSX (etiquetas globales)
|
||||
export const Div: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Span: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const P: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const H1: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const H2: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const H3: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const H4: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const H5: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const H6: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Button: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const A: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Img: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Input: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Textarea: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Select: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Option: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Form: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Label: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Ul: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Ol: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Li: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Table: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Tr: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Td: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Th: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Section: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Article: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Aside: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Nav: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Header: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Footer: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export const Main: (props?: TagProps, children?: any[]) => HTMLElement;
|
||||
export type TagHelper = (
|
||||
props?: any,
|
||||
children?: any
|
||||
) => HTMLElement | SVGElement | Text;
|
||||
|
||||
export interface SigProAPI {
|
||||
// ============================================================================
|
||||
// Default Export
|
||||
// ============================================================================
|
||||
|
||||
declare const SigPro: {
|
||||
$: typeof $;
|
||||
$$: typeof $$;
|
||||
$_: typeof $_;
|
||||
untrack: typeof untrack;
|
||||
Render: typeof Render;
|
||||
Watch: typeof Watch;
|
||||
Tag: typeof Tag;
|
||||
If: typeof If;
|
||||
For: typeof For;
|
||||
Router: typeof Router;
|
||||
Mount: typeof Mount;
|
||||
Share: typeof Share;
|
||||
Use: typeof Use;
|
||||
}
|
||||
watch: typeof watch;
|
||||
h: typeof h;
|
||||
when: typeof when;
|
||||
each: typeof each;
|
||||
router: typeof router;
|
||||
mount: typeof mount;
|
||||
batch: typeof batch;
|
||||
};
|
||||
|
||||
declare const SigPro: SigProAPI;
|
||||
export default SigPro;
|
||||
|
||||
// ============================================================================
|
||||
// Global augmentation for browser environments
|
||||
// ============================================================================
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
div: TagProps;
|
||||
span: TagProps;
|
||||
p: TagProps;
|
||||
h1: TagProps;
|
||||
h2: TagProps;
|
||||
h3: TagProps;
|
||||
h4: TagProps;
|
||||
h5: TagProps;
|
||||
h6: TagProps;
|
||||
button: TagProps;
|
||||
a: TagProps;
|
||||
img: TagProps;
|
||||
input: TagProps;
|
||||
textarea: TagProps;
|
||||
select: TagProps;
|
||||
option: TagProps;
|
||||
form: TagProps;
|
||||
label: TagProps;
|
||||
ul: TagProps;
|
||||
ol: TagProps;
|
||||
li: TagProps;
|
||||
table: TagProps;
|
||||
tr: TagProps;
|
||||
td: TagProps;
|
||||
th: TagProps;
|
||||
section: TagProps;
|
||||
article: TagProps;
|
||||
aside: TagProps;
|
||||
nav: TagProps;
|
||||
header: TagProps;
|
||||
footer: TagProps;
|
||||
main: TagProps;
|
||||
}
|
||||
interface Element extends HTMLElement {}
|
||||
interface Window {
|
||||
$: typeof $;
|
||||
$$: typeof $$;
|
||||
watch: typeof watch;
|
||||
h: typeof h;
|
||||
when: typeof when;
|
||||
each: typeof each;
|
||||
router: typeof router;
|
||||
mount: typeof mount;
|
||||
batch: typeof batch;
|
||||
SigPro: typeof SigPro;
|
||||
|
||||
// Tag helpers (lowercase)
|
||||
a: TagHelper;
|
||||
abbr: TagHelper;
|
||||
article: TagHelper;
|
||||
aside: TagHelper;
|
||||
audio: TagHelper;
|
||||
b: TagHelper;
|
||||
blockquote: TagHelper;
|
||||
br: TagHelper;
|
||||
button: TagHelper;
|
||||
canvas: TagHelper;
|
||||
caption: TagHelper;
|
||||
cite: TagHelper;
|
||||
code: TagHelper;
|
||||
col: TagHelper;
|
||||
colgroup: TagHelper;
|
||||
datalist: TagHelper;
|
||||
dd: TagHelper;
|
||||
del: TagHelper;
|
||||
details: TagHelper;
|
||||
dfn: TagHelper;
|
||||
dialog: TagHelper;
|
||||
div: TagHelper;
|
||||
dl: TagHelper;
|
||||
dt: TagHelper;
|
||||
em: TagHelper;
|
||||
embed: TagHelper;
|
||||
fieldset: TagHelper;
|
||||
figcaption: TagHelper;
|
||||
figure: TagHelper;
|
||||
footer: TagHelper;
|
||||
form: TagHelper;
|
||||
h1: TagHelper;
|
||||
h2: TagHelper;
|
||||
h3: TagHelper;
|
||||
h4: TagHelper;
|
||||
h5: TagHelper;
|
||||
h6: TagHelper;
|
||||
header: TagHelper;
|
||||
hr: TagHelper;
|
||||
i: TagHelper;
|
||||
iframe: TagHelper;
|
||||
img: TagHelper;
|
||||
input: TagHelper;
|
||||
ins: TagHelper;
|
||||
kbd: TagHelper;
|
||||
label: TagHelper;
|
||||
legend: TagHelper;
|
||||
li: TagHelper;
|
||||
main: TagHelper;
|
||||
mark: TagHelper;
|
||||
meter: TagHelper;
|
||||
nav: TagHelper;
|
||||
object: TagHelper;
|
||||
ol: TagHelper;
|
||||
optgroup: TagHelper;
|
||||
option: TagHelper;
|
||||
output: TagHelper;
|
||||
p: TagHelper;
|
||||
picture: TagHelper;
|
||||
pre: TagHelper;
|
||||
progress: TagHelper;
|
||||
section: TagHelper;
|
||||
select: TagHelper;
|
||||
slot: TagHelper;
|
||||
small: TagHelper;
|
||||
source: TagHelper;
|
||||
span: TagHelper;
|
||||
strong: TagHelper;
|
||||
sub: TagHelper;
|
||||
summary: TagHelper;
|
||||
sup: TagHelper;
|
||||
svg: TagHelper;
|
||||
table: TagHelper;
|
||||
tbody: TagHelper;
|
||||
td: TagHelper;
|
||||
template: TagHelper;
|
||||
textarea: TagHelper;
|
||||
tfoot: TagHelper;
|
||||
th: TagHelper;
|
||||
thead: TagHelper;
|
||||
time: TagHelper;
|
||||
tr: TagHelper;
|
||||
u: TagHelper;
|
||||
ul: TagHelper;
|
||||
video: TagHelper;
|
||||
}
|
||||
}
|
||||
440
sigpro.js
440
sigpro.js
@@ -1,440 +0,0 @@
|
||||
let activeEffect = null;
|
||||
let currentOwner = null;
|
||||
const effectQueue = new Set();
|
||||
let isFlushing = false;
|
||||
const MOUNTED_NODES = new WeakMap();
|
||||
|
||||
const doc = document;
|
||||
const isArr = Array.isArray;
|
||||
const assign = Object.assign;
|
||||
const createEl = (t) => doc.createElement(t);
|
||||
const createText = (t) => doc.createTextNode(String(t ?? ""));
|
||||
const isFunc = (f) => typeof f === "function";
|
||||
const isObj = (o) => typeof o === "object" && o !== null;
|
||||
|
||||
const runWithContext = (effect, callback) => {
|
||||
const previousEffect = activeEffect;
|
||||
activeEffect = effect;
|
||||
try { return callback(); }
|
||||
finally { activeEffect = previousEffect; }
|
||||
};
|
||||
|
||||
const cleanupNode = (node) => {
|
||||
if (node._cleanups) {
|
||||
node._cleanups.forEach((dispose) => dispose());
|
||||
node._cleanups.clear();
|
||||
}
|
||||
node.childNodes?.forEach(cleanupNode);
|
||||
};
|
||||
|
||||
const flushEffects = () => {
|
||||
if (isFlushing) return;
|
||||
isFlushing = true;
|
||||
while (effectQueue.size > 0) {
|
||||
const sortedEffects = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
|
||||
effectQueue.clear();
|
||||
for (const effect of sortedEffects) {
|
||||
if (!effect._deleted) effect();
|
||||
}
|
||||
}
|
||||
isFlushing = false;
|
||||
};
|
||||
|
||||
const trackSubscription = (subscribers) => {
|
||||
if (activeEffect && !activeEffect._deleted) {
|
||||
subscribers.add(activeEffect);
|
||||
activeEffect._deps.add(subscribers);
|
||||
}
|
||||
};
|
||||
|
||||
const triggerUpdate = (subscribers) => {
|
||||
subscribers.forEach((effect) => {
|
||||
if (effect === activeEffect || effect._deleted) return;
|
||||
if (effect._isComputed) {
|
||||
effect.markDirty();
|
||||
if (effect._subs) triggerUpdate(effect._subs);
|
||||
} else {
|
||||
effectQueue.add(effect);
|
||||
}
|
||||
});
|
||||
if (!isFlushing) queueMicrotask(flushEffects);
|
||||
};
|
||||
|
||||
const Render = (renderFn) => {
|
||||
const cleanups = new Set();
|
||||
const previousOwner = currentOwner;
|
||||
const container = createEl("div");
|
||||
container.style.display = "contents";
|
||||
currentOwner = { cleanups };
|
||||
|
||||
const processResult = (result) => {
|
||||
if (!result) return;
|
||||
if (result._isRuntime) {
|
||||
cleanups.add(result.destroy);
|
||||
container.appendChild(result.container);
|
||||
} else if (isArr(result)) {
|
||||
result.forEach(processResult);
|
||||
} else {
|
||||
container.appendChild(result instanceof Node ? result : createText(result));
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
|
||||
} finally { currentOwner = previousOwner; }
|
||||
|
||||
return {
|
||||
_isRuntime: true,
|
||||
container,
|
||||
destroy: () => {
|
||||
cleanups.forEach((fn) => fn());
|
||||
cleanupNode(container);
|
||||
container.remove();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const $ = (initialValue, storageKey = null) => {
|
||||
const subscribers = new Set();
|
||||
|
||||
if (isFunc(initialValue)) {
|
||||
let cachedValue, isDirty = true;
|
||||
const effect = () => {
|
||||
if (effect._deleted) return;
|
||||
effect._deps.forEach((dep) => dep.delete(effect));
|
||||
effect._deps.clear();
|
||||
|
||||
runWithContext(effect, () => {
|
||||
const newValue = initialValue();
|
||||
if (!Object.is(cachedValue, newValue) || isDirty) {
|
||||
cachedValue = newValue;
|
||||
isDirty = false;
|
||||
triggerUpdate(subscribers);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
assign(effect, {
|
||||
_deps: new Set(),
|
||||
_isComputed: true,
|
||||
_subs: subscribers,
|
||||
_deleted: false,
|
||||
markDirty: () => (isDirty = true),
|
||||
stop: () => {
|
||||
effect._deleted = true;
|
||||
effect._deps.forEach((dep) => dep.delete(effect));
|
||||
subscribers.clear();
|
||||
}
|
||||
});
|
||||
|
||||
if (currentOwner) currentOwner.cleanups.add(effect.stop);
|
||||
return () => { if (isDirty) effect(); trackSubscription(subscribers); return cachedValue; };
|
||||
}
|
||||
|
||||
let value = initialValue;
|
||||
if (storageKey) {
|
||||
try {
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (saved !== null) value = JSON.parse(saved);
|
||||
} catch (e) { console.warn("SigPro Storage Lock", e); }
|
||||
}
|
||||
|
||||
return (...args) => {
|
||||
if (args.length) {
|
||||
const nextValue = isFunc(args[0]) ? args[0](value) : args[0];
|
||||
if (!Object.is(value, nextValue)) {
|
||||
value = nextValue;
|
||||
if (storageKey) localStorage.setItem(storageKey, JSON.stringify(value));
|
||||
triggerUpdate(subscribers);
|
||||
}
|
||||
}
|
||||
trackSubscription(subscribers);
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
const $$ = (object, cache = new WeakMap()) => {
|
||||
if (!isObj(object)) return object;
|
||||
if (cache.has(object)) return cache.get(object);
|
||||
|
||||
const keySubscribers = {};
|
||||
const proxy = new Proxy(object, {
|
||||
get(target, key) {
|
||||
if (activeEffect) trackSubscription(keySubscribers[key] ??= new Set());
|
||||
const value = Reflect.get(target, key);
|
||||
return isObj(value) ? $$(value, cache) : value;
|
||||
},
|
||||
set(target, key, value) {
|
||||
if (Object.is(target[key], value)) return true;
|
||||
const success = Reflect.set(target, key, value);
|
||||
if (keySubscribers[key]) triggerUpdate(keySubscribers[key]);
|
||||
return success;
|
||||
}
|
||||
});
|
||||
|
||||
cache.set(object, proxy);
|
||||
return proxy;
|
||||
};
|
||||
|
||||
const Watch = (target, callbackFn) => {
|
||||
const isExplicit = isArr(target);
|
||||
const callback = isExplicit ? callbackFn : target;
|
||||
if (!isFunc(callback)) return () => { };
|
||||
|
||||
const owner = currentOwner;
|
||||
const runner = () => {
|
||||
if (runner._deleted) return;
|
||||
runner._deps.forEach((dep) => dep.delete(runner));
|
||||
runner._deps.clear();
|
||||
runner._cleanups.forEach((cleanup) => cleanup());
|
||||
runner._cleanups.clear();
|
||||
|
||||
const previousOwner = currentOwner;
|
||||
runner.depth = activeEffect ? activeEffect.depth + 1 : 0;
|
||||
|
||||
runWithContext(runner, () => {
|
||||
currentOwner = { cleanups: runner._cleanups };
|
||||
if (isExplicit) {
|
||||
runWithContext(null, callback);
|
||||
target.forEach((dep) => isFunc(dep) && dep());
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
currentOwner = previousOwner;
|
||||
});
|
||||
};
|
||||
|
||||
assign(runner, {
|
||||
_deps: new Set(),
|
||||
_cleanups: new Set(),
|
||||
_deleted: false,
|
||||
stop: () => {
|
||||
if (runner._deleted) return;
|
||||
runner._deleted = true;
|
||||
effectQueue.delete(runner);
|
||||
runner._deps.forEach((dep) => dep.delete(runner));
|
||||
runner._cleanups.forEach((cleanup) => cleanup());
|
||||
if (owner) owner.cleanups.delete(runner.stop);
|
||||
}
|
||||
});
|
||||
|
||||
if (owner) owner.cleanups.add(runner.stop);
|
||||
runner();
|
||||
return runner.stop;
|
||||
};
|
||||
|
||||
const Tag = (tag, props = {}, children = []) => {
|
||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||
children = props; props = {};
|
||||
}
|
||||
|
||||
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
|
||||
const element = isSVG
|
||||
? doc.createElementNS("http://www.w3.org/2000/svg", tag)
|
||||
: createEl(tag);
|
||||
|
||||
element._cleanups = new Set();
|
||||
element.onUnmount = (fn) => element._cleanups.add(fn);
|
||||
const booleanAttributes = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
|
||||
|
||||
const updateAttribute = (name, value) => {
|
||||
const sanitized = (name === 'src' || name === 'href') && String(value).toLowerCase().includes('javascript:') ? '#' : value;
|
||||
if (booleanAttributes.includes(name)) {
|
||||
element[name] = !!sanitized;
|
||||
sanitized ? element.setAttribute(name, "") : element.removeAttribute(name);
|
||||
} else {
|
||||
sanitized == null ? element.removeAttribute(name) : element.setAttribute(name, sanitized);
|
||||
}
|
||||
};
|
||||
|
||||
for (let [key, value] of Object.entries(props)) {
|
||||
if (key === "ref") { (isFunc(value) ? value(element) : (value.current = element)); continue; }
|
||||
|
||||
const isSignal = isFunc(value);
|
||||
if (key.startsWith("on")) {
|
||||
const eventName = key.slice(2).toLowerCase().split(".")[0];
|
||||
element.addEventListener(eventName, value);
|
||||
element._cleanups.add(() => element.removeEventListener(eventName, value));
|
||||
} else if (isSignal) {
|
||||
element._cleanups.add(Watch(() => {
|
||||
const currentVal = value();
|
||||
key === "class" ? (element.className = currentVal || "") : updateAttribute(key, currentVal);
|
||||
}));
|
||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(element.tagName) && (key === "value" || key === "checked")) {
|
||||
const event = key === "checked" ? "change" : "input";
|
||||
const handler = (e) => value(e.target[key]);
|
||||
element.addEventListener(event, handler);
|
||||
element._cleanups.add(() => element.removeEventListener(event, handler));
|
||||
}
|
||||
} else {
|
||||
updateAttribute(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const appendChildNode = (child) => {
|
||||
if (isArr(child)) return child.forEach(appendChildNode);
|
||||
if (isFunc(child)) {
|
||||
const marker = createText("");
|
||||
element.appendChild(marker);
|
||||
let currentNodes = [];
|
||||
element._cleanups.add(Watch(() => {
|
||||
const result = child();
|
||||
const nextNodes = (isArr(result) ? result : [result]).map((node) =>
|
||||
node?._isRuntime ? node.container : node instanceof Node ? node : createText(node)
|
||||
);
|
||||
currentNodes.forEach((node) => { cleanupNode(node); node.remove(); });
|
||||
nextNodes.forEach((node) => marker.parentNode?.insertBefore(node, marker));
|
||||
currentNodes = nextNodes;
|
||||
}));
|
||||
} else {
|
||||
element.appendChild(child instanceof Node ? child : createText(child));
|
||||
}
|
||||
};
|
||||
|
||||
appendChildNode(children);
|
||||
return element;
|
||||
};
|
||||
|
||||
const If = (condition, thenVal, otherwiseVal = null, transition = null) => {
|
||||
const marker = createText("");
|
||||
const container = Tag("div", { style: "display:contents" }, [marker]);
|
||||
let currentView = null, lastState = null;
|
||||
|
||||
Watch(() => {
|
||||
const state = !!(isFunc(condition) ? condition() : condition);
|
||||
if (state === lastState) return;
|
||||
lastState = state;
|
||||
|
||||
const dispose = () => { if (currentView) currentView.destroy(); currentView = null; };
|
||||
|
||||
if (currentView && !state && transition?.out) {
|
||||
transition.out(currentView.container, dispose);
|
||||
} else {
|
||||
dispose();
|
||||
}
|
||||
|
||||
const branch = state ? thenVal : otherwiseVal;
|
||||
if (branch) {
|
||||
currentView = Render(() => isFunc(branch) ? branch() : branch);
|
||||
container.insertBefore(currentView.container, marker);
|
||||
if (state && transition?.in) transition.in(currentView.container);
|
||||
}
|
||||
});
|
||||
|
||||
return container;
|
||||
};
|
||||
|
||||
const For = (source, renderFn, keyFn, tag = "div", props = { style: "display:contents" }) => {
|
||||
const marker = createText("");
|
||||
const container = Tag(tag, props, [marker]);
|
||||
let viewCache = new Map();
|
||||
|
||||
Watch(() => {
|
||||
const items = (isFunc(source) ? source() : source) || [];
|
||||
const nextCache = new Map();
|
||||
const order = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const key = keyFn ? keyFn(item, i) : i;
|
||||
let view = viewCache.get(key);
|
||||
|
||||
if (!view) {
|
||||
const result = renderFn(item, i);
|
||||
view = result instanceof Node
|
||||
? { container: result, destroy: () => { cleanupNode(result); result.remove(); } }
|
||||
: Render(() => result);
|
||||
}
|
||||
|
||||
viewCache.delete(key);
|
||||
nextCache.set(key, view);
|
||||
order.push(key);
|
||||
}
|
||||
|
||||
viewCache.forEach(v => v.destroy());
|
||||
|
||||
let anchor = marker;
|
||||
for (let i = order.length - 1; i >= 0; i--) {
|
||||
const view = nextCache.get(order[i]);
|
||||
if (view.container.nextSibling !== anchor) {
|
||||
container.insertBefore(view.container, anchor);
|
||||
}
|
||||
anchor = view.container;
|
||||
}
|
||||
viewCache = nextCache;
|
||||
});
|
||||
|
||||
return container;
|
||||
};
|
||||
|
||||
const Router = (routes) => {
|
||||
const currentPath = $(window.location.hash.replace(/^#/, "") || "/");
|
||||
window.addEventListener("hashchange", () => currentPath(window.location.hash.replace(/^#/, "") || "/"));
|
||||
const outlet = Tag("div", { class: "router-transition" });
|
||||
let currentView = null;
|
||||
|
||||
Watch([currentPath], async () => {
|
||||
const path = currentPath();
|
||||
const route = routes.find(r => {
|
||||
const routeParts = r.path.split("/").filter(Boolean);
|
||||
const pathParts = path.split("/").filter(Boolean);
|
||||
return routeParts.length === pathParts.length && routeParts.every((part, i) => part.startsWith(":") || part === pathParts[i]);
|
||||
}) || routes.find(r => r.path === "*");
|
||||
|
||||
if (route) {
|
||||
let component = route.component;
|
||||
if (isFunc(component) && component.toString().includes('import')) {
|
||||
component = (await component()).default || (await component());
|
||||
}
|
||||
|
||||
const params = {};
|
||||
route.path.split("/").filter(Boolean).forEach((part, i) => {
|
||||
if (part.startsWith(":")) params[part.slice(1)] = path.split("/").filter(Boolean)[i];
|
||||
});
|
||||
|
||||
if (currentView) currentView.destroy();
|
||||
if (Router.params) Router.params(params);
|
||||
|
||||
currentView = Render(() => {
|
||||
try {
|
||||
return isFunc(component) ? component(params) : component;
|
||||
} catch (e) {
|
||||
return Tag("div", { class: "p-4 text-error" }, "Error loading view");
|
||||
}
|
||||
});
|
||||
outlet.appendChild(currentView.container);
|
||||
}
|
||||
});
|
||||
return outlet;
|
||||
};
|
||||
|
||||
Router.params = $({});
|
||||
Router.to = (path) => (window.location.hash = path.replace(/^#?\/?/, "#/"));
|
||||
Router.back = () => window.history.back();
|
||||
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||
|
||||
const Mount = (component, target) => {
|
||||
const targetEl = typeof target === "string" ? doc.querySelector(target) : target;
|
||||
if (!targetEl) return;
|
||||
if (MOUNTED_NODES.has(targetEl)) MOUNTED_NODES.get(targetEl).destroy();
|
||||
const instance = Render(isFunc(component) ? component : () => component);
|
||||
targetEl.replaceChildren(instance.container);
|
||||
MOUNTED_NODES.set(targetEl, instance);
|
||||
return instance;
|
||||
};
|
||||
|
||||
|
||||
const SigPro = { $, $$, Render, Watch, Tag, If, For, Router, Mount };
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
assign(window, SigPro);
|
||||
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(" ");
|
||||
tags.forEach((tag) => {
|
||||
const helper = tag[0].toUpperCase() + tag.slice(1);
|
||||
if (!(helper in window)) window[helper] = (p, c) => Tag(tag, p, c);
|
||||
});
|
||||
window.SigPro = Object.freeze(SigPro);
|
||||
}
|
||||
|
||||
export { $, $$, Render, Watch, Tag, If, For, Router, Mount };
|
||||
export default SigPro;
|
||||
855
sigpro.ts
855
sigpro.ts
@@ -1,855 +0,0 @@
|
||||
type EffectFn = {
|
||||
(): void;
|
||||
e: Set<Set<EffectFn>>;
|
||||
c?: EffectFn[];
|
||||
d?: boolean;
|
||||
};
|
||||
|
||||
type Signal<T> = {
|
||||
(): T;
|
||||
(next: T | ((prev: T) => T)): T;
|
||||
react(): T;
|
||||
};
|
||||
|
||||
type CleanupFn = () => void;
|
||||
|
||||
|
||||
const DANGEROUS_PROTOCOLS = /^(javascript|data|vbscript):/i;
|
||||
const DANGEROUS_ATTRIBUTES = /^on/i;
|
||||
|
||||
const sanitizeUrl = (url: unknown): string => {
|
||||
const str = String(url ?? '').trim().toLowerCase();
|
||||
if (DANGEROUS_PROTOCOLS.test(str)) return '#';
|
||||
return str;
|
||||
};
|
||||
|
||||
const sanitizeAttribute = (name: string, value: unknown): string | null => {
|
||||
if (value == null) return null;
|
||||
|
||||
const strValue = String(value);
|
||||
|
||||
if (DANGEROUS_ATTRIBUTES.test(name)) {
|
||||
console.warn(`[SigPro] XSS prevention: blocked attribute "${name}"`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'src' || name === 'href') {
|
||||
return sanitizeUrl(strValue);
|
||||
}
|
||||
|
||||
return strValue;
|
||||
};
|
||||
|
||||
let activeEffect: EffectFn | null = null;
|
||||
let isScheduled = false;
|
||||
const queue = new Set<EffectFn>();
|
||||
|
||||
const tick = (): void => {
|
||||
while (queue.size) {
|
||||
const runs = [...queue];
|
||||
queue.clear();
|
||||
runs.forEach(fn => fn());
|
||||
}
|
||||
isScheduled = false;
|
||||
};
|
||||
|
||||
const schedule = (fn: EffectFn): void => {
|
||||
if (fn.d) return;
|
||||
queue.add(fn);
|
||||
if (!isScheduled) {
|
||||
queueMicrotask(tick);
|
||||
isScheduled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const depend = (subs: Set<EffectFn>): void => {
|
||||
if (activeEffect && !activeEffect.c) {
|
||||
subs.add(activeEffect);
|
||||
activeEffect.e.add(subs);
|
||||
}
|
||||
};
|
||||
|
||||
export const effect = (fn: () => any, isScope: boolean = false): CleanupFn => {
|
||||
let cleanup: CleanupFn | null = null;
|
||||
|
||||
const run = () => {
|
||||
if (run.d) return;
|
||||
const prev = activeEffect;
|
||||
activeEffect = run;
|
||||
const result = fn();
|
||||
if (typeof result === 'function') cleanup = result;
|
||||
activeEffect = prev;
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
if (run.d) return;
|
||||
run.d = true;
|
||||
run.e.forEach(subs => subs.delete(run));
|
||||
run.e.clear();
|
||||
cleanup?.();
|
||||
run.c?.forEach(f => f());
|
||||
};
|
||||
|
||||
run.e = new Set();
|
||||
run.d = false;
|
||||
if (isScope) run.c = [];
|
||||
|
||||
run();
|
||||
activeEffect?.c?.push(stop);
|
||||
|
||||
return stop;
|
||||
};
|
||||
|
||||
effect.react = <T>(fn: () => T): T => {
|
||||
const prev = activeEffect;
|
||||
activeEffect = null;
|
||||
const result = fn();
|
||||
activeEffect = prev;
|
||||
return result;
|
||||
};
|
||||
|
||||
export function $<T>(initial: T, storageKey?: string): Signal<T>;
|
||||
export function $<T>(fn: () => T, storageKey?: string): Signal<T>;
|
||||
export function $<T>(initial: T | (() => T), storageKey?: string): Signal<T> {
|
||||
const isComputed = typeof initial === 'function';
|
||||
|
||||
if (!isComputed) {
|
||||
let value = initial as T;
|
||||
const subs = new Set<EffectFn>();
|
||||
|
||||
if (storageKey) {
|
||||
try {
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (saved !== null) value = JSON.parse(saved);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
const signalFn = ((...args: [] | [T | ((prev: T) => T)]) => {
|
||||
if (args.length === 0) {
|
||||
return value;
|
||||
}
|
||||
const next = typeof args[0] === 'function'
|
||||
? (args[0] as (prev: T) => T)(value)
|
||||
: args[0];
|
||||
if (Object.is(value, next)) return value;
|
||||
value = next;
|
||||
if (storageKey) {
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify(value));
|
||||
} catch { }
|
||||
}
|
||||
subs.forEach(fn => schedule(fn));
|
||||
return value;
|
||||
}) as Signal<T>;
|
||||
|
||||
signalFn.react = () => {
|
||||
depend(subs);
|
||||
return value;
|
||||
};
|
||||
|
||||
return signalFn;
|
||||
}
|
||||
|
||||
let cached: T;
|
||||
let dirty = true;
|
||||
const subs = new Set<EffectFn>();
|
||||
const fn = initial as () => T;
|
||||
|
||||
effect(() => {
|
||||
const newValue = fn();
|
||||
if (!Object.is(cached, newValue) || dirty) {
|
||||
cached = newValue;
|
||||
dirty = false;
|
||||
subs.forEach(fn => schedule(fn));
|
||||
}
|
||||
});
|
||||
|
||||
const computedFn = (() => {
|
||||
return cached;
|
||||
}) as Signal<T>;
|
||||
|
||||
computedFn.react = () => {
|
||||
depend(subs);
|
||||
return cached;
|
||||
};
|
||||
|
||||
return computedFn;
|
||||
}
|
||||
|
||||
export const scope = (fn: () => any): CleanupFn => effect(fn, true);
|
||||
|
||||
type WatchSource<T> = () => T;
|
||||
|
||||
export const watch = <T>(
|
||||
source: WatchSource<T>,
|
||||
callback: (newValue: T, oldValue: T) => any
|
||||
): CleanupFn => {
|
||||
let first = true;
|
||||
let oldValue: T;
|
||||
|
||||
return effect(() => {
|
||||
const newValue = source();
|
||||
if (!first) {
|
||||
effect.react(() => callback(newValue, oldValue));
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
oldValue = newValue;
|
||||
});
|
||||
};
|
||||
|
||||
const reactiveCache = new WeakMap<object, object>();
|
||||
|
||||
export function $$<T extends object>(obj: T): T {
|
||||
if (reactiveCache.has(obj)) {
|
||||
return reactiveCache.get(obj) as T;
|
||||
}
|
||||
|
||||
const subs: Record<string | symbol, Set<EffectFn>> = {};
|
||||
|
||||
const proxy = new Proxy(obj, {
|
||||
get(target, key: string | symbol, receiver) {
|
||||
const subsForKey = subs[key] ??= new Set();
|
||||
depend(subsForKey);
|
||||
const val = Reflect.get(target, key, receiver);
|
||||
return (val && typeof val === 'object') ? $$(val) : val;
|
||||
},
|
||||
set(target, key: string | symbol, val, receiver) {
|
||||
if (Object.is(target[key as keyof T], val)) return true;
|
||||
const success = Reflect.set(target, key, val, receiver);
|
||||
if (subs[key]) {
|
||||
subs[key].forEach(fn => schedule(fn));
|
||||
if (!isScheduled) {
|
||||
queueMicrotask(tick);
|
||||
isScheduled = true;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
});
|
||||
|
||||
reactiveCache.set(obj, proxy);
|
||||
return proxy;
|
||||
}
|
||||
|
||||
type Context = {
|
||||
m: CleanupFn[];
|
||||
u: CleanupFn[];
|
||||
p: Record<string | symbol, any>;
|
||||
};
|
||||
|
||||
let context: Context | null = null;
|
||||
|
||||
export const onMount = (fn: CleanupFn): void => {
|
||||
context?.m.push(fn);
|
||||
};
|
||||
|
||||
export const onUnmount = (fn: CleanupFn): void => {
|
||||
context?.u.push(fn);
|
||||
};
|
||||
|
||||
export const share = (key: string | symbol, value: any): void => {
|
||||
if (context) context.p[key] = value;
|
||||
};
|
||||
|
||||
export const use = (key: string | symbol, defaultValue?: any): any => {
|
||||
if (context && key in context.p) return context.p[key];
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
export function createContext<T>(defaultValue?: T): {
|
||||
Provider: (props: { value: T; children?: any }) => any;
|
||||
use: () => T;
|
||||
} {
|
||||
const key = Symbol('context');
|
||||
|
||||
const useContext = (): T => {
|
||||
// Buscar en el stack de contextos
|
||||
let current = context;
|
||||
while (current) {
|
||||
if (key in current.p) {
|
||||
return current.p[key] as T;
|
||||
}
|
||||
// Subir al contexto padre (si existiera)
|
||||
current = null; // Por ahora, solo contexto actual
|
||||
}
|
||||
if (defaultValue !== undefined) return defaultValue;
|
||||
throw new Error(`Context not found: ${String(key)}`);
|
||||
};
|
||||
|
||||
const Provider = ({ value, children }: { value: T; children?: any }) => {
|
||||
// Guardar contexto anterior
|
||||
const prevContext = context;
|
||||
|
||||
// Crear nuevo contexto o extender el existente
|
||||
if (!context) {
|
||||
context = { m: [], u: [], p: {} };
|
||||
}
|
||||
|
||||
// Guardar valor
|
||||
context.p[key] = value;
|
||||
|
||||
// Renderizar hijos
|
||||
const result = h('div', { style: 'display: contents' }, children);
|
||||
|
||||
// Restaurar contexto anterior
|
||||
context = prevContext;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return { Provider, use: useContext };
|
||||
}
|
||||
|
||||
export function createSharedContext<T>(key: string | symbol, initialValue: T): {
|
||||
set: (value: T) => void;
|
||||
get: () => T;
|
||||
} {
|
||||
// Inicializar si estamos en un componente
|
||||
if (context && !(key in context.p)) {
|
||||
share(key, initialValue);
|
||||
}
|
||||
|
||||
return {
|
||||
set: (value: T) => share(key, value),
|
||||
get: () => use(key) as T
|
||||
};
|
||||
}
|
||||
|
||||
type Component<P = Record<string, any>> = (
|
||||
props: P,
|
||||
ctx: { children?: any[]; emit: (event: string, ...args: any[]) => any }
|
||||
) => any;
|
||||
|
||||
type ElementWithLifecycle = Node & {
|
||||
$c?: Context;
|
||||
$s?: CleanupFn;
|
||||
$l?: (done: CleanupFn) => void;
|
||||
};
|
||||
|
||||
const isFn = (v: unknown): v is Function => typeof v === 'function';
|
||||
const isNode = (v: unknown): v is Node => v instanceof Node;
|
||||
|
||||
const append = (parent: Node, child: any): void => {
|
||||
if (child === null) return;
|
||||
|
||||
if (isFn(child)) {
|
||||
const anchor = document.createTextNode('');
|
||||
parent.appendChild(anchor);
|
||||
let nodes: Node[] = [];
|
||||
|
||||
effect(() => {
|
||||
effect(() => {
|
||||
const newNodes = [child()]
|
||||
.flat(Infinity)
|
||||
.map((node: any) => isFn(node) ? node() : node)
|
||||
.flat(Infinity)
|
||||
.filter((node: any) => node !== null)
|
||||
.map((node: any) => isNode(node) ? node : document.createTextNode(String(node)));
|
||||
|
||||
const oldNodes = nodes.filter(node => {
|
||||
const keep = newNodes.includes(node);
|
||||
if (!keep) remove(node);
|
||||
return keep;
|
||||
});
|
||||
|
||||
const oldIdxs = new Map(oldNodes.map((node, i) => [node, i]));
|
||||
|
||||
for (let i = newNodes.length - 1, p = oldNodes.length - 1; i >= 0; i--) {
|
||||
const node = newNodes[i];
|
||||
const ref = newNodes[i + 1] || anchor;
|
||||
|
||||
if (!oldIdxs.has(node)) {
|
||||
anchor.parentNode?.insertBefore(node, ref);
|
||||
(node as ElementWithLifecycle).$c?.m.forEach(fn => fn());
|
||||
} else if (oldNodes[p] !== node) {
|
||||
if (newNodes[i - 1] !== oldNodes[p]) {
|
||||
anchor.parentNode?.insertBefore(oldNodes[p], node);
|
||||
oldNodes[oldIdxs.get(node)!] = oldNodes[p];
|
||||
oldNodes[p] = node;
|
||||
p--;
|
||||
}
|
||||
anchor.parentNode?.insertBefore(node, ref);
|
||||
} else {
|
||||
p--;
|
||||
}
|
||||
}
|
||||
nodes = newNodes;
|
||||
});
|
||||
}, true);
|
||||
} else if (isNode(child)) {
|
||||
parent.appendChild(child);
|
||||
} else {
|
||||
parent.appendChild(document.createTextNode(String(child)));
|
||||
}
|
||||
};
|
||||
|
||||
const remove = (node: Node): void => {
|
||||
const el = node as ElementWithLifecycle;
|
||||
el.$s?.();
|
||||
el.$l?.(() => node.remove());
|
||||
if (!el.$l) node.remove();
|
||||
};
|
||||
|
||||
const render = (fn: Function, ...data: any[]): Node => {
|
||||
let node: any;
|
||||
const stop = effect(() => {
|
||||
node = fn(...data);
|
||||
if (isFn(node)) node = node();
|
||||
}, true);
|
||||
if (node) node.$s = stop;
|
||||
return node;
|
||||
};
|
||||
|
||||
export const h = (tag: any, props?: any, ...children: any[]): any => {
|
||||
props = props || {};
|
||||
children = children.flat(Infinity);
|
||||
|
||||
if (isFn(tag)) {
|
||||
const prev = context;
|
||||
context = { m: [], u: [], p: { ...(prev?.p || {}) } };
|
||||
let el: any;
|
||||
|
||||
const stop = effect(() => {
|
||||
el = tag(props, {
|
||||
children,
|
||||
emit: (evt: string, ...args: any[]) =>
|
||||
props[`on${evt[0].toUpperCase()}${evt.slice(1)}`]?.(...args),
|
||||
});
|
||||
return () => el.$c.u.forEach((fn: CleanupFn) => fn());
|
||||
}, true);
|
||||
|
||||
if (isNode(el) || isFn(el)) {
|
||||
el.$c = context;
|
||||
el.$s = stop;
|
||||
}
|
||||
|
||||
context = prev;
|
||||
return el;
|
||||
}
|
||||
|
||||
if (!tag) return () => children;
|
||||
|
||||
let el: ElementWithLifecycle;
|
||||
let is_svg = false;
|
||||
|
||||
try {
|
||||
el = document.createElement(tag);
|
||||
if (el instanceof HTMLUnknownElement) {
|
||||
is_svg = true;
|
||||
el = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
||||
}
|
||||
} catch {
|
||||
is_svg = true;
|
||||
el = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
||||
}
|
||||
|
||||
// Código MEJORADO
|
||||
const booleanAttributes = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
|
||||
|
||||
for (const key in props) {
|
||||
if (key.startsWith('on')) {
|
||||
const eventName = key.slice(2).toLowerCase();
|
||||
el.addEventListener(eventName, props[key]);
|
||||
} else if (key === "ref") {
|
||||
if (isFn(props[key])) {
|
||||
props[key](el);
|
||||
} else {
|
||||
props[key].current = el;
|
||||
}
|
||||
} else if (isFn(props[key])) {
|
||||
effect(() => {
|
||||
const val = props[key]();
|
||||
if (key === 'className') {
|
||||
el.setAttribute('class', String(val ?? ''));
|
||||
} else if (booleanAttributes.includes(key)) {
|
||||
(el as any)[key] = !!val;
|
||||
val ? el.setAttribute(key, '') : el.removeAttribute(key);
|
||||
} else if (key in el && !is_svg) {
|
||||
(el as any)[key] = val;
|
||||
} else {
|
||||
const safeVal = sanitizeAttribute(key, val);
|
||||
if (safeVal !== null) el.setAttribute(key, safeVal);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const value = props[key];
|
||||
if (key === 'className') {
|
||||
el.setAttribute('class', String(value ?? ''));
|
||||
} else if (booleanAttributes.includes(key)) {
|
||||
(el as any)[key] = !!value;
|
||||
value ? el.setAttribute(key, '') : el.removeAttribute(key);
|
||||
} else if (key in el && !is_svg) {
|
||||
(el as any)[key] = value;
|
||||
} else {
|
||||
const safeVal = sanitizeAttribute(key, value);
|
||||
if (safeVal !== null) el.setAttribute(key, safeVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
children.forEach((child: any) => append(el, child));
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
export const If = (
|
||||
cond: (() => boolean) | boolean,
|
||||
renderFn: any,
|
||||
fallback: any = null
|
||||
): (() => any) => {
|
||||
let cached: any;
|
||||
let current: boolean | null = null;
|
||||
|
||||
return () => {
|
||||
const show = isFn(cond) ? cond() : cond;
|
||||
if (show !== current) {
|
||||
cached = show ? render(renderFn) : (isFn(fallback) ? render(fallback) : fallback);
|
||||
}
|
||||
current = show;
|
||||
return cached;
|
||||
};
|
||||
};
|
||||
|
||||
export const For = <T>(
|
||||
list: (() => T[]) | T[] | { value: T[] },
|
||||
key: string | ((item: T, index: number) => string | number),
|
||||
renderFn: (item: T, index: number) => any
|
||||
): (() => any[]) => {
|
||||
let cache = new Map<string | number, any>();
|
||||
|
||||
return () => {
|
||||
const next = new Map();
|
||||
const items = (isFn(list) ? list() : (list as any).value || list) as T[];
|
||||
|
||||
const nodes = items.map((item, index) => {
|
||||
const idx = isFn(key) ? key(item, index) : key ? (item as any)[key] : index;
|
||||
let node = cache.get(idx);
|
||||
if (!node) {
|
||||
node = render(renderFn, item, index);
|
||||
}
|
||||
next.set(idx, node);
|
||||
return node;
|
||||
});
|
||||
|
||||
cache = next;
|
||||
return nodes;
|
||||
};
|
||||
};
|
||||
|
||||
type TransitionClasses = [string, string, string];
|
||||
|
||||
type TransitionConfig = {
|
||||
enter?: TransitionClasses;
|
||||
idle?: string;
|
||||
leave?: TransitionClasses;
|
||||
};
|
||||
|
||||
export const Transition = (
|
||||
{ enter: e, idle, leave: l }: TransitionConfig,
|
||||
{ children: [c] }: { children: any[] }
|
||||
): any => {
|
||||
const decorate = (el: any): any => {
|
||||
if (!isNode(el)) return el;
|
||||
|
||||
const addClass = (c?: string) => c && (el as HTMLElement).classList.add(...c.split(' '));
|
||||
const removeClass = (c?: string) => c && (el as HTMLElement).classList.remove(...c.split(' '));
|
||||
|
||||
if (e) {
|
||||
requestAnimationFrame(() => {
|
||||
addClass(e[1]);
|
||||
requestAnimationFrame(() => {
|
||||
addClass(e[0]);
|
||||
removeClass(e[1]);
|
||||
addClass(e[2]);
|
||||
el.addEventListener('transitionend', () => {
|
||||
removeClass(e[2]);
|
||||
removeClass(e[0]);
|
||||
addClass(idle);
|
||||
}, { once: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (l) {
|
||||
(el as ElementWithLifecycle).$l = (done: CleanupFn) => {
|
||||
removeClass(idle);
|
||||
addClass(l[1]);
|
||||
requestAnimationFrame(() => {
|
||||
addClass(l[0]);
|
||||
removeClass(l[1]);
|
||||
addClass(l[2]);
|
||||
el.addEventListener('transitionend', () => {
|
||||
removeClass(l[2]);
|
||||
removeClass(l[0]);
|
||||
done();
|
||||
}, { once: true });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
if (!c) return null;
|
||||
if (isFn(c)) {
|
||||
return () => decorate(c());
|
||||
}
|
||||
return decorate(c);
|
||||
};
|
||||
|
||||
type Route = {
|
||||
path: string;
|
||||
component: Component;
|
||||
};
|
||||
|
||||
type RouterInstance = {
|
||||
view: Node;
|
||||
to: (path: string) => void;
|
||||
back: () => void;
|
||||
params: Signal<Record<string, string>>;
|
||||
};
|
||||
|
||||
export const Router = (routes: Route[]): RouterInstance => {
|
||||
const getPath = () => window.location.hash.slice(1) || "/";
|
||||
const path = $(getPath());
|
||||
const params = $<Record<string, string>>({});
|
||||
|
||||
const matchRoute = (path: string): { component: Component; params: Record<string, string> } | null => {
|
||||
for (const route of routes) {
|
||||
const routeParts = route.path.split("/").filter(Boolean);
|
||||
const pathParts = path.split("/").filter(Boolean);
|
||||
|
||||
if (routeParts.length !== pathParts.length) continue;
|
||||
|
||||
const matchedParams: Record<string, string> = {};
|
||||
let ok = true;
|
||||
|
||||
for (let i = 0; i < routeParts.length; i++) {
|
||||
if (routeParts[i].startsWith(":")) {
|
||||
matchedParams[routeParts[i].slice(1)] = pathParts[i];
|
||||
} else if (routeParts[i] !== pathParts[i]) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) return { component: route.component, params: matchedParams };
|
||||
}
|
||||
|
||||
const wildcard = routes.find(r => r.path === "*");
|
||||
if (wildcard) return { component: wildcard.component, params: {} };
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
window.addEventListener("hashchange", () => path(getPath()));
|
||||
|
||||
const outlet = h("div");
|
||||
|
||||
effect(() => {
|
||||
const matched = matchRoute(path());
|
||||
if (!matched) return;
|
||||
|
||||
params(matched.params);
|
||||
|
||||
while (outlet.firstChild) outlet.removeChild(outlet.firstChild);
|
||||
outlet.appendChild(h(matched.component));
|
||||
});
|
||||
|
||||
return {
|
||||
view: outlet,
|
||||
to: (p: string) => { window.location.hash = p; },
|
||||
back: () => window.history.back(),
|
||||
params: () => params,
|
||||
};
|
||||
};
|
||||
|
||||
export const mount = (
|
||||
component: Component,
|
||||
target: string | HTMLElement,
|
||||
props?: Record<string, any>
|
||||
): CleanupFn => {
|
||||
const targetEl = typeof target === "string" ? document.querySelector(target) : target;
|
||||
if (!targetEl) throw new Error("Target element not found");
|
||||
const el = h(component, props);
|
||||
targetEl.appendChild(el);
|
||||
(el as ElementWithLifecycle).$c?.m.forEach(fn => fn());
|
||||
return () => remove(el);
|
||||
};
|
||||
|
||||
export default (
|
||||
target: HTMLElement,
|
||||
root: Component,
|
||||
props?: Record<string, any>
|
||||
): CleanupFn => {
|
||||
const el = h(root, props);
|
||||
target.appendChild(el);
|
||||
(el as ElementWithLifecycle).$c?.m.forEach(fn => fn());
|
||||
return () => remove(el);
|
||||
};
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
type Element = HTMLElement | Text | DocumentFragment | string | number | boolean | null | undefined;
|
||||
|
||||
interface IntrinsicElements {
|
||||
// Elementos HTML
|
||||
div: HTMLAttributes;
|
||||
span: HTMLAttributes;
|
||||
p: HTMLAttributes;
|
||||
a: AnchorHTMLAttributes;
|
||||
button: ButtonHTMLAttributes;
|
||||
input: InputHTMLAttributes;
|
||||
form: FormHTMLAttributes;
|
||||
img: ImgHTMLAttributes;
|
||||
ul: HTMLAttributes;
|
||||
ol: HTMLAttributes;
|
||||
li: HTMLAttributes;
|
||||
h1: HTMLAttributes;
|
||||
h2: HTMLAttributes;
|
||||
h3: HTMLAttributes;
|
||||
h4: HTMLAttributes;
|
||||
h5: HTMLAttributes;
|
||||
h6: HTMLAttributes;
|
||||
section: HTMLAttributes;
|
||||
article: HTMLAttributes;
|
||||
header: HTMLAttributes;
|
||||
footer: HTMLAttributes;
|
||||
nav: HTMLAttributes;
|
||||
main: HTMLAttributes;
|
||||
aside: HTMLAttributes;
|
||||
label: HTMLAttributes;
|
||||
select: SelectHTMLAttributes;
|
||||
option: OptionHTMLAttributes;
|
||||
textarea: TextareaHTMLAttributes;
|
||||
table: TableHTMLAttributes;
|
||||
tr: HTMLAttributes;
|
||||
td: HTMLAttributes;
|
||||
th: HTMLAttributes;
|
||||
hr: HTMLAttributes;
|
||||
br: HTMLAttributes;
|
||||
|
||||
// SVG
|
||||
svg: SVGAttributes;
|
||||
path: SVGAttributes;
|
||||
circle: SVGAttributes;
|
||||
rect: SVGAttributes;
|
||||
line: SVGAttributes;
|
||||
g: SVGAttributes;
|
||||
}
|
||||
|
||||
interface HTMLAttributes {
|
||||
id?: string;
|
||||
className?: string;
|
||||
class?: string;
|
||||
style?: string | Partial<CSSStyleDeclaration>;
|
||||
children?: any;
|
||||
ref?: ((el: any) => void) | { current: any };
|
||||
|
||||
// Eventos
|
||||
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;
|
||||
onMouseEnter?: (event: MouseEvent) => void;
|
||||
onMouseLeave?: (event: MouseEvent) => void;
|
||||
|
||||
// Atributos ARIA
|
||||
role?: string;
|
||||
'aria-label'?: string;
|
||||
'aria-hidden'?: boolean | 'true' | 'false';
|
||||
'aria-expanded'?: boolean | 'true' | 'false';
|
||||
|
||||
// Atributos comunes
|
||||
tabIndex?: number;
|
||||
title?: string;
|
||||
draggable?: boolean;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
interface AnchorHTMLAttributes extends HTMLAttributes {
|
||||
href?: string;
|
||||
target?: '_blank' | '_self' | '_parent' | '_top';
|
||||
rel?: string;
|
||||
}
|
||||
|
||||
interface ButtonHTMLAttributes extends HTMLAttributes {
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface InputHTMLAttributes extends HTMLAttributes {
|
||||
type?: 'text' | 'password' | 'email' | 'number' | 'checkbox' | 'radio' | 'file' | 'date';
|
||||
value?: string | number;
|
||||
checked?: boolean;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
name?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
interface FormHTMLAttributes extends HTMLAttributes {
|
||||
action?: string;
|
||||
method?: 'get' | 'post';
|
||||
}
|
||||
|
||||
interface ImgHTMLAttributes extends HTMLAttributes {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
loading?: 'lazy' | 'eager';
|
||||
}
|
||||
|
||||
interface SelectHTMLAttributes extends HTMLAttributes {
|
||||
value?: string | string[];
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
interface OptionHTMLAttributes extends HTMLAttributes {
|
||||
value?: string | number;
|
||||
selected?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface TextareaHTMLAttributes extends HTMLAttributes {
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
}
|
||||
|
||||
interface TableHTMLAttributes extends HTMLAttributes {
|
||||
border?: number;
|
||||
cellPadding?: number | string;
|
||||
cellSpacing?: number | string;
|
||||
}
|
||||
|
||||
interface SVGAttributes {
|
||||
viewBox?: string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
strokeWidth?: number | string;
|
||||
xmlns?: string;
|
||||
children?: any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { h as jsx, h as jsxs, h as Fragment };
|
||||
|
||||
export type { Signal, Component, CleanupFn };
|
||||
308
sigpro.ui.d.ts
vendored
Normal file
308
sigpro.ui.d.ts
vendored
Normal file
@@ -0,0 +1,308 @@
|
||||
// sigpro.ui.d.ts
|
||||
|
||||
declare module "sigpro.ui" {
|
||||
import type { Signal } from "./sigpro";
|
||||
|
||||
// Utilidades
|
||||
function hide(): void;
|
||||
|
||||
// Toast
|
||||
function toast(
|
||||
message: string | (() => any) | any,
|
||||
type?: "alert-success" | "alert-error" | "alert-warning" | "alert-info",
|
||||
duration?: number
|
||||
): () => void;
|
||||
|
||||
// Calendar
|
||||
interface CalendarProps {
|
||||
class?: string;
|
||||
value?: Signal<string | { start?: string; end?: string; startHour?: number; endHour?: number }> | any;
|
||||
range?: boolean | (() => boolean);
|
||||
hour?: boolean;
|
||||
onChange?: (value: any) => void;
|
||||
}
|
||||
function calendar(p: CalendarProps): HTMLElement;
|
||||
|
||||
// Pallete
|
||||
interface PalleteProps {
|
||||
class?: string;
|
||||
value?: Signal<string> | ((v: string) => void);
|
||||
onchange?: (color: string) => void;
|
||||
}
|
||||
function pallete(p: PalleteProps): HTMLElement;
|
||||
|
||||
// UI Components
|
||||
namespace ui {
|
||||
// Accordion
|
||||
function accordion(p: { class?: string; name?: string; checked?: boolean }, ...c: any[]): HTMLElement;
|
||||
function accordion_title(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function accordion_content(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Alert
|
||||
function alert(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Autocomplete
|
||||
interface AutocompleteProps {
|
||||
label?: string;
|
||||
items: string[] | { label: string; value: any }[] | Signal<any[]>;
|
||||
value?: Signal<string> | ((v: any) => void);
|
||||
placeholder?: string;
|
||||
class?: string;
|
||||
icon?: string;
|
||||
onChange?: (v: any) => void;
|
||||
}
|
||||
function autocomplete(p: AutocompleteProps): HTMLElement;
|
||||
|
||||
// Avatar
|
||||
function avatar(p: { class?: string; innerClass?: string }, ...c: any[]): HTMLElement;
|
||||
function avatar_group(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Badge
|
||||
function badge(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Breadcrumbs
|
||||
function breadcrumbs(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Button
|
||||
function button(p: { class?: string; onclick?: (e: Event) => void; disabled?: boolean | Signal<boolean> }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Card
|
||||
function card(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function card_title(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function card_body(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function card_actions(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Carousel
|
||||
function carousel(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function carousel_item(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Chat
|
||||
function chat(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function chat_image(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function chat_header(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function chat_bubble(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function chat_footer(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Checkbox
|
||||
function checkbox(p: { class?: string; checked?: boolean | Signal<boolean>; [key: string]: any }): HTMLElement;
|
||||
|
||||
// Colorpicker
|
||||
function colorpicker(p: AutocompleteProps): HTMLElement;
|
||||
|
||||
// Combo
|
||||
interface ComboProps {
|
||||
label?: string;
|
||||
value?: Signal<string> | string | ((v: string) => void);
|
||||
placeholder?: string;
|
||||
class?: string;
|
||||
icon?: string;
|
||||
custom?: any;
|
||||
disabled?: boolean | (() => boolean);
|
||||
readonly?: string;
|
||||
}
|
||||
function combo(p: ComboProps, c?: Function): HTMLElement;
|
||||
|
||||
// Datepicker
|
||||
interface DatepickerProps {
|
||||
label?: string;
|
||||
value?: Signal<string | { start?: string; end?: string }>;
|
||||
range?: boolean | (() => boolean);
|
||||
placeholder?: string;
|
||||
class?: string;
|
||||
fromPlaceholder?: string;
|
||||
toPlaceholder?: string;
|
||||
onChange?: (v: any) => void;
|
||||
}
|
||||
function datepicker(p: DatepickerProps): HTMLElement;
|
||||
|
||||
// Divider
|
||||
function divider(p: { class?: string }): HTMLElement;
|
||||
|
||||
// Drawer
|
||||
function drawer(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function drawer_toggle(p: { class?: string }): HTMLElement;
|
||||
function drawer_content(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function drawer_side(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function drawer_overlay(p: { class?: string }): HTMLElement;
|
||||
|
||||
// Dropdown
|
||||
function dropdown(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function dropdown_button(p: { class?: string; tabindex?: string; role?: string }, ...c: any[]): HTMLElement;
|
||||
function dropdown_content(p: { class?: string; tabindex?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// FAB
|
||||
function fab(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function fab_button(p: { class?: string; tabindex?: string; role?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Fieldset
|
||||
function fieldset(p: { class?: string; label?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// File
|
||||
function file(p: { class?: string; multiple?: boolean; accept?: string; onchange?: (e: Event) => void }): HTMLElement;
|
||||
interface FileDragProps {
|
||||
class?: string;
|
||||
drag?: boolean | Signal<boolean>;
|
||||
ondrag?: (v: boolean) => void;
|
||||
ondrop?: (files: FileList) => void;
|
||||
}
|
||||
function file_drag(p: FileDragProps, ...c: any[]): HTMLElement;
|
||||
interface FilePreviewProps {
|
||||
class?: string;
|
||||
files?: File[] | Signal<File[]>;
|
||||
onremove?: (index: number) => void;
|
||||
}
|
||||
function file_preview(p: FilePreviewProps): HTMLElement;
|
||||
function file_error(p: { class?: string; message?: string }): HTMLElement;
|
||||
|
||||
// Float
|
||||
function float(p: { label?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Indicator
|
||||
function indicator(p: { class?: string; value?: any; badgeClass?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Input
|
||||
function input(p: {
|
||||
label?: string;
|
||||
class?: string;
|
||||
icon?: string;
|
||||
right?: any;
|
||||
type?: string | (() => string);
|
||||
placeholder?: string;
|
||||
value?: Signal<string> | string;
|
||||
[key: string]: any
|
||||
}): HTMLElement;
|
||||
|
||||
// Kbd
|
||||
function kbd(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Label
|
||||
function label(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Loading
|
||||
function loading(p: { class?: string }): HTMLElement;
|
||||
|
||||
// Menu
|
||||
function menu(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
interface MenuItem {
|
||||
label?: string;
|
||||
href?: string;
|
||||
items?: MenuItem[];
|
||||
open?: boolean;
|
||||
submenuClass?: string;
|
||||
}
|
||||
function menu_items(p: { items?: MenuItem[] }): HTMLElement[];
|
||||
|
||||
// Modal
|
||||
function modal(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function modal_box(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function modal_action(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Navbar
|
||||
function navbar(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Option
|
||||
function option(p: { [key: string]: any }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Password
|
||||
function password(p: { label?: string; class?: string; value?: Signal<string> }): HTMLElement;
|
||||
|
||||
// Progress
|
||||
function progress(p: { class?: string; value?: number | Signal<number>; max?: number | string }): HTMLElement;
|
||||
|
||||
// Radial
|
||||
function radial(p: { class?: string; value?: number | Signal<number>; role?: string }): HTMLElement;
|
||||
|
||||
// Radio
|
||||
function radio(p: { class?: string; name?: string; checked?: boolean | Signal<boolean>; [key: string]: any }): HTMLElement;
|
||||
|
||||
// Range
|
||||
function range(p: { class?: string; min?: number; max?: number; value?: number | Signal<number>; oninput?: (e: Event) => void }): HTMLElement;
|
||||
|
||||
// Rating
|
||||
interface RatingProps {
|
||||
class?: string;
|
||||
count?: number;
|
||||
mask?: string;
|
||||
itemClass?: string;
|
||||
name?: string;
|
||||
value?: Signal<number> | number;
|
||||
offset?: number;
|
||||
onChange?: (i: number) => void;
|
||||
}
|
||||
function rating(p: RatingProps): HTMLElement;
|
||||
|
||||
// Search
|
||||
function search(p: { label?: string; class?: string; icon?: string; value?: Signal<string>; placeholder?: string }): HTMLElement;
|
||||
|
||||
// Select
|
||||
function select(p: { class?: string; [key: string]: any }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Stack
|
||||
function stack(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Stat
|
||||
function stat(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function stat_figure(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function stat_title(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function stat_value(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function stat_desc(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Steps
|
||||
function steps(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function step(p: { class?: string; dataContent?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Swap
|
||||
function swap(p: { class?: string; value?: Signal<boolean> | boolean }, ...c: any[]): HTMLElement;
|
||||
function swap_on(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function swap_off(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Table
|
||||
function table(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function table_head(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function table_body(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function table_foot(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function table_row(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function table_th(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function table_td(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Tabs
|
||||
function tabs(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
interface TabProps {
|
||||
class?: string;
|
||||
classContent?: string;
|
||||
name?: string;
|
||||
label?: string;
|
||||
content?: any;
|
||||
checked?: boolean | (() => boolean);
|
||||
tabs?: Signal<any[]> | ((v: any[]) => void);
|
||||
index?: number;
|
||||
closable?: boolean;
|
||||
onclick?: () => void;
|
||||
}
|
||||
function tab(p: TabProps): any[];
|
||||
|
||||
// Textarea
|
||||
function textarea(p: { class?: string; [key: string]: any }): HTMLElement;
|
||||
|
||||
// Textrotate
|
||||
function textrotate(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Theme
|
||||
function theme(p?: { value?: string; class?: string }): HTMLElement;
|
||||
|
||||
// Timeline
|
||||
function timeline(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function timeline_start(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function timeline_middle(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
function timeline_end(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Toggle
|
||||
function toggle(p: { class?: string; value?: string | Signal<string>; checked?: boolean | Signal<boolean>; [key: string]: any }): HTMLElement;
|
||||
|
||||
// Tooltip
|
||||
function tooltip(p: { class?: string; tip?: string }, ...c: any[]): HTMLElement;
|
||||
|
||||
// Validator
|
||||
function validator(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||
}
|
||||
}
|
||||
501
sigpro2.js
501
sigpro2.js
@@ -1,501 +0,0 @@
|
||||
// =====================
|
||||
// SECURITY
|
||||
// =====================
|
||||
const DANGEROUS_PROTOCOLS = /^(javascript|data|vbscript):/i;
|
||||
const DANGEROUS_ATTR = /^on/i;
|
||||
|
||||
const sanitizeUrl = (url) => {
|
||||
const str = String(url ?? "").trim().toLowerCase().replace(/\s+/g, "");
|
||||
return DANGEROUS_PROTOCOLS.test(str) ? "#" : str;
|
||||
};
|
||||
|
||||
const sanitizeAttr = (name, value) => {
|
||||
if (value == null) return null;
|
||||
if (DANGEROUS_ATTR.test(name)) return null;
|
||||
if (name === "srcdoc") return null;
|
||||
|
||||
const str = String(value);
|
||||
|
||||
if (name === "href" || name === "src") {
|
||||
return sanitizeUrl(str);
|
||||
}
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
// =====================
|
||||
// CORE
|
||||
// =====================
|
||||
let activeEffect = null;
|
||||
let currentOwner = null;
|
||||
const effectQueue = new Set();
|
||||
let isFlushing = false;
|
||||
const MOUNTED_NODES = new WeakMap();
|
||||
|
||||
const doc = document;
|
||||
const isArr = Array.isArray;
|
||||
const assign = Object.assign;
|
||||
const createEl = (t) => doc.createElement(t);
|
||||
const createText = (t) => doc.createTextNode(String(t ?? ""));
|
||||
const isFunc = (f) => typeof f === "function";
|
||||
const isObj = (o) => typeof o === "object" && o !== null;
|
||||
|
||||
const runWithContext = (effect, callback) => {
|
||||
const prev = activeEffect;
|
||||
activeEffect = effect;
|
||||
try { return callback(); }
|
||||
finally { activeEffect = prev; }
|
||||
};
|
||||
|
||||
const cleanupNode = (node) => {
|
||||
if (node._cleanups) {
|
||||
node._cleanups.forEach((d) => d());
|
||||
node._cleanups.clear();
|
||||
}
|
||||
node.childNodes?.forEach(cleanupNode);
|
||||
};
|
||||
|
||||
const flushEffects = () => {
|
||||
if (isFlushing) return;
|
||||
isFlushing = true;
|
||||
|
||||
while (effectQueue.size) {
|
||||
const list = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
|
||||
effectQueue.clear();
|
||||
for (const e of list) if (!e._deleted) e();
|
||||
}
|
||||
|
||||
isFlushing = false;
|
||||
};
|
||||
|
||||
const track = (subs) => {
|
||||
if (activeEffect && !activeEffect._deleted) {
|
||||
subs.add(activeEffect);
|
||||
activeEffect._deps.add(subs);
|
||||
}
|
||||
};
|
||||
|
||||
const trigger = (subs) => {
|
||||
subs.forEach((e) => {
|
||||
if (e === activeEffect || e._deleted) return;
|
||||
if (e._isComputed) {
|
||||
e.markDirty();
|
||||
if (e._subs) trigger(e._subs);
|
||||
} else {
|
||||
effectQueue.add(e);
|
||||
}
|
||||
});
|
||||
if (!isFlushing) queueMicrotask(flushEffects);
|
||||
};
|
||||
|
||||
// =====================
|
||||
// RENDER
|
||||
// =====================
|
||||
const Render = (fn) => {
|
||||
const cleanups = new Set();
|
||||
const prevOwner = currentOwner;
|
||||
const container = createEl("div");
|
||||
container.style.display = "contents";
|
||||
|
||||
currentOwner = { cleanups };
|
||||
|
||||
const process = (res) => {
|
||||
if (!res) return;
|
||||
if (res._isRuntime) {
|
||||
cleanups.add(res.destroy);
|
||||
container.appendChild(res.container);
|
||||
} else if (isArr(res)) {
|
||||
res.forEach(process);
|
||||
} else {
|
||||
container.appendChild(res instanceof Node ? res : createText(res));
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
process(fn({ onCleanup: (f) => cleanups.add(f) }));
|
||||
} finally {
|
||||
currentOwner = prevOwner;
|
||||
}
|
||||
|
||||
return {
|
||||
_isRuntime: true,
|
||||
container,
|
||||
destroy: () => {
|
||||
cleanups.forEach((f) => f());
|
||||
cleanupNode(container);
|
||||
container.remove();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// =====================
|
||||
// SIGNAL
|
||||
// =====================
|
||||
const $ = (init, key = null) => {
|
||||
const subs = new Set();
|
||||
|
||||
if (isFunc(init)) {
|
||||
let val, dirty = true;
|
||||
|
||||
const effect = () => {
|
||||
if (effect._deleted) return;
|
||||
|
||||
effect._deps.forEach(d => d.delete(effect));
|
||||
effect._deps.clear();
|
||||
|
||||
runWithContext(effect, () => {
|
||||
const next = init();
|
||||
if (!Object.is(val, next) || dirty) {
|
||||
val = next;
|
||||
dirty = false;
|
||||
trigger(subs);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
assign(effect, {
|
||||
_deps: new Set(),
|
||||
_isComputed: true,
|
||||
_subs: subs,
|
||||
_deleted: false,
|
||||
markDirty: () => dirty = true,
|
||||
stop: () => {
|
||||
effect._deleted = true;
|
||||
effect._deps.forEach(d => d.delete(effect));
|
||||
subs.clear();
|
||||
}
|
||||
});
|
||||
|
||||
if (currentOwner) currentOwner.cleanups.add(effect.stop);
|
||||
|
||||
return () => {
|
||||
if (dirty) effect();
|
||||
track(subs);
|
||||
return val;
|
||||
};
|
||||
}
|
||||
|
||||
let val = init;
|
||||
|
||||
if (key) {
|
||||
try {
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved != null) val = JSON.parse(saved);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return (...args) => {
|
||||
if (args.length) {
|
||||
const next = isFunc(args[0]) ? args[0](val) : args[0];
|
||||
if (!Object.is(val, next)) {
|
||||
val = next;
|
||||
if (key) localStorage.setItem(key, JSON.stringify(val));
|
||||
trigger(subs);
|
||||
}
|
||||
}
|
||||
track(subs);
|
||||
return val;
|
||||
};
|
||||
};
|
||||
|
||||
// =====================
|
||||
// REACTIVE OBJECT
|
||||
// =====================
|
||||
const $$ = (obj, cache = new WeakMap()) => {
|
||||
if (!isObj(obj)) return obj;
|
||||
if (cache.has(obj)) return cache.get(obj);
|
||||
|
||||
const subs = {};
|
||||
|
||||
const proxy = new Proxy(obj, {
|
||||
get(t, k) {
|
||||
track(subs[k] ??= new Set());
|
||||
const v = Reflect.get(t, k);
|
||||
return isObj(v) ? $$(v, cache) : v;
|
||||
},
|
||||
set(t, k, v) {
|
||||
if (Object.is(t[k], v)) return true;
|
||||
const ok = Reflect.set(t, k, v);
|
||||
subs[k] && trigger(subs[k]);
|
||||
return ok;
|
||||
}
|
||||
});
|
||||
|
||||
cache.set(obj, proxy);
|
||||
return proxy;
|
||||
};
|
||||
|
||||
// =====================
|
||||
// WATCH
|
||||
// =====================
|
||||
const Watch = (target, cb) => {
|
||||
const explicit = isArr(target);
|
||||
const fn = explicit ? cb : target;
|
||||
if (!isFunc(fn)) return () => {};
|
||||
|
||||
const owner = currentOwner;
|
||||
|
||||
const runner = () => {
|
||||
if (runner._deleted) return;
|
||||
|
||||
runner._deps.forEach(d => d.delete(runner));
|
||||
runner._deps.clear();
|
||||
|
||||
runner._cleanups.forEach(c => c());
|
||||
runner._cleanups.clear();
|
||||
|
||||
const prevOwner = currentOwner;
|
||||
runner.depth = activeEffect ? activeEffect.depth + 1 : 0;
|
||||
|
||||
runWithContext(runner, () => {
|
||||
currentOwner = { cleanups: runner._cleanups };
|
||||
|
||||
if (explicit) {
|
||||
runWithContext(null, fn);
|
||||
target.forEach(d => isFunc(d) && d());
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
|
||||
currentOwner = prevOwner;
|
||||
});
|
||||
};
|
||||
|
||||
assign(runner, {
|
||||
_deps: new Set(),
|
||||
_cleanups: new Set(),
|
||||
_deleted: false,
|
||||
stop: () => {
|
||||
if (runner._deleted) return;
|
||||
runner._deleted = true;
|
||||
effectQueue.delete(runner);
|
||||
runner._deps.forEach(d => d.delete(runner));
|
||||
runner._cleanups.forEach(c => c());
|
||||
if (owner) owner.cleanups.delete(runner.stop);
|
||||
}
|
||||
});
|
||||
|
||||
if (owner) owner.cleanups.add(runner.stop);
|
||||
runner();
|
||||
return runner.stop;
|
||||
};
|
||||
|
||||
// =====================
|
||||
// TAG (SECURE)
|
||||
// =====================
|
||||
const Tag = (tag, props = {}, children = []) => {
|
||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||
children = props; props = {};
|
||||
}
|
||||
|
||||
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
|
||||
const el = isSVG
|
||||
? doc.createElementNS("http://www.w3.org/2000/svg", tag)
|
||||
: createEl(tag);
|
||||
|
||||
el._cleanups = new Set();
|
||||
el.onUnmount = (fn) => el._cleanups.add(fn);
|
||||
|
||||
const booleanAttrs = ["disabled","checked","required","readonly","selected","multiple","autofocus"];
|
||||
|
||||
const setAttr = (k, v) => {
|
||||
const safe = sanitizeAttr(k, v);
|
||||
if (safe == null) return el.removeAttribute(k);
|
||||
|
||||
if (booleanAttrs.includes(k)) {
|
||||
el[k] = !!safe;
|
||||
safe ? el.setAttribute(k, "") : el.removeAttribute(k);
|
||||
} else {
|
||||
el.setAttribute(k, safe);
|
||||
}
|
||||
};
|
||||
|
||||
for (let [k, v] of Object.entries(props)) {
|
||||
if (k === "ref") {
|
||||
isFunc(v) ? v(el) : (v.current = el);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (k.startsWith("on")) {
|
||||
const evt = k.slice(2).toLowerCase().split(".")[0];
|
||||
el.addEventListener(evt, v);
|
||||
el._cleanups.add(() => el.removeEventListener(evt, v));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isFunc(v)) {
|
||||
el._cleanups.add(Watch(() => {
|
||||
const val = v();
|
||||
k === "class" ? (el.className = val || "") : setAttr(k, val);
|
||||
}));
|
||||
} else {
|
||||
setAttr(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
const append = (c) => {
|
||||
if (isArr(c)) return c.forEach(append);
|
||||
|
||||
if (isFunc(c)) {
|
||||
const marker = createText("");
|
||||
el.appendChild(marker);
|
||||
let nodes = [];
|
||||
|
||||
el._cleanups.add(Watch(() => {
|
||||
const res = c();
|
||||
const next = (isArr(res) ? res : [res]).map(n =>
|
||||
n?._isRuntime ? n.container : n instanceof Node ? n : createText(n)
|
||||
);
|
||||
|
||||
nodes.forEach(n => { cleanupNode(n); n.remove(); });
|
||||
next.forEach(n => marker.parentNode?.insertBefore(n, marker));
|
||||
nodes = next;
|
||||
}));
|
||||
} else {
|
||||
el.appendChild(c instanceof Node ? c : createText(c));
|
||||
}
|
||||
};
|
||||
|
||||
append(children);
|
||||
return el;
|
||||
};
|
||||
|
||||
// =====================
|
||||
// IF
|
||||
// =====================
|
||||
const If = (cond, a, b = null) => {
|
||||
const marker = createText("");
|
||||
const container = Tag("div", { style: "display:contents" }, [marker]);
|
||||
let view = null;
|
||||
|
||||
Watch(() => {
|
||||
const state = !!(isFunc(cond) ? cond() : cond);
|
||||
if (view) view.destroy();
|
||||
|
||||
const branch = state ? a : b;
|
||||
if (branch) {
|
||||
view = Render(() => isFunc(branch) ? branch() : branch);
|
||||
container.insertBefore(view.container, marker);
|
||||
}
|
||||
});
|
||||
|
||||
return container;
|
||||
};
|
||||
|
||||
// =====================
|
||||
// FOR (OPTIMIZED)
|
||||
// =====================
|
||||
const For = (source, renderFn, keyFn, tag = "div", props = { style: "display:contents" }) => {
|
||||
const marker = createText("");
|
||||
const container = Tag(tag, props, [marker]);
|
||||
let cache = new Map();
|
||||
|
||||
Watch(() => {
|
||||
const items = (isFunc(source) ? source() : source) || [];
|
||||
const next = new Map();
|
||||
const order = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const key = keyFn ? keyFn(item, i) : i;
|
||||
let view = cache.get(key);
|
||||
|
||||
if (!view) {
|
||||
const res = renderFn(item, i);
|
||||
view = res instanceof Node
|
||||
? { container: res, destroy: () => { cleanupNode(res); res.remove(); } }
|
||||
: Render(() => res);
|
||||
}
|
||||
|
||||
cache.delete(key);
|
||||
next.set(key, view);
|
||||
order.push(key);
|
||||
}
|
||||
|
||||
cache.forEach(v => v.destroy());
|
||||
|
||||
let anchor = marker;
|
||||
for (let i = order.length - 1; i >= 0; i--) {
|
||||
const v = next.get(order[i]);
|
||||
if (v.container.nextSibling !== anchor) {
|
||||
container.insertBefore(v.container, anchor);
|
||||
}
|
||||
anchor = v.container;
|
||||
}
|
||||
|
||||
cache = next;
|
||||
});
|
||||
|
||||
return container;
|
||||
};
|
||||
|
||||
// =====================
|
||||
// ROUTER
|
||||
// =====================
|
||||
const Router = (routes) => {
|
||||
const path = $(window.location.hash.replace(/^#/, "") || "/");
|
||||
|
||||
window.addEventListener("hashchange", () =>
|
||||
path(window.location.hash.replace(/^#/, "") || "/")
|
||||
);
|
||||
|
||||
const outlet = Tag("div");
|
||||
let view = null;
|
||||
|
||||
Watch([path], () => {
|
||||
const p = path();
|
||||
|
||||
const route = routes.find(r => {
|
||||
const rp = r.path.split("/").filter(Boolean);
|
||||
const pp = p.split("/").filter(Boolean);
|
||||
return rp.length === pp.length && rp.every((x, i) => x.startsWith(":") || x === pp[i]);
|
||||
}) || routes.find(r => r.path === "*");
|
||||
|
||||
if (route) {
|
||||
if (view) view.destroy();
|
||||
view = Render(() => route.component());
|
||||
outlet.appendChild(view.container);
|
||||
}
|
||||
});
|
||||
|
||||
return outlet;
|
||||
};
|
||||
|
||||
// =====================
|
||||
// MOUNT
|
||||
// =====================
|
||||
const Mount = (component, target) => {
|
||||
const el = typeof target === "string" ? doc.querySelector(target) : target;
|
||||
if (!el) return;
|
||||
|
||||
if (MOUNTED_NODES.has(el)) MOUNTED_NODES.get(el).destroy();
|
||||
|
||||
const instance = Render(isFunc(component) ? component : () => component);
|
||||
el.replaceChildren(instance.container);
|
||||
|
||||
MOUNTED_NODES.set(el, instance);
|
||||
return instance;
|
||||
};
|
||||
|
||||
// =====================
|
||||
// GLOBAL + TAG HELPERS
|
||||
// =====================
|
||||
const SigPro = { $, $$, Render, Watch, Tag, If, For, Router, Mount };
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
Object.assign(window, SigPro);
|
||||
|
||||
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(" ");
|
||||
|
||||
tags.forEach(tag => {
|
||||
const name = tag[0].toUpperCase() + tag.slice(1);
|
||||
if (!(name in window)) {
|
||||
window[name] = (p, c) => Tag(tag, p, c);
|
||||
}
|
||||
});
|
||||
|
||||
window.SigPro = Object.freeze(SigPro);
|
||||
}
|
||||
|
||||
export { $, $$, Render, Watch, Tag, If, For, Router, Mount };
|
||||
export default SigPro;
|
||||
443
sigpro_work.js
443
sigpro_work.js
@@ -1,443 +0,0 @@
|
||||
//sigpro
|
||||
let activeEffect = null;
|
||||
let currentOwner = null;
|
||||
const effectQueue = new Set();
|
||||
let isFlushing = false;
|
||||
const MOUNTED_NODES = new WeakMap();
|
||||
const reactiveCache = new WeakMap();
|
||||
const DANGEROUS_PROTOCOLS = /^(javascript|data|vbscript):/i;
|
||||
const sanitizeUrl = (url) => {
|
||||
const str = String(url ?? '').trim().toLowerCase();
|
||||
return DANGEROUS_PROTOCOLS.test(str) ? '#' : str;
|
||||
};
|
||||
const doc = document;
|
||||
const createEl = (t) => doc.createElement(t);
|
||||
const createText = (t) => doc.createTextNode(String(t ?? ""));
|
||||
|
||||
const flushEffects = () => {
|
||||
if (isFlushing) return;
|
||||
isFlushing = true;
|
||||
const runs = [...effectQueue];
|
||||
effectQueue.clear();
|
||||
runs.forEach((e) => { if (!e._deleted) e(); });
|
||||
isFlushing = false;
|
||||
};
|
||||
|
||||
const cleanupNode = (node) => {
|
||||
if (node._cleanups) {
|
||||
node._cleanups.forEach((d) => d());
|
||||
node._cleanups.clear();
|
||||
}
|
||||
node.childNodes?.forEach(cleanupNode);
|
||||
};
|
||||
|
||||
const untrack = (fn) => {
|
||||
const prev = activeEffect;
|
||||
activeEffect = null;
|
||||
try { return fn(); }
|
||||
finally { activeEffect = prev; }
|
||||
};
|
||||
|
||||
const $ = (val, key = null) => {
|
||||
const subs = new Set();
|
||||
if (key) {
|
||||
try {
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved != null) val = JSON.parse(saved);
|
||||
} catch {}
|
||||
}
|
||||
const sig = (...args) => {
|
||||
if (args.length) {
|
||||
const next = typeof args[0] === "function" ? untrack(() => args[0](val)) : args[0];
|
||||
if (!Object.is(val, next)) {
|
||||
val = next;
|
||||
if (key) localStorage.setItem(key, JSON.stringify(val));
|
||||
subs.forEach(e => effectQueue.add(e));
|
||||
if (!isFlushing) queueMicrotask(flushEffects);
|
||||
}
|
||||
} else if (activeEffect && !activeEffect._deleted) {
|
||||
subs.add(activeEffect);
|
||||
activeEffect._deps.add(subs);
|
||||
}
|
||||
return val;
|
||||
};
|
||||
sig._isSig = true;
|
||||
return sig;
|
||||
};
|
||||
|
||||
const Computed = (fn) => {
|
||||
const subs = new Set();
|
||||
let cached, dirty = true;
|
||||
|
||||
const runner = Effect(() => {
|
||||
if (!dirty) {
|
||||
dirty = true;
|
||||
subs.forEach(e => effectQueue.add(e));
|
||||
if (!isFlushing) queueMicrotask(flushEffects);
|
||||
}
|
||||
});
|
||||
|
||||
const sig = () => {
|
||||
if (dirty) {
|
||||
cached = fn();
|
||||
dirty = false;
|
||||
}
|
||||
if (activeEffect && !activeEffect._deleted) {
|
||||
subs.add(activeEffect);
|
||||
activeEffect._deps.add(subs);
|
||||
}
|
||||
return cached;
|
||||
};
|
||||
|
||||
sig._isSig = true;
|
||||
return sig;
|
||||
};
|
||||
|
||||
const Store = (obj) => {
|
||||
if (obj === null || typeof obj !== "object" || obj._isSig) return obj;
|
||||
if (reactiveCache.has(obj)) return reactiveCache.get(obj);
|
||||
|
||||
const subs = new Map();
|
||||
const proxy = new Proxy(obj, {
|
||||
get(target, key) {
|
||||
if (!subs.has(key)) subs.set(key, new Set());
|
||||
const keySubs = subs.get(key);
|
||||
|
||||
if (activeEffect && !activeEffect._deleted) {
|
||||
keySubs.add(activeEffect);
|
||||
activeEffect._deps.add(keySubs);
|
||||
}
|
||||
|
||||
const value = Reflect.get(target, key);
|
||||
return (typeof value === "object" && value !== null) ? Store(value) : value;
|
||||
},
|
||||
set(target, key, value) {
|
||||
const prev = Reflect.get(target, key);
|
||||
if (Object.is(prev, value)) return true;
|
||||
|
||||
const result = Reflect.set(target, key, value);
|
||||
const keySubs = subs.get(key);
|
||||
|
||||
if (keySubs) {
|
||||
keySubs.forEach(e => effectQueue.add(e));
|
||||
if (!isFlushing) queueMicrotask(flushEffects);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
reactiveCache.set(obj, proxy);
|
||||
return proxy;
|
||||
};
|
||||
|
||||
const Effect = (cb) => {
|
||||
if (typeof cb !== "function") return () => { };
|
||||
const owner = currentOwner;
|
||||
const runner = () => {
|
||||
if (runner._deleted) return;
|
||||
runner._deps.forEach(d => d.delete(runner));
|
||||
runner._deps.clear();
|
||||
runner._cleanups.forEach(c => c());
|
||||
runner._cleanups.clear();
|
||||
const prevOwner = currentOwner;
|
||||
const prevEffect = activeEffect;
|
||||
currentOwner = { cleanups: runner._cleanups, parent: owner };
|
||||
activeEffect = runner;
|
||||
try { cb(); }
|
||||
finally {
|
||||
currentOwner = prevOwner;
|
||||
activeEffect = prevEffect;
|
||||
}
|
||||
};
|
||||
runner._deps = new Set();
|
||||
runner._cleanups = new Set();
|
||||
runner._deleted = false;
|
||||
runner.stop = () => {
|
||||
if (runner._deleted) return;
|
||||
runner._deleted = true;
|
||||
effectQueue.delete(runner);
|
||||
runner._deps.forEach(d => d.delete(runner));
|
||||
runner._cleanups.forEach(c => c());
|
||||
if (owner) owner.cleanups.delete(runner.stop);
|
||||
};
|
||||
if (owner) owner.cleanups.add(runner.stop);
|
||||
runner();
|
||||
return runner.stop;
|
||||
};
|
||||
|
||||
const Watch = (source, cb) => {
|
||||
let oldValue;
|
||||
let first = true;
|
||||
return Effect(() => {
|
||||
const newValue = typeof source === "function" ? source() : source();
|
||||
|
||||
if (!first) {
|
||||
untrack(() => cb(newValue, oldValue));
|
||||
}
|
||||
|
||||
first = false;
|
||||
oldValue = newValue;
|
||||
});
|
||||
};
|
||||
|
||||
const Tag = (tag, props = {}, children = []) => {
|
||||
if (props instanceof Node || Array.isArray(props) || typeof props !== "object") {
|
||||
children = props; props = {};
|
||||
}
|
||||
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
|
||||
const el = isSVG
|
||||
? doc.createElementNS("http://www.w3.org/2000/svg", tag)
|
||||
: createEl(tag);
|
||||
el._cleanups = new Set();
|
||||
el.onUnmount = (fn) => el._cleanups.add(fn);
|
||||
const booleanAttrs = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
|
||||
|
||||
for (let [k, v] of Object.entries(props)) {
|
||||
if (k === "ref") {
|
||||
typeof v === "function" ? v(el) : (v.current = el);
|
||||
continue;
|
||||
}
|
||||
if (k.startsWith("on")) {
|
||||
const evt = k.slice(2).toLowerCase().split(".")[0];
|
||||
el.addEventListener(evt, v);
|
||||
el._cleanups.add(() => el.removeEventListener(evt, v));
|
||||
continue;
|
||||
}
|
||||
const setAttr = (val) => {
|
||||
if (k === "class") el.className = val || "";
|
||||
else if (booleanAttrs.includes(k)) {
|
||||
el[k] = !!val;
|
||||
val ? el.setAttribute(k, "") : el.removeAttribute(k);
|
||||
} else {
|
||||
const finalVal = (k === 'src' || k === 'href') ? sanitizeUrl(val) : val;
|
||||
el.setAttribute(k, finalVal);
|
||||
}
|
||||
};
|
||||
if (typeof v === "function") {
|
||||
el._cleanups.add(Effect(() => setAttr(v())));
|
||||
} else {
|
||||
setAttr(v);
|
||||
}
|
||||
}
|
||||
|
||||
const append = (c) => {
|
||||
if (Array.isArray(c)) return c.forEach(append);
|
||||
if (typeof c === "function") {
|
||||
const marker = createText("");
|
||||
el.appendChild(marker);
|
||||
let nodes = [];
|
||||
el._cleanups.add(Effect(() => {
|
||||
const res = c();
|
||||
const next = (Array.isArray(res) ? res : [res]).map(n =>
|
||||
n?._isRuntime ? n.container : n instanceof Node ? n : createText(n)
|
||||
);
|
||||
nodes.forEach(n => { cleanupNode(n); n.remove(); });
|
||||
next.forEach(n => marker.parentNode?.insertBefore(n, marker));
|
||||
nodes = next;
|
||||
}));
|
||||
} else {
|
||||
el.appendChild(c instanceof Node ? c : createText(c));
|
||||
}
|
||||
};
|
||||
append(children);
|
||||
return el;
|
||||
};
|
||||
|
||||
const Render = (fn) => {
|
||||
const cleanups = new Set();
|
||||
const prevOwner = currentOwner;
|
||||
const container = createEl("div");
|
||||
container.style.display = "contents";
|
||||
|
||||
currentOwner = {
|
||||
cleanups,
|
||||
parent: prevOwner,
|
||||
context: null
|
||||
};
|
||||
|
||||
const process = (res) => {
|
||||
if (!res) return;
|
||||
if (res._isRuntime) {
|
||||
cleanups.add(res.destroy);
|
||||
container.appendChild(res.container);
|
||||
} else if (Array.isArray(res)) {
|
||||
res.forEach(process);
|
||||
} else {
|
||||
container.appendChild(res instanceof Node ? res : createText(res));
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
process(fn({ onCleanup: (f) => cleanups.add(f) }));
|
||||
} finally {
|
||||
currentOwner = prevOwner;
|
||||
}
|
||||
|
||||
return {
|
||||
_isRuntime: true,
|
||||
container,
|
||||
destroy: () => {
|
||||
cleanups.forEach((f) => f());
|
||||
cleanupNode(container);
|
||||
container.remove();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const Share = (key, value) => {
|
||||
if (!currentOwner) return;
|
||||
if (!currentOwner.context) currentOwner.context = new Map();
|
||||
currentOwner.context.set(key, value);
|
||||
};
|
||||
|
||||
const Use = (key, defaultValue) => {
|
||||
let owner = currentOwner;
|
||||
while (owner) {
|
||||
if (owner.context && owner.context.has(key)) {
|
||||
return owner.context.get(key);
|
||||
}
|
||||
owner = owner.parent;
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
const If = (cond, a, b = null, options = {}) => {
|
||||
const marker = createText("");
|
||||
const container = Tag("div", { style: "display:contents" }, [marker]);
|
||||
let currentView = null;
|
||||
let lastState = null;
|
||||
|
||||
Effect(() => {
|
||||
const state = !!(typeof cond === "function" ? cond() : cond);
|
||||
if (state === lastState) return;
|
||||
lastState = state;
|
||||
|
||||
const branch = state ? a : b;
|
||||
const oldView = currentView;
|
||||
|
||||
if (oldView) {
|
||||
if (options.off) {
|
||||
options.off(oldView.container, () => oldView.destroy());
|
||||
} else {
|
||||
oldView.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
if (branch) {
|
||||
currentView = Render(() => typeof branch === "function" ? branch() : branch);
|
||||
const el = currentView.container;
|
||||
container.insertBefore(el, marker);
|
||||
|
||||
if (options.on) {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => options.on(el));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
currentView = null;
|
||||
}
|
||||
});
|
||||
|
||||
return container;
|
||||
};
|
||||
|
||||
const For = (source, renderFn, keyFn, tag = "div", props = { style: "display:contents" }) => {
|
||||
const marker = createText("");
|
||||
const container = Tag(tag, props, [marker]);
|
||||
let cache = new Map();
|
||||
Effect(() => {
|
||||
const items = (typeof source === "function" ? source() : source) || [];
|
||||
const next = new Map();
|
||||
const order = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const key = keyFn ? keyFn(item, i) : i;
|
||||
let view = cache.get(key);
|
||||
if (!view) {
|
||||
const res = renderFn(item, i);
|
||||
view = res instanceof Node
|
||||
? { container: res, destroy: () => { cleanupNode(res); res.remove(); } }
|
||||
: Render(() => res);
|
||||
}
|
||||
cache.delete(key);
|
||||
next.set(key, view);
|
||||
order.push(key);
|
||||
}
|
||||
cache.forEach(v => v.destroy());
|
||||
let anchor = marker;
|
||||
for (let i = order.length - 1; i >= 0; i--) {
|
||||
const v = next.get(order[i]);
|
||||
if (v.container.nextSibling !== anchor) {
|
||||
container.insertBefore(v.container, anchor);
|
||||
}
|
||||
anchor = v.container;
|
||||
}
|
||||
cache = next;
|
||||
});
|
||||
return container;
|
||||
};
|
||||
|
||||
const Router = (routes) => {
|
||||
const path = $(window.location.hash.replace(/^#/, "") || "/");
|
||||
window.addEventListener("hashchange", () =>
|
||||
path(window.location.hash.replace(/^#/, "") || "/")
|
||||
);
|
||||
const outlet = Tag("div");
|
||||
let view = null;
|
||||
Effect(() => {
|
||||
const p = path();
|
||||
const route = routes.find(r => {
|
||||
const rp = r.path.split("/").filter(Boolean);
|
||||
const pp = p.split("/").filter(Boolean);
|
||||
return rp.length === pp.length && rp.every((x, i) => x.startsWith(":") || x === pp[i]);
|
||||
}) || routes.find(r => r.path === "*");
|
||||
if (route) {
|
||||
const params = {};
|
||||
const rp = route.path.split("/").filter(Boolean);
|
||||
const pp = p.split("/").filter(Boolean);
|
||||
rp.forEach((part, i) => {
|
||||
if (part.startsWith(":")) params[part.slice(1)] = pp[i];
|
||||
});
|
||||
|
||||
Router.params(params);
|
||||
|
||||
if (view) view.destroy();
|
||||
view = Render(() => route.component(params));
|
||||
outlet.appendChild(view.container);
|
||||
}
|
||||
});
|
||||
return outlet;
|
||||
};
|
||||
|
||||
Router.params = $({});
|
||||
Router.to = (p) => (window.location.hash = p.replace(/^#?\/?/, "#/"));
|
||||
Router.back = () => window.history.back();
|
||||
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||
|
||||
const Mount = (component, target) => {
|
||||
const el = typeof target === "string" ? doc.querySelector(target) : target;
|
||||
if (!el) return;
|
||||
if (MOUNTED_NODES.has(el)) MOUNTED_NODES.get(el).destroy();
|
||||
const instance = Render(typeof component === "function" ? component : () => component);
|
||||
el.replaceChildren(instance.container);
|
||||
MOUNTED_NODES.set(el, instance);
|
||||
return instance;
|
||||
};
|
||||
|
||||
const SigPro = { $, Computed, Store, untrack, Render, Effect, Watch, Tag, If, For, Router, Mount, Share, Use };
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
Object.assign(window, SigPro);
|
||||
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(" ");
|
||||
tags.forEach(tag => {
|
||||
const name = tag[0].toUpperCase() + tag.slice(1);
|
||||
if (!(name in window)) {
|
||||
window[name] = (p, c) => Tag(tag, p, c);
|
||||
}
|
||||
});
|
||||
window.SigPro = Object.freeze(SigPro);
|
||||
}
|
||||
|
||||
export { $, Computed, Store, untrack, Render, Effect, Watch, Tag, If, For, Router, Mount, Share, Use };
|
||||
export { Tag as jsx, Tag as jsxs, Tag as Fragment };
|
||||
export default SigPro;
|
||||
272
sigwork.js
272
sigwork.js
@@ -1,272 +0,0 @@
|
||||
/*
|
||||
* SigPro
|
||||
*/
|
||||
|
||||
// --- 1. CORE REACTIVO ---
|
||||
let activeEffect = null, currentContext = null, isScheduled = false;
|
||||
const queue = new Set(), nodeContexts = new WeakMap(), reactiveCache = new WeakMap();
|
||||
|
||||
const tick = () => {
|
||||
while (queue.size) {
|
||||
const runs = [...queue];
|
||||
queue.clear();
|
||||
runs.forEach(fn => fn());
|
||||
}
|
||||
isScheduled = false;
|
||||
};
|
||||
|
||||
const track = (subs) => {
|
||||
if (activeEffect) {
|
||||
subs.add(activeEffect);
|
||||
activeEffect.deps.add(subs);
|
||||
}
|
||||
};
|
||||
|
||||
export const untrack = (fn) => {
|
||||
const prev = activeEffect;
|
||||
activeEffect = null;
|
||||
const res = fn();
|
||||
activeEffect = prev;
|
||||
return res;
|
||||
};
|
||||
|
||||
// --- 2. ESTADOS Y REACTIVIDAD ---
|
||||
export const Signal = (value) => {
|
||||
const subs = new Set();
|
||||
return {
|
||||
_isSig: true,
|
||||
get value() { track(subs); return value; },
|
||||
set value(v) {
|
||||
if (v === value) return;
|
||||
value = v;
|
||||
subs.forEach(f => queue.add(f));
|
||||
if (!isScheduled) { isScheduled = true; queueMicrotask(tick); }
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const Reactive = (obj) => {
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
if (reactiveCache.has(obj)) return reactiveCache.get(obj);
|
||||
const subs = {};
|
||||
const proxy = new Proxy(obj, {
|
||||
get(t, k) {
|
||||
track(subs[k] ??= new Set());
|
||||
const val = t[k];
|
||||
return (val && typeof val === 'object') ? Reactive(val) : val;
|
||||
},
|
||||
set(t, k, v) {
|
||||
if (t[k] === v) return true;
|
||||
t[k] = v;
|
||||
if (subs[k]) {
|
||||
subs[k].forEach(f => queue.add(f));
|
||||
if (!isScheduled) { isScheduled = true; queueMicrotask(tick); }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
reactiveCache.set(obj, proxy);
|
||||
return proxy;
|
||||
};
|
||||
|
||||
export const Computed = (fn) => {
|
||||
const c = Signal(fn());
|
||||
Effect(() => c.value = fn());
|
||||
return { get value() { return c.value; } };
|
||||
};
|
||||
|
||||
export const Watch = (source, cb) => {
|
||||
let old;
|
||||
return Effect(() => {
|
||||
const val = typeof source === 'function' ? source() : source.value;
|
||||
const prev = old;
|
||||
old = val;
|
||||
untrack(() => cb(val, prev));
|
||||
});
|
||||
};
|
||||
|
||||
export const Storage = (key, val) => {
|
||||
const saved = localStorage.getItem(key);
|
||||
const s = Signal(saved !== null ? JSON.parse(saved) : val);
|
||||
Effect(() => localStorage.setItem(key, JSON.stringify(s.value)));
|
||||
return s;
|
||||
};
|
||||
|
||||
// --- 3. EFECTOS Y CICLO DE VIDA ---
|
||||
export const Effect = (fn) => {
|
||||
let cleanup;
|
||||
const runner = () => {
|
||||
if (cleanup) cleanup();
|
||||
const prevEff = activeEffect;
|
||||
activeEffect = runner;
|
||||
cleanup = fn();
|
||||
activeEffect = prevEff;
|
||||
};
|
||||
runner.deps = new Set();
|
||||
if (activeEffect?.scopes) activeEffect.scopes.push(runner);
|
||||
else if (currentContext) currentContext.cleanups.push(runner);
|
||||
runner();
|
||||
return () => { if (cleanup) cleanup(); runner.deps.forEach(s => s.delete(runner)); };
|
||||
};
|
||||
|
||||
export const Scope = (fn) => {
|
||||
const scopes = [];
|
||||
const prev = activeEffect;
|
||||
activeEffect = { scopes };
|
||||
fn();
|
||||
activeEffect = prev;
|
||||
return () => scopes.forEach(s => s());
|
||||
};
|
||||
|
||||
export const onMount = f => currentContext?.mount.push(f);
|
||||
export const onUnmount = f => currentContext?.unmount.push(f);
|
||||
|
||||
// --- 4. RENDERER & DIFFING ---
|
||||
const isNode = v => v instanceof Node;
|
||||
const DANGEROUS = /^(javascript|data|vbscript):/i;
|
||||
const sanitize = v => DANGEROUS.test(String(v)) ? '#' : v;
|
||||
|
||||
export const destroy = async (node) => {
|
||||
if (!node) return;
|
||||
const ctx = nodeContexts.get(node);
|
||||
if (node.off) await node.off(node); // Soporte para tu If/Transition manual
|
||||
if (ctx) {
|
||||
ctx.unmount.forEach(f => f());
|
||||
ctx.cleanups.forEach(f => f());
|
||||
nodeContexts.delete(node);
|
||||
}
|
||||
const children = Array.from(node.childNodes);
|
||||
for (const child of children) await destroy(child);
|
||||
node.remove();
|
||||
};
|
||||
|
||||
const append = (parent, child) => {
|
||||
if (child == null) return;
|
||||
if (typeof child === 'function') {
|
||||
const anchor = document.createTextNode('');
|
||||
parent.appendChild(anchor);
|
||||
let nodes = [];
|
||||
Effect(async () => {
|
||||
const next = [child()].flat(Infinity)
|
||||
.map(n => typeof n === 'function' ? n() : n).flat(Infinity)
|
||||
.filter(n => n != null)
|
||||
.map(n => isNode(n) ? n : document.createTextNode(String(n)));
|
||||
|
||||
const nextSet = new Set(next);
|
||||
for (const n of nodes) { if (!nextSet.has(n)) await destroy(n); }
|
||||
|
||||
const oldIdxs = new Map(nodes.filter(n => nextSet.has(n)).map((n, i) => [n, i]));
|
||||
let p = oldIdxs.size - 1;
|
||||
|
||||
for (let i = next.length - 1; i >= 0; i--) {
|
||||
const node = next[i];
|
||||
const ref = next[i+1] || anchor;
|
||||
if (!oldIdxs.has(node) || nodes[p] !== node) {
|
||||
parent.insertBefore(node, ref);
|
||||
if (node.on) queueMicrotask(() => node.on(node));
|
||||
} else p--;
|
||||
}
|
||||
nodes = next;
|
||||
});
|
||||
} else {
|
||||
const n = isNode(child) ? child : document.createTextNode(String(child));
|
||||
parent.appendChild(n);
|
||||
if (n.on) queueMicrotask(() => n.on(n));
|
||||
}
|
||||
};
|
||||
|
||||
export const h = (tag, props = {}, ...children) => {
|
||||
props = props || {};
|
||||
const flat = children.flat(Infinity);
|
||||
if (!tag) return () => flat;
|
||||
if (tag === 'component') return () => h(props.is, props, ...flat);
|
||||
|
||||
if (typeof tag === 'function') {
|
||||
const ctx = { mount: [], unmount: [], cleanups: [], Share: {}, parent: currentContext };
|
||||
const prev = currentContext;
|
||||
currentContext = ctx;
|
||||
const el = tag(props, { children: flat });
|
||||
if (isNode(el)) nodeContexts.set(el, ctx);
|
||||
currentContext = prev;
|
||||
queueMicrotask(() => ctx.mount.forEach(f => f()));
|
||||
return el;
|
||||
}
|
||||
|
||||
const isSVG = /^(svg|path|circle|rect|g)$/i.test(tag);
|
||||
const el = isSVG ? document.createElementNS("http://www.w3.org/2000/svg", tag) : document.createElement(tag);
|
||||
|
||||
for (const k in props) {
|
||||
const v = props[k];
|
||||
if (k.startsWith('on')) el.addEventListener(k.slice(2).toLowerCase(), v);
|
||||
else if (k === 'ref') { if (typeof v === 'function') v(el); else if (v?._isSig) v.value = el; }
|
||||
else if (typeof v === 'function' || v?._isSig) {
|
||||
Effect(() => {
|
||||
const val = typeof v === 'function' ? v() : v.value;
|
||||
if (k === 'on' || k === 'off') { el[k] = val; return; }
|
||||
const attr = (k === 'href' || k === 'src') ? sanitize(val) : val;
|
||||
if (!isSVG && k in el) el[k] = attr; else el.setAttribute(k, attr);
|
||||
});
|
||||
} else {
|
||||
if (k === 'on' || k === 'off') { el[k] = v; continue; }
|
||||
const attr = (k === 'href' || k === 'src') ? sanitize(v) : v;
|
||||
if (!isSVG && k in el) el[k] = attr; else el.setAttribute(k, attr);
|
||||
}
|
||||
}
|
||||
flat.forEach(c => append(el, c));
|
||||
return el;
|
||||
};
|
||||
|
||||
// --- 5. COMPONENTES Y RUTAS ---
|
||||
export const If = (c, t, e) => () => c() ? t() : (e ? e() : null);
|
||||
|
||||
export const For = (l, k, r) => {
|
||||
let cache = new Map();
|
||||
return () => {
|
||||
const items = typeof l === 'function' ? l() : l.value;
|
||||
const next = new Map();
|
||||
const res = items.map((item, i) => {
|
||||
const id = k ? k(item, i) : (item.id || i);
|
||||
const n = cache.get(id) || r(item, i);
|
||||
next.set(id, n);
|
||||
return n;
|
||||
});
|
||||
cache = next; return res;
|
||||
};
|
||||
};
|
||||
|
||||
export const Router = (routes) => {
|
||||
const path = Signal(window.location.hash.replace(/^#/, '') || '/');
|
||||
window.onhashchange = () => path.value = window.location.hash.replace(/^#/, '') || '/';
|
||||
const outlet = h('div', { class: 'router-outlet' });
|
||||
let view = null;
|
||||
Effect(async () => {
|
||||
const r = routes.find(x => x.path === path.value) || routes.find(x => x.path === '*');
|
||||
if (view) await destroy(view);
|
||||
if (r) {
|
||||
view = r.component();
|
||||
outlet.appendChild(view);
|
||||
}
|
||||
});
|
||||
return outlet;
|
||||
};
|
||||
|
||||
export const Mount = (r, t) => {
|
||||
const el = typeof r === 'function' ? r() : r;
|
||||
const container = (typeof t === 'string' ? document.querySelector(t) : t);
|
||||
container.replaceChildren(el);
|
||||
return () => destroy(el);
|
||||
};
|
||||
|
||||
// --- 6. CONTEXTO (DEPENDENCY INJECTION) ---
|
||||
export const Share = (k, v) => { if (currentContext) currentContext.Share[k] = v; };
|
||||
export const Use = (k, d) => {
|
||||
let c = currentContext;
|
||||
while (c) { if (c.Share[k] !== undefined) return c.Share[k]; c = c.parent; }
|
||||
return d;
|
||||
};
|
||||
|
||||
export default {
|
||||
Signal, Reactive, Computed, Effect, Watch, Storage,
|
||||
untrack, Scope, h, If, For, Router, Mount,
|
||||
onMount, onUnmount, Share, Use
|
||||
};
|
||||
@@ -1,272 +0,0 @@
|
||||
/*
|
||||
* Sigwork v1.1 - [Sig]nal-based Frontend Frame[work]
|
||||
* Fixed: Memory Leaks, Fragment Lifecycle, and List Reconciler.
|
||||
*/
|
||||
|
||||
const isFn = (v) => typeof v === 'function';
|
||||
const isNode = (v) => v instanceof Node;
|
||||
|
||||
// --- Schedule System ---
|
||||
let isScheduled = false;
|
||||
const queue = new Set();
|
||||
const tick = () => {
|
||||
queue.forEach(fn => fn());
|
||||
queue.clear();
|
||||
isScheduled = false;
|
||||
}
|
||||
|
||||
// --- Effects System ---
|
||||
let activeEffect = null;
|
||||
export const effect = (fn, is_scope = false) => {
|
||||
let cleanup = null;
|
||||
const run = () => {
|
||||
stop(); // Limpia antes de re-ejecutar
|
||||
const prev = activeEffect;
|
||||
activeEffect = run;
|
||||
try { cleanup = fn(); } finally { activeEffect = prev; }
|
||||
}
|
||||
const stop = () => {
|
||||
run.e.forEach(subs => subs.delete(run));
|
||||
run.e.clear();
|
||||
if (isFn(cleanup)) cleanup();
|
||||
if (run.c) {
|
||||
run.c.forEach(s => s());
|
||||
run.c.length = 0;
|
||||
}
|
||||
}
|
||||
run.e = new Set();
|
||||
if (is_scope) run.c = [];
|
||||
run();
|
||||
if (activeEffect?.c) activeEffect.c.push(stop);
|
||||
return stop;
|
||||
}
|
||||
|
||||
export const scope = f => effect(f, true);
|
||||
|
||||
const track = (subs) => {
|
||||
if (activeEffect && !activeEffect.c) {
|
||||
subs.add(activeEffect);
|
||||
activeEffect.e.add(subs);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Signals Core ---
|
||||
export const signal = (value) => {
|
||||
const subs = new Set();
|
||||
return {
|
||||
get value() { track(subs); return value; },
|
||||
set value(newValue) {
|
||||
if (newValue === value) return;
|
||||
value = newValue;
|
||||
subs.forEach(fn => queue.add(fn));
|
||||
if (!isScheduled) { isScheduled = true; queueMicrotask(tick); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const untrack = (fn) => {
|
||||
const prev = activeEffect;
|
||||
activeEffect = null;
|
||||
const result = fn();
|
||||
activeEffect = prev;
|
||||
return result;
|
||||
}
|
||||
|
||||
export const computed = (fn) => {
|
||||
const sig = signal();
|
||||
effect(() => sig.value = fn());
|
||||
return { get value() { return sig.value; } };
|
||||
}
|
||||
|
||||
const reactiveCache = new WeakMap();
|
||||
export const reactive = (obj) => {
|
||||
if (reactiveCache.has(obj)) return reactiveCache.get(obj);
|
||||
const subs = {};
|
||||
const proxy = new Proxy(obj, {
|
||||
get(t, key) {
|
||||
track(subs[key] ??= new Set());
|
||||
const val = t[key];
|
||||
return (val && typeof val === 'object') ? reactive(val) : val;
|
||||
},
|
||||
set(t, key, val) {
|
||||
if (t[key] === val) return true;
|
||||
t[key] = val;
|
||||
if (subs[key]) {
|
||||
subs[key].forEach(fn => queue.add(fn));
|
||||
if (!isScheduled) { isScheduled = true; queueMicrotask(tick); }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
reactiveCache.set(obj, proxy);
|
||||
return proxy;
|
||||
}
|
||||
|
||||
export const watch = (source, cb) => {
|
||||
let first = true, oldValue;
|
||||
return effect(() => {
|
||||
const newValue = isFn(source) ? source() : source.value;
|
||||
if (!first) untrack(() => cb(newValue, oldValue));
|
||||
else first = false;
|
||||
oldValue = newValue;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Rendering System ---
|
||||
let context = null;
|
||||
export const onMount = (fn) => context?.m.push(fn);
|
||||
export const onUnmount = (fn) => context?.u.push(fn);
|
||||
export const provide = (key, value) => context && (context.p[key] = value);
|
||||
export const inject = (key, dft) => context && (key in context.p ? context.p[key] : dft);
|
||||
|
||||
const remove = (node) => {
|
||||
if (Array.isArray(node)) return node.forEach(remove);
|
||||
node.$s?.();
|
||||
if (node.$c) node.$c.u.forEach(f => f());
|
||||
const done = () => node.remove();
|
||||
node.$l ? node.$l(done) : done();
|
||||
}
|
||||
|
||||
const render = (fn, ...data) => {
|
||||
let node;
|
||||
const stop = effect(() => {
|
||||
node = fn(...data);
|
||||
if (isFn(node)) node = node();
|
||||
}, true);
|
||||
if (node) node.$s = stop;
|
||||
return node;
|
||||
}
|
||||
|
||||
export const h = (tag, props = {}, ...children) => {
|
||||
children = children.flat(Infinity);
|
||||
|
||||
if (isFn(tag)) {
|
||||
const prev = context;
|
||||
context = { m: [], u: [], p: { ...(prev?.p || {}) } };
|
||||
const ctx = context;
|
||||
let el;
|
||||
const stop = effect(() => {
|
||||
el = tag(props, { children, emit: (evt, ...args) => props[`on${evt[0].toUpperCase()}${evt.slice(1)}`]?.(...args) });
|
||||
return () => ctx.u.forEach(f => f());
|
||||
}, true);
|
||||
|
||||
// Normalización para asegurar que el nodo tenga metadatos
|
||||
const out = isNode(el) ? el : document.createTextNode(String(el));
|
||||
out.$c = ctx;
|
||||
out.$s = stop;
|
||||
context = prev;
|
||||
return out;
|
||||
}
|
||||
|
||||
if (!tag) return children;
|
||||
|
||||
const isSvg = tag === 'svg' || tag === 'path' || tag === 'circle';
|
||||
const el = isSvg ? document.createElementNS("http://www.w3.org/2000/svg", tag) : document.createElement(tag);
|
||||
|
||||
for (const key in props) {
|
||||
if (key.startsWith('on')) el.addEventListener(key.slice(2).toLowerCase(), props[key]);
|
||||
else if (key === "ref") isFn(props[key]) ? props[key](el) : props[key].value = el;
|
||||
else if (isFn(props[key])) effect(() => el[key] = props[key]());
|
||||
else el[key] = props[key];
|
||||
}
|
||||
|
||||
children.forEach(child => append(el, child));
|
||||
return el;
|
||||
}
|
||||
|
||||
const append = (parent, child) => {
|
||||
if (child == null) return;
|
||||
if (isFn(child)) {
|
||||
const anchor = document.createTextNode('');
|
||||
parent.appendChild(anchor);
|
||||
let nodes = [];
|
||||
effect(() => {
|
||||
const raw = [child()].flat(Infinity).filter(n => n != null);
|
||||
const newNodes = raw.map(n => isNode(n) ? n : document.createTextNode(String(n)));
|
||||
nodes.forEach(n => { if (!newNodes.includes(n)) remove(n); });
|
||||
newNodes.forEach((n, i) => {
|
||||
if (!nodes.includes(n)) {
|
||||
parent.insertBefore(n, newNodes[i+1] || anchor);
|
||||
if (n.$c) n.$c.m.forEach(f => f());
|
||||
}
|
||||
});
|
||||
nodes = newNodes;
|
||||
}, true);
|
||||
} else {
|
||||
parent.appendChild(isNode(child) ? child : document.createTextNode(String(child)));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helpers & Built-in ---
|
||||
export const If = (cond, renderFn, fallback = null) => {
|
||||
let cached, current;
|
||||
return () => {
|
||||
const show = !!cond();
|
||||
if (show !== current) {
|
||||
if (cached) remove(cached);
|
||||
cached = show ? render(renderFn) : (isFn(fallback) ? render(fallback) : fallback);
|
||||
current = show;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
export const For = (list, key, renderFn) => {
|
||||
let cache = new Map();
|
||||
return () => {
|
||||
const next = new Map();
|
||||
const items = isFn(list) ? list() : (list.value || list);
|
||||
const res = items.map((item, i) => {
|
||||
const id = isFn(key) ? key(item, i) : (key ? item[id] : item);
|
||||
let node = cache.get(id);
|
||||
if (!node) node = render(renderFn, item, i);
|
||||
next.set(id, node);
|
||||
return node;
|
||||
});
|
||||
cache.forEach((node, id) => { if (!next.has(id)) remove(node); });
|
||||
cache = next;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export const Component = ({ is, ...props }, { children }) => () => h(isFn(is) ? is() : is, props, children);
|
||||
|
||||
export const Transition = ({ enter: e, idle, leave: l }, { children: [c] }) => {
|
||||
const decorate = (el) => {
|
||||
if (!isNode(el)) return el;
|
||||
const addClass = c => c && el.classList.add(...c.split(' '));
|
||||
const removeClass = c => c && el.classList.remove(...c.split(' '));
|
||||
|
||||
if (e) {
|
||||
requestAnimationFrame(() => {
|
||||
addClass(e[1]);
|
||||
requestAnimationFrame(() => {
|
||||
addClass(e[0]); removeClass(e[1]); addClass(e[2]);
|
||||
el.addEventListener('transitionend', () => {
|
||||
removeClass(e[2]); removeClass(e[0]); addClass(idle);
|
||||
}, { once: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
if (l) {
|
||||
el.$l = (done) => {
|
||||
removeClass(idle); addClass(l[1]);
|
||||
requestAnimationFrame(() => {
|
||||
addClass(l[0]); removeClass(l[1]); addClass(l[2]);
|
||||
el.addEventListener('transitionend', () => {
|
||||
removeClass(l[2]); removeClass(l[0]); done();
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
return el;
|
||||
}
|
||||
return isFn(c) ? () => decorate(c()) : decorate(c);
|
||||
}
|
||||
|
||||
export default (target, root, props) => {
|
||||
const el = h(root, props);
|
||||
target.appendChild(el);
|
||||
if (el.$c) el.$c.m.forEach(f => f());
|
||||
return () => remove(el);
|
||||
}
|
||||
66843
src/grid-e/main.esm.mjs
Normal file
66843
src/grid-e/main.esm.mjs
Normal file
File diff suppressed because one or more lines are too long
131
src/grid-e/package.json
Normal file
131
src/grid-e/package.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"name": "ag-grid-enterprise",
|
||||
"version": "35.2.0",
|
||||
"description": "Advanced Data Grid / Data Table supporting Javascript / Typescript / React / Angular / Vue",
|
||||
"main": "./dist/package/main.cjs.js",
|
||||
"types": "./dist/types/src/main.d.ts",
|
||||
"module": "./main.esm.mjs",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/package/main.esm.mjs",
|
||||
"types": "./dist/types/src/main.d.ts",
|
||||
"require": "./dist/package/main.cjs.js",
|
||||
"default": "./dist/package/main.cjs.js"
|
||||
},
|
||||
"./styles/ag-grid-no-native-widgets.css": "./styles/ag-grid-no-native-widgets.css",
|
||||
"./styles/ag-grid-no-native-widgets.min.css": "./styles/ag-grid-no-native-widgets.min.css",
|
||||
"./styles/ag-grid.css": "./styles/ag-grid.css",
|
||||
"./styles/ag-grid.min.css": "./styles/ag-grid.min.css",
|
||||
"./styles/ag-theme-alpine-no-font.css": "./styles/ag-theme-alpine-no-font.css",
|
||||
"./styles/ag-theme-alpine-no-font.min.css": "./styles/ag-theme-alpine-no-font.min.css",
|
||||
"./styles/ag-theme-alpine.css": "./styles/ag-theme-alpine.css",
|
||||
"./styles/ag-theme-alpine.min.css": "./styles/ag-theme-alpine.min.css",
|
||||
"./styles/ag-theme-balham-no-font.css": "./styles/ag-theme-balham-no-font.css",
|
||||
"./styles/ag-theme-balham-no-font.min.css": "./styles/ag-theme-balham-no-font.min.css",
|
||||
"./styles/ag-theme-balham.css": "./styles/ag-theme-balham.css",
|
||||
"./styles/ag-theme-balham.min.css": "./styles/ag-theme-balham.min.css",
|
||||
"./styles/ag-theme-material-no-font.css": "./styles/ag-theme-material-no-font.css",
|
||||
"./styles/ag-theme-material-no-font.min.css": "./styles/ag-theme-material-no-font.min.css",
|
||||
"./styles/ag-theme-material.css": "./styles/ag-theme-material.css",
|
||||
"./styles/ag-theme-material.min.css": "./styles/ag-theme-material.min.css",
|
||||
"./styles/ag-theme-quartz-no-font.css": "./styles/ag-theme-quartz-no-font.css",
|
||||
"./styles/ag-theme-quartz-no-font.min.css": "./styles/ag-theme-quartz-no-font.min.css",
|
||||
"./styles/ag-theme-quartz.css": "./styles/ag-theme-quartz.css",
|
||||
"./styles/ag-theme-quartz.min.css": "./styles/ag-theme-quartz.min.css",
|
||||
"./styles/agGridAlpineFont.css": "./styles/agGridAlpineFont.css",
|
||||
"./styles/agGridAlpineFont.min.css": "./styles/agGridAlpineFont.min.css",
|
||||
"./styles/agGridBalhamFont.css": "./styles/agGridBalhamFont.css",
|
||||
"./styles/agGridBalhamFont.min.css": "./styles/agGridBalhamFont.min.css",
|
||||
"./styles/agGridClassicFont.css": "./styles/agGridClassicFont.css",
|
||||
"./styles/agGridClassicFont.min.css": "./styles/agGridClassicFont.min.css",
|
||||
"./styles/agGridMaterialFont.css": "./styles/agGridMaterialFont.css",
|
||||
"./styles/agGridMaterialFont.min.css": "./styles/agGridMaterialFont.min.css",
|
||||
"./styles/agGridQuartzFont.css": "./styles/agGridQuartzFont.css",
|
||||
"./styles/agGridQuartzFont.min.css": "./styles/agGridQuartzFont.min.css",
|
||||
"./styles": "./styles/_index.scss"
|
||||
},
|
||||
"sideEffects": [
|
||||
"./styles/ag-grid-no-native-widgets.css",
|
||||
"./styles/ag-grid-no-native-widgets.min.css",
|
||||
"./styles/ag-grid.css",
|
||||
"./styles/ag-grid.min.css",
|
||||
"./styles/ag-theme-alpine-no-font.css",
|
||||
"./styles/ag-theme-alpine-no-font.min.css",
|
||||
"./styles/ag-theme-alpine.css",
|
||||
"./styles/ag-theme-alpine.min.css",
|
||||
"./styles/ag-theme-balham-no-font.css",
|
||||
"./styles/ag-theme-balham-no-font.min.css",
|
||||
"./styles/ag-theme-balham.css",
|
||||
"./styles/ag-theme-balham.min.css",
|
||||
"./styles/ag-theme-material-no-font.css",
|
||||
"./styles/ag-theme-material-no-font.min.css",
|
||||
"./styles/ag-theme-material.css",
|
||||
"./styles/ag-theme-material.min.css",
|
||||
"./styles/ag-theme-quartz-no-font.css",
|
||||
"./styles/ag-theme-quartz-no-font.min.css",
|
||||
"./styles/ag-theme-quartz.css",
|
||||
"./styles/ag-theme-quartz.min.css",
|
||||
"./styles/agGridAlpineFont.css",
|
||||
"./styles/agGridAlpineFont.min.css",
|
||||
"./styles/agGridBalhamFont.css",
|
||||
"./styles/agGridBalhamFont.min.css",
|
||||
"./styles/agGridClassicFont.css",
|
||||
"./styles/agGridClassicFont.min.css",
|
||||
"./styles/agGridMaterialFont.css",
|
||||
"./styles/agGridMaterialFont.min.css",
|
||||
"./styles/agGridQuartzFont.css",
|
||||
"./styles/agGridQuartzFont.min.css"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ag-grid/ag-grid.git"
|
||||
},
|
||||
"keywords": [
|
||||
"ag",
|
||||
"ag-grid",
|
||||
"datagrid",
|
||||
"data-grid",
|
||||
"datatable",
|
||||
"data-table",
|
||||
"grid",
|
||||
"table",
|
||||
"react",
|
||||
"table",
|
||||
"angular",
|
||||
"angular-component",
|
||||
"react",
|
||||
"react-component",
|
||||
"reactjs",
|
||||
"vue",
|
||||
"vuejs"
|
||||
],
|
||||
"author": "Sean Landsman <sean@thelandsmans.com>",
|
||||
"license": "Commercial",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ag-grid/ag-grid/issues"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie >= 0",
|
||||
"not ie_mob >= 0",
|
||||
"not blackberry > 0"
|
||||
],
|
||||
"homepage": "https://www.ag-grid.com/",
|
||||
"dependencies": {
|
||||
"ag-grid-community": "35.2.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"ag-charts-community": "13.2.0",
|
||||
"ag-charts-enterprise": "13.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ag-charts-community": "13.2.0",
|
||||
"ag-charts-enterprise": "13.2.0",
|
||||
"@types/jest": "^29.5.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-canvas-mock": "2.5.2",
|
||||
"canvas": "^3.2.1"
|
||||
}
|
||||
}
|
||||
147
src/sigpro.convert.js
Normal file
147
src/sigpro.convert.js
Normal file
@@ -0,0 +1,147 @@
|
||||
/// <reference path="../sigpro.d.ts" />
|
||||
|
||||
var { $ } = window.SigPro;
|
||||
|
||||
function html2sigpro(h, mode = "tags") {
|
||||
const B = new Set(["allowfullscreen", "async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "formnovalidate", "hidden", "ismap", "itemscope", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected", "truespeed"]);
|
||||
const esc = v => v.replace(/"/g, '\\"');
|
||||
|
||||
const bP = el => {
|
||||
let a = [...el.attributes].map(({ name: n, value: v }) =>
|
||||
/^on/i.test(n) ? `${n}: (e) => { ${v.replace(/\s+/g, " ").trim()} }` :
|
||||
(B.has(n.toLowerCase()) && (!v || v == n)) ? `${n}: true` : `${n}: "${esc(v)}"`
|
||||
);
|
||||
return a.length ? `{ ${a.join(", ")} }` : "";
|
||||
};
|
||||
|
||||
const cN = (n, d = 0) => {
|
||||
let s = " ".repeat(d);
|
||||
if (n.nodeType == 3) {
|
||||
let t = n.textContent;
|
||||
return t.trim() ? `${s}"${esc(t)}"` : "";
|
||||
}
|
||||
if (n.nodeType == 1) {
|
||||
let tag = n.tagName.toLowerCase();
|
||||
let props = bP(n);
|
||||
let prefix = mode === "core" ? `h('${tag}'` : tag;
|
||||
|
||||
let children = [...n.childNodes].map(i => cN(i, d + 1)).filter(Boolean);
|
||||
const hasProps = !!props;
|
||||
|
||||
if (mode === "core") {
|
||||
if (!children.length) return hasProps ? `${s}${prefix}, ${props})` : `${s}${prefix})`;
|
||||
if (children.length === 1 && !children[0].includes("\n"))
|
||||
return hasProps ? `${s}${prefix}, ${props}, ${children[0].trim()})` : `${s}${prefix}, ${children[0].trim()})`;
|
||||
return hasProps ? `${s}${prefix}, ${props}, [\n${children.join(",\n")}\n${s}])` : `${s}${prefix}, [\n${children.join(",\n")}\n${s}])`;
|
||||
} else {
|
||||
if (!children.length) return hasProps ? `${s}${prefix}(${props})` : `${s}${prefix}`;
|
||||
if (children.length === 1 && !children[0].includes("\n"))
|
||||
return hasProps ? `${s}${prefix}(${props}, ${children[0].trim()})` : `${s}${prefix}(${children[0].trim()})`;
|
||||
return hasProps ? `${s}${prefix}(${props}, [\n${children.join(",\n")}\n${s}])` : `${s}${prefix}([\n${children.join(",\n")}\n${s}])`;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const r = [...new DOMParser().parseFromString(h, "text/html").body.childNodes].map(n => cN(n)).filter(Boolean);
|
||||
return r.length == 1 ? r[0].trim() : `[\n${r.join(",\n")}\n]`;
|
||||
}
|
||||
|
||||
const converter = () => {
|
||||
const inH = $("");
|
||||
const outS = $("");
|
||||
const mode = $("tags");
|
||||
const previewHtml = $("");
|
||||
|
||||
const cnv = () => {
|
||||
try {
|
||||
outS(html2sigpro(inH(), mode()));
|
||||
} catch (e) {
|
||||
outS("Error: " + e.message);
|
||||
}
|
||||
previewHtml(inH());
|
||||
};
|
||||
|
||||
const clearAll = () => {
|
||||
inH("");
|
||||
outS("");
|
||||
mode("tags");
|
||||
previewHtml("");
|
||||
};
|
||||
|
||||
const txS = "width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical";
|
||||
const btS = "padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px";
|
||||
|
||||
return div({ style: "margin:20px auto;font-family:sans-serif" }, [
|
||||
h1("HTML → SigPro"),
|
||||
div({ style: "margin-bottom:10px" }, [
|
||||
div({ style: "display:flex;gap:20px;flex-wrap:wrap;margin-top:5px" }, [
|
||||
label({ style: "display:flex;align-items:center;gap:6px" }, [
|
||||
"Core",
|
||||
input({ type: "radio", name: "mode", value: "core", checked: mode() === "core", onchange: e => { if (e.target.checked) { mode("core"); cnv(); } } }),
|
||||
span("core — h('tag', props, ...)")
|
||||
]),
|
||||
label({ style: "display:flex;align-items:center;gap:6px" }, [
|
||||
"Tags",
|
||||
input({ type: "radio", name: "mode", value: "tags", checked: mode() === "tags", onchange: e => { if (e.target.checked) { mode("tags"); cnv(); } } }),
|
||||
span("tags — tag({ props }, ...)")
|
||||
])
|
||||
])
|
||||
]),
|
||||
div({ style: "margin-top:15px;display:flex;gap:10px" }, [
|
||||
button({ style: btS + ";background:#3b82f6;color:#fff", onclick: cnv }, "Convert"),
|
||||
button({ style: btS + ";background:#d1d5db", onclick: clearAll }, "Clear")
|
||||
]),
|
||||
div({ style: "display:grid;grid-template-columns:1fr;gap:15px;margin-top:15px;width:100%" }, [
|
||||
div({ style: "border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column" }, [
|
||||
label({ style: "font-weight:bold;margin-bottom:8px" }, "HTML Input"),
|
||||
textarea({
|
||||
style: txS,
|
||||
placeholder: "Paste your HTML here...",
|
||||
value: inH,
|
||||
oninput: e => { inH(e.target.value); cnv(); }
|
||||
})
|
||||
]),
|
||||
div({ style: "border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column" }, [
|
||||
div({ style: "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px" }, [
|
||||
span({ style: "font-weight:bold" }, "SigPro Output"),
|
||||
button({
|
||||
style: "padding:4px 8px;background:#10b981;color:white;border:none;border-radius:4px;cursor:pointer;font-size:12px",
|
||||
onclick: () => { navigator.clipboard.writeText(outS()); alert("Copied!"); }
|
||||
}, "Copy")
|
||||
]),
|
||||
textarea({ style: txS + ";background:#f9fafb", readonly: true, value: outS, placeholder: "Converted code will appear here..." })
|
||||
]),
|
||||
div({ style: "border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column" }, [
|
||||
label({ style: "font-weight:bold;margin-bottom:8px" }, "Live Preview"),
|
||||
iframe({
|
||||
style: "width:100%;height:200px;border:1px solid #e2e8f0;border-radius:4px;background:white;",
|
||||
srcdoc: () => {
|
||||
const html = previewHtml() || "";
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<style>
|
||||
body { padding: 10px; margin: 0; font-family: sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${html}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
},
|
||||
sandbox: "allow-same-origin allow-scripts allow-popups allow-forms allow-modals"
|
||||
})
|
||||
])
|
||||
]),
|
||||
]);
|
||||
};
|
||||
|
||||
window.html2sigpro = html2sigpro;
|
||||
window.converter = converter;
|
||||
18
src/sigpro.db.js
Normal file
18
src/sigpro.db.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export const db = async (url, data = {}, loading = null) => {
|
||||
if (loading) loading(true);
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
throw new Error(`Error ${res.status}: ${errorText}`);
|
||||
}
|
||||
return await res.json();
|
||||
} finally {
|
||||
if (loading) loading(false);
|
||||
}
|
||||
};
|
||||
21
src/sigpro.editor.js
Normal file
21
src/sigpro.editor.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/// <reference path="../sigpro.d.ts" />
|
||||
|
||||
const { $, isF } = window.SigPro
|
||||
|
||||
export const Editor = p => {
|
||||
let { value: v, class: x } = p, i = $(0), s = $(""), c = $(0), t = $(0), o = $(0), S = () => window.getSelection().rangeCount ? window.getSelection().getRangeAt(0) : 0, R, r, E = "😀 😊 😉 🧐 😮 🤔 😅 😂 😍 😘 🥰 👍 👎 👌 🤝 🤞 👋 👏 🙌 🙏 💪 ☝️ 👇 👈 👉 🖕 ✅ ⚠️ 🚀 📢 ✉️ ❤️".split(" "),
|
||||
U = () => { t(t() + 1); R && c(R.innerText.length) }, N = () => { if (R) { let H = R.innerHTML; isF(v) ? v(H) : p.onchange?.(H); U() } },
|
||||
X = (m, k = null) => { R?.focus(); if (r) { let w = window.getSelection(); w.removeAllRanges(); w.addRange(r) } document.execCommand(m, 0, k); r = 0; N() },
|
||||
q = (k, Z = null) => { t(); if (!R || i()) return 0; try { if (k == 'formatBlock') { let n = window.getSelection().getRangeAt(0).commonAncestorContainer; while (n && n !== R) { if (n.tagName === Z) return 1; n = n.parentNode } return 0 } return document.queryCommandState(k) } catch (e) { return 0 } },
|
||||
hU = f => { if (f) { let d = new FileReader; d.onload = e => { let I = f.type.startsWith('image/'), z = e.target.result; X("insertHTML", I ? div({ style: "display:inline-block;resize:both;overflow:hidden;vertical-align:bottom;width:200px;border:1px dashed #ccc;padding:2px;cursor:pointer", class: "resizable-img-container" }, [img({ src: z, style: "width:100%;height:100%;object-fit:contain;pointer-events:none" })]) : a({ href: z, download: f.name, contenteditable: "false", style: "display:inline-flex;align-items:center;gap:5px;padding:4px 8px;border:1px solid #ccc;border-radius:4px;background:#f9f9f9;text-decoration:none;color:#333;font-size:12px;margin:2px;cursor:pointer" }, [span({ class: "icon-[lucide--paperclip] w-3 h-3" }), f.name])) }; d.readAsDataURL(f) } },
|
||||
B = (I, m, k) => button({ type: "button", class: () => `btn btn-ghost btn-xs ${q(m, k) ? 'btn-active bg-primary/20' : ''}`, onclick: () => typeof m == 'function' ? m() : X(m, k) }, [span({ class: `icon-[lucide--${I}]` })]);
|
||||
|
||||
return div({ class: `border border-base-300 rounded-box bg-base-100 overflow-hidden flex flex-col ${x||""}` }, [
|
||||
div({ class: "flex flex-wrap items-center gap-1 p-2 border-b border-base-300 bg-base-200 sticky top-0 z-20" }, [
|
||||
div({ class: "flex flex-wrap gap-1 flex-1" }, [B("bold", "bold"), B("italic", "italic"), B("underline", "underline"), input({ type: "color", class: "w-5 h-5 p-0 bg-transparent cursor-pointer", oninput: e => X("foreColor", e.target.value) }), span({ class: "w-px h-5 bg-base-300 mx-1" }), B("align-left", "justifyLeft"), B("align-center", "justifyCenter"), B("align-right", "justifyRight"), span({ class: "w-px h-5 bg-base-300 mx-1" }), B("list", "insertUnorderedList"), B("list-ordered", "insertOrderedList"), B("indent-decrease", "outdent"), B("indent-increase", "indent"), B("quote", () => X("formatBlock", q('formatBlock', 'BLOCKQUOTE') ? 'P' : 'BLOCKQUOTE'), 'BLOCKQUOTE'), span({ class: "w-px h-5 bg-base-300 mx-1" }), B("link", () => { let u = prompt('URL:'); u && X("createLink", u) }), B("paperclip", () => { let I = document.createElement('input'); I.type = 'file'; I.onchange = e => hU(e.target.files[0]); I.click() }), div({ class: "relative" }, [B("smile", () => { r = S(); o(!o()) }), div({ class: "absolute top-full left-0 mt-1 p-2 bg-base-100 border shadow-xl rounded-box w-52 z-50 flex flex-wrap gap-1", style: () => o() ? "" : "display:none" }, E.map(e => span({ class: "cursor-pointer p-1 text-lg", onclick: () => { X("insertText", e); o(0) } }, e)))]), B("undo-2", "undo"), B("redo-2", "redo")]), B("code-2", () => { if (!i()) s(R?.innerHTML || ""); else if (R) { R.innerHTML = s(); N() } i(!i()) })]),
|
||||
div({ class: "relative flex-1 flex flex-col", onclick: () => o(0) }, [
|
||||
div({ ref: l => { if (l && !R) { R = l; l.innerHTML = (isF(v) ? v() : v) || ""; document.execCommand("defaultParagraphSeparator", 0, "br"); l.onclick = e => { let c = e.target.closest('.resizable-img-container'); if (c) { let I = c.querySelector('img'); I && (k => { let O = document.createElement('div'); O.style = "position:fixed;top:0;left:0;width:100%;height:100%;background:#000e;z-index:9999;display:flex;align-items:center;justify-content:center;cursor:zoom-out"; O.onclick = () => O.remove(); let M = document.createElement('img'); M.src = k; M.style = "max-width:95%;max-height:95%"; O.appendChild(M); document.body.appendChild(O) })(I.src) } } } }, style: () => `min-height:22rem;${i() ? 'display:none' : ''}`, class: "p-4 outline-none text-base-content leading-relaxed [&>div]:m-0 [&>p]:m-0 [&>div]:min-h-[1em] [&_.resizable-img-container]:hover:border-primary [&_blockquote]:border-l-4 [&_blockquote]:border-base-300 [&_blockquote]:pl-4 [&_blockquote]:italic [&_ul]:list-disc [&_ul]:pl-8 [&_ol]:list-decimal [&_ol]:pl-8", contenteditable: "true", oninput: N, onkeydown: e => e.key === 'Tab' && (e.preventDefault(), X("indent")), onkeyup: () => { U(); r = S() }, onmouseup: N, onpaste: e => { e.preventDefault(); X('insertText', e.clipboardData.getData('text/plain')) }, ondrop: e => { e.preventDefault(); hU(e.dataTransfer.files[0]) }, ondragover: e => e.preventDefault() }),
|
||||
textarea({ class: "w-full flex-1 min-h-[22rem] p-4 font-mono text-sm bg-base-200 border-0", style: () => i() ? '' : 'display:none', value: s, oninput: e => { s(e.target.value); if (R) R.innerHTML = e.target.value; p.onchange?.(e.target.value) } })]),
|
||||
div({ class: "px-3 py-1 border-t text-[10px] text-right opacity-60" }, [span(() => c())])
|
||||
])
|
||||
}
|
||||
207
src/sigpro.grid.js
Normal file
207
src/sigpro.grid.js
Normal file
@@ -0,0 +1,207 @@
|
||||
/// <reference path="../sigpro.d.ts" />
|
||||
|
||||
const { h, watch, onUnmount } = window.SigPro
|
||||
|
||||
import {
|
||||
ModuleRegistry,
|
||||
ValidationModule,
|
||||
ColumnAutoSizeModule,
|
||||
CellStyleModule,
|
||||
QuickFilterModule,
|
||||
RowSelectionModule,
|
||||
TextEditorModule,
|
||||
ClientSideRowModelModule,
|
||||
themeQuartz,
|
||||
createGrid,
|
||||
NumberFilterModule,
|
||||
TextFilterModule,
|
||||
DateFilterModule
|
||||
} from "ag-grid-community";
|
||||
import {
|
||||
MultiFilterModule,
|
||||
SetFilterModule,
|
||||
CellSelectionModule,
|
||||
PivotModule,
|
||||
MasterDetailModule,
|
||||
SideBarModule,
|
||||
ColumnsToolPanelModule,
|
||||
ColumnMenuModule,
|
||||
StatusBarModule,
|
||||
ExcelExportModule,
|
||||
ClipboardModule,
|
||||
ContextMenuModule
|
||||
} from "./grid-e";
|
||||
|
||||
ModuleRegistry.registerModules([
|
||||
ValidationModule,
|
||||
ColumnAutoSizeModule,
|
||||
CellStyleModule,
|
||||
QuickFilterModule,
|
||||
RowSelectionModule,
|
||||
TextEditorModule,
|
||||
ClientSideRowModelModule,
|
||||
MultiFilterModule,
|
||||
CellSelectionModule,
|
||||
PivotModule,
|
||||
MasterDetailModule,
|
||||
SideBarModule,
|
||||
ColumnsToolPanelModule,
|
||||
ColumnMenuModule,
|
||||
StatusBarModule,
|
||||
ExcelExportModule,
|
||||
ClipboardModule,
|
||||
NumberFilterModule,
|
||||
TextFilterModule,
|
||||
SetFilterModule,
|
||||
DateFilterModule,
|
||||
ContextMenuModule
|
||||
]);
|
||||
|
||||
const Grid = (props) => {
|
||||
const { data, options, api, on, class: className, style = "height: 100%; width: 100%", dark } = props;
|
||||
let gridApi = null;
|
||||
let cleanupFn = null;
|
||||
|
||||
const getDark = () =>
|
||||
dark !== undefined
|
||||
? (typeof dark === 'function' ? dark() : dark)
|
||||
: document.documentElement.getAttribute('data-theme') === 'dark' ||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
const getTheme = () => {
|
||||
const isDark = getDark();
|
||||
|
||||
if (isDark) {
|
||||
return themeQuartz.withParams({
|
||||
headerFontSize: 14,
|
||||
headerVerticalPaddingScale: 0.4,
|
||||
rowVerticalPaddingScale: 0.4,
|
||||
backgroundColor: "#1d1d1d",
|
||||
foregroundColor: "#ffffff",
|
||||
headerBackgroundColor: "#2a2a2a",
|
||||
headerForegroundColor: "#ffffff",
|
||||
oddRowBackgroundColor: "#262626",
|
||||
borderColor: "#404040",
|
||||
browserColorScheme: "dark"
|
||||
});
|
||||
}
|
||||
|
||||
return themeQuartz.withParams({
|
||||
browserColorScheme: "light",
|
||||
headerFontSize: 14,
|
||||
headerVerticalPaddingScale: 0.4,
|
||||
rowVerticalPaddingScale: 0.4
|
||||
});
|
||||
};
|
||||
|
||||
const initGrid = (container) => {
|
||||
if (cleanupFn) {
|
||||
cleanupFn();
|
||||
cleanupFn = null;
|
||||
}
|
||||
if (gridApi && !gridApi.isDestroyed()) {
|
||||
gridApi.destroy();
|
||||
if (api) api.current = null;
|
||||
gridApi = null;
|
||||
}
|
||||
|
||||
if (!container) return;
|
||||
|
||||
const initialData = typeof data === "function" ? data() : data;
|
||||
const initialOptions = typeof options === "function" ? options() : options;
|
||||
|
||||
const commonEvents = [
|
||||
'onFilterChanged', 'onModelUpdated', 'onGridSizeChanged',
|
||||
'onFirstDataRendered', 'onRowValueChanged', 'onSelectionChanged',
|
||||
'onCellClicked', 'onCellDoubleClicked', 'onCellValueChanged',
|
||||
'onRowClicked', 'onSortChanged', 'onContextMenu',
|
||||
'onColumnResized', 'onColumnMoved', 'onRowDataUpdated',
|
||||
'onCellEditingStarted', 'onCellEditingStopped',
|
||||
'onPaginationChanged', 'onBodyScroll'
|
||||
];
|
||||
|
||||
const eventHandlers = {};
|
||||
commonEvents.forEach(eventName => {
|
||||
if (on?.[eventName]) {
|
||||
eventHandlers[eventName] = (params) => on[eventName](params);
|
||||
}
|
||||
});
|
||||
|
||||
const gridOptions = {
|
||||
...initialOptions,
|
||||
theme: getTheme(),
|
||||
rowData: initialData || [],
|
||||
onGridReady: (params) => {
|
||||
gridApi = params.api;
|
||||
if (api) api.current = gridApi;
|
||||
if (on?.onGridReady) on.onGridReady(params);
|
||||
|
||||
if (initialOptions?.autoSizeColumns) {
|
||||
params.api.autoSizeAllColumns();
|
||||
}
|
||||
},
|
||||
...eventHandlers
|
||||
};
|
||||
|
||||
gridApi = createGrid(container, gridOptions);
|
||||
|
||||
const stopData = watch(() => {
|
||||
if (!gridApi || gridApi.isDestroyed()) return;
|
||||
const newData = typeof data === "function" ? data() : data;
|
||||
if (Array.isArray(newData)) {
|
||||
const currentData = gridApi.getGridOption("rowData");
|
||||
if (newData !== currentData) {
|
||||
gridApi.setGridOption("rowData", newData);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const stopTheme = watch(() => {
|
||||
if (!gridApi || gridApi.isDestroyed()) return;
|
||||
getDark();
|
||||
const newTheme = getTheme();
|
||||
const currentTheme = gridApi.getGridOption("theme");
|
||||
if (JSON.stringify(newTheme) !== JSON.stringify(currentTheme)) {
|
||||
gridApi.setGridOption("theme", newTheme);
|
||||
}
|
||||
});
|
||||
|
||||
const stopOptions = watch(() => {
|
||||
if (!gridApi || gridApi.isDestroyed() || !options) return;
|
||||
const newOptions = typeof options === "function" ? options() : options;
|
||||
if (newOptions) {
|
||||
Object.entries(newOptions).forEach(([key, val]) => {
|
||||
try {
|
||||
gridApi.setGridOption(key, val);
|
||||
} catch (e) { }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
cleanupFn = () => {
|
||||
stopData();
|
||||
stopTheme();
|
||||
stopOptions();
|
||||
if (gridApi && !gridApi.isDestroyed()) {
|
||||
gridApi.destroy();
|
||||
if (api) api.current = null;
|
||||
gridApi = null;
|
||||
}
|
||||
};
|
||||
|
||||
onUnmount(() => {
|
||||
if (cleanupFn) {
|
||||
cleanupFn();
|
||||
cleanupFn = null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return h("div", {
|
||||
class: className,
|
||||
style: style,
|
||||
ref: initGrid
|
||||
});
|
||||
};
|
||||
|
||||
export { Grid };
|
||||
254
src/sigpro.js
Normal file
254
src/sigpro.js
Normal file
@@ -0,0 +1,254 @@
|
||||
export const isF = f => typeof f == "function";
|
||||
export const isO = o => o && typeof o == "object";
|
||||
export const isA = Array.isArray;
|
||||
const doc = typeof document < "u" ? document : null;
|
||||
const txt = s => doc.createTextNode(s == null ? "" : String(s));
|
||||
const toNd = n => n?._rt ? n._cnt : (n instanceof Node ? n : txt(n));
|
||||
export const fragment = p => p.children;
|
||||
export const val = v => isF(v) ? v() : v;
|
||||
|
||||
let aEff = null, aOwn = null, isFlushing = 0, bDepth = 0;
|
||||
const eQ = new Set(), MOUNTED = new WeakMap();
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg", XLINK = "http://www.w3.org/1999/xlink";
|
||||
const SVG_TAGS = new Set("svg,path,circle,rect,line,polyline,polygon,g,defs,text,textPath,tspan,use,symbol,image,marker,ellipse".split(","));
|
||||
const DANG_ATTR = new Set(["src", "href", "formaction", "action", "background", "code", "archive"]);
|
||||
|
||||
const clr = s => { if (s) { s.forEach(f => f()); s.clear(); } };
|
||||
const dispose = e => {
|
||||
if (!e || e._x) return;
|
||||
e._x = 1;
|
||||
let st = [e], c;
|
||||
while ((c = st.pop())) {
|
||||
clr(c._c);
|
||||
if (c._ch) { c._ch.forEach(x => st.push(x)); c._ch.clear(); }
|
||||
if (c._d) { c._d.forEach(d => d.delete(c)); c._d.clear(); }
|
||||
}
|
||||
};
|
||||
|
||||
export const onUnmount = f => aOwn && (aOwn._c ||= new Set()).add(f);
|
||||
const untrack = f => { let p = aEff; aEff = null; try { return f() } finally { aEff = p } };
|
||||
|
||||
const createEffect = (f, isC = 0) => {
|
||||
const e = () => {
|
||||
if (e._x) return;
|
||||
if (e._d) e._d.forEach(s => s.delete(e));
|
||||
clr(e._c);
|
||||
let pE = aEff, pO = aOwn;
|
||||
aEff = aOwn = e;
|
||||
try { return e._res = f(); }
|
||||
catch (err) { console.error("[SigPro]", err); }
|
||||
finally { aEff = pE; aOwn = pO; }
|
||||
};
|
||||
e._d = e._c = e._ch = null;
|
||||
e._x = 0; e._iC = isC;
|
||||
e._dp = aEff ? aEff._dp + 1 : 0;
|
||||
e._m = []; e._p = aOwn;
|
||||
if (aOwn) (aOwn._ch ||= new Set()).add(e);
|
||||
return e;
|
||||
};
|
||||
|
||||
const flush = () => {
|
||||
if (isFlushing) return;
|
||||
isFlushing = 1;
|
||||
let q = [...eQ].sort((a, b) => a._dp - b._dp);
|
||||
eQ.clear();
|
||||
for (let e of q) if (!e._x) e();
|
||||
isFlushing = 0;
|
||||
};
|
||||
|
||||
export const batch = f => {
|
||||
bDepth++;
|
||||
try { return f() } finally { if (!--bDepth && eQ.size && !isFlushing) flush() }
|
||||
};
|
||||
|
||||
const trkUpd = (s, trg = 0) => {
|
||||
if (!trg && aEff && !aEff._x) {
|
||||
s.add(aEff);
|
||||
(aEff._d ||= new Set()).add(s);
|
||||
} else if (trg && s.size) {
|
||||
let q = 0;
|
||||
for (let e of s) {
|
||||
if (e === aEff || e._x) continue;
|
||||
if (e._iC) { e._dt = 1; if (e._sb) trkUpd(e._sb, 1); }
|
||||
else { eQ.add(e); q = 1; }
|
||||
}
|
||||
if (q && !isFlushing && !bDepth) queueMicrotask(flush);
|
||||
}
|
||||
};
|
||||
|
||||
export const $ = (v, k = null) => {
|
||||
let s = new Set();
|
||||
if (isF(v)) {
|
||||
let c, cp = () => {
|
||||
if (cp._dt) {
|
||||
let p = aEff; aEff = cp;
|
||||
try { let n = v(); if (!Object.is(c, n)) { c = n; trkUpd(s, 1); } }
|
||||
finally { aEff = p; }
|
||||
cp._dt = 0;
|
||||
}
|
||||
trkUpd(s); return c;
|
||||
};
|
||||
cp._iC = cp._dt = 1; cp._sb = s; cp._d = null; cp._x = 0;
|
||||
return cp;
|
||||
}
|
||||
if (k) try { v = JSON.parse(localStorage.getItem(k)) ?? v } catch (e) { }
|
||||
return (...a) => {
|
||||
if (a.length) {
|
||||
let n = isF(a[0]) ? a[0](v) : a[0];
|
||||
if (!Object.is(v, n)) { v = n; if (k) localStorage.setItem(k, JSON.stringify(v)); trkUpd(s, 1); }
|
||||
}
|
||||
trkUpd(s); return v;
|
||||
};
|
||||
};
|
||||
|
||||
export const watch = (src, cb) => {
|
||||
let e = createEffect(cb ? () => { let v = isA(src) ? src.map(s => s()) : src(); untrack(() => cb(v)) } : src);
|
||||
e(); return () => dispose(e);
|
||||
};
|
||||
|
||||
const clnNd = n => {
|
||||
if (!n) return;
|
||||
clr(n._c);
|
||||
if (n._oE) dispose(n._oE);
|
||||
if (n.childNodes) n.childNodes.forEach(clnNd);
|
||||
};
|
||||
|
||||
const valAtt = (k, v) => (v == null || v === false) ? null :
|
||||
(DANG_ATTR.has(k) || k.startsWith("on")) && /^\s*(javascript|data|vbscript):/i.test(String(v)) ? '#' : v;
|
||||
|
||||
export const h = (tag, prp = {}, ch = []) => {
|
||||
if (prp instanceof Node || isA(prp) || !isO(prp)) { ch = prp; prp = {}; }
|
||||
|
||||
if (isF(tag)) {
|
||||
let e = createEffect(() => e._res = tag(prp, { children: ch, emit: (ev, ...a) => prp[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...a) }));
|
||||
e();
|
||||
if (e._res == null) return null;
|
||||
let nd = e._res instanceof Node || (isA(e._res) && e._res.every(n => n instanceof Node)) ? e._res : txt(e._res);
|
||||
let att = n => { if (isO(n) && !n._rt) { n._m = e._m || []; n._c = e._c || new Set(); n._oE = e; } };
|
||||
isA(nd) ? nd.forEach(att) : att(nd);
|
||||
return nd;
|
||||
}
|
||||
|
||||
let isS = SVG_TAGS.has(tag), el = isS ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag);
|
||||
el._c = new Set();
|
||||
|
||||
for (let k in prp) {
|
||||
let v = prp[k];
|
||||
if (k === "ref") { isF(v) ? v(el) : (v.current = el); continue; }
|
||||
if (isS && k.startsWith("xlink:")) {
|
||||
let cv = valAtt(k.slice(6), v);
|
||||
cv == null ? el.removeAttributeNS(XLINK, k.slice(6)) : el.setAttributeNS(XLINK, k.slice(6), cv);
|
||||
continue;
|
||||
}
|
||||
if (k.startsWith("on")) {
|
||||
let ev = k.slice(2).toLowerCase(); el.addEventListener(ev, v);
|
||||
let off = () => el.removeEventListener(ev, v); el._c.add(off); onUnmount(off);
|
||||
} else if (isF(v)) {
|
||||
let e = createEffect(() => {
|
||||
let r = valAtt(k, v());
|
||||
if (k === "class") el.className = r || "";
|
||||
else if (r == null) el.removeAttribute(k);
|
||||
else if (k === "style" && typeof r == "string") el.setAttribute("style", r);
|
||||
else if (k in el && !isS) el[k] = r;
|
||||
else el.setAttribute(k, r === true ? "" : r);
|
||||
});
|
||||
e(); el._c.add(() => dispose(e)); onUnmount(() => dispose(e));
|
||||
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
|
||||
el.addEventListener(k === "checked" ? "change" : "input", ev => v(ev.target[k]));
|
||||
}
|
||||
} else {
|
||||
let r = valAtt(k, v);
|
||||
if (r != null) {
|
||||
if (k === "style" && typeof r == "string") el.setAttribute("style", r);
|
||||
else if (k in el && !isS) el[k] = r;
|
||||
else el.setAttribute(k, r === true ? "" : r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const app = c => {
|
||||
if (isA(c)) return c.forEach(app);
|
||||
if (isF(c)) {
|
||||
let anc = txt(""), cur = []; el.appendChild(anc);
|
||||
let e = createEffect(() => {
|
||||
let r = c(), nxt = (isA(r) ? r : [r]).map(toNd), ref = anc;
|
||||
cur.forEach(n => { n._rt ? n.destroy() : clnNd(n); if (n.parentNode) n.remove(); });
|
||||
for (let i = nxt.length - 1; i >= 0; i--) {
|
||||
let nd = nxt[i];
|
||||
if (nd.parentNode !== ref.parentNode) ref.parentNode?.insertBefore(nd, ref);
|
||||
if (nd._m) nd._m.forEach(f => f()); ref = nd;
|
||||
}
|
||||
cur = nxt;
|
||||
});
|
||||
e(); el._c.add(() => dispose(e)); onUnmount(() => dispose(e));
|
||||
} else {
|
||||
let nd = toNd(c); el.appendChild(nd);
|
||||
if (nd._m) nd._m.forEach(f => f());
|
||||
}
|
||||
};
|
||||
app(ch); return el;
|
||||
};
|
||||
|
||||
export const render = rFn => {
|
||||
let c = new Set(), pO = aOwn, pE = aEff, cnt = doc.createElement("div");
|
||||
cnt.style.display = "contents"; cnt.setAttribute("role", "presentation");
|
||||
aOwn = { _c: c }; aEff = null;
|
||||
const pRes = r => {
|
||||
if (!r) return;
|
||||
if (r._rt) { c.add(r.destroy); cnt.appendChild(r._cnt); }
|
||||
else if (isA(r)) r.forEach(pRes);
|
||||
else cnt.appendChild(r instanceof Node ? r : txt(r));
|
||||
};
|
||||
try { pRes(rFn({ onCleanup: f => c.add(f) })); } finally { aOwn = pO; aEff = pE; }
|
||||
return { _rt: 1, _cnt: cnt, destroy: () => { clr(c); clnNd(cnt); cnt.remove(); } };
|
||||
};
|
||||
|
||||
export const when = (c, Y, N = null) => {
|
||||
let anc = txt(""), rt = h("div", { style: "display:contents" }, [anc]), v;
|
||||
watch(() => !!val(c), s => {
|
||||
if (v) { v.destroy(); v = null; }
|
||||
let ct = s ? Y : N;
|
||||
if (ct) { v = render(() => val(ct)); rt.insertBefore(v._cnt, anc); }
|
||||
});
|
||||
onUnmount(() => v?.destroy()); return rt;
|
||||
};
|
||||
|
||||
export const each = (s, fn, kF) => {
|
||||
let anc = txt(""), rt = h("div", { style: "display:contents" }, [anc]), cch = new Map();
|
||||
watch(() => val(s) || [], it => {
|
||||
let nCc = new Map(), nOd = [];
|
||||
for (let i = 0, l = (it || []).length; i < l; i++) {
|
||||
let t = it[i], k = kF ? (t?.[kF] ?? i) : (t?.id ?? i), v = cch.get(k);
|
||||
if (!v) v = render(() => fn(t, i)); else cch.delete(k);
|
||||
nCc.set(k, v); nOd.push(v);
|
||||
}
|
||||
cch.forEach(v => v.destroy());
|
||||
let ref = anc;
|
||||
for (let i = nOd.length - 1; i >= 0; i--) {
|
||||
let nd = nOd[i]._cnt;
|
||||
if (nd.nextSibling !== ref) rt.insertBefore(nd, ref); ref = nd;
|
||||
}
|
||||
cch = nCc;
|
||||
});
|
||||
return rt;
|
||||
};
|
||||
|
||||
export const mount = (c, tgt) => {
|
||||
let t = typeof tgt == "string" ? doc.querySelector(tgt) : tgt;
|
||||
if (!t) return;
|
||||
if (MOUNTED.has(t)) MOUNTED.get(t).destroy();
|
||||
let i = render(isF(c) ? c : () => c);
|
||||
t.replaceChildren(i._cnt); MOUNTED.set(t, i); return i;
|
||||
};
|
||||
|
||||
// const htmlTags = "a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video";
|
||||
|
||||
export const SigPro = { $, watch, batch, h, fragment, render, mount, when, each, onUnmount, val, isA, isF, isO };
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.SigPro = SigPro;
|
||||
// htmlTags.split(" ").forEach(tag => {
|
||||
// window[tag] = (props, children) => h(tag, props, children);
|
||||
// });
|
||||
}
|
||||
21
src/sigpro.locale.js
Normal file
21
src/sigpro.locale.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { $ } = window.SigPro;
|
||||
|
||||
const currentLocale = $("en");
|
||||
const translations = {};
|
||||
|
||||
export const addLang = obj => {
|
||||
for (const locale of Object.keys(obj)) {
|
||||
if (!translations[locale]) translations[locale] = {};
|
||||
Object.assign(translations[locale], obj[locale]);
|
||||
}
|
||||
};
|
||||
|
||||
export const setLocale = locale => {
|
||||
if (locale && translations[locale]) {
|
||||
currentLocale(locale);
|
||||
}
|
||||
};
|
||||
|
||||
export const t = key => {
|
||||
return () => translations[currentLocale()]?.[key] ?? key;
|
||||
};
|
||||
49
src/sigpro.router.js
Normal file
49
src/sigpro.router.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const { $, h, watch, render, isF } = window.SigPro;
|
||||
|
||||
const getHash = () => window.location.hash.slice(1) || "/";
|
||||
const currentPath = $(getHash());
|
||||
|
||||
window.addEventListener("hashchange", () => currentPath(getHash()));
|
||||
|
||||
export const routerParams = $({});
|
||||
|
||||
export const router = routes => {
|
||||
const hook = h("div", { class: "router-hook" });
|
||||
let currentView = null;
|
||||
|
||||
watch([currentPath], () => {
|
||||
const cur = currentPath();
|
||||
|
||||
const route = routes.find(r => {
|
||||
const p1 = r.path.split("/").filter(Boolean);
|
||||
const p2 = cur.split("/").filter(Boolean);
|
||||
return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i]);
|
||||
}) || routes.find(r => r.path === "*");
|
||||
|
||||
if (route) {
|
||||
currentView?.destroy();
|
||||
|
||||
const params = {};
|
||||
route.path.split("/").filter(Boolean).forEach((p, i) => {
|
||||
if (p[0] === ":") params[p.slice(1)] = cur.split("/").filter(Boolean)[i];
|
||||
});
|
||||
|
||||
routerParams(params);
|
||||
|
||||
currentView = render(() => isF(route.component) ? route.component(params) : route.component);
|
||||
|
||||
hook.replaceChildren(currentView.container);
|
||||
}
|
||||
});
|
||||
|
||||
hook.destroy = () => {
|
||||
currentView?.destroy();
|
||||
};
|
||||
|
||||
return hook;
|
||||
};
|
||||
|
||||
router.params = routerParams;
|
||||
router.to = p => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||
router.back = () => window.history.back();
|
||||
router.path = () => currentPath();
|
||||
108
src/sigpro.ui.css
Normal file
108
src/sigpro.ui.css
Normal file
@@ -0,0 +1,108 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
@plugin "@iconify/tailwind4";
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "splight";
|
||||
default: true;
|
||||
prefersdark: false;
|
||||
color-scheme: "light";
|
||||
--color-base-100: oklch(100% 0 0);
|
||||
--color-base-200: oklch(98% 0 0);
|
||||
--color-base-300: oklch(92% 0 0);
|
||||
--color-base-content: oklch(25% 0.006 285);
|
||||
--color-primary: oklch(25% 0.006 285);
|
||||
--color-primary-content: oklch(98% 0 0);
|
||||
--color-secondary: oklch(55% 0.046 257.417);
|
||||
--color-secondary-content: oklch(98% 0 0);
|
||||
--color-accent: oklch(96% 0 0);
|
||||
--color-accent-content: oklch(25% 0.006 285);
|
||||
--color-neutral: oklch(14% 0.005 285.823);
|
||||
--color-neutral-content: oklch(92% 0.004 286.32);
|
||||
--color-info: oklch(74% 0.16 232);
|
||||
--color-success: oklch(62% 0.17 163);
|
||||
--color-warning: oklch(82% 0.18 84);
|
||||
--color-error: oklch(60% 0.25 27);
|
||||
--radius-selector: 0.5rem;
|
||||
--radius-field: 0.5rem;
|
||||
--radius-box: 0.5rem;
|
||||
--size-selector: 0.25rem;
|
||||
--size-field: 0.25rem;
|
||||
--border: 1px;
|
||||
--depth: 1;
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "spdark";
|
||||
default: false;
|
||||
prefersdark: true;
|
||||
color-scheme: "dark";
|
||||
--color-base-100: oklch(15% 0.005 285.823);
|
||||
--color-base-200: oklch(20% 0.005 285.823);
|
||||
--color-base-300: oklch(30% 0.005 285.823);
|
||||
--color-base-content: oklch(92% 0.004 286.32);
|
||||
--color-primary: oklch(98% 0 0);
|
||||
--color-primary-content: oklch(15% 0 0);
|
||||
--color-secondary: oklch(65% 0.046 257.417);
|
||||
--color-secondary-content: oklch(15% 0.005 285.823);
|
||||
--color-accent: oklch(25% 0 0);
|
||||
--color-accent-content: oklch(98% 0 0);
|
||||
--color-neutral: oklch(92% 0.004 286.32);
|
||||
--color-neutral-content: oklch(14% 0.005 285.823);
|
||||
--color-info: oklch(70% 0.1 230);
|
||||
--color-success: oklch(65% 0.15 160);
|
||||
--color-warning: oklch(85% 0.15 90);
|
||||
--color-error: oklch(55% 0.2 27);
|
||||
--radius-selector: 0.5rem;
|
||||
--radius-field: 0.5rem;
|
||||
--radius-box: 0.5rem;
|
||||
--size-selector: 0.25rem;
|
||||
--size-field: 0.25rem;
|
||||
--border: 1px;
|
||||
--depth: 1;
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input,
|
||||
.label,
|
||||
.select,
|
||||
.textarea {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 4px 0px;
|
||||
}
|
||||
|
||||
&:hover:not(:focus) {
|
||||
background-color: oklch(from var(--color-base-100) calc(l - 0.03) c h);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.floating-label span {
|
||||
color: oklch(30% 0.01 260);
|
||||
font-size: 1.1rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.floating-label:focus-within span {
|
||||
color: oklch(25% 0.02 260);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.floating-label:has(input:not(:placeholder-shown)) span {
|
||||
color: oklch(28% 0.01 260);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Solo para la Demo de docsify */
|
||||
.markdown-section progress.progress {
|
||||
all: revert-layer;
|
||||
}
|
||||
367
src/sigpro.ui.js
Normal file
367
src/sigpro.ui.js
Normal file
@@ -0,0 +1,367 @@
|
||||
/// <reference path="../sigpro.d.ts" />
|
||||
|
||||
const { $, h, mount, watch, val, isF, isA, isO } = window.SigPro;
|
||||
|
||||
export const hide = () => document.activeElement?.blur();
|
||||
|
||||
export const ui = {
|
||||
accordion: (p, c) => h("div", { ...p, class: `collapse ${p.class || ''}` }, [h("input", { type: "radio", name: p.name, checked: p.checked }), c]),
|
||||
accordion_title: (p, c) => h("div", { ...p, class: `collapse-title ${p.class || ''}` }, c),
|
||||
accordion_content: (p, c) => h("div", { ...p, class: `collapse-content ${p.class || ''}` }, c),
|
||||
alert: (p, c) => h("div", { ...p, class: `alert ${p.class || ''}` }, c),
|
||||
autocomplete: (p) => ui.combo(p, ({ query, close, setValue }) =>
|
||||
h("ul", { class: "menu bg-base-100 w-full" }, () => {
|
||||
const q = String(val(query)).toLowerCase();
|
||||
const list = (val(p.items) || []).filter(i =>
|
||||
(isO(i) ? (i.label ?? i.value) : String(i)).toLowerCase().includes(q)
|
||||
);
|
||||
return list.length
|
||||
? list.map((item, idx) =>
|
||||
h("li", { key: item.value ?? idx },
|
||||
h("a", {
|
||||
onclick: e => {
|
||||
e.preventDefault();
|
||||
const v = item?.value ?? item;
|
||||
setValue(isO(item) ? (item.label ?? item.value) : String(item));
|
||||
if (isF(p.value)) p.value(v); else p.onChange?.(v);
|
||||
close();
|
||||
}
|
||||
}, isO(item) ? (item.label ?? item.value) : item)
|
||||
)
|
||||
)
|
||||
: [h("li", { class: "disabled" }, h("a", {}, "Sin resultados"))];
|
||||
})
|
||||
),
|
||||
avatar: (p, c) => h("div", { ...p, class: `avatar ${p.class || ''}` }, h("div", { class: p.innerClass || '' }, c)),
|
||||
avatar_group: (p, c) => h("div", { ...p, class: `avatar-group -space-x-6 ${p.class || ''}` }, c),
|
||||
badge: (p, c) => h("span", { ...p, class: `badge ${p.class || ''}` }, c),
|
||||
breadcrumbs: (p, c) => h("div", { ...p, class: `breadcrumbs ${p.class || ''}` }, c),
|
||||
button: (p, c) => h("button", { ...p, class: `btn ${p.class || ''}` }, c),
|
||||
card: (p, c) => h("div", { ...p, class: `card ${p.class || ''}` }, c),
|
||||
card_title: (p, c) => h("div", { ...p, class: `card-title ${p.class || ''}` }, c),
|
||||
card_body: (p, c) => h("div", { ...p, class: `card-body ${p.class || ''}` }, c),
|
||||
card_actions: (p, c) => h("div", { ...p, class: `card-actions ${p.class || ''}` }, c),
|
||||
carousel: (p, c) => h("div", { ...p, class: `carousel ${p.class || ''}` }, c),
|
||||
carousel_item: (p, c) => h("div", { ...p, class: `carousel-item ${p.class || ''}` }, c),
|
||||
chat: (p, c) => h("div", { ...p, class: `chat ${p.class || ''}` }, c),
|
||||
chat_image: (p, c) => h("div", { ...p, class: `chat-image avatar ${p.class || ''}` }, c),
|
||||
chat_header: (p, c) => h("div", { ...p, class: `chat-header ${p.class || ''}` }, c),
|
||||
chat_bubble: (p, c) => h("div", { ...p, class: `chat-bubble ${p.class || ''}` }, c),
|
||||
chat_footer: (p, c) => h("div", { ...p, class: `chat-footer ${p.class || ''}` }, c),
|
||||
checkbox: (p) => h("input", { ...p, type: "checkbox", class: `checkbox ${p.class || ''}` }),
|
||||
colorpicker: (p) => ui.combo({
|
||||
...p, custom: () => h("span", {
|
||||
class: "w-4 h-4 rounded border border-base-300",
|
||||
style: `background:${val(p.value) || '#000'}`
|
||||
})
|
||||
}, ({ close, setValue }) =>
|
||||
pallete({ ...p, onchange: (c) => { setValue(c); close(); } })
|
||||
),
|
||||
combo: (p, c) => {
|
||||
const { placeholder = "", class: cls = "" } = p;
|
||||
const query = isF(p.value) ? p.value : $(p.value ?? "");
|
||||
let inputEl, open = $(false);
|
||||
|
||||
return ui.float({ label: p.label }, [
|
||||
h("div", { class: `dropdown w-full ${cls} ${val(open) ? "dropdown-open" : ""}` }, [
|
||||
h("label", { class: "input w-full" }, [
|
||||
h("span", { class: p.icon ?? "icon-[lucide--search]" }),
|
||||
p.custom ?? null,
|
||||
h("input", {
|
||||
type: "search", placeholder, tabindex: "0",
|
||||
value: query,
|
||||
onfocus: () => open(true),
|
||||
ref: el => inputEl = el
|
||||
})
|
||||
]),
|
||||
h("div", {
|
||||
class: "dropdown-content bg-base-100 rounded-box z-50 max-w-80 shadow-sm",
|
||||
onmousedown: e => e.preventDefault()
|
||||
}, () => val(open) && typeof c === "function"
|
||||
? c({ query, open, close: () => { open(false); inputEl?.blur(); }, setValue: v => query(v) })
|
||||
: null
|
||||
)
|
||||
])
|
||||
]);
|
||||
},
|
||||
datepicker: (p) => {
|
||||
const range = isF(p.range) ? p.range() : p.range;
|
||||
if (!range) return ui.combo({ value: (isF(p.value) ? p.value() : p.value) || '', ...p },
|
||||
({ close, setValue }) => h("div", { class: "w-80" },
|
||||
calendar({ ...p, class: "w-full", onChange: v => { setValue(v); close(); if (isF(p.value)) p.value(v) } })
|
||||
)
|
||||
);
|
||||
const v = $(isF(p.value) ? p.value() : p.value || { start: null, end: null });
|
||||
const start = $((v() || {}).start || ''), end = $((v() || {}).end || '');
|
||||
const sync = () => { v({ start: start(), end: end() }); if (isF(p.value)) p.value(v()); };
|
||||
const cal = (key, sig, ph, dis) => ui.combo({ value: sig, placeholder: ph, class: "flex-1", disabled: dis },
|
||||
({ close, setValue }) => h("div", { class: "w-72" },
|
||||
calendar({
|
||||
...p, class: "w-full", value: v, range: true, onChange: r => {
|
||||
v(r); start(r?.start || ''); end(r?.end || ''); setValue(r?.[key] || ''); if (r?.end) close(); if (isF(p.value)) p.value(r)
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
return h("div", { class: `flex gap-1 ${p.class || ''}`, onchange: sync }, [
|
||||
cal('start', start, p.fromPlaceholder || "Inicio"),
|
||||
cal('end', end, p.toPlaceholder || "Fin", () => !v()?.start)
|
||||
]);
|
||||
},
|
||||
dialog: (p, c) => {
|
||||
const pos = $(p.pos || { x: 100, y: 100 });
|
||||
const show = $(p.show || false);
|
||||
const anim = $(false);
|
||||
|
||||
watch(show, s => s ? setTimeout(() => anim(true), 10) : anim(false));
|
||||
|
||||
return h("div", {
|
||||
class: () => `fixed z-50 transition-opacity duration-300 ${show() && anim() ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'}`,
|
||||
style: () => `left: ${pos().x}px; top: ${pos().y}px; transition: left 50ms, top 50ms;`
|
||||
}, [
|
||||
h("div", { class: `bg-base-100 rounded-box shadow-2xl border ${p.class || ''}` }, [
|
||||
p.title && h("div", {
|
||||
class: "flex justify-between items-center cursor-move p-2 border-b select-none bg-base-200 rounded-t-box",
|
||||
onmousedown: e => {
|
||||
const s = { x: e.clientX - pos().x, y: e.clientY - pos().y };
|
||||
const m = ev => pos({ x: ev.clientX - s.x, y: ev.clientY - s.y });
|
||||
const u = () => { document.removeEventListener('mousemove', m); document.removeEventListener('mouseup', u); };
|
||||
document.addEventListener('mousemove', m); document.addEventListener('mouseup', u);
|
||||
e.preventDefault();
|
||||
}
|
||||
}, [h("span", { class: "font-bold" }, p.title), h("button", { class: "btn btn-sm btn-circle btn-ghost", onclick: () => show(false) }, "✕")]),
|
||||
h("div", { class: "p-4" }, c),
|
||||
p.footer && h("div", { class: "p-2 border-t flex justify-end gap-2" }, p.footer)
|
||||
])
|
||||
]);
|
||||
},
|
||||
divider: (p) => h("div", { ...p, class: `divider ${p.class || ''}` }),
|
||||
drawer: (p, c) => h("div", { ...p, class: `drawer ${p.class || ''}` }, c),
|
||||
drawer_toggle: (p) => h("input", { ...p, type: "checkbox", class: `drawer-toggle ${p.class || ''}` }),
|
||||
drawer_content: (p, c) => h("div", { ...p, class: `drawer-content ${p.class || ''}` }, c),
|
||||
drawer_side: (p, c) => h("div", { ...p, class: `drawer-side ${p.class || ''}` }, c),
|
||||
drawer_overlay: (p) => h("label", { ...p, class: `drawer-overlay ${p.class || ''}` }),
|
||||
dropdown: (p, c) => h("div", { ...p, class: `dropdown ${p.class || ''}` }, c),
|
||||
dropdown_button: (p, c) => h("div", { ...p, tabindex: "0", role: "button", class: `btn ${p.class || ''}` }, c),
|
||||
dropdown_content: (p, c) => h("div", { ...p, tabindex: "0", class: `dropdown-content ${p.class || ''}` }, c),
|
||||
fab: (p, c) => h("div", { ...p, class: `fab ${p.class || ''}` }, c),
|
||||
fab_button: (p, c) => h("div", { ...p, tabindex: "0", role: "button", class: `btn ${p.class || ''}` }, c),
|
||||
fieldset: (p, c) => h("fieldset", { class: `fieldset ${p.class || ''}` }, [h("legend", { class: "fieldset-legend" }, p.label), c]),
|
||||
file: (p) => h("input", { ...p, type: "file", class: `file-input ${p.class || ''}` }),
|
||||
file_drag: (p, c) => h("label", {
|
||||
class: () => `relative flex items-center justify-between h-12 px-4 border-2 border-dashed rounded-lg cursor-pointer transition-all ${p.drag ? 'border-primary bg-primary/10' : 'border-base-content/20 bg-base-100'} ${p.class || ''}`,
|
||||
ondragover: (e) => { e.preventDefault(); p.ondrag?.(true); },
|
||||
ondragleave: () => p.ondrag?.(false),
|
||||
ondrop: (e) => { e.preventDefault(); p.ondrag?.(false); p.ondrop?.(e.dataTransfer.files); }
|
||||
}, c),
|
||||
file_preview: (p) => h("ul", { class: `mt-2 space-y-1 ${p.class || ''}` },
|
||||
(p.files || []).map((f, i) =>
|
||||
h("li", { class: "flex items-center justify-between p-1.5 pl-3 text-xs bg-base-200/50 rounded-md border" }, [
|
||||
h("div", { class: "flex items-center gap-2 truncate opacity-70" }, [
|
||||
h("span", {}, "📄"),
|
||||
h("span", { class: "truncate max-w-[180px]" }, f.name),
|
||||
h("span", { class: "text-[9px] opacity-50" }, `(${~~(f.size / 1024)}KB)`)
|
||||
]),
|
||||
h("button", { class: "btn btn-ghost btn-xs btn-circle", onclick: () => p.onremove?.(i) }, h("span", { class: "icon-[lucide--x]" }))
|
||||
])
|
||||
)
|
||||
),
|
||||
file_error: (p) => h("div", { class: `text-[10px] text-error mt-1 px-1 ${p.class || ''}` }, p.message),
|
||||
float: (p, c) => h("label", { class: "floating-label" }, [h("span", {}, p.label ?? null), c]),
|
||||
indicator: (p, c) => h("div", { ...p, class: `indicator ${p.class || ''}` }, [p.value && h("span", { class: `indicator-item badge ${p.badgeClass || ''}` }, p.value), c]),
|
||||
input: (p) => ui.float({ label: p.label }, [
|
||||
h("label", { class: "input w-full" }, [
|
||||
span({ class: `${p.icon ?? ''}` }),
|
||||
h("input", { ...p, class: `w-full ${p.class || ''}` }),
|
||||
p.right || null
|
||||
])
|
||||
]),
|
||||
kbd: (p, c) => h("kbd", { ...p, class: `kbd ${p.class || ''}` }, c),
|
||||
label: (p, c) => h("span", { ...p, class: `label ${p.class || ''}` }, c),
|
||||
loading: (p) => h("span", { ...p, class: `loading loading-spinner ${p.class || ''}` }),
|
||||
menu: (p, c) => h("ul", { ...p, class: `menu ${p.class || ''}` }, c),
|
||||
menu_title: (p, c) => h('li', { ...p, class: "menu-title" }, c),
|
||||
menu_item: (p) =>
|
||||
p.items
|
||||
? h('li', {}, [h('details', { open: p.open || false }, [h('summary', {}, p.label), h('ul', { class: p.submenuClass || '' }, p.items.map(i => ui.menu_item(i)))])])
|
||||
: h('li', {}, p.href || p.onclick
|
||||
? h('a', { ...(p.href ? { href: p.href } : {}), onclick: p.onclick }, p.label)
|
||||
: p.label),
|
||||
modal: (p, c) => h("dialog", { ...p, class: `modal ${p.class || ''}` }, [c, h("form", { method: "dialog", class: "modal-backdrop" }, h("button", {}, "close"))]),
|
||||
modal_box: (p, c) => h("div", { ...p, class: `modal-box ${p.class || ''}` }, [h("form", { method: "dialog" }, h("button", { class: "btn btn-sm btn-circle btn-ghost absolute right-2 top-2" }, "✕")), c]),
|
||||
modal_action: (p, c) => h("div", { ...p, class: `modal-action ${p.class || ''}` }, c),
|
||||
navbar: (p, c) => h("div", { ...p, class: `navbar ${p.class || ''}` }, c),
|
||||
option: (p, c) => h("option", { ...p }, c),
|
||||
password: (p) => {
|
||||
const show = $(false);
|
||||
const { right, ...rest } = p;
|
||||
return ui.input({
|
||||
...rest,
|
||||
type: () => val(show) ? "text" : "password",
|
||||
icon: "icon-[lucide--lock]",
|
||||
right: ui.swap({ value: show, class: "swap-rotate" }, [
|
||||
ui.swap_on({}, span({ class: "icon-[lucide--eye]" })),
|
||||
ui.swap_off({}, span({ class: "icon-[lucide--eye-off]" }))
|
||||
])
|
||||
});
|
||||
},
|
||||
progress: (p) => h("progress", { ...p, class: `progress ${p.class || ''}` }),
|
||||
radial: (p) => h("div", { ...p, class: `radial-progress ${p.class || ''}`, style: `--value:${val(p.value) ?? 0}`, role: "progressbar" }, p.value ?? ""),
|
||||
radio: (p) => h("input", { ...p, type: "radio", class: `radio ${p.class || ''}` }),
|
||||
range: (p) => h("input", { ...p, type: "range", class: `range ${p.class || ''}` }),
|
||||
rating: (p) => h("div", { class: `rating ${p.class || ''}` },
|
||||
[...Array(p.count || 5)].map((_, i) =>
|
||||
h("input", {
|
||||
class: `mask ${p.mask || 'mask-star'} ${p.itemClass || ''}`,
|
||||
name: p.name,
|
||||
type: "radio",
|
||||
checked: () => val(p.value) === (p.offset ? i + p.offset : i),
|
||||
onclick: () => isF(p.value) ? p.value(i) : p.onChange?.(i)
|
||||
})
|
||||
)
|
||||
),
|
||||
search: (p) => ui.input({ ...p, type: "search", icon: p.icon ?? "icon-[lucide--search]" }),
|
||||
select: (p, c) => h("select", { ...p, class: `select ${p.class || ''}` }, c),
|
||||
stack: (p, c) => h("div", { ...p, class: `stack ${p.class || ''}` }, c),
|
||||
stat: (p, c) => h("div", { ...p, class: `stat ${p.class || ''}` }, c),
|
||||
stat_figure: (p, c) => h("div", { ...p, class: `stat-figure ${p.class || ''}` }, c),
|
||||
stat_title: (p, c) => h("div", { ...p, class: `stat-title ${p.class || ''}` }, c),
|
||||
stat_value: (p, c) => h("div", { ...p, class: `stat-value ${p.class || ''}` }, c),
|
||||
stat_desc: (p, c) => h("div", { ...p, class: `stat-desc ${p.class || ''}` }, c),
|
||||
steps: (p, c) => h("ul", { ...p, class: `steps ${p.class || ''}` }, c),
|
||||
step: (p, c) => h("li", { ...p, class: `step ${p.class || ''}`, "data-content": p.dataContent }, c),
|
||||
swap: (p, c) => h("label", { class: `swap ${p.class || ''}` }, [h("input", { type: "checkbox", checked: p.value }), ...(isA(c) ? c : [c])]),
|
||||
swap_on: (p, c) => h("div", { ...p, class: `swap-on ${p.class || ''}` }, c),
|
||||
swap_off: (p, c) => h("div", { ...p, class: `swap-off ${p.class || ''}` }, c),
|
||||
table: (p, c) => h("table", { ...p, class: `table ${p.class || ''}` }, c),
|
||||
thead: (p, c) => h("thead", { ...p, class: p.class || '' }, c),
|
||||
tbody: (p, c) => h("tbody", { ...p, class: p.class || '' }, c),
|
||||
tfoot: (p, c) => h("tfoot", { ...p, class: p.class || '' }, c),
|
||||
tr: (p, c) => h("tr", { ...p, class: p.class || '' }, c),
|
||||
th: (p, c) => h("th", { ...p, class: p.class || '' }, c),
|
||||
td: (p, c) => h("td", { ...p, class: p.class || '' }, c),
|
||||
tabs: (p, c) => div({ ...p, class: `tabs ${p.class || ''}` }, c),
|
||||
tab: (p) => {
|
||||
const close = () => p.tabs?.(p.tabs().filter((_, idx) => idx !== p.index))
|
||||
return [
|
||||
h('label', { class: `tab ${p.class || ''}` }, [
|
||||
h('input', { type: 'radio', name: p.name, checked: p.checked || undefined }),
|
||||
p.label,
|
||||
p.closable ? h('span', {
|
||||
class: 'ml-1 inline-flex items-center justify-center w-4 h-4 rounded-full hover:bg-base-300 text-base-content/60 hover:text-base-content cursor-pointer',
|
||||
onclick: (e) => { e.stopPropagation(); close() }
|
||||
}, h('span', { class: 'icon-[lucide--x] w-3 h-3' })) : null,
|
||||
]),
|
||||
div({ class: `tab-content bg-base-100 border-base-300 p-6 ${p?.classContent || ''}` }, p.content),
|
||||
]
|
||||
},
|
||||
textarea: (p) => h("textarea", { ...p, class: `textarea ${p.class || ''}` }),
|
||||
textrotate: (p, c) => h("span", { ...p, class: `text-rotate ${p.class || ''}` }, h("span", {}, c)),
|
||||
theme: (p) => ui.swap({ class: `text-xl ${p.class || ''}`, value: p.value }, [
|
||||
ui.swap_on({}, span({ class: "icon-[lucide--moon]" })),
|
||||
ui.swap_off({}, span({ class: "icon-[lucide--sun]" }))
|
||||
]),
|
||||
timeline: (p, c) => h("ul", { ...p, class: `timeline ${p.class || ''}` }, c),
|
||||
timeline_start: (p, c) => h("div", { ...p, class: `timeline-start ${p.class || ''}` }, c),
|
||||
timeline_middle: (p, c) => h("div", { ...p, class: `timeline-middle ${p.class || ''}` }, c),
|
||||
timeline_end: (p, c) => h("div", { ...p, class: `timeline-end ${p.class || ''}` }, c),
|
||||
toggle: (p) => h("input", { ...p, type: "checkbox", class: `toggle ${p.class || ''}` }),
|
||||
tooltip: (p, c) => h('div', { class: `tooltip ${p.class || ''}`, "data-tip": p.tip }, c),
|
||||
validator: (p, c) => h("div", { ...p, class: `validator-hint ${p.class || ''}` }, c),
|
||||
};
|
||||
|
||||
export const calendar = p => {
|
||||
let [d, hv, sh, eh] = [$(new Date()), $(0), $(0), $(0)], now = new Date(),
|
||||
F = v => v ? `${v.getFullYear()}-${String(v.getMonth() + 1).padStart(2, '0')}-${String(v.getDate()).padStart(2, '0')}` : '',
|
||||
P = n => (n < 10 ? '0' : '') + n,
|
||||
M = (m, y = 0) => d(new Date(d().getFullYear() + y, d().getMonth() + m, 1)),
|
||||
V = () => typeof p.value == 'function' ? p.value() : p.value,
|
||||
G = () => typeof p.range == 'function' ? p.range() : p.range,
|
||||
L = dt => {
|
||||
let s = F(dt), v = V(), r = G();
|
||||
if (!r) return p.onChange?.(p.hour ? `${s}T${P(sh())}:00:00` : s);
|
||||
if (!v?.start || v.end) return p.onChange?.({ start: s, end: null, ...(p.hour && { startHour: sh() }) });
|
||||
let nv = s < v.start ? { start: s, end: v.start } : { start: v.start, end: s };
|
||||
p.onChange?.({ ...nv, ...(p.hour && { startHour: v.startHour ?? sh(), endHour: eh() }) });
|
||||
},
|
||||
I = ({ v, on }) => h('div', { class: 'flex-1 flex gap-2 items-center' }, [
|
||||
h('input', { type: 'range', min: 0, max: 23, value: v, class: 'range range-xs', oninput: e => on(+e.target.value) }),
|
||||
h('span', { class: 'text-sm font-mono' }, () => P(v()) + ':00')
|
||||
]);
|
||||
|
||||
return h('div', { class: `p-4 bg-base-100 rounded-box w-80 select-none ${p.class || ''}` }, [
|
||||
h('div', { class: 'flex justify-between items-center mb-4' }, [
|
||||
h('div', { class: 'flex gap-1' }, [
|
||||
h('button', { class: 'btn btn-ghost btn-xs', onclick: () => M(0, -1) }, h('span', { class: 'icon-[lucide--chevrons-left]' })),
|
||||
h('button', { class: 'btn btn-ghost btn-xs', onclick: () => M(-1, 0) }, h('span', { class: 'icon-[lucide--chevron-left]' }))
|
||||
]),
|
||||
h('span', { class: 'font-bold uppercase' }, () => d().toLocaleString('es', { month: 'short', year: 'numeric' })),
|
||||
h('div', { class: 'flex gap-1' }, [
|
||||
h('button', { class: 'btn btn-ghost btn-xs', onclick: () => M(1, 0) }, h('span', { class: 'icon-[lucide--chevron-right]' })),
|
||||
h('button', { class: 'btn btn-ghost btn-xs', onclick: () => M(0, 1) }, h('span', { class: 'icon-[lucide--chevrons-right]' }))
|
||||
])
|
||||
]),
|
||||
h('div', { class: 'grid grid-cols-7 gap-1', onmouseleave: () => hv(null) }, [
|
||||
...'LMXJVSD'.split('').map(l => h('div', { class: 'text-[10px] opacity-40 font-bold text-center' }, l)),
|
||||
() => {
|
||||
let y = d().getFullYear(), m = d().getMonth(), first = (new Date(y, m, 1).getDay() + 6) % 7;
|
||||
return [...Array(first).fill(h('div')), ...Array(new Date(y, m + 1, 0).getDate()).keys()].map(i => {
|
||||
if (typeof i != 'number') return i;
|
||||
let day = i + 1, ds = F(new Date(y, m, day)), today = F(now) == ds;
|
||||
return h('button', {
|
||||
type: 'button', onclick: () => L(new Date(y, m, day)), onmouseenter: () => G() && hv(ds),
|
||||
class: () => {
|
||||
let v = V(), hov = hv(), s = v?.start || (typeof v == 'string' ? v.slice(0, 10) : 0),
|
||||
isE = v?.end == ds, isS = s == ds,
|
||||
inR = G() && v?.start && (v.end ? (ds > v.start && ds < v.end) : (hov && ((ds > s && ds <= hov) || (ds < s && ds >= hov))));
|
||||
return `btn btn-xs p-0 aspect-square min-h-0 h-auto font-normal relative ${isS || isE ? 'btn-primary z-10' : inR ? 'bg-primary/20 border-none rounded-none' : 'btn-ghost'} ${today ? 'ring-1 ring-primary font-black' : ''}`
|
||||
}
|
||||
}, day)
|
||||
})
|
||||
}
|
||||
]),
|
||||
p.hour && h('div', { class: 'mt-3 pt-2 border-t flex gap-4' }, G() ? [I({ v: sh, on: sh }), I({ v: eh, on: eh })] : [I({ v: sh, on: sh })])
|
||||
])
|
||||
}
|
||||
|
||||
export const pallete = p => {
|
||||
let L = s => (s || '').toLowerCase(),
|
||||
C = ['#000', '#1A1A1A', '#333', '#4D4D4D', '#666', '#808080', '#B3B3B3', '#FFF', '#450a0a', '#7f1d1d', '#991b1b', '#b91c1c', '#dc2626', '#ef4444', '#f87171', '#fca5a5', '#431407', '#7c2d12', '#9a3412', '#c2410c', '#ea580c', '#f97316', '#fb923c', '#ffedd5', '#713f12', '#a16207', '#ca8a04', '#eab308', '#facc15', '#fde047', '#fef08a', '#fff9c4', '#064e3b', '#065f46', '#059669', '#10b981', '#34d399', '#4ade80', '#84cc16', '#d9f99d', '#082f49', '#075985', '#0284c7', '#0ea5e9', '#38bdf8', '#7dd3fc', '#22d3ee', '#cffafe', '#1e1b4b', '#312e81', '#4338ca', '#4f46e5', '#6366f1', '#818cf8', '#a5b4fc', '#e0e7ff', '#2e1065', '#4c1d95', '#6d28d9', '#7c3aed', '#8b5cf6', '#a855f7', '#d946ef', '#fae8ff'];
|
||||
|
||||
return h('div', { class: `p-3 bg-base-100 rounded-box shadow w-64 ${p.class || ''}` },
|
||||
h('div', { class: 'grid grid-cols-8 gap-1' },
|
||||
C.map(c => h('button', {
|
||||
type: 'button',
|
||||
style: `background:${c}`,
|
||||
onclick: () => (isF(p.value) ? p.value(c) : p.onchange?.(c), hide()),
|
||||
class: () => `size-6 rounded-sm transition-all hover:scale-125 hover:z-10 active:scale-95 border border-black/5 p-0 min-h-0 ${L(val(p.value)) == L(c) ? 'ring-2 ring-offset-1 ring-primary z-10 scale-110' : ''}`
|
||||
}))
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const toast = (m, t = "alert-success", d = 3500) => {
|
||||
let C = document.getElementById("stc"), T, E, w = h("div", { style: "display:contents" });
|
||||
if (!C) document.body.append(C = h("div", { id: "stc", class: "fixed top-0 right-0 z-[9999] p-4 flex flex-col items-end gap-2 pointer-events-none" }));
|
||||
C.append(w);
|
||||
|
||||
const i = mount(() => {
|
||||
let v = $(0), l = $(0);
|
||||
E = () => l() || (l(1), clearTimeout(T), setTimeout(() => (i.destroy(), w.remove(), C.firstChild || C.remove()), 300));
|
||||
setTimeout(() => v(1));
|
||||
return h("div", {
|
||||
class: () => `alert alert-soft ${t} shadow-lg transition-all duration-300 inline-flex w-auto pointer-events-auto ${l() ? 'translate-x-full opacity-0' : v() ? 'translate-x-0 opacity-100' : 'translate-x-10 opacity-0'}`
|
||||
}, [
|
||||
typeof m == 'function' ? m() : typeof m == 'string' ? h("span", m) : m,
|
||||
h("button", { class: "btn btn-xs btn-circle btn-ghost", onclick: E }, h("span", { class: "icon-[lucide--x]" }))
|
||||
])
|
||||
}, w);
|
||||
|
||||
if (d > 0) T = setTimeout(E, d);
|
||||
return E;
|
||||
};
|
||||
|
||||
window.ui = ui;
|
||||
window.toast = toast;
|
||||
window.calendar = calendar;
|
||||
window.pallete = pallete
|
||||
@@ -1,15 +1,8 @@
|
||||
/**
|
||||
* SigPro Vite Plugin - File-based Routing
|
||||
* @module sigpro/vite
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
export default function sigproRouter() {
|
||||
// sigproRouter for Vite
|
||||
export function sigproRouter() {
|
||||
const virtualModuleId = 'virtual:sigpro-routes';
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
|
||||
// Helper para escanear archivos
|
||||
const getFiles = (dir) => {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs.readdirSync(dir, { recursive: true })
|
||||
@@ -17,14 +10,12 @@ export default function sigproRouter() {
|
||||
.map(file => path.resolve(dir, file));
|
||||
};
|
||||
|
||||
// Transformador de ruta de archivo a URL de router
|
||||
const pathToUrl = (pagesDir, filePath) => {
|
||||
let relative = path.relative(pagesDir, filePath)
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/\.(js|jsx)$/, '')
|
||||
.replace(/\/index$/, '')
|
||||
.replace(/^index$/, '');
|
||||
|
||||
return ('/' + relative)
|
||||
.replace(/\/+/g, '/')
|
||||
.replace(/\[\.\.\.([^\]]+)\]/g, '*')
|
||||
@@ -34,18 +25,13 @@ export default function sigproRouter() {
|
||||
|
||||
return {
|
||||
name: 'sigpro-router',
|
||||
|
||||
resolveId(id) {
|
||||
if (id === virtualModuleId) return resolvedVirtualModuleId;
|
||||
},
|
||||
|
||||
load(id) {
|
||||
if (id !== resolvedVirtualModuleId) return;
|
||||
|
||||
const root = process.cwd();
|
||||
const pagesDir = path.resolve(root, 'src/pages');
|
||||
|
||||
// Obtenemos y ordenamos archivos (rutas estáticas primero, luego dinámicas)
|
||||
const files = getFiles(pagesDir).sort((a, b) => {
|
||||
const urlA = pathToUrl(pagesDir, a);
|
||||
const urlB = pathToUrl(pagesDir, b);
|
||||
@@ -55,23 +41,15 @@ export default function sigproRouter() {
|
||||
});
|
||||
|
||||
let routeEntries = '';
|
||||
|
||||
files.forEach((fullPath) => {
|
||||
const urlPath = pathToUrl(pagesDir, fullPath);
|
||||
// Hacemos la ruta relativa al proyecto para que el import de Vite sea limpio
|
||||
const relativeImport = './' + path.relative(root, fullPath).replace(/\\/g, '/');
|
||||
|
||||
routeEntries += ` { path: '${urlPath}', component: async () => (await import('/${relativeImport}')).default },\n`;
|
||||
routeEntries += ` { path: '${urlPath}', component: () => import('/${relativeImport}') },\n`;
|
||||
});
|
||||
|
||||
// Fallback 404 si no existe una ruta comodín
|
||||
if (!routeEntries.includes("path: '*'")) {
|
||||
routeEntries += ` { path: '*', component: () => document.createTextNode('404 - Not Found') },\n`;
|
||||
routeEntries += ` { path: '*', component: () => ({ default: () => document.createTextNode('404 - Not Found') }) },\n`;
|
||||
}
|
||||
|
||||
return `export const routes = [\n${routeEntries}];`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { sigproRouter };
|
||||
117
src/tailwind
Normal file
117
src/tailwind
Normal file
@@ -0,0 +1,117 @@
|
||||
const layout = [
|
||||
'join', 'join-vertical', 'lg:join-horizontal',
|
||||
'divider', 'divider-horizontal',
|
||||
'validator', 'validator-hint',
|
||||
'glass'
|
||||
]
|
||||
|
||||
const icons = [
|
||||
'icon-[lucide--calendar]', 'icon-[lucide--chevrons-left]',
|
||||
'icon-[lucide--chevron-left]', 'icon-[lucide--chevron-right]',
|
||||
'icon-[lucide--chevrons-right]', 'icon-[lucide--info]',
|
||||
'icon-[lucide--check-circle]', 'icon-[lucide--alert-triangle]',
|
||||
'icon-[lucide--alert-circle]', 'icon-[lucide--heart]',
|
||||
'icon-[lucide--upload]', 'icon-[lucide--x]', 'icon-[lucide--text]',
|
||||
'icon-[lucide--lock]', 'icon-[lucide--hash]', 'icon-[lucide--mail]',
|
||||
'icon-[lucide--search]', 'icon-[lucide--phone]', 'icon-[lucide--link]',
|
||||
'icon-[lucide--eye-off]', 'icon-[lucide--eye]'
|
||||
]
|
||||
|
||||
const inputs = [
|
||||
'input', 'input-bordered', 'input-ghost',
|
||||
'input-primary', 'input-secondary', 'input-accent',
|
||||
'input-info', 'input-success', 'input-warning', 'input-error',
|
||||
'input-xs', 'input-sm', 'input-md', 'input-lg',
|
||||
'floating-label'
|
||||
]
|
||||
|
||||
const alerts = [
|
||||
'alert', 'alert-info', 'alert-success', 'alert-warning', 'alert-error',
|
||||
'alert-soft', 'alert-outline', 'alert-dash'
|
||||
]
|
||||
|
||||
const avatars = [
|
||||
'avatar', 'avatar-group', 'avatar-online', 'avatar-offline', 'avatar-placeholder'
|
||||
]
|
||||
|
||||
const badges = [
|
||||
'badge', 'badge-primary', 'badge-secondary', 'badge-accent',
|
||||
'badge-info', 'badge-success', 'badge-warning', 'badge-error',
|
||||
'badge-outline', 'badge-soft', 'badge-dash',
|
||||
'badge-xs', 'badge-sm', 'badge-md', 'badge-lg'
|
||||
]
|
||||
|
||||
const buttons = [
|
||||
'btn', 'btn-primary', 'btn-secondary', 'btn-accent',
|
||||
'btn-ghost', 'btn-info', 'btn-success', 'btn-warning',
|
||||
'btn-error', 'btn-neutral',
|
||||
'btn-xs', 'btn-sm', 'btn-md', 'btn-lg', 'btn-xl',
|
||||
'btn-outline', 'btn-soft', 'btn-dash', 'btn-link',
|
||||
'btn-circle', 'btn-square', 'btn-wide', 'btn-block',
|
||||
'btn-active', 'btn-disabled'
|
||||
]
|
||||
|
||||
const checkboxes = [
|
||||
'checkbox', 'checkbox-primary', 'checkbox-secondary', 'checkbox-accent',
|
||||
'checkbox-info', 'checkbox-success', 'checkbox-warning', 'checkbox-error',
|
||||
'checkbox-xs', 'checkbox-sm', 'checkbox-md', 'checkbox-lg'
|
||||
]
|
||||
|
||||
const toggles = [
|
||||
'toggle', 'toggle-primary', 'toggle-secondary', 'toggle-accent',
|
||||
'toggle-xs', 'toggle-sm', 'toggle-md', 'toggle-lg'
|
||||
]
|
||||
|
||||
const chats = [
|
||||
'chat', 'chat-end', 'chat-start', 'chat-image',
|
||||
'chat-header', 'chat-footer', 'chat-bubble'
|
||||
]
|
||||
|
||||
const drawers = [
|
||||
'drawer', 'drawer-end', 'drawer-toggle', 'drawer-content',
|
||||
'drawer-side', 'drawer-overlay'
|
||||
]
|
||||
|
||||
const dropdowns = [
|
||||
'dropdown', 'dropdown-content', 'dropdown-end',
|
||||
'dropdown-top', 'dropdown-bottom', 'dropdown-left', 'dropdown-right'
|
||||
]
|
||||
|
||||
const misc = [
|
||||
'breadcrumbs', 'fab', 'fieldset', 'fieldset-legend',
|
||||
'indicator', 'indicator-item', 'menu', 'menu-dropdown', 'menu-dropdown-show',
|
||||
'kbd', 'kbd-xs', 'kbd-sm', 'kbd-md', 'kbd-lg', 'kbd-xl',
|
||||
'list', 'list-row', 'list-bullet', 'list-image', 'list-none',
|
||||
'mask', 'mask-star', 'mask-star-2', 'mask-heart', 'mask-circle',
|
||||
'modal', 'modal-box', 'modal-action', 'modal-backdrop',
|
||||
'modal-open', 'modal-middle', 'modal-top', 'modal-bottom',
|
||||
'navbar', 'navbar-start', 'navbar-center', 'navbar-end',
|
||||
'progress', 'progress-neutral', 'progress-primary', 'progress-secondary',
|
||||
'progress-accent', 'progress-info', 'progress-success',
|
||||
'progress-warning', 'progress-error', 'radial-progress',
|
||||
'radio', 'radio-primary', 'radio-secondary', 'radio-accent',
|
||||
'radio-info', 'radio-success', 'radio-warning', 'radio-error',
|
||||
'radio-xs', 'radio-sm', 'radio-md', 'radio-lg',
|
||||
'range', 'range-primary', 'range-secondary', 'range-accent',
|
||||
'range-info', 'range-success', 'range-warning', 'range-error',
|
||||
'range-xs', 'range-sm', 'range-md', 'range-lg',
|
||||
'rating', 'rating-half', 'rating-hidden',
|
||||
'select', 'select-bordered', 'select-primary', 'select-secondary',
|
||||
'select-accent', 'select-info', 'select-success',
|
||||
'select-warning', 'select-error',
|
||||
'select-xs', 'select-sm', 'select-md', 'select-lg',
|
||||
'stack', 'stack-top', 'stack-bottom', 'stack-start', 'stack-end',
|
||||
'stat', 'stat-figure', 'stat-title', 'stat-value', 'stat-desc',
|
||||
'swap', 'swap-on', 'swap-off', 'swap-active',
|
||||
'swap-rotate', 'swap-flip', 'swap-indeterminate',
|
||||
'table', 'table-zebra', 'table-pin-rows', 'table-pin-cols',
|
||||
'table-xs', 'table-sm', 'table-md', 'table-lg',
|
||||
'tabs', 'tabs-box', 'tabs-lift', 'tabs-border', 'tab', 'tab-content',
|
||||
'timeline', 'timeline-vertical', 'timeline-horizontal',
|
||||
'timeline-compact', 'timeline-start', 'timeline-middle',
|
||||
'timeline-end', 'timeline-box',
|
||||
'tooltip', 'tooltip-top', 'tooltip-bottom', 'tooltip-left', 'tooltip-right',
|
||||
'tooltip-primary', 'tooltip-secondary', 'tooltip-accent',
|
||||
'tooltip-info', 'tooltip-success', 'tooltip-warning', 'tooltip-error',
|
||||
'tooltip-open'
|
||||
]
|
||||
Reference in New Issue
Block a user