Compare commits

..

111 Commits

Author SHA1 Message Date
eb1c81ec26 Modular router db and locale 2026-05-22 23:05:08 +02:00
8481e339cc dislog 2026-05-15 00:42:08 +02:00
d83aff6229 dialog quick 2026-05-15 00:36:52 +02:00
8df06d9c12 faltaba watch 2026-05-15 00:34:04 +02:00
72bfc2b5c1 dialog no modal 2026-05-15 00:32:38 +02:00
1db7b81eb7 dialog 2026-05-15 00:22:55 +02:00
1607b41ebc new dialog 2026-05-15 00:18:50 +02:00
27d9474610 menu_items 2026-05-14 21:48:57 +02:00
3dea037697 change theme 2026-05-14 20:42:54 +02:00
9fc4eaebbb include css 2026-05-14 17:15:15 +02:00
c86c37aec2 menu title 2026-05-14 17:12:57 +02:00
26464d2161 fault isA 2026-05-14 16:59:35 +02:00
dc9af3181f when problem 2026-05-14 16:57:56 +02:00
00ad6d7f9f new.d.ts 2026-05-14 15:33:58 +02:00
5efb9e0f96 remove icon 2026-05-14 15:23:26 +02:00
651d9587c2 del to destroy 2026-05-14 14:28:07 +02:00
3fe05d40e6 Update docs 2026-05-14 14:21:21 +02:00
0b3eb0159f unify 2026-05-14 13:41:32 +02:00
1349d431e9 Eliminar .github/workflows/sync-github.yml 2026-05-14 13:38:34 +02:00
6f538b8613 Actualizar .github/workflows/unpublish-npm.yml 2026-05-14 13:37:04 +02:00
7d8db0192a solved toast 2026-05-14 12:11:02 +02:00
1d71340552 Repair combo 2026-05-13 13:22:52 +02:00
06c7763b34 Actualizar .github/workflows/publish-gitea.yml 2026-05-12 23:59:52 +02:00
d48241a9d9 Actualizar .github/workflows/docs.yaml 2026-05-12 23:59:20 +02:00
2a482f2340 reconvert sigpro/ui 2026-05-12 23:57:32 +02:00
1800b16940 minify 2026-05-10 02:37:57 +02:00
8b2e67b3b0 remove IIFE 2026-05-10 01:33:44 +02:00
0a790de054 1.2.39 recover window 2026-05-10 00:00:15 +02:00
c01b41d892 1.2.38 2026-05-09 17:10:14 +02:00
5a2cefa115 include convert html to sigpro 2026-05-09 17:09:56 +02:00
a0701422f5 Megacompact f1 2026-05-09 16:06:54 +02:00
645f9b42b0 Actualizar .github/workflows/docs.yaml 2026-05-09 12:09:50 +02:00
8796b9f94d Create plus
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
2026-05-08 14:56:57 +02:00
afa2817118 Update
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
2026-05-08 12:50:20 +02:00
369a35d92a add utils
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 4s
2026-05-08 12:47:51 +02:00
610c9a9586 remove extra 2026-05-05 22:36:58 +02:00
cd58b97d09 1.2.36 2026-05-05 16:43:42 +02:00
e4b08a0aad Readme 2026-05-05 16:28:27 +02:00
439809b1e7 Modular router && remove $$
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 9s
2026-05-05 16:27:53 +02:00
ab0e6e0697 Clean Code include src folder
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 4s
2026-05-04 12:06:09 +02:00
39a67b94fc Clean UMD
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 4s
2026-05-03 22:11:23 +02:00
820d55b012 Docs
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 4s
2026-05-02 00:37:15 +02:00
f3fb26354c Separate ESM IIFE in dual files
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 4s
2026-05-01 23:11:47 +02:00
8a9805b79a Actualizar Readme.md 2026-04-30 11:24:58 +02:00
bef6c20231 Include Fragment for JSX
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
2026-04-30 11:19:37 +02:00
b1fa97afc3 Actualizar Readme.md 2026-04-30 09:16:33 +02:00
a35ea1e38e Actualizar Readme.md 2026-04-30 09:16:07 +02:00
69e277d726 Actualizar Readme.md 2026-04-30 09:15:03 +02:00
9da5bd74f9 Actualizar Readme.md 2026-04-30 09:13:29 +02:00
7251573e28 Actualizar Readme.md 2026-04-30 09:12:51 +02:00
1ca67dd4a0 Actualizar Readme.md 2026-04-30 09:11:14 +02:00
db9bd39679 Actualizar Readme.md 2026-04-30 09:10:29 +02:00
2b303fc55c Actualizar Readme.md 2026-04-30 09:09:56 +02:00
f98cb19ee1 Increase exports
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 9s
2026-04-29 22:54:00 +02:00
f28594348e update ts 2026-04-29 22:45:47 +02:00
7d4340a987 add email 2026-04-29 22:43:50 +02:00
f11dd340ff include author in package 2026-04-29 20:31:44 +02:00
6d7ac2d2e9 remove sigpro demo
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
2026-04-29 17:48:38 +02:00
0df4b3912d 1.2.26 add style option to h()
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
2026-04-29 16:54:54 +02:00
771f4a9f83 include onUnmount 2026-04-28 22:31:25 +02:00
d46c5ca3af update Docs
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 4s
2026-04-28 19:05:23 +02:00
4526726b1b remove import meta
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
2026-04-28 18:58:02 +02:00
2a0ce8c68f Returno to inytegrate Tags in Core
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 4s
2026-04-28 18:42:56 +02:00
995f1557bf Correct error Tags
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 4s
2026-04-28 17:33:51 +02:00
6a33b7df07 remove old code 2026-04-27 16:07:40 +02:00
99780e8399 New modular Sigpro
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
2026-04-27 15:22:57 +02:00
b931434edc include core 2026-04-27 12:46:29 +02:00
dc2f6f8736 Actualizar Readme.md 2026-04-27 12:43:50 +02:00
7cce0e5e59 more keywords 2026-04-27 12:32:07 +02:00
03da2b7cd3 include modular functions 2026-04-27 12:20:26 +02:00
496ad150ce Clear Code, small organized
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
2026-04-27 10:36:18 +02:00
a65219759d Improved XXS
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 4s
2026-04-27 10:32:11 +02:00
25975eb89a Corrected docs 2026-04-27 10:31:58 +02:00
04052ef7b4 Actualizar .github/workflows/sync-github.yml 2026-04-26 21:00:13 +02:00
3faf1fe5a6 Actualizar .github/workflows/publish-npm.yml 2026-04-26 20:59:58 +02:00
76a97fe2a2 Actualizar .github/workflows/docs.yaml 2026-04-26 20:59:41 +02:00
fb7ebe5fec Remove tags 2026-04-26 19:32:30 +02:00
9b9284d3d1 Actualizar .github/workflows/sync-to-github.yml 2026-04-26 19:30:55 +02:00
83c5279ab9 Actualizar .github/workflows/publish.yml 2026-04-26 19:14:44 +02:00
ab36557c8c Actualizar package.json 2026-04-26 19:10:58 +02:00
1c45dc5466 Actualizar .github/workflows/sync-to-github.yml 2026-04-26 18:49:29 +02:00
ee5e6e5207 Actualizar .github/workflows/sync-to-github.yml 2026-04-26 18:45:55 +02:00
29dda4c07e Actualizar .github/workflows/sync-to-github.yml 2026-04-26 18:43:54 +02:00
ab1413ca5a Actualizar .github/workflows/sync-to-github.yml 2026-04-26 18:42:03 +02:00
a5ecc17166 Eliminar .github/workflows/ping.yml
Some checks failed
Sync selected files to GitHub / sync (push) Failing after 1s
2026-04-26 18:40:24 +02:00
fdffac2a72 Actualizar .github/workflows/sync-to-github.yml
Some checks failed
Sync selected files to GitHub / sync (push) Failing after 2s
2026-04-26 18:32:29 +02:00
5b0cfad9b8 Actualizar .github/workflows/ping.yml
Some checks failed
Sync selected files to GitHub / sync (push) Failing after 1s
2026-04-26 18:31:51 +02:00
ccdbeb1b16 Actualizar .github/workflows/sync-to-github.yml
Some checks failed
Sync selected files to GitHub / sync (push) Failing after 1s
2026-04-26 18:30:50 +02:00
2f1cfae0b2 Actualizar .github/workflows/ping.yml
Some checks failed
Sync selected files to GitHub / sync (push) Failing after 2s
2026-04-26 18:27:07 +02:00
6e0c21eddc Actualizar .github/workflows/sync-to-github.yml
Some checks failed
Sync selected files to GitHub / sync (push) Failing after 2s
2026-04-26 18:21:20 +02:00
20f7242e83 Actualizar .github/workflows/sync-to-github.yml
Some checks failed
Sync selected files to GitHub / sync (push) Failing after 3s
2026-04-26 18:17:15 +02:00
73d5c12f13 Actualizar .github/workflows/ping.yml
Some checks failed
Sync selected files to GitHub / sync (push) Failing after 2s
2026-04-26 18:14:17 +02:00
c653d361d6 Añadir .github/workflows/ping.yml
Some checks failed
Sync selected files to GitHub / sync (push) Failing after 1s
2026-04-26 18:12:14 +02:00
1006a42284 Actualizar .github/workflows/sync-to-github.yml
Some checks failed
Sync selected files to GitHub / sync (push) Failing after 1s
2026-04-26 18:04:53 +02:00
ba6d731377 Actualizar .github/workflows/sync-to-github.yml
Some checks are pending
Sync selected files to GitHub / sync (push) Waiting to run
2026-04-26 18:02:28 +02:00
91225e185d Actualizar .github/workflows/sync-to-github.yml
Some checks failed
Sync selected files to GitHub / sync (push) Has been cancelled
2026-04-26 18:02:09 +02:00
c28b1860e7 Añadir .github/workflows/sync-to-github.yml
Some checks failed
Sync selected files to GitHub / sync (push) Failing after 37s
2026-04-26 17:52:22 +02:00
7287e9c094 Actualizar package.json 2026-04-26 16:14:23 +02:00
14f06c3a88 Actualizar package.json 2026-04-26 16:09:41 +02:00
5bf1ecd4f0 Actualizar docs/README.md
Some checks failed
Deploy Docs to Synology / deploy (push) Successful in 3s
Publish to NPM / build (release) Failing after 13s
2026-04-26 16:05:17 +02:00
2d97d7d117 Actualizar docs/README.md
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
2026-04-26 16:03:07 +02:00
0d59518a80 Actualizar docs/README.md
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
2026-04-26 16:02:10 +02:00
4690aa5013 Actualizar Readme.md 2026-04-26 16:01:12 +02:00
0837030da8 Actualizar Readme.md 2026-04-26 15:58:06 +02:00
e659aa940f Actualizar Readme.md 2026-04-26 15:57:36 +02:00
fb1ac8c9c3 Actualizar Readme.md 2026-04-26 15:56:58 +02:00
69b2dd723c 1.2.20 2026-04-26 15:44:41 +02:00
60ff7f4e99 Añadir .github/workflows/unpublish.yml 2026-04-26 15:43:56 +02:00
becc4b8227 dist 1.2.20
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
2026-04-26 15:39:07 +02:00
af5bd1a537 Tree shaking ESM autoimport IIFE
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
2026-04-26 15:38:26 +02:00
a792e72b63 Update Docs 2026-04-26 15:38:10 +02:00
70 changed files with 70054 additions and 4045 deletions

View File

@@ -1,11 +1,6 @@
name: Deploy Docs to Synology name: Deploy Docs to Synology
on: on:
push:
branches:
- main
paths:
- 'docs/**'
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -17,7 +12,7 @@ jobs:
--dns 192.168.1.1 --dns 192.168.1.1
--add-host git.natxocc.com:host-gateway --add-host git.natxocc.com:host-gateway
--add-host gitea:host-gateway --add-host gitea:host-gateway
-v /volume1/webdocs/sigpro:/mnt/nas_docs # <--- Cambiamos el nombre interno -v /volume1/webdocs/sigpro:/mnt/nas_docs
steps: steps:
- name: Checkout código - name: Checkout código
@@ -25,14 +20,9 @@ jobs:
with: with:
fetch-depth: 1 fetch-depth: 1
env: env:
# Mantenemos tu configuración original que SÍ llegaba a conectar
GIT_CONFIG_PARAMETERS: "'url.https://git.natxocc.com/.insteadOf=http://gitea:3000/'" GIT_CONFIG_PARAMETERS: "'url.https://git.natxocc.com/.insteadOf=http://gitea:3000/'"
- name: Copiar archivos - name: Copiar archivos
run: | run: |
# Copiamos a la ruta montada
cp -r docs/. /mnt/nas_docs/ cp -r docs/. /mnt/nas_docs/
# Esta es la prueba real. Si esto sale en el log y el runner
# está en modo 'privileged', los verás en el File Station.
ls -la /mnt/nas_docs ls -la /mnt/nas_docs

View File

@@ -1,4 +1,4 @@
name: Publicar Paquete SigPro (NPM) name: Publish to SigPro (NPM)
on: on:
workflow_dispatch: workflow_dispatch:
@@ -20,7 +20,6 @@ jobs:
- name: Checkout código - name: Checkout código
uses: actions/checkout@v4 uses: actions/checkout@v4
env: env:
# ESTO ES LO QUE NO DEBÍ QUITAR: El mapeo de la URL interna a la externa
GIT_CONFIG_PARAMETERS: "'url.https://git.natxocc.com/.insteadOf=http://gitea:3000/'" GIT_CONFIG_PARAMETERS: "'url.https://git.natxocc.com/.insteadOf=http://gitea:3000/'"
- name: Instalar Bun - name: Instalar Bun
@@ -39,8 +38,6 @@ jobs:
# 1. Definimos la URL del registro # 1. Definimos la URL del registro
REGISTRY="git.natxocc.com/api/packages/natxocc/npm/" REGISTRY="git.natxocc.com/api/packages/natxocc/npm/"
# 2. Configuramos el .npmrc usando tu secreto PACK_TOKEN
echo "//${REGISTRY}:_authToken=${{ secrets.PACK_TOKEN }}" > ~/.npmrc echo "//${REGISTRY}:_authToken=${{ secrets.PACK_TOKEN }}" > ~/.npmrc
# 3. Publicamos
npm publish --registry "https://${REGISTRY}" --userconfig ~/.npmrc npm publish --registry "https://${REGISTRY}" --userconfig ~/.npmrc

38
.github/workflows/publish-npm.yml vendored Normal file
View 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 }}

View File

@@ -1,43 +0,0 @@
name: Publish to NPM
on:
release:
types: [published]
jobs:
build:
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: Forzar Dominio Limpio
run: |
# Redirigimos cualquier intento (IP o nombre interno) al dominio limpio
git config --global url."http://git.natxocc.com/".insteadOf "http://gitea:3000/"
git config --global url."http://git.natxocc.com/".insteadOf "http://192.168.1.100:3333/"
git config --global --add safe.directory /workspace/natxocc/sigpro
git config --global http.sslVerify false
- 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/'"
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install & Publish
run: |
bun install
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
npm publish --access public
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

22
.github/workflows/unpublish-npm.yml vendored Normal file
View 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 }}

View File

@@ -1,15 +1,12 @@
# `SigPro` ⚛️ Blazing fast, zero-overhead, vanilla JS renderer with atomic reactivity.
# `SigPro`
[![npm version](https://img.shields.io/npm/v/sigpro.svg)](https://www.npmjs.com/package/sigpro) [![npm version](https://img.shields.io/npm/v/sigpro.svg)](https://www.npmjs.com/package/sigpro)
[![bundle size](https://img.shields.io/bundlephobia/minzip/sigpro)](https://bundlephobia.com/package/sigpro) ![js size](https://img.shields.io/badge/js_size-2.8_kB_brotli-blue)
[![size](https://img.badgesize.io/natxocc/sigpro/main/sigpro/index.js?compression=gzip)](https://github.com/natxocc/sigpro)
[![license](https://img.shields.io/npm/l/sigpro)](https://github.com/natxocc/sigpro/blob/main/LICENSE) [![license](https://img.shields.io/npm/l/sigpro)](https://github.com/natxocc/sigpro/blob/main/LICENSE)
### **The Atomic Reactivity Engine for the Modern Web.** [**Explore the Docs →**](https://sigpro.natxocc.com/#/)
**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)
--- ---
@@ -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? 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**. * **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. * **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. * **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. * **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. * **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. * **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. * **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. * **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. * **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. 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).* *Lower is better. Measured in milliseconds (ms).*
| Benchmark Test | **SigPro** | SolidJS | Vue 3 | React 18 | | 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 | | **Direct Selection** (on click) | **17.5ms** | ~18ms | ~32ms | ~65ms |
| **Initial Render** (1k rows) | **~35ms** | ~32ms | ~45ms | ~70ms | | **Initial Render** (1k rows) | **~35ms** | ~32ms | ~45ms | ~70ms |
### 🧠 Memory Footprint ### Memory Footprint
*Lower is better. Measured in Megabytes (MB) after 1k rows.* *Lower is better. Measured in Megabytes (MB) after 1k rows.*
| Metric | **SigPro** | Vanilla JS | Svelte | React | | Metric | **SigPro** | Vanilla JS | Svelte | React |
@@ -65,6 +62,8 @@ Create reactive, persistent components with a syntax that feels like Vanilla JS,
``` ```
```javascript ```javascript
import { $, mount } from "sigpro";
const Counter = () => { const Counter = () => {
// Simple signal // Simple signal
const value = $(100); const value = $(100);
@@ -74,14 +73,14 @@ const Counter = () => {
const doubleValue = $(()=> value() * count()); const doubleValue = $(()=> value() * count());
// Create fast HTML with pure JS // Create fast HTML with pure JS
return Div({ class: "card" }, [ return div({ class: "card" }, [
H1(`Count: ${count()}, Reference: ${value()}, Double x Ref: ${doubleValue()}`), h1(() => `Count: ${count()}, Reference: ${value()}, Double x Ref: ${doubleValue()}`),
P("Atomic updates. Zero re-renders of the parent tree."), p("Atomic updates. Zero re-renders of the parent tree."),
Button({ onclick: () => count(c => c + 1)}, "Increment +1") 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 | | 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 | | **State Logic** | **Atomic Signals** | Virtual DOM Diffing | Compiler Dirty Bits |
| **Update Speed** | **Direct Node Access** | Component Re-render | Block Reconciliation | | **Update Speed** | **Direct Node Access** | Component Re-render | Block Reconciliation |
| **Native Persistence** | **Included ($)** | Requires Plugins | Manual | | **Native Persistence** | **Included ($)** | Requires Plugins | Manual |
@@ -123,7 +122,7 @@ src/
```javascript ```javascript
// vite.config.js // vite.config.js
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { sigproRouter } from 'sigpro/vite'; import { sigproRouter } from 'sigpro/router';
export default defineConfig({ export default defineConfig({
plugins: [sigproRouter()] plugins: [sigproRouter()]

1
dist/sigpro.db.js vendored Normal file
View 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

File diff suppressed because one or more lines are too long

602
dist/sigpro.esm.js vendored
View File

@@ -1,602 +0,0 @@
// sigpro.js
var isFunc = (f) => typeof f === "function";
var isObj = (o) => o && typeof o === "object";
var isArr = Array.isArray;
var doc = typeof document !== "undefined" ? document : null;
var ensureNode = (n) => n?._isRuntime ? n.container : n instanceof Node ? n : doc.createTextNode(n == null ? "" : String(n));
var activeEffect = null;
var activeOwner = null;
var isFlushing = false;
var batchDepth = 0;
var effectQueue = new Set;
var proxyCache = new WeakMap;
var ITER = Symbol("iter");
var MOUNTED_NODES = new WeakMap;
var dispose = (eff) => {
if (!eff || eff._disposed)
return;
eff._disposed = true;
const stack = [eff];
while (stack.length) {
const e = stack.pop();
if (e._cleanups) {
e._cleanups.forEach((fn) => fn());
e._cleanups.clear();
}
if (e._children) {
e._children.forEach((child) => stack.push(child));
e._children.clear();
}
if (e._deps) {
e._deps.forEach((depSet) => depSet.delete(e));
e._deps.clear();
}
}
};
var onUnmount = (fn) => {
if (activeOwner)
(activeOwner._cleanups ||= new Set).add(fn);
};
var untrack = (fn) => {
const p = activeEffect;
activeEffect = null;
try {
return fn();
} finally {
activeEffect = p;
}
};
var createEffect = (fn, isComputed = false) => {
const effect = () => {
if (effect._disposed)
return;
if (effect._deps)
effect._deps.forEach((s) => s.delete(effect));
if (effect._cleanups) {
effect._cleanups.forEach((c) => c());
effect._cleanups.clear();
}
const prevEffect = activeEffect;
const prevOwner = activeOwner;
activeEffect = activeOwner = effect;
try {
return effect._result = fn();
} catch (e) {
console.error("[SigPro]", e);
} finally {
activeEffect = prevEffect;
activeOwner = prevOwner;
}
};
effect._deps = effect._cleanups = effect._children = null;
effect._disposed = false;
effect._isComputed = isComputed;
effect._depth = activeEffect ? activeEffect._depth + 1 : 0;
effect._mounts = [];
effect._parent = activeOwner;
if (activeOwner)
(activeOwner._children ||= new Set).add(effect);
return effect;
};
var flush = () => {
if (isFlushing)
return;
isFlushing = true;
const sorted = Array.from(effectQueue).sort((a, b) => a._depth - b._depth);
effectQueue.clear();
for (const e of sorted)
if (!e._disposed)
e();
isFlushing = false;
};
var batch = (fn) => {
batchDepth++;
try {
return fn();
} finally {
batchDepth--;
if (batchDepth === 0 && effectQueue.size > 0 && !isFlushing) {
flush();
}
}
};
var trackUpdate = (subs, trigger = false) => {
if (!trigger && activeEffect && !activeEffect._disposed) {
subs.add(activeEffect);
(activeEffect._deps ||= new Set).add(subs);
} else if (trigger && subs.size > 0) {
let hasQueue = false;
for (const e of subs) {
if (e === activeEffect || e._disposed)
continue;
if (e._isComputed) {
e._dirty = true;
if (e._subs)
trackUpdate(e._subs, true);
} else {
effectQueue.add(e);
hasQueue = true;
}
}
if (hasQueue && !isFlushing && batchDepth === 0)
queueMicrotask(flush);
}
};
var $ = (val, key = null) => {
const subs = new Set;
if (isFunc(val)) {
let cache;
const computed = () => {
if (computed._dirty) {
const prev = activeEffect;
activeEffect = computed;
try {
const next = val();
if (!Object.is(cache, next)) {
cache = next;
trackUpdate(subs, true);
}
} finally {
activeEffect = prev;
}
computed._dirty = false;
}
trackUpdate(subs);
return cache;
};
computed._isComputed = true;
computed._subs = subs;
computed._dirty = true;
computed._deps = null;
computed._disposed = false;
computed.stop = () => {};
if (activeOwner)
onUnmount(computed.stop);
return computed;
}
if (key)
try {
val = JSON.parse(localStorage.getItem(key)) ?? val;
} catch (e) {}
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));
trackUpdate(subs, true);
}
}
trackUpdate(subs);
return val;
};
};
var $$ = (target) => {
if (!isObj(target))
return target;
const cached = proxyCache.get(target);
if (cached)
return cached;
const subs = new Map;
const getSubs = (key) => {
let set = subs.get(key);
if (!set)
subs.set(key, set = new Set);
return set;
};
const proxy = new Proxy(target, {
get(target2, key, receiver) {
if (typeof key !== "symbol")
trackUpdate(getSubs(key));
return $$(Reflect.get(target2, key, receiver));
},
set(target2, key, value, receiver) {
const hadKey = Reflect.has(target2, key);
const oldValue = Reflect.get(target2, key, receiver);
const result = Reflect.set(target2, key, value, receiver);
if (result && !Object.is(oldValue, value)) {
trackUpdate(getSubs(key), true);
if (!hadKey)
trackUpdate(getSubs(ITER), true);
}
return result;
},
deleteProperty(target2, key) {
const result = Reflect.deleteProperty(target2, key);
if (result) {
trackUpdate(getSubs(key), true);
trackUpdate(getSubs(ITER), true);
}
return result;
},
ownKeys(target2) {
trackUpdate(getSubs(ITER));
return Reflect.ownKeys(target2);
}
});
proxyCache.set(target, proxy);
return proxy;
};
var watch = (sources, cb) => {
if (cb === undefined) {
const effect2 = createEffect(sources);
effect2();
return () => dispose(effect2);
}
const effect = createEffect(() => {
const vals = Array.isArray(sources) ? sources.map((s) => s()) : sources();
untrack(() => cb(vals));
});
effect();
return () => dispose(effect);
};
var cleanupNode = (node) => {
if (!node)
return;
if (node._cleanups) {
node._cleanups.forEach((fn) => fn());
node._cleanups.clear();
}
if (node._ownerEffect)
dispose(node._ownerEffect);
if (node.childNodes)
node.childNodes.forEach((n) => cleanupNode(n));
};
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
var isDangerousAttr = (key) => key === "src" || key === "href" || key.startsWith("on");
var validateAttr = (key, val) => {
if (val == null || val === false)
return null;
if (isDangerousAttr(key)) {
const sVal = String(val);
if (DANGEROUS_PROTOCOL.test(sVal)) {
console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`);
return "#";
}
}
return val;
};
var h = (tag, props = {}, children = []) => {
if (props instanceof Node || isArr(props) || !isObj(props)) {
children = props;
props = {};
}
if (isFunc(tag)) {
const effect = createEffect(() => {
const result2 = tag(props, {
children,
emit: (ev, ...args) => props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...args)
});
effect._result = result2;
return result2;
});
effect();
const result = effect._result;
if (result == null)
return null;
const node = result instanceof Node || isArr(result) && result.every((n) => n instanceof Node) ? result : doc.createTextNode(String(result));
const attach = (n) => {
if (isObj(n) && !n._isRuntime) {
n._mounts = effect._mounts || [];
n._cleanups = effect._cleanups || new Set;
n._ownerEffect = effect;
}
};
isArr(node) ? node.forEach(attach) : attach(node);
return node;
}
const isSVG = /^(svg|path|circle|rect|line|poly(line|gon)|g|defs|text(path)?|tspan|use|symbol|image|marker|ellipse)$/i.test(tag);
const el = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : doc.createElement(tag);
el._cleanups = new Set;
for (let k in props) {
if (!props.hasOwnProperty(k))
continue;
let v = props[k];
if (k === "ref") {
isFunc(v) ? v(el) : v.current = el;
continue;
}
if (isSVG && k.startsWith("xlink:")) {
const ns = "http://www.w3.org/1999/xlink";
v == null ? el.removeAttributeNS(ns, k.slice(6)) : el.setAttributeNS(ns, k.slice(6), v);
continue;
}
if (k.startsWith("on")) {
const ev = k.slice(2).toLowerCase();
el.addEventListener(ev, v);
const off = () => el.removeEventListener(ev, v);
el._cleanups.add(off);
onUnmount(off);
} else if (isFunc(v)) {
const effect = createEffect(() => {
const val = validateAttr(k, v());
if (k === "class")
el.className = val || "";
else if (val == null)
el.removeAttribute(k);
else if (k in el && !isSVG)
el[k] = val;
else
el.setAttribute(k, val === true ? "" : val);
});
effect();
el._cleanups.add(() => dispose(effect));
onUnmount(() => dispose(effect));
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
const evType = k === "checked" ? "change" : "input";
el.addEventListener(evType, (ev) => v(ev.target[k]));
}
} else {
const val = validateAttr(k, v);
if (val != null) {
if (k in el && !isSVG)
el[k] = val;
else
el.setAttribute(k, val === true ? "" : val);
}
}
}
const append = (c) => {
if (isArr(c))
return c.forEach(append);
if (isFunc(c)) {
const anchor = doc.createTextNode("");
el.appendChild(anchor);
let currentNodes = [];
const effect = createEffect(() => {
const res = c();
const next = (isArr(res) ? res : [res]).map(ensureNode);
currentNodes.forEach((n) => {
if (n._isRuntime)
n.destroy();
else
cleanupNode(n);
if (n.parentNode)
n.remove();
});
let ref = anchor;
for (let i = next.length - 1;i >= 0; i--) {
const node = next[i];
if (node.parentNode !== ref.parentNode)
ref.parentNode?.insertBefore(node, ref);
if (node._mounts)
node._mounts.forEach((fn) => fn());
ref = node;
}
currentNodes = next;
});
effect();
el._cleanups.add(() => dispose(effect));
onUnmount(() => dispose(effect));
} else {
const node = ensureNode(c);
el.appendChild(node);
if (node._mounts)
node._mounts.forEach((fn) => fn());
}
};
append(children);
return el;
};
var render = (renderFn) => {
const cleanups = new Set;
const previousOwner = activeOwner;
const previousEffect = activeEffect;
const container = doc.createElement("div");
container.style.display = "contents";
container.setAttribute("role", "presentation");
activeOwner = { _cleanups: cleanups };
activeEffect = null;
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 : doc.createTextNode(String(result == null ? "" : result)));
}
};
try {
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
} finally {
activeOwner = previousOwner;
activeEffect = previousEffect;
}
return {
_isRuntime: true,
container,
destroy: () => {
cleanups.forEach((fn) => fn());
cleanupNode(container);
container.remove();
}
};
};
var when = (cond, SIP, NOP = null) => {
const anchor = doc.createTextNode("");
const root = h("div", { style: "display:contents" }, [anchor]);
let currentView = null;
watch(() => !!(isFunc(cond) ? cond() : cond), (show) => {
if (currentView) {
currentView.destroy();
currentView = null;
}
const content = show ? SIP : NOP;
if (content) {
currentView = render(() => isFunc(content) ? content() : content);
root.insertBefore(currentView.container, anchor);
}
});
onUnmount(() => currentView?.destroy());
return root;
};
var fx = ({ name, duration = 200, scale, slide, rotate, blur }, child) => {
const el = typeof child === "function" ? child() : child;
if (!(el instanceof Node))
return el;
if (name) {
el.style.animation = `${name}-in ${duration}ms`;
return el;
}
const hasTransform = scale || slide || rotate || blur;
const initialTransform = [
scale ? "scale(0.95)" : "",
slide ? "translateY(-10px)" : "",
rotate ? "rotate(-2deg)" : ""
].filter(Boolean).join(" ");
el.style.transition = `all ${duration}ms ease`;
el.style.opacity = "0";
if (hasTransform)
el.style.transform = initialTransform;
if (blur)
el.style.filter = "blur(4px)";
requestAnimationFrame(() => {
el.style.opacity = "1";
if (hasTransform)
el.style.transform = "none";
if (blur)
el.style.filter = "none";
});
return el;
};
var each = (src, itemFn, keyField) => {
const anchor = doc.createTextNode("");
const root = h("div", { style: "display:contents" }, [anchor]);
let cache = new Map;
watch(() => (isFunc(src) ? src() : src) || [], (items) => {
const nextCache = new Map;
const nextOrder = [];
const newItems = items || [];
for (let i = 0;i < newItems.length; i++) {
const item = newItems[i];
const key = keyField ? item?.[keyField] ?? i : item?.id ?? i;
let view = cache.get(key);
if (!view)
view = render(() => itemFn(item, i));
else
cache.delete(key);
nextCache.set(key, view);
nextOrder.push(view);
}
cache.forEach((view) => view.destroy());
let lastRef = anchor;
for (let i = nextOrder.length - 1;i >= 0; i--) {
const view = nextOrder[i];
const node = view.container;
if (node.nextSibling !== lastRef)
root.insertBefore(node, lastRef);
lastRef = node;
}
cache = nextCache;
});
return root;
};
var router = (routes) => {
const getHash = () => window.location.hash.slice(1) || "/";
const path = $(getHash());
const handler = () => path(getHash());
window.addEventListener("hashchange", handler);
onUnmount(() => window.removeEventListener("hashchange", handler));
const hook = h("div", { class: "router-hook" });
let currentView = null;
watch([path], () => {
const cur = path();
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];
});
router.params(params);
currentView = render(() => isFunc(route.component) ? route.component(params) : route.component);
hook.replaceChildren(currentView.container);
}
});
return hook;
};
router.params = $({});
router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
router.back = () => window.history.back();
router.path = () => window.location.hash.replace(/^#/, "") || "/";
var req = ({ url, method = "GET", headers = {} }) => {
const loading = $(false);
const error = $(null);
const data = $(null);
let controller = null;
let timeoutId = null;
const run = async (body = null) => {
controller?.abort();
clearTimeout(timeoutId);
controller = new AbortController;
timeoutId = setTimeout(() => controller.abort(), 1e4);
loading(true);
error(null);
try {
const isFormData = body instanceof FormData;
const res = await fetch(url, {
method,
headers: isFormData ? headers : { "Content-Type": "application/json", ...headers },
body: isFormData ? body : body ? JSON.stringify(body) : undefined,
signal: controller.signal
});
const text = await res.text();
const json = text ? JSON.parse(text) : null;
if (!res.ok)
throw new Error(json?.message || res.statusText);
data(json);
return json;
} catch (e) {
if (e.name !== "AbortError")
error(e.message);
throw e;
} finally {
loading(false);
clearTimeout(timeoutId);
controller = null;
timeoutId = null;
}
};
const abort = () => controller?.abort();
return { run, abort, loading, error, data };
};
var mount = (comp, target) => {
const t = typeof target === "string" ? doc.querySelector(target) : target;
if (!t)
return;
if (MOUNTED_NODES.has(t))
MOUNTED_NODES.get(t).destroy();
const inst = render(isFunc(comp) ? comp : () => comp);
t.replaceChildren(inst.container);
MOUNTED_NODES.set(t, inst);
return inst;
};
var SigPro = Object.freeze({ $, $$, watch, h, when, each, fx, router, req, mount, batch });
if (typeof window !== "undefined") {
Object.assign(window, SigPro);
"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((tag) => {
window[tag] = (props, children) => h(tag, props, children);
});
}
export {
when,
watch,
router,
req,
mount,
h,
fx,
each,
batch,
$$,
$
};

File diff suppressed because one or more lines are too long

78
dist/sigpro.grid.js vendored Normal file

File diff suppressed because one or more lines are too long

646
dist/sigpro.js vendored

File diff suppressed because one or more lines are too long

1
dist/sigpro.locale.js vendored Normal file
View 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

File diff suppressed because one or more lines are too long

1
dist/sigpro.router.js vendored Normal file
View 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

File diff suppressed because one or more lines are too long

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
View 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};

View File

View File

@@ -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">Finegrained signals update exactly what changes. No VDOM 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">ULTRATHIN</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">COMPILERFREE</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>HighEfficiency 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>&lt;div&gt;Hello&lt;/div&gt;</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 (VDOM / Parser)</td><td><strong>Zero</strong></td></tr>
<tr><td>Reactivity</td><td>Componentwide</td><td><strong>Atomic (Nodelevel)</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 (~3090KB) 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 highres icon.</li>
</ul>
| Feature | Standard HTML / JSX | SigPro Functional | <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>
| **Syntax** | `<div>Hello</div>` | `Div("Hello")` | <p><code>div()</code>, <code>button()</code>, <code>span()</code>… These aren't just wrappers; they are preoptimized 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>
| **Processing** | Parse → Diff → Patch | Direct API Call |
| **Overhead** | High (V-DOM / Parser) | **Zero** |
| **Reactivity** | Component-wide | **Atomic (Node-level)** |
### Less Code, More Power <h4 class="text-xl font-semibold mt-6 mb-2">2. The "NoBundle" Bundle</h4>
By sharing a miniscule runtime, your final application bundle is **infinitely smaller**. <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". <h4 class="text-xl font-semibold mt-6 mb-2">3. Shared Runtime</h4>
* **Solid/Svelte:** You still depend on a compilation step that generates extra boilerplate. <p>All components share the same atomic engine. One signal can update a single character in a paragraph across 100 components without ever reevaluating the component functions themselves.</p>
* **SigPro:** You ship **Pure Vanilla JS**. The runtime is so small that it often costs less than a single high-res icon. </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 ultrafast, modern apps with <strong>True Vanilla Performance</strong>.</p></div></div></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="text-center py-10 opacity-30 font-mono text-xs tracking-widest uppercase">Precision Reactive Engine — NatxoCC</div> <div class="text-center py-10 opacity-30 font-mono text-xs tracking-widest uppercase">Precision Reactive Engine — NatxoCC</div>

View File

@@ -2,19 +2,23 @@
* **Introduction** * **Introduction**
* [Installation](install.md) * [Installation](install.md)
* [Vite Plugin](vite/plugin.md) * [Router](router.md)
* **API Reference** * **API Reference**
* [Quick Start](api/quick.md) * [Quick Start](api/quick.md)
* [Signals & Proxies](api/signal.md) * [$ignal](api/signal.md)
* [watch](api/watch.md) * [watch](api/watch.md)
* [when](api/when.md) * [when](api/when.md)
* [each](api/each.md) * [each](api/each.md)
* [router](api/router.md)
* [fx](api/fx.md)
* [req](api/req.md)
* [mount](api/mount.md) * [mount](api/mount.md)
* [h](api/h.md) * [h](api/h.md)
* **Concepts**
* [Tags](api/tags.md) * [Tags](api/tags.md)
* [Global Store](api/global.md) * [Global Store](api/global.md)
* [JSX Style](api/jsx.md) * [JSX Style](api/jsx.md)
* [HTML converter](convert.md)
* [UI](ui.md)
* **UI**
* [WIP]

View File

@@ -8,7 +8,7 @@ The `each` function is a highperformance keyed list renderer. It maps a react
each( each(
source: Signal<any[]> | (() => any[]) | any[], source: Signal<any[]> | (() => any[]) | any[],
itemFn: (item: any, index: number) => Node | (() => Node), itemFn: (item: any, index: number) => Node | (() => Node),
keyFn?: (item: any, index: number) => string | number keyField?: string
): HTMLElement ): HTMLElement
``` ```
@@ -16,7 +16,7 @@ each(
| :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- |
| **`source`** | `Signal`, `() => any[]`, or `any[]` | Yes | The reactive array to iterate over. | | **`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. | | **`itemFn`** | `(item, index) => Node` | Yes | Returns a DOM node (or a function that returns a node) for each item. |
| **`keyFn`** | `(item, index) => string/number` | No | Extracts a unique key. Default: `item?.id ?? index`. | | **`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. **Returns:** A `div` with `style="display: contents"` that contains the live list.
@@ -26,7 +26,7 @@ each(
### 1. Basic Keyed List (Recommended) ### 1. Basic Keyed List (Recommended)
Always provide a unique `id` as the key. This allows SigPro to reuse DOM nodes when the list is reordered or filtered. 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 ```javascript
const users = $([ const users = $([
@@ -37,14 +37,14 @@ const users = $([
ul({ class: "list" }, [ ul({ class: "list" }, [
each(users, each(users,
(user) => li({ class: "p-2" }, user.name), (user) => li({ class: "p-2" }, user.name),
(user) => user.id // stable unique key "id" // ← use property "id" as stable key
) )
]); ]);
``` ```
### 2. Automatic Key (Simple Lists) ### 2. Automatic Key (Simple Lists)
If you omit the `keyFn`, `each` defaults to `item?.id ?? index`. For primitive arrays or objects without an `id`, the index is used. If you omit the `keyField`, `each` defaults to `item?.id ?? index`. For primitive arrays or objects without an `id`, the index is used.
```javascript ```javascript
const tags = $(["Tech", "JS", "Web"]); const tags = $(["Tech", "JS", "Web"]);
@@ -55,7 +55,20 @@ div({ class: "flex gap-1" }, [
]); ]);
``` ```
### 3. Dynamic Content Using Functions ### 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 reexecuted every time the items data changes (but the node is reused). If your `itemFn` returns a **function**, that function is reexecuted every time the items data changes (but the node is reused).
@@ -69,11 +82,11 @@ each(todos,
input({ type: "checkbox", checked: () => todo.done, onInput: e => todo.done = e.target.checked }), input({ type: "checkbox", checked: () => todo.done, onInput: e => todo.done = e.target.checked }),
span(() => todo.done ? s(todo.text) : todo.text) span(() => todo.done ? s(todo.text) : todo.text)
]), ]),
(todo) => todo.id "id"
); );
``` ```
### 4. Source as a Plain Array or Function ### 5. Source as a Plain Array or Function
`source` can be a plain array (nonreactive) or a function that returns an array it will still react to changes if signals are read inside the function. `source` can be a plain array (nonreactive) or a function that returns an array it will still react to changes if signals are read inside the function.
@@ -86,7 +99,7 @@ const filteredTodos = () => {
return all; return all;
}; };
each(filteredTodos, (todo) => li(todo.text), (todo) => todo.id); each(filteredTodos, (todo) => li(todo.text), "id");
``` ```
--- ---
@@ -95,7 +108,7 @@ each(filteredTodos, (todo) => li(todo.text), (todo) => todo.id);
When the `source` changes, `each`: When the `source` changes, `each`:
1. **Compares keys** between the old and new items using the `keyFn`. 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. 2. **Reuses existing DOM nodes** for keys that stay the same.
3. **Moves nodes** if order changed (no recreation). 3. **Moves nodes** if order changed (no recreation).
4. **Creates new nodes** for new keys. 4. **Creates new nodes** for new keys.
@@ -107,8 +120,8 @@ When the `source` changes, `each`:
## Performance Tips ## Performance Tips
- **Stable keys** Use a real `id` (like a database primary key). Avoid `Math.random()` or array `index` for lists that can be reordered. - **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 a local state, using a stable key ensures that state is preserved even when the list is filtered or sorted. - **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. - **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.
--- ---
@@ -151,7 +164,7 @@ const App = () =>
span(`${item.name} $${item.price}`), span(`${item.name} $${item.price}`),
button({ onClick: () => removeItem(item.id) }, "X") button({ onClick: () => removeItem(item.id) }, "X")
]), ]),
(item) => item.id "id"
) )
) )
]); ]);

View File

@@ -1,182 +0,0 @@
# Animation Helper: `fx( )`
The `fx` function applies simple **enter animations** to DOM elements. You can either use a predefined CSS keyframes animation or declare inline transition effects (scale, slide, rotate, blur). It is designed to be used when dynamically creating elements especially inside `when` or `each` branches.
## Function Signature
```typescript
fx(
options: {
name?: string; // CSS keyframes animation name (will append '-in')
duration?: number; // Animation duration in ms (default: 200)
scale?: boolean; // Start with scale(0.95) → none
slide?: boolean; // Start with translateY(-10px) → none
rotate?: boolean; // Start with rotate(-2deg) → none
blur?: boolean; // Start with blur(4px) → none
},
child: Node | (() => Node)
): Node
```
| Parameter | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **`options`** | `object` | Yes | Animation configuration. |
| **`options.name`** | `string` | No | Name of a CSS `@keyframes` animation. The actual animation name becomes `${name}-in`. |
| **`options.duration`** | `number` | No | Duration in milliseconds (default `200`). |
| **`options.scale`** | `boolean` | No | Add a scale transform from `0.95` to `none`. |
| **`options.slide`** | `boolean` | No | Add a vertical slide from `translateY(-10px)` to `none`. |
| **`options.rotate`** | `boolean` | No | Add a small rotation from `rotate(-2deg)` to `none`. |
| **`options.blur`** | `boolean` | No | Add a blur filter from `blur(4px)` to `none`. |
| **`child`** | `Node` or `() => Node` | Yes | The element to animate. If a function is passed, it is called to obtain the node. |
**Returns:** The same DOM node (or the child if it is not a `Node`), after applying the animation setup.
---
## Usage Patterns
### 1. Named CSS Keyframes Animation
Define a `@keyframes` rule in your CSS, for example:
```css
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
```
Then apply it with `fx`:
```javascript
const MyComponent = () =>
fx({ name: "fade", duration: 300 },
div("I will fade in")
);
```
> The animation name used is `${name}-in`. In this example: `fade-in`.
### 2. Inline Transition (Scale + Opacity)
No CSS keyframes needed. The element starts with `opacity: 0` and `transform: scale(0.95)`, then transitions to `opacity: 1` and `transform: none`.
```javascript
fx({ scale: true, duration: 200 },
button({ onClick: () => alert("Hi") }, "Click me")
);
```
### 3. Combining Multiple Effects
You can combine `scale`, `slide`, `rotate`, and `blur` at the same time.
```javascript
fx({ scale: true, slide: true, blur: true, duration: 250 },
div({ class: "card" }, "Smooth enter")
);
```
### 4. Using with `when` (Conditional Rendering)
Wrap the branch content with `fx` to animate entering elements.
```javascript
when(show,
() => fx({ slide: true },
div("This slides in when visible")
)
);
```
### 5. Using a Function as Child
If the element is created inside a function (e.g. to avoid recreation until needed), pass a function that returns the node.
```javascript
fx({ scale: true },
() => div("Lazy created and then animated")
);
```
---
## What Happens Under the Hood
### With `name` (CSS animation)
- Sets `el.style.animation = `${name}-in ${duration}ms``.
- The element animates according to your keyframes.
- No further inline style changes are applied.
### Without `name` (transition effects)
- Sets `el.style.transition = `all ${duration}ms ease``.
- Sets initial `opacity: 0`.
- Applies initial transforms (`scale`, `slide`, `rotate`) if selected.
- Applies initial `filter: blur(4px)` if `blur: true`.
- In the next animation frame (via `requestAnimationFrame`), sets:
- `opacity: 1`
- `transform: none`
- `filter: none`
- The element transitions smoothly from the start state to the final state.
> **Important:** The element must be **in the DOM** when the animation starts. `fx` does **not** automatically mount the node you must already have appended it or be about to mount it. In practice, when you call `fx` inside a component that is being mounted, the element will be added to the DOM shortly after, and the animation runs correctly.
---
## Complete Example
```javascript
const App = () =>
div([
fx({ name: "fade", duration: 400 },
h1("Welcome to SigPro")
),
fx({ scale: true, slide: true, duration: 250 },
button({ onClick: () => alert("Animated!") }, "Animated button")
)
]);
mount(App, "#app");
```
With accompanying CSS:
```css
@keyframes fade-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
```
---
## Notes
- `fx` is **not** required for basic reactivity it is purely a visual helper for enter animations.
- For exit animations (when an element is removed), use CSS transitions on the element itself combined with `when` or consider adding a wrapper that delays removal. SigPro does not include builtin exit animations.
- The function returns the same node you passed, so you can inline it inside `h` or tag helpers:
```javascript
div([
fx({ scale: true }, span("Hello"))
])
```
- If `child` is not a DOM node (e.g., a string or number), `fx` returns it unchanged no animation is applied.
---
## Summary
| Option | Effect |
| :--- | :--- |
| `name` | Uses `@keyframes ${name}-in` CSS animation. |
| `duration` | Controls animation/transition length (ms). |
| `scale` | Start scale `0.95``none`. |
| `slide` | Start `translateY(-10px)``none`. |
| `rotate` | Start `rotate(-2deg)``none`. |
| `blur` | Start `blur(4px)``none`. |
Combine options to create smooth, modern entrance effects without writing extra CSS.

View File

@@ -2,6 +2,8 @@
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.** 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) ## 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.
@@ -12,7 +14,7 @@ Creating a dedicated file allows you to export only what you need. This modulari
```javascript ```javascript
// auth.js // auth.js
import { $ } from 'sigpro'; // or just rely on global `$` after import import { $ } from 'sigpro';
// A simple global signal // A simple global signal
export const user = $({ name: "Guest", loggedIn: false }); export const user = $({ name: "Guest", loggedIn: false });
@@ -109,6 +111,7 @@ export const filteredTodos = $(() => {
```javascript ```javascript
// components/TodoApp.js // components/TodoApp.js
import 'sigpro';
import { todos, filter, addTodo, toggleTodo, filteredTodos } from "../store/todos.js"; import { todos, filter, addTodo, toggleTodo, filteredTodos } from "../store/todos.js";
const TodoApp = () => const TodoApp = () =>

View File

@@ -2,6 +2,8 @@
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. 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 ## Function Signature
```typescript ```typescript
@@ -120,23 +122,23 @@ h('svg', { width: 100, height: 100 }, [
--- ---
## `h` vs Global Tag Helpers ## `h` vs Tag Helpers
| Feature | `h('div', ...)` | `div(...)` | | Feature | `h('div', ...)` | `div(...)` (tag helper) |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| **Dynamic tag names** | ✅ `h(tagName, ...)` | ❌ Must know tag name at write time | | **Dynamic tag names** | ✅ `h(tagName, ...)` | ❌ Must know tag name at write time |
| **Explicit style** | More verbose | Cleaner, DSLlike | | **Explicit style** | More verbose | Cleaner, DSLlike |
| **Tree shaking** | Same | Same (helpers are generated once) | | **Availability** | Import or global | Import or global (same) |
| **Performance** | Identical | Identical (helpers call `h` internally) | | **Performance** | Identical | Identical (helpers call `h` internally) |
> **Recommendation:** Use global 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, ...)`). > **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 ## Complete Example
```javascript ```javascript
import { $, h, mount } from 'sigpro'; import 'sigpro';
const dynamicTag = $('h1'); const dynamicTag = $('h1');
@@ -155,5 +157,5 @@ mount(App, '#app');
- `h` is the lowlevel DOM builder used internally by all tag helpers. - `h` is the lowlevel DOM builder used internally by all tag helpers.
- It supports reactive attributes, reactive children, twoway binding, event listeners, and SVG. - It supports reactive attributes, reactive children, twoway binding, event listeners, and SVG.
- Use `h` directly when you need a **dynamic tag name**; otherwise, prefer the convenient global helpers. - 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. - Components written with `h` are fully reactive and automatically cleaned up.

View File

@@ -1,15 +1,4 @@
Aquí tienes el archivo `h.md` actualizado, que ahora incluye: # Hyperscript & Tag Helpers & JSX Style
- La función `h` (hyperscript)
- Los helpers globales de etiquetas (lowercase)
- Una nueva sección sobre **JSX con SigPro** (configuración, ejemplos y alternativas como `htm`).
El documento está en inglés, como los originales, y listo para integrarse en tu documentación.
---
```markdown
# Hyperscript & Tag Helpers
SigPro provides two complementary ways to create DOM elements: SigPro provides two complementary ways to create DOM elements:
@@ -20,135 +9,6 @@ Both are reactive, autocleanup, and support SVG, events, twoway binding, a
--- ---
## `h( )` Hyperscript Function
The `h` function is the **core DOM builder** of SigPro. Use it directly when you need a dynamic tag name or prefer an explicit style.
### 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. |
| **`children`** | `any` | Optional. Text, nodes, arrays, or reactive functions. |
**Returns:** A DOM node (or array of nodes when the tag is a component that returns an array).
### Usage Examples
```js
// Basic element
h('div', {}, 'Hello world');
// With attributes and events
h('button', { class: 'btn', onclick: () => alert('clicked') }, 'Click me');
// Nested children
h('div', { class: 'container' }, [
h('h1', {}, 'Title'),
h('p', {}, 'Paragraph')
]);
// Reactive child (function)
const count = $(0);
h('div', {}, [
h('p', {}, () => `Count: ${count()}`),
h('button', { onclick: () => count(count() + 1) }, '+1')
]);
// Reactive attribute
const theme = $('dark');
h('div', { class: () => `box ${theme()}` }, 'Themed box');
// Two-way binding on input
const name = $('');
h('input', { type: 'text', value: name, placeholder: 'Your name' });
h('p', {}, () => `Hello, ${name()}`);
// Component as tag
const Button = (props, { children }) =>
h('button', { class: 'btn', onclick: props.onClick }, children);
h(Button, { onClick: () => alert('clicked') }, 'Click me');
// SVG (auto-namespace)
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 }` direct DOM node access. |
| **`onEvent`** | Any prop starting with `on` (e.g., `onClick`) is an event listener autoremoved on cleanup. |
| **`value` / `checked`** | When a signal is passed, creates twoway binding for form elements. |
| **`class`** | Use `class` (not `className`). Accepts a string or reactive function. |
---
## Global Tag Helpers (Lowercase)
When you import SigPro (either via `import 'sigpro'` or the CDN), it automatically injects a helper function for **every standard HTML tag** directly onto `window`. These helpers are **lowercase** and work exactly like `h`, but with a cleaner syntax.
### Available Helpers
All standard HTML5 tags: `div`, `span`, `p`, `section`, `nav`, `header`, `footer`, `h1``h6`, `ul`, `ol`, `li`, `button`, `a`, `input`, `form`, `table`, `svg`, `circle`, etc.
### Usage Examples
```js
// Instead of h('div', ...)
div({ class: 'container' }, 'Content');
// Children only (skip props)
section([
h2('Title'),
p('Paragraph')
]);
// Reactive attribute
const theme = $('light');
div({ class: () => `app-${theme()}` }, 'Themed');
// Two-way binding
const search = $('');
input({ type: 'text', value: search, placeholder: 'Search...' });
p(() => `You typed: ${search()}`);
// Dynamic children
const count = $(0);
div([
p(() => `Count: ${count()}`),
button({ onClick: () => count(count() + 1) }, '+1')
]);
```
### Complete Example
```js
const App = () =>
div({ class: 'app' }, [
h1('Welcome'),
input({ value: name, placeholder: 'Your name' }),
p(() => `Hello, ${name() || 'stranger'}!`),
button({ onClick: () => alert('Hi') }, 'Click me')
]);
mount(App, '#app');
```
---
## JSX with SigPro ## JSX with SigPro
SigPro works seamlessly with JSX. You can use JSX as a compiletime syntax sugar for `h` calls. SigPro works seamlessly with JSX. You can use JSX as a compiletime syntax sugar for `h` calls.
@@ -318,6 +178,3 @@ mount(App, '#app');
| **`htm`** | Optional | `` html`<div>...</div>` `` | Buildless but HTMLlike syntax | | **`htm`** | Optional | `` html`<div>...</div>` `` | Buildless but HTMLlike syntax |
> **Tip:** All approaches are fully reactive, support twoway binding, events, SVG, and automatic cleanup. Choose the one that fits your workflow. > **Tip:** All approaches are fully reactive, support twoway binding, events, SVG, and automatic cleanup. Choose the one that fits your workflow.
```
```

View File

@@ -17,6 +17,8 @@ mount(component: Function | Node, target: string | HTMLElement): RuntimeObject
- `container`: The actual DOM element created by the renderer. - `container`: The actual DOM element created by the renderer.
- `destroy()`: A method to completely unmount and clean up the application. - `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.
--- ---
## Usage Patterns ## Usage Patterns
@@ -117,7 +119,8 @@ When `destroy()` is called (or when a new mount replaces an old one), everything
## Complete Example ## Complete Example
```javascript ```javascript
import { $, mount, div, h1, button } from 'sigpro'; import { $, mount } from 'sigpro';
const App = () => { const App = () => {
const count = $(0); const count = $(0);
@@ -145,4 +148,4 @@ setTimeout(() => runtime.destroy(), 10000);
| Manual destruction | `const app = mount(App, '#app'); app.destroy();` | | Manual destruction | `const app = mount(App, '#app'); app.destroy();` |
| Autoreplace on same target | Just call `mount` again SigPro handles cleanup. | | Autoreplace on same target | Just call `mount` again SigPro handles cleanup. |
> **Note:** The function name is `mount` (lowercase). It is exported from SigPro and also available globally after importing the library. The target must exist in the DOM at the time of mounting. > **Note:** The target must exist in the DOM at the time of mounting.

View File

@@ -1,15 +1,6 @@
# SigPro 1.2.18 Complete API Reference # SigPro Complete API Reference
SigPro is a **RealDOM first** reactive microframework. No virtual DOM, no diffing overhead it updates the DOM directly with surgical precision. Builtin automatic cleanup prevents memory leaks, and the API is designed to be both tiny and powerful. ## Core Reactivity
```javascript
import { $, $$, watch, h, when, each, fx, router, req, mount, batch } from 'sigpro'
// or use globally as window.$ etc.
```
---
## 🔁 Core Reactivity
### `$(value, localStorageKey?)` Signal & Computed ### `$(value, localStorageKey?)` Signal & Computed
@@ -35,24 +26,6 @@ count(5) // triggers log: count=5, double=10
--- ---
### `$$(object)` Deep Reactive Proxy
Makes a plain object (and all nested objects) deeply reactive. Any property access is tracked, any mutation triggers updates.
```javascript
const state = $$({ user: { name: 'Alice', age: 30 }, items: [1,2,3] })
watch(() => {
console.log(state.user.name) // tracks `user.name`
})
state.user.name = 'Bob' // triggers the effect
```
> **Note**: `$$` caches proxies per original object, so calling `$$` multiple times on the same object returns the same proxy.
---
### `watch(source, callback?)` Reactive Effect ### `watch(source, callback?)` Reactive Effect
Two modes: Two modes:
@@ -80,7 +53,7 @@ watch([count, double], ([newCount, newDouble]) => {
--- ---
## 🧱 Components & DOM (Hyperscript) ## Components & DOM (Hyperscript)
### `h(tag, props, children)` Create DOM Nodes ### `h(tag, props, children)` Create DOM Nodes
@@ -94,7 +67,7 @@ The universal builder. `props` can be omitted. Children can be strings, numbers,
| Twoway binding | `value: mySignal` (works on `input`, `textarea`, `select`) | | Twoway binding | `value: mySignal` (works on `input`, `textarea`, `select`) |
| Refs | `ref: (el) => ...` or `ref: { current: null }` | | Refs | `ref: (el) => ...` or `ref: { current: null }` |
| SVG support | tag names like `svg`, `circle`, `path` sets correct namespace | | SVG support | tag names like `svg`, `circle`, `path` sets correct namespace |
| Dangerous URL sanitising | `href` / `src` with `javascript:` or `data:` are blocked → `'#'` | | 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 reexecuted and the DOM patched automatically: **Dynamic children** pass a function as a child, it will be reexecuted and the DOM patched automatically:
@@ -106,20 +79,13 @@ h('div', {}, [
### Tag shortcuts ### Tag shortcuts
SigPro defines **all standard HTML5 tags** as PascalCase globals (when run in browser) and also exports them as named exports. Example: Tag helpers **are exported** from the core.
```javascript
Div({ class: 'container' }, [
H1({}, 'Title'),
Button({ onClick: () => alert('hi') }, 'Click me')
])
```
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`. 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 ## Flow Control Components
### `when(condition, thenComponent, elseComponent?)` ### `when(condition, thenComponent, elseComponent?)`
@@ -128,8 +94,8 @@ Reactive conditional rendering. `condition` can be a boolean, a signal, or any f
```javascript ```javascript
when( when(
() => user.loggedIn(), () => user.loggedIn(),
() => Div({}, 'Welcome back!'), () => div({}, 'Welcome back!'),
() => Button({ onClick: () => login() }, 'Login') () => button({ onClick: () => login() }, 'Login')
) )
``` ```
@@ -152,19 +118,7 @@ When the array changes, elements are added, removed, or reordered with minimal D
--- ---
## 💥 Effects & Lifecycle ## Batch
### `onUnmount(fn)`
Inside a component (function called from `h`), registers a cleanup function that runs when that component is removed from the DOM.
```javascript
const Timer = () => {
const interval = setInterval(() => console.log('tick'), 1000)
onUnmount(() => clearInterval(interval))
return Div({}, 'Timer running')
}
```
### `batch(fn)` ### `batch(fn)`
@@ -178,107 +132,7 @@ batch(() => {
}) })
``` ```
### `untrack(fn)` ## Mounting `mount(component, target)`
Run a function without tracking any signal reads.
```javascript
const logCount = () => {
untrack(() => console.log('count is', count()))
}
```
---
## ✨ Animations `fx(options, child)`
Applies smooth enter animations (CSS transitions / keyframes). Returns the modified element.
```javascript
fx({ name: 'fade', duration: 300 },
Div({}, 'Hello')
)
```
**Options**
- `name` uses predefined keyframes `${name}-in` (you must define them in your CSS)
- `duration` in ms (default 200)
- `scale` adds `scale(0.95)``none`
- `slide` adds `translateY(-10px)``none`
- `rotate` adds `rotate(-2deg)``none`
- `blur` adds `blur(4px)``none`
If `name` is given, it sets `animation: ${name}-in ${duration}ms`. Otherwise it applies a smooth transition from the initial transform/filter to the final state.
---
## 🧭 Router `router(routes)`
Hashbased SPA router. Returns a DOM node that renders the current route.
```javascript
const routes = [
{ path: '/', component: () => Div({}, 'Home') },
{ path: '/user/:id', component: (params) => Div({}, `User ${params.id}`) },
{ path: '*', component: () => Div({}, '404') }
]
const App = () => Div({}, [
A({ href: '#/' }, 'Home'),
A({ href: '#/user/42' }, 'User 42'),
router(routes)
])
```
**API**
| Method | Description |
|--------|-------------|
| `router.params()` | Returns a reactive signal of current route params (e.g., `{ id: '42' }`). |
| `router.to(path)` | Navigate to a new hash (e.g., `router.to('/user/5')`). Prepend `#` automatically. |
| `router.back()` | Go back in history. |
| `router.path()` | Returns current hash path without `#` (e.g., `/user/42`). |
---
## 🌐 HTTP Requests `req(config)`
Creates a reactive request controller with builtin loading/error/data signals and abort support.
```javascript
const fetchUser = req({ url: '/api/user/1', method: 'GET' })
// start the request
fetchUser.run().catch(console.error)
// reactively display state
watch(() => {
if (fetchUser.loading()) console.log('loading...')
if (fetchUser.error()) console.error(fetchUser.error())
if (fetchUser.data()) console.log(fetchUser.data())
})
// abort if needed
fetchUser.abort()
```
**Options**
- `url` (required)
- `method` (default `'GET'`)
- `headers` (object, default `{}`)
**Return value**
- `run(body?)` initiates the request, returns a promise.
- `abort()` aborts the current request (AbortController).
- `loading` signal (boolean)
- `error` signal (`null` or error message)
- `data` signal (`null` or parsed JSON)
> **Note**: Automatically sets `Content-Type: application/json` unless `body` is a `FormData`. Timeout after 10 seconds aborts the request.
---
## 🚀 Mounting `mount(component, target)`
Clears the target element and mounts the application. Returns the runtime instance (which has a `.destroy()` method). Clears the target element and mounts the application. Returns the runtime instance (which has a `.destroy()` method).
@@ -292,7 +146,7 @@ If you mount again on the same target, the previous instance is automatically de
--- ---
## 🧹 Global Cleanup & Memory ## Global Cleanup & Memory
SigPro tracks every effect, DOM event listener, and nested component. When a component is unmounted: SigPro tracks every effect, DOM event listener, and nested component. When a component is unmounted:
- All its effects are disposed. - All its effects are disposed.
@@ -304,50 +158,20 @@ You never need to manually clean up just write reactive code.
--- ---
## 📦 Full Example Counter with Persistence ## Full Example Counter with Persistence
```javascript ```javascript
import { $, watch, h, mount } from 'sigpro' import { $, mount } from 'sigpro';
const count = $(0, 'counter') // persists in localStorage const count = $(0, 'counter') // persists in localStorage
const App = () => const App = () =>
Div({ class: 'counter' }, [ div({ class: 'counter' }, [
H1({}, () => `Count: ${count()}`), h1({}, () => `Count: ${count()}`),
Button({ onClick: () => count(count() + 1) }, '+'), button({ onClick: () => count(count() + 1) }, '+'),
Button({ onClick: () => count(count() - 1) }, '-'), button({ onClick: () => count(count() - 1) }, '-'),
Button({ onClick: () => count(0) }, 'Reset') button({ onClick: () => count(0) }, 'Reset')
]) ])
mount(App, '#app') mount(App, '#app')
``` ```
---
## 🔧 Customising the API (Renaming)
You can rename everything in one line:
```javascript
import { $ as signal, watch as effect, h as element, mount as render } from 'sigpro'
```
Or assign globally:
```javascript
window.myReactive = $
```
All functions are also exposed on the global `window` object when included via `<script>`.
---
## 📜 License & Version
Current version: **1.2.18**
Released under MIT.
Zero dependencies, ~3KB gzipped.
---
> **Need legacy IE support?** Not supported requires modern JavaScript (Proxy, WeakMap, etc.).

View File

@@ -1,182 +0,0 @@
# HTTP Requests: `req( )`
The `req` function creates a **reactive HTTP request controller**. It returns signals for `loading`, `error`, and `data`, plus a `run` method to execute the request and an `abort` method to cancel it. All signals update automatically as the request progresses.
## Function Signature
```typescript
req(config: {
url: string;
method?: string; // default: 'GET'
headers?: Record<string, string>;
}): {
run: (body?: any) => Promise<any>;
abort: () => void;
loading: Signal<boolean>;
error: Signal<string | null>;
data: Signal<any | null>;
}
```
| Parameter | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **`url`** | `string` | Yes | The endpoint to call. |
| **`method`** | `string` | No | HTTP method (`'GET'`, `'POST'`, etc.). Default `'GET'`. |
| **`headers`** | `object` | No | Custom headers (will be merged with defaults). |
**Returns:** A controller object with reactive signals and methods.
---
## Usage Patterns
### 1. Basic GET Request
```javascript
const users = req({ url: '/api/users' });
// Start the request
users.run().catch(console.error);
// React to the response
watch(() => {
if (users.loading()) console.log('Loading...');
if (users.error()) console.error(users.error());
if (users.data()) console.log('Data:', users.data());
});
```
### 2. POST Request with Body
```javascript
const createUser = req({ url: '/api/users', method: 'POST' });
const handleSubmit = async (formData) => {
try {
await createUser.run(formData);
alert('User created!');
} catch (err) {
// Error already in createUser.error()
}
};
```
### 3. Aborting a Request
```javascript
const search = req({ url: '/api/search' });
// Abort if the user types again quickly
let timeout;
input({ onInput: (e) => {
clearTimeout(timeout);
search.abort(); // cancel previous in-flight request
timeout = setTimeout(() => search.run({ q: e.target.value }), 300);
}});
```
### 4. Reactive UI with Signals
```javascript
const profile = req({ url: '/api/me' });
const App = () =>
div([
when(() => profile.loading(),
() => div("Loading...")
),
when(() => profile.error(),
() => div("Error: " + profile.error())
),
when(() => profile.data(),
() => div([
h2(profile.data().name),
p(profile.data().email)
])
)
]);
profile.run();
mount(App, '#app');
```
---
## Request Lifecycle
When you call `run(body?)`:
1. Any previous request is **aborted** automatically.
2. `loading` becomes `true`.
3. `error` is cleared.
4. A 10second timeout is set (autoabort).
5. The request is sent using `fetch`.
6. On success: `data` is set with the parsed JSON, `loading` becomes `false`.
7. On error: `error` is set with the message, `data` is cleared, `loading` becomes `false`.
> **Important:** The response body is parsed as JSON. If the response is not OK or the JSON parsing fails, `error` is set and the promise rejects.
---
## Automatic Headers
- If `body` is a plain object or array, the request automatically includes `Content-Type: application/json` (unless you override it in `headers`).
- If `body` is a `FormData` instance, the `Content-Type` is not set (browser will set it automatically with the correct boundary).
- Other headers can be added via the `headers` option.
```javascript
const upload = req({
url: '/upload',
method: 'POST',
headers: { 'X-Custom': 'value' }
});
const formData = new FormData();
formData.append('file', fileInput.files[0]);
upload.run(formData);
```
---
## Error Handling
- Network errors, timeouts, and HTTP error statuses (4xx, 5xx) all set `error` and cause the promise to reject.
- The `error` signal contains a humanreadable message.
- Abort errors (calling `abort()` or timeout) are **silent** they do not set `error` or reject the promise?
Actually, according to the source: if `e.name === 'AbortError'`, it does **not** call `error(e.message)`, but the promise still rejects with the AbortError. You can handle it with `.catch()` if needed.
---
## Complete Example
```javascript
const api = req({ url: 'https://jsonplaceholder.typicode.com/posts/1' });
const App = () =>
div({ class: "demo" }, [
when(() => api.loading(), () => p("⏳ Loading...")),
when(() => api.error(), () => p("❌ " + api.error())),
when(() => api.data(), () => div([
h2(api.data().title),
p(api.data().body)
])),
button({
onClick: () => api.run(),
disabled: () => api.loading()
}, "Fetch")
]);
mount(App, '#app');
```
---
## Summary
| Member | Type | Description |
| :--- | :--- | :--- |
| `run(body?)` | `(any) => Promise` | Starts the request. Returns a promise. |
| `abort()` | `() => void` | Cancels the current request. |
| `loading` | `Signal<boolean>` | `true` while request is in flight. |
| `error` | `Signal<string\|null>` | Contains an error message, or `null`. |
| `data` | `Signal<any\|null>` | Contains the parsed response (JSON), or `null`. |

View File

@@ -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 ## Function Signature
@@ -27,7 +27,7 @@ Creates a writable signal. It returns a function that acts as both **getter** an
<div id="demo-signal-simple"></div> <div id="demo-signal-simple"></div>
```js ```javascript
{ {
const count = $(0); const count = $(0);
const App = () => div({ class: "example" }, [ const App = () => div({ class: "example" }, [
@@ -44,7 +44,7 @@ Creates a writable signal that syncs with the browser's storage.
<div id="demo-signal-persist"></div> <div id="demo-signal-persist"></div>
```js ```javascript
{ {
const theme = $("light", "theme-persist-demo"); const theme = $("light", "theme-persist-demo");
const App = () => div([ const App = () => div([
@@ -62,7 +62,7 @@ Creates a read-only signal that updates automatically when any signal used insid
<div id="demo-signal-computed"></div> <div id="demo-signal-computed"></div>
```js ```javascript
{ {
const price = $(100); const price = $(100);
const tax = $(0.21); const tax = $(0.21);
@@ -86,7 +86,7 @@ When calling the setter, you can pass an **updater function** to access the curr
<div id="demo-signal-updater"></div> <div id="demo-signal-updater"></div>
```js ```javascript
{ {
const list = $(["A", "B"]); const list = $(["A", "B"]);
const App = () => div([ const App = () => div([
@@ -99,175 +99,181 @@ When calling the setter, you can pass an **updater function** to access the curr
--- ---
# 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. For nested objects, **compose signals** instead of using magic proxies. This gives you explicit control over reactivity and memory.
## 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
### 1. Simple Object ### 1. Simple Object
<div id="demo-dollar-simple"></div> <div id="demo-compose-simple"></div>
```js ```javascript
{ {
const state = $$({ count: 0, name: "Juan" }); const count = $(0);
watch(() => console.log(`Count is now ${state.count}`)); const name = $("Juan");
// Optionally create a derived combined state
const state = $(() => ({ count: count(), name: name() }));
const App = () => div([ const App = () => div([
p(() => `Count: ${state.count}, Name: ${state.name}`), p(() => `Count: ${count()}, Name: ${name()}`),
button({ onClick: () => state.count++ }, "Increment count"), button({ onClick: () => count(count() + 1) }, "Increment count"),
button({ onClick: () => state.name = state.name === "Juan" ? "Ana" : "Juan" }, "Toggle name") button({ onClick: () => name(name() === "Juan" ? "Ana" : "Juan") }, "Toggle name")
]); ]);
setTimeout(() => mount(App, '#demo-dollar-simple'), 50); setTimeout(() => mount(App, '#demo-compose-simple'), 50);
} }
``` ```
### 2. Deep Reactivity ### 2. Deeply Nested State
<div id="demo-dollar-deep"></div> <div id="demo-compose-deep"></div>
```js ```javascript
{ {
const user = $$({ const profileName = $("Juan");
profile: { const profileCity = $("Madrid");
name: "Juan", const profileZip = $("28001");
address: { city: "Madrid", zip: "28001" }
}
});
watch(() => user.profile.address.city, () => console.log("City changed")); // Computed derived values
const fullAddress = $(() => `${profileCity()}, ${profileZip()}`);
watch(profileCity, () => console.log("City changed to:", profileCity()));
const App = () => div([ const App = () => div([
p(() => `City: ${user.profile.address.city}`), p(() => `Name: ${profileName()}`),
button({ onClick: () => user.profile.address.city = "Barcelona" }, "Change to Barcelona") p(() => `City: ${profileCity()}`),
p(() => `Full address: ${fullAddress()}`),
button({ onClick: () => profileCity("Barcelona") }, "Change to Barcelona")
]); ]);
setTimeout(() => mount(App, '#demo-dollar-deep'), 50); setTimeout(() => mount(App, '#demo-compose-deep'), 50);
} }
``` ```
### 3. Arrays ### 3. Arrays
<div id="demo-dollar-array"></div> <div id="demo-compose-array"></div>
```js ```javascript
{ {
const todos = $$([ const todos = $([
{ id: 1, text: "Learn SigPro", done: false }, { id: 1, text: "Learn SigPro", done: false },
{ id: 2, text: "Build an app", done: false } { id: 2, text: "Build an app", done: false }
]); ]);
watch(() => todos.length, () => console.log(`You have ${todos.length} todos`)); const todoCount = $(() => todos().length);
watch(todoCount, () => console.log(`You have ${todoCount()} todos`));
const App = () => div([ const App = () => div([
ul(() => todos.map(todo => li(todo.text + (todo.done ? " ✓" : "")))), ul(() => todos().map(todo => li(todo.text + (todo.done ? " ✓" : "")))),
button({ onClick: () => todos.push({ id: Date.now(), text: "New todo", done: false }) }, "Add todo"), button({ onClick: () => todos(prev => [...prev, { id: Date.now(), text: "New todo", done: false }]) }, "Add todo"),
button({ onClick: () => todos[0].done = !todos[0].done }, "Toggle first todo") button({ onClick: () => {
const updated = [...todos()];
updated[0] = { ...updated[0], done: !updated[0].done };
todos(updated);
}}, "Toggle first todo")
]); ]);
setTimeout(() => mount(App, '#demo-dollar-array'), 50); setTimeout(() => mount(App, '#demo-compose-array'), 50);
} }
``` ```
### 4. Mixed with Signals ### 4. Complete Form Example
<div id="demo-dollar-mixed"></div> <div id="demo-compose-form"></div>
```js ```javascript
{ {
const form = $$({ const email = $("");
fields: { email: "", password: "" }, const password = $("");
isValid: $(false) const isValid = $(() => email().includes("@") && password().length > 6);
});
const canSubmit = $(() => watch(isValid, valid => console.log("Form valid:", valid));
form.fields.email.includes("@") &&
form.fields.password.length > 6
);
watch(canSubmit, valid => form.isValid(valid));
const App = () => div([ const App = () => div([
input({ type: "email", placeholder: "Email", value: () => form.fields.email, onInput: e => form.fields.email = e.target.value }), input({
input({ type: "password", placeholder: "Password", value: () => form.fields.password, onInput: e => form.fields.password = e.target.value }), type: "email",
p(() => `Form valid: ${form.isValid() ? "Yes" : "No"}`) 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-dollar-mixed'), 50); setTimeout(() => mount(App, '#demo-compose-form'), 50);
} }
``` ```
--- ---
## Key Differences: `$()` vs `$$()` ## Best Practices for Complex State
| Feature | `$()` Signal | `$$()` Reactive | ### ✅ DO: Compose signals explicitly
| :--- | :--- | :--- |
| **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 |
--- ```javascript
// Clear, predictable, and memory-safe
## When to Use Each const user = {
name: $("Juan"),
### Use `$()` when: email: $("juan@example.com"),
preferences: {
<div id="demo-use-dollar"></div> theme: $("dark"),
notifications: $(true)
```js }
{
const count = $(0);
const firstName = $("John");
const lastName = $("Doe");
const fullName = $(() => `${firstName()} ${lastName()}`);
const App = () => div([
p(() => `Count: ${count()}`),
button({ onClick: () => count(count() + 1) }, "Count up"),
p(() => `Full name: ${fullName()}`),
input({ value: firstName, placeholder: "First name" }),
input({ value: lastName, placeholder: "Last name" })
]);
setTimeout(() => mount(App, '#demo-use-dollar'), 50);
} }
// Computed values derived from composition
const userDisplay = $(() => `${user.name()} <${user.email()}>`)
``` ```
### Use `$$()` when: ### ✅ DO: Create store patterns
<div id="demo-use-dollar-dollar"></div> ```javascript
const createUserStore = () => {
const name = $("")
const email = $("")
```js const isValid = $(() => name().length > 0 && email().includes("@"))
{
const form = $$({ email: "", password: "" });
const settings = $$({ theme: "dark", notifications: true });
const App = () => div([ const actions = {
input({ placeholder: "Email", onInput: e => form.email = e.target.value }), setName: (value) => name(value),
input({ placeholder: "Password", type: "password", onInput: e => form.password = e.target.value }), setEmail: (value) => email(value),
p(() => `Email: ${form.email}, Password: ${form.password}`), reset: () => {
button({ onClick: () => settings.theme = settings.theme === "dark" ? "light" : "dark" }, "Toggle theme"), name("")
p(() => `Current theme: ${settings.theme}`) email("")
]); }
setTimeout(() => mount(App, '#demo-use-dollar-dollar'), 50); }
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(), ...) // ✅
``` ```
--- ---
@@ -275,116 +281,122 @@ $$<T extends object>(obj: T): T
## Important Notes ## Important Notes
### ✅ DO: ### ✅ DO:
```js ```javascript
// Access properties directly // Update by recreating objects for arrays
state.count = 10; todos(prev => [...prev, newTodo])
state.user.name = "Ana";
todos.push(newItem);
// Track in effects // Update objects immutably
watch(() => state.count, () => {}); const current = user()
watch(() => state.user.name, () => {}); user({ ...current, name: "Ana" })
// Track individual signals
watch(() => user.name(), () => {})
watch(() => user.email(), () => {})
``` ```
### ❌ DON'T: ### ❌ DON'T:
```js ```javascript
// Destructuring breaks reactivity // Mutate objects directly
const { count, user } = state; // ❌ count and user are not reactive user().name = "Ana" // ❌ Not reactive
// Reassigning the whole object // Mutate arrays in place
state = { count: 10 }; // ❌ Loses reactivity todos().push(newTodo) // ❌ Not reactive
// Using primitive directly // Destructure in component bodies
const count = $$(0); // ❌ Doesn't work (use $() instead) const { name, email } = user // ❌ Breaks reactivity
``` ```
--- ---
## Automatic Cleanup ## 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 ```javascript
- No manual cleanup needed // Effects are automatically disposed when components unmount
- Works with `watch`, `when`, and `each` const name = $("Juan")
watch(name, () => console.log("Name changed"))
--- // Manual cleanup if needed
const stop = watch(name, callback)
## Technical Comparison stop() // Clean up manually
```
| 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) |
--- ---
## Complete Example ## Complete Example
<div id="demo-complete"></div> <div id="demo-complete-final"></div>
```js ```javascript
{ {
const app = { // All state as explicit signals
theme: $("dark", "theme_complete"), const theme = $("dark", "theme_complete")
sidebarOpen: $(true), const sidebarOpen = $(true)
user: $$({ name: "", email: "", preferences: { notifications: true, language: "es" } }), const userName = $("")
isLoggedIn: $(() => !!app.user.name), const userEmail = $("")
login(name, email) { const notifications = $(true)
app.user.name = name; const language = $("es")
app.user.email = email;
},
logout() {
app.user.name = "";
app.user.email = "";
app.user.preferences.notifications = true;
}
};
// 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([ const LoginForm = () => div([
input({ placeholder: "Name", onInput: e => app.user.name = e.target.value }), input({
input({ placeholder: "Email", onInput: e => app.user.email = e.target.value }), placeholder: "Name",
button({ onClick: () => app.login(app.user.name, app.user.email) }, "Login") onInput: e => userName(e.target.value)
]); }),
input({
placeholder: "Email",
onInput: e => userEmail(e.target.value)
}),
button({
onClick: () => login(userName(), userEmail())
}, "Login")
])
const UserProfile = () => div([ const UserProfile = () => div([
h2(() => `Welcome ${app.user.name}`), h2(() => `Welcome ${userName()}`),
p(() => `Email: ${app.user.email}`), p(() => `Email: ${userEmail()}`),
p(() => `Notifications: ${app.user.preferences.notifications ? "ON" : "OFF"}`), p(() => `Notifications: ${notifications() ? "ON" : "OFF"}`),
button({ onClick: () => app.user.preferences.notifications = !app.user.preferences.notifications }, "Toggle Notifications"), p(() => `Language: ${language()}`),
button({ onClick: app.logout }, "Logout") button({
]); onClick: () => notifications(!notifications())
}, "Toggle Notifications"),
button({ onClick: logout }, "Logout")
])
const App = () => div({ class: "complete-example" }, [ const App = () => div({ class: "complete-example" }, [
when(() => app.isLoggedIn(), () => UserProfile(), () => LoginForm()) when(() => isLoggedIn(), () => UserProfile(), () => LoginForm())
]); ])
setTimeout(() => mount(App, '#demo-complete'), 50); setTimeout(() => mount(App, '#demo-complete-final'), 50)
} }
``` ```
--- ---
## Migration from `$()` to `$$()` ## Summary
If you have code using nested signals: With **only `$()`** as your reactive primitive:
```js -**Explicit** - You know exactly what's reactive
// Before - Manual nesting -**Memory safe** - No hidden proxies or WeakMap caches
const user = $({ -**Predictable** - No magic, just signals
name: $(""), -**Performant** - Minimal overhead
email: $("") -**Debuggable** - Clear data flow
});
user().name("Juan"); // Need to call inner signal
// 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.

View File

@@ -1,51 +1,52 @@
# Global Tag Helpers # Global Tag Helpers
In **SigPro**, you don't need to manually type `h('div', ...)` for every element. To keep your code declarative and readable, the engine automatically generates **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 ## 1. How it Works
SigPro iterates through a list 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:** `h('button', { onclick: ... }, 'Click')` > **Note:** All tag helpers are **lowercase** (e.g., `div`, `span`, `button`) and can be used directly once globally enabled.
* **SigPro Style:** `button({ onclick: ... }, 'Click')`
> **Note:** All tag helpers are **lowercase** (e.g. `div`, `span`, `button`). PascalCase versions (`Div`, `Span`, `Button`) are **not** created. This keeps the syntax close to raw HTML. > If you prefer to avoid globals, you can always use `h('div', ...)` directly—its perfectly fine.
> **Autocleanup:** 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 **lowercase** names to match HTML tags: 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` | | **Structure** | `div`, `span`, `p`, `section`, `nav`, `main`, `header`, `footer`, `article`, `aside` |
| **Typography** | `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `ul`, `ol`, `li`, `dl`, `dt`, `dd`, `strong`, `em`, `code`, `pre`, `small`, `b`, `u`, `mark` | | **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` | | **Interactive** | `button`, `a`, `label`, `br`, `hr`, `details`, `summary`, `dialog` |
| **Forms** | `form`, `input`, `select`, `option`, `textarea`, `fieldset`, `legend` | | **Forms** | `form`, `input`, `select`, `option`, `textarea`, `fieldset`, `legend` |
| **Tables** | `table`, `thead`, `tbody`, `tr`, `th`, `td`, `tfoot`, `caption` | | **Tables** | `table`, `thead`, `tbody`, `tr`, `th`, `td`, `tfoot`, `caption` |
| **Media** | `img`, `canvas`, `video`, `audio`, `svg`, `iframe`, `picture`, `source` | | **Media** | `img`, `canvas`, `video`, `audio`, `svg`, `iframe`, `picture`, `source` |
Full list includes: `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`. 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 ## 3. Usage Patterns
SigPro tag helpers are flexible. They automatically detect if you are passing attributes, children, or both.
### A. Attributes + Children ### A. Attributes + Children
```javascript ```javascript
div({ class: 'container', id: 'main' }, [ div({ class: 'container', id: 'main' }, [
h1("Welcome to SigPro"), h1("Welcome to SigPro"),
p("The zero-VDOM framework.") p("The zeroVDOM framework.")
]); ]);
``` ```
### B. Children Only ### B. Children Only
If you don't need attributes, you can pass the content directly as the first argument. If you don't need attributes, pass the content directly as the first argument.
```javascript ```javascript
section([ section([
@@ -56,56 +57,9 @@ section([
--- ---
## 4. Reactive Power ## 4. Custom Components with `h()` or Tag Helpers
These helpers are natively wired into SigPro's reactivity system. While the tag helpers cover all standard HTML tags, you can create reusable components using them directly.
### Reactive Attributes (OneWay)
Pass a **function** that returns the value. SigPro creates an internal effect to keep the DOM in sync.
```javascript
const theme = $("light");
div({
class: () => `app-box ${theme()}`
}, "Themeable Box");
```
### TwoWay Binding (Automatic)
SigPro automatically bridges the **signal** and the **input element** bidirectionally when you assign a signal to `value` or `checked`.
```javascript
const search = $("");
input({
type: "text",
placeholder: "Search...",
value: search
});
```
> **Pro Tip:** If you want an input to be **readonly** but still reactive, wrap the signal in an anonymous function: `value: () => search()`. This prevents backward synchronization.
### Dynamic Children
You can pass a function as a child it will be reexecuted whenever any signal inside changes, and the DOM will be patched surgically.
```javascript
const count = $(0);
div([
p(() => `Count is ${count()}`),
button({ onClick: () => count(count() + 1) }, "Increment")
]);
```
---
## 5. Custom Components with `h()`
While the global tag helpers cover all standard HTML tags, you can create reusable components using the `h` function directly (or by returning the result of tag helpers).
### Basic Component ### Basic Component
@@ -144,60 +98,3 @@ const Timer = () => {
return el; return el;
}; };
``` ```
---
## 6. Comparison with `h()`
| Use case | Recommendation |
| :--- | :--- |
| Standard tags (`div`, `span`, `button`) | Use global helpers: `div()`, `span()`, `button()` |
| Dynamic tag names (unknown at write time) | Use `h(tagName, props, children)` |
| Components returning a single node | Any function that returns a node (using helpers or `h`) |
> **Autocleanup:** All tag helpers and `h` automatically dispose effects, event listeners, and nested components when removed from the DOM.
---
## 7. Complete Example
```javascript
const App = () =>
div({ class: "app" }, [
h1("Welcome"),
input({
placeholder: "Your name",
value: nameSignal,
onInput: (e) => nameSignal(e.target.value)
}),
p(() => `Hello, ${nameSignal() || "stranger"}!`),
button({ onClick: () => alert("Clicked") }, "Click me")
]);
mount(App, '#app');
```
---
<div class="alert alert-info">
<div>
<h3>Important Notes</h3>
<ul>
<li><b>Naming:</b> All tag helpers are <b>lowercase</b>. There are no PascalCase helpers (<code>Div</code>, <code>Button</code>).</li>
<li><b>Global availability:</b> After importing SigPro (via <code>import 'sigpro'</code> or CDN), all helpers are on <code>window</code>. You can use them anywhere without importing.</li>
<li><b>Custom components:</b> Use PascalCase for your own component functions to visually distinguish them from builtin tags (e.g., <code>UserCard</code>).</li>
</ul>
</div>
</div>
---
## 8. Summary
| Feature | Description |
| :--- | :--- |
| **Tag helpers** | Lowercase functions for every HTML element (e.g., `div()`, `button()`). |
| **Reactive attributes** | Pass a function to any attribute to keep it synced. |
| **Twoway binding** | Assign a signal directly to `value` or `checked` on form elements. |
| **Dynamic children** | Pass a function as a child for live updating content. |
| **Autocleanup** | All effects, events, and children are disposed when the element is removed. |

View File

@@ -20,6 +20,8 @@ watch(deps: Signal[], callback: (values: any[]) => void): StopFunction
**Returns:** A `StopFunction` that, when called, destroys the watcher and releases memory. **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 ## Usage Patterns
@@ -95,7 +97,7 @@ This is achieved via `queueMicrotask`, ensuring optimal performance.
## Key Points ## Key Points
- **Function name:** `watch` (lowercase) exported from SigPro and also available globally. - **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`. - **Auto mode:** `watch(fn)` automatically tracks any signals read inside `fn`.
- **Explicit mode:** `watch([sig1, sig2], (values) => {...})` only reruns when listed signals change; callback receives an array of their new values. - **Explicit mode:** `watch([sig1, sig2], (values) => {...})` only reruns 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. - **Stop function:** returned by both modes; call it to dispose the effect and its children.
@@ -120,4 +122,3 @@ watch([count, step], ([newCount, newStep]) => {
count(5); // logs: auto + explicit count(5); // logs: auto + explicit
step(2); // logs: explicit only (auto does not track step) step(2); // logs: explicit only (auto does not track step)
``` ```

View File

@@ -20,7 +20,7 @@ when(
**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. **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.
> **Note:** The function name is `when` (lowercase). It is exported from SigPro and also available globally after importing. > **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.
--- ---

121
docs/convert.js Normal file
View 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
View 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');
```

View File

@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="es"> <html lang="es" data-theme="splight">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>SigPro Docs</title> <title>SigPro Docs</title>
@@ -10,12 +10,12 @@
rel="stylesheet" rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify/lib/themes/vue.css" href="//cdn.jsdelivr.net/npm/docsify/lib/themes/vue.css"
/> />
<link href="./sigpro.ui.css" rel="stylesheet" type="text/css" />
<link <!-- <link
href="https://cdn.jsdelivr.net/npm/daisyui@5" href="https://cdn.jsdelivr.net/npm/daisyui@5"
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
/> /> -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head> </head>
@@ -51,7 +51,7 @@
); );
codeBlocks.forEach((code) => { codeBlocks.forEach((code) => {
try { try {
const scriptContent = `(function() { ${code.innerText} })();`; const scriptContent = `(function() { ${code.innerText} }).call(window);`;
const runDemo = new Function(scriptContent); const runDemo = new Function(scriptContent);
runDemo(); runDemo();
} catch (err) { } catch (err) {
@@ -64,43 +64,19 @@
}; };
</script> </script>
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script> <script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
<script src="./sigpro.js"></script> <script type="module">
import * as SigPro from "./sigpro.js";
Object.assign(window, SigPro);
import "./sigpro.tags.js";
import "./sigpro.ui.js";
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/lib/docsify.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script> <script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script>
<style>
button, input {
font-family: inherit;
font-size: 1rem;
}
button {
background-color: #3b82f6;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
border: none;
cursor: pointer;
margin: 0.25rem;
transition: background-color 0.2s;
}
button:hover {
background-color: #2563eb;
}
input {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 0.375rem;
margin: 0.25rem;
background-color: white;
}
input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59,130,246,0.2);
}
/* Opcional: para listas y párrafos dentro de demos */
.example, div:has(> button) {
padding: 0.5rem;
}
</style>
</body> </body>
</html> </html>

View File

@@ -1,4 +1,4 @@
# Installation & Setup (SigPro 1.2.18) # Installation & Setup
SigPro is designed to be drop-in ready. Whether you are building a complex application with a bundler or a simple reactive widget in a single HTML file, SigPro scales with your needs. SigPro is designed to be drop-in ready. Whether you are building a complex application with a bundler or a simple reactive widget in a single HTML file, SigPro scales with your needs.
@@ -12,44 +12,25 @@ Choose the method that best fits your workflow:
```bash ```bash
npm install sigpro 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 bun add sigpro
``` ```
</div> </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"> <div class="tab-content bg-base-100 border-base-300 rounded-box p-6">
```html ```html
<script type="module"> <script type="module">
// Import the core it auto-installs itself globally
import 'https://cdn.jsdelivr.net/npm/sigpro@1.2.18/+esm'; import { $, h, mount } from "https://cdn.jsdelivr.net/npm/sigpro@latest/dist/sigpro.esm.min.js";
// Now $, $$, watch, h, when, each, fx, router, req, mount, batch and all lowercase tag helpers (div, button, etc.) are available
const count = $(0);
mount(() => h("h1", {}, () => `Count: ${count()}`), "#app");
</script> </script>
``` ```
@@ -63,39 +44,30 @@ bun add sigpro
SigPro uses **lowercase** Tag Helpers (e.g., `div`, `button`) to keep the syntax close to raw HTML, while still being pure JavaScript functions. 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"> <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"> <div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
```javascript ```javascript
// File: App.js // App.js Use named imports for the core, activate helpers if needed
import 'sigpro'; // auto-installs globals import { $, mount } from "sigpro";
export const App = () => { const App = () => {
const count = $(0); const count = $(0);
// Tag helpers like div, h1, button are available globally (lowercase)
return div({ class: "card p-4" }, [ return div({ class: "card p-4" }, [
h1(() => `Count is: ${count()}`), h1(() => `Count is: ${count()}`),
button( button(
{ { class: "btn btn-primary", onclick: () => count(count() + 1) },
class: "btn btn-primary", "Increment",
onclick: () => count(count() + 1),
},
"Increment"
), ),
]); ]);
}; };
// File: main.js mount(App, "#app");
import 'sigpro';
import { App } from './App.js';
mount(App, '#app');
``` ```
</div> </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"> <div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
```html ```html
@@ -105,23 +77,20 @@ mount(App, '#app');
<div id="app"></div> <div id="app"></div>
<script type="module"> <script type="module">
import 'https://cdn.jsdelivr.net/npm/sigpro@1.2.18/+esm'; // Import the core
import { $, h, mount } from "https://cdn.jsdelivr.net/npm/sigpro@latest/dist/sigpro.esm.min.js";
const name = $('Developer'); const name = $("Developer");
// Lowercase tag helpers: section, h2, input
const App = () => const App = () =>
section({ class: "container" }, [ section({ class: "container" }, [
h2(() => `Welcome, ${name()}`), h2(() => `Welcome, ${name()}`),
input({ input({
type: "text", type: "text",
class: "input input-bordered", class: "input input-bordered",
value: name, // ✅ Two-way binding: signal as value + automatic input event value: name,
placeholder: "Type your name...", placeholder: "Type your name...",
}), }),
]); ]);
mount(App, "#app");
mount(App, '#app');
</script> </script>
</body> </body>
</html> </html>
@@ -132,56 +101,24 @@ mount(App, '#app');
--- ---
## 3. Global by Design ## 3. Why no build step?
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'` (or importing from the CDN), the framework automatically "hydrates" the global `window` object.
- **Core Functions:** You get immediate access to `$`, `$$`, `watch`, `h`, `when`, `each`, `fx`, `router`, `req`, `mount`, `batch` anywhere in your scripts.
- **Auto-Installation:** This happens instantly upon import thanks to its built-in selfinstallation, making it "Plug & Play" for both local projects and CDN usage.
- **Lowercase Tag Helpers:** All standard HTML tags are pre-registered as global functions (`div`, `span`, `button`, `section`, `input`, `h1`, `h2`, etc.).
- **Clean UI Syntax:** Write UI structures that look almost like HTML but are pure, reactive JavaScript: `div({ class: "card" }, [ h1("Title") ])`.
- **Tree Shaking Friendly:** For maximum optimization, you can still use named imports: `import { $, watch, mount } from 'sigpro'`. Modern bundlers (Vite, esbuild) will prune unused code.
- **Custom Components:** We recommend using **PascalCase** for your own components (e.g., `UserCard()`) to distinguish them from built-in lowercase tag helpers.
---
## 4. 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. 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. - **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. - **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** | **~3KB** | ~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. - **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. - **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. - **Zero Magic**: No hidden compilers. What you write is what runs in the browser.
- **Global by Design**: Tag helpers and core functions 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 ## 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. 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.

View File

@@ -17,6 +17,8 @@ router(routes: Route[]): HTMLElement
**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. **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 ## Usage Patterns
@@ -26,6 +28,9 @@ router(routes: Route[]): HTMLElement
Place the `router` element where you want the page content to appear. Inside the routes array, define your routes. Place the `router` element where you want the page content to appear. Inside the routes array, define your routes.
```javascript ```javascript
// remember import router
import { router } from 'sigpro/router'
const Home = () => h1("Home Page"); const Home = () => h1("Home Page");
const UserProfile = (params) => h1(`User ID: ${params.id}`); const UserProfile = (params) => h1(`User ID: ${params.id}`);
const NotFound = () => h1("404 Page not found"); const NotFound = () => h1("404 Page not found");
@@ -150,7 +155,7 @@ If you want the router outlet to have no layout impact, you can set `display: co
## Complete Example ## Complete Example
```javascript ```javascript
import { $, router, mount } from "sigpro"; import { mount } from 'sigpro';
const Home = () => div("Welcome home"); const Home = () => div("Welcome home");
const About = () => div("About us"); const About = () => div("About us");
@@ -185,3 +190,156 @@ mount(App, "#app");
| `router.back()` | Goes back in history. | | `router.back()` | Goes back in history. |
| `router.path()` | Returns the current path without `#`. | | `router.path()` | Returns the current path without `#`. |
| `router.params()` | Reactive signal of the current route parameters. | | `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
View 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
View 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

File diff suppressed because one or more lines are too long

78
docs/sigpro.grid.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
docs/sigpro.locale.js Normal file
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

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
View 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
View 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
View 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",
);
```

View File

@@ -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.

View File

@@ -1,2 +0,0 @@
// index.js
export * from './sigpro.js';

View File

@@ -1,60 +1,112 @@
{ {
"name": "sigpro", "name": "sigpro",
"version": "1.2.19", "version": "1.2.40",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"main": "./dist/sigpro.esm.min.js", "author": {
"module": "./dist/sigpro.esm.min.js", "name": "NatxoCC",
"unpkg": "./dist/sigpro.min.js", "email": "sigpro@natxocc.com",
"jsdelivr": "./dist/sigpro.min.js", "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", "types": "./sigpro.d.ts",
"exports": { "exports": {
".": { ".": {
"import": "./dist/sigpro.esm.min.js", "types": "./sigpro.d.ts",
"script": "./dist/sigpro.js", "import": "./dist/sigpro.js",
"types": "./sigpro.d.ts" "default": "./dist/sigpro.js"
}, },
"./vite": "./vite/index.js", "./db": {
"./vite/*": "./vite/*.js" "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": [ "files": [
"index.js", "dist/",
"sigpro.js",
"dist",
"vite",
"README.md", "README.md",
"LICENSE" "LICENSE",
"sigpro.d.ts",
"sigpro.ui.d.ts"
], ],
"homepage": "https://sigpro.natxocc.com/#/", "homepage": "https://sigpro.natxocc.com/#/",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.natxocc.com/natxocc/sigpro" "url": "https://github.com/natxocc/sigpro"
}, },
"bugs": { "bugs": {
"url": "https://git.natxocc.com/natxocc/sigpro/issues" "url": "https://github.com/natxocc/sigpro/issues",
}, "email": "sigpro@natxocc.com"
"publishConfig": {
"registry": "https://git.natxocc.com/api/packages/natxocc/npm/"
}, },
"scripts": { "scripts": {
"del": "bun pm cache rm && rm -f bun.lockb && rm -f bun.lock",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"prebuild": "npm run clean", "prebuild": "npm run clean",
"build:iife": "bun build ./index.js --bundle --outfile=./dist/sigpro.js --format=iife --global-name=SigPro", "build:core": "bun build ./src/sigpro.js --bundle --outfile=./dist/sigpro.js --format=esm --minify",
"build:docs": "bun build ./index.js --bundle --outfile=./docs/sigpro.js --format=iife --global-name=SigPro", "build:db": "bun build ./src/sigpro.db.js --bundle --outfile=./dist/sigpro.db.js --format=esm --minify",
"build:iife:min": "bun build ./index.js --bundle --outfile=./dist/sigpro.min.js --format=iife --global-name=SigPro --minify", "build:router": "bun build ./src/sigpro.router.js --bundle --outfile=./dist/sigpro.router.js --format=esm --external ./src/sigpro.js --minify",
"build:esm": "bun build ./index.js --bundle --outfile=./dist/sigpro.esm.js --format=esm", "build:locale": "bun build ./src/sigpro.locale.js --bundle --outfile=./dist/sigpro.locale.js --format=esm --external ./src/sigpro.js --minify",
"build:esm:min": "bun build ./index.js --bundle --outfile=./dist/sigpro.esm.min.js --format=esm --minify", "build:ui": "bun build ./src/sigpro.ui.js --bundle --outfile=./dist/sigpro.ui.js --format=esm --external ./src/sigpro.js --minify",
"build": "bun run build:iife && bun run build:iife:min && bun run build:esm && bun run build:esm:min && bun run build:docs", "build:grid": "bun build ./src/sigpro.grid.js --bundle --external sigpro --outfile=./dist/sigpro.grid.js --format=esm --minify",
"docs": "bun x serve docs", "build:editor": "bun build ./src/sigpro.editor.js --bundle --external sigpro --outfile=./dist/sigpro.editor.js --format=esm --minify",
"prepublishOnly": "npm run build" "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": [ "keywords": [
"signals", "signals",
"reactivity",
"reactive", "reactive",
"web-components", "pure",
"vanilla-js", "vanilla",
"reactive-programming", "js",
"signals-library", "ui",
"fine-grained-reactivity" "dom",
"state",
"frontend",
"spa",
"lightweight",
"sigpro"
] ]
} }

40
sigpro.d.ts vendored
View File

@@ -1,5 +1,5 @@
/** /**
* SigPro 1.2.19 * SigPro
* A minimalistic reactive library with fine-grained reactivity, * A minimalistic reactive library with fine-grained reactivity,
* direct DOM updates, and built-in component helpers. * direct DOM updates, and built-in component helpers.
*/ */
@@ -118,24 +118,6 @@ export function each<T>(
keyField?: keyof T keyField?: keyof T
): HTMLElement; ): HTMLElement;
/**
* Simple animation helper for enter transitions.
*
* @param options - Animation settings
* @param child - Node or function returning node
* @returns The animated node
*/
export function fx(
options: {
name?: string;
duration?: number;
scale?: boolean;
slide?: boolean;
rotate?: boolean;
blur?: boolean;
},
child: Node | (() => Node)
): Node;
// ============================================================================ // ============================================================================
// Router // Router
@@ -168,22 +150,6 @@ export namespace router {
export function path(): string; export function path(): string;
} }
// ============================================================================
// HTTP Requests
// ============================================================================
export function req(config: {
url: string;
method?: string;
headers?: Record<string, string>;
}): {
run: (body?: any) => Promise<any>;
abort: () => void;
loading: Signal<boolean>;
error: Signal<string | null>;
data: Signal<any | null>;
};
// ============================================================================ // ============================================================================
// Mount API // Mount API
// ============================================================================ // ============================================================================
@@ -315,9 +281,7 @@ declare const SigPro: {
h: typeof h; h: typeof h;
when: typeof when; when: typeof when;
each: typeof each; each: typeof each;
fx: typeof fx;
router: typeof router; router: typeof router;
req: typeof req;
mount: typeof mount; mount: typeof mount;
batch: typeof batch; batch: typeof batch;
}; };
@@ -336,9 +300,7 @@ declare global {
h: typeof h; h: typeof h;
when: typeof when; when: typeof when;
each: typeof each; each: typeof each;
fx: typeof fx;
router: typeof router; router: typeof router;
req: typeof req;
mount: typeof mount; mount: typeof mount;
batch: typeof batch; batch: typeof batch;
SigPro: typeof SigPro; SigPro: typeof SigPro;

590
sigpro.js
View File

@@ -1,590 +0,0 @@
// sigpro 1.2.19
const isFunc = f => typeof f === "function"
const isObj = o => o && typeof o === "object"
const isArr = Array.isArray
const doc = typeof document !== "undefined" ? document : null
const ensureNode = n => n?._isRuntime ? n.container : (n instanceof Node ? n : doc.createTextNode(n == null ? "" : String(n)))
let activeEffect = null
let activeOwner = null
let isFlushing = false
let batchDepth = 0
const effectQueue = new Set()
const proxyCache = new WeakMap()
const ITER = Symbol('iter')
const MOUNTED_NODES = new WeakMap()
const dispose = eff => {
if (!eff || eff._disposed) return
eff._disposed = true
const stack = [eff]
while (stack.length) {
const e = stack.pop()
if (e._cleanups) {
e._cleanups.forEach(fn => fn())
e._cleanups.clear()
}
if (e._children) {
e._children.forEach(child => stack.push(child))
e._children.clear()
}
if (e._deps) {
e._deps.forEach(depSet => depSet.delete(e))
e._deps.clear()
}
}
}
const onUnmount = fn => {
if (activeOwner) (activeOwner._cleanups ||= new Set()).add(fn)
}
const untrack = fn => {
const p = activeEffect
activeEffect = null
try { return fn() } finally { activeEffect = p }
}
const createEffect = (fn, isComputed = false) => {
const effect = () => {
if (effect._disposed) return
if (effect._deps) effect._deps.forEach(s => s.delete(effect))
if (effect._cleanups) {
effect._cleanups.forEach(c => c())
effect._cleanups.clear()
}
const prevEffect = activeEffect
const prevOwner = activeOwner
activeEffect = activeOwner = effect
try {
return effect._result = fn()
} catch (e) {
console.error("[SigPro]", e)
} finally {
activeEffect = prevEffect
activeOwner = prevOwner
}
}
effect._deps = effect._cleanups = effect._children = null
effect._disposed = false
effect._isComputed = isComputed
effect._depth = activeEffect ? activeEffect._depth + 1 : 0
effect._mounts = []
effect._parent = activeOwner
if (activeOwner) (activeOwner._children ||= new Set()).add(effect)
return effect
}
const flush = () => {
if (isFlushing) return
isFlushing = true
const sorted = Array.from(effectQueue).sort((a, b) => a._depth - b._depth)
effectQueue.clear()
for (const e of sorted) if (!e._disposed) e()
isFlushing = false
}
const batch = fn => {
batchDepth++
try {
return fn()
} finally {
batchDepth--
if (batchDepth === 0 && effectQueue.size > 0 && !isFlushing) {
flush()
}
}
}
const trackUpdate = (subs, trigger = false) => {
if (!trigger && activeEffect && !activeEffect._disposed) {
subs.add(activeEffect)
; (activeEffect._deps ||= new Set()).add(subs)
} else if (trigger && subs.size > 0) {
let hasQueue = false
for (const e of subs) {
if (e === activeEffect || e._disposed) continue
if (e._isComputed) {
e._dirty = true
if (e._subs) trackUpdate(e._subs, true)
} else {
effectQueue.add(e)
hasQueue = true
}
}
if (hasQueue && !isFlushing && batchDepth === 0) queueMicrotask(flush)
}
}
const $ = (val, key = null) => {
const subs = new Set()
if (isFunc(val)) {
let cache
const computed = () => {
if (computed._dirty) {
const prev = activeEffect
activeEffect = computed
try {
const next = val()
if (!Object.is(cache, next)) {
cache = next
trackUpdate(subs, true)
}
} finally {
activeEffect = prev
}
computed._dirty = false
}
trackUpdate(subs)
return cache
}
computed._isComputed = true
computed._subs = subs
computed._dirty = true
computed._deps = null
computed._disposed = false
computed.stop = () => { }
if (activeOwner) onUnmount(computed.stop)
return computed
}
if (key) try { val = JSON.parse(localStorage.getItem(key)) ?? val } catch (e) { }
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))
trackUpdate(subs, true)
}
}
trackUpdate(subs)
return val
}
}
const $$ = (target) => {
if (!isObj(target)) return target
const cached = proxyCache.get(target)
if (cached) return cached
const subs = new Map()
const getSubs = (key) => {
let set = subs.get(key)
if (!set) subs.set(key, set = new Set())
return set
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
if (typeof key !== 'symbol') trackUpdate(getSubs(key))
return $$(Reflect.get(target, key, receiver))
},
set(target, key, value, receiver) {
const hadKey = Reflect.has(target, key)
const oldValue = Reflect.get(target, key, receiver)
const result = Reflect.set(target, key, value, receiver)
if (result && !Object.is(oldValue, value)) {
trackUpdate(getSubs(key), true)
if (!hadKey) trackUpdate(getSubs(ITER), true)
}
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
if (result) {
trackUpdate(getSubs(key), true)
trackUpdate(getSubs(ITER), true)
}
return result
},
ownKeys(target) {
trackUpdate(getSubs(ITER))
return Reflect.ownKeys(target)
}
})
proxyCache.set(target, proxy)
return proxy
}
const watch = (sources, cb) => {
if (cb === undefined) {
const effect = createEffect(sources)
effect()
return () => dispose(effect)
}
const effect = createEffect(() => {
const vals = Array.isArray(sources) ? sources.map(s => s()) : sources()
untrack(() => cb(vals))
})
effect()
return () => dispose(effect)
}
const cleanupNode = (node) => {
if (!node) return;
if (node._cleanups) {
node._cleanups.forEach(fn => fn());
node._cleanups.clear();
}
if (node._ownerEffect) dispose(node._ownerEffect);
if (node.childNodes) node.childNodes.forEach(n => cleanupNode(n));
};
const DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i
const isDangerousAttr = key => key === 'src' || key === 'href' || key.startsWith('on')
const validateAttr = (key, val) => {
if (val == null || val === false) return null
if (isDangerousAttr(key)) {
const sVal = String(val)
if (DANGEROUS_PROTOCOL.test(sVal)) {
console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`)
return '#'
}
}
return val
}
const h = (tag, props = {}, children = []) => {
if (props instanceof Node || isArr(props) || !isObj(props)) {
children = props
props = {}
}
if (isFunc(tag)) {
const effect = createEffect(() => {
const result = tag(props, {
children,
emit: (ev, ...args) => props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...args)
})
effect._result = result
return result
})
effect()
const result = effect._result
if (result == null) return null
const node = (result instanceof Node || (isArr(result) && result.every(n => n instanceof Node)))
? result
: doc.createTextNode(String(result))
const attach = n => {
if (isObj(n) && !n._isRuntime) {
n._mounts = effect._mounts || []
n._cleanups = effect._cleanups || new Set()
n._ownerEffect = effect
}
}
isArr(node) ? node.forEach(attach) : attach(node)
return node
}
const isSVG = /^(svg|path|circle|rect|line|poly(line|gon)|g|defs|text(path)?|tspan|use|symbol|image|marker|ellipse)$/i.test(tag);
const el = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : doc.createElement(tag)
el._cleanups = new Set()
for (let k in props) {
if (!props.hasOwnProperty(k)) continue
let v = props[k]
if (k === "ref") {
isFunc(v) ? v(el) : (v.current = el)
continue
}
if (isSVG && k.startsWith("xlink:")) {
const ns = "http://www.w3.org/1999/xlink"
v == null ? el.removeAttributeNS(ns, k.slice(6)) : el.setAttributeNS(ns, k.slice(6), v)
continue
}
if (k.startsWith("on")) {
const ev = k.slice(2).toLowerCase()
el.addEventListener(ev, v)
const off = () => el.removeEventListener(ev, v)
el._cleanups.add(off)
onUnmount(off)
} else if (isFunc(v)) {
const effect = createEffect(() => {
const val = validateAttr(k, v())
if (k === "class") el.className = val || ""
else if (val == null) el.removeAttribute(k)
else if (k in el && !isSVG) el[k] = val
else el.setAttribute(k, val === true ? "" : val)
})
effect()
el._cleanups.add(() => dispose(effect))
onUnmount(() => dispose(effect))
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
const evType = k === "checked" ? "change" : "input"
el.addEventListener(evType, ev => v(ev.target[k]))
}
} else {
const val = validateAttr(k, v)
if (val != null) {
if (k in el && !isSVG) el[k] = val
else el.setAttribute(k, val === true ? "" : val)
}
}
}
const append = c => {
if (isArr(c)) return c.forEach(append)
if (isFunc(c)) {
const anchor = doc.createTextNode("")
el.appendChild(anchor)
let currentNodes = []
const effect = createEffect(() => {
const res = c()
const next = (isArr(res) ? res : [res]).map(ensureNode)
currentNodes.forEach(n => {
if (n._isRuntime) n.destroy()
else cleanupNode(n)
if (n.parentNode) n.remove()
})
let ref = anchor
for (let i = next.length - 1; i >= 0; i--) {
const node = next[i]
if (node.parentNode !== ref.parentNode) ref.parentNode?.insertBefore(node, ref)
if (node._mounts) node._mounts.forEach(fn => fn())
ref = node
}
currentNodes = next
})
effect()
el._cleanups.add(() => dispose(effect))
onUnmount(() => dispose(effect))
} else {
const node = ensureNode(c)
el.appendChild(node)
if (node._mounts) node._mounts.forEach(fn => fn())
}
}
append(children)
return el
}
const render = renderFn => {
const cleanups = new Set()
const previousOwner = activeOwner
const previousEffect = activeEffect
const container = doc.createElement("div")
container.style.display = "contents"
container.setAttribute("role", "presentation")
activeOwner = { _cleanups: cleanups }
activeEffect = null
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 : doc.createTextNode(String(result == null ? "" : result)))
}
}
try {
processResult(renderFn({ onCleanup: fn => cleanups.add(fn) }))
} finally {
activeOwner = previousOwner
activeEffect = previousEffect
}
return {
_isRuntime: true,
container,
destroy: () => {
cleanups.forEach(fn => fn())
cleanupNode(container)
container.remove()
}
}
}
const when = (cond, SIP, NOP = null) => {
const anchor = doc.createTextNode("")
const root = h("div", { style: "display:contents" }, [anchor])
let currentView = null
watch(
() => !!(isFunc(cond) ? cond() : cond),
show => {
if (currentView) {
currentView.destroy()
currentView = null
}
const content = show ? SIP : NOP
if (content) {
currentView = render(() => isFunc(content) ? content() : content)
root.insertBefore(currentView.container, anchor)
}
}
)
onUnmount(() => currentView?.destroy())
return root
}
const fx = ({ name, duration = 200, scale, slide, rotate, blur }, child) => {
const el = typeof child === "function" ? child() : child;
if (!(el instanceof Node)) return el;
if (name) {
el.style.animation = `${name}-in ${duration}ms`;
return el;
}
const hasTransform = scale || slide || rotate || blur;
const initialTransform = [
scale ? "scale(0.95)" : "",
slide ? "translateY(-10px)" : "",
rotate ? "rotate(-2deg)" : ""
].filter(Boolean).join(" ");
el.style.transition = `all ${duration}ms ease`;
el.style.opacity = "0";
if (hasTransform) el.style.transform = initialTransform;
if (blur) el.style.filter = "blur(4px)";
requestAnimationFrame(() => {
el.style.opacity = "1";
if (hasTransform) el.style.transform = "none";
if (blur) el.style.filter = "none";
});
return el;
};
const each = (src, itemFn, keyField) => {
const anchor = doc.createTextNode("")
const root = h("div", { style: "display:contents" }, [anchor])
let cache = new Map()
watch(() => (isFunc(src) ? src() : src) || [], items => {
const nextCache = new Map()
const nextOrder = []
const newItems = items || []
for (let i = 0; i < newItems.length; i++) {
const item = newItems[i]
const key = keyField ? (item?.[keyField] ?? i) : (item?.id ?? i)
let view = cache.get(key)
if (!view) view = render(() => itemFn(item, i))
else cache.delete(key)
nextCache.set(key, view)
nextOrder.push(view)
}
cache.forEach(view => view.destroy())
let lastRef = anchor
for (let i = nextOrder.length - 1; i >= 0; i--) {
const view = nextOrder[i]
const node = view.container
if (node.nextSibling !== lastRef) root.insertBefore(node, lastRef)
lastRef = node
}
cache = nextCache
})
return root
}
const router = routes => {
const getHash = () => window.location.hash.slice(1) || "/"
const path = $(getHash())
const handler = () => path(getHash())
window.addEventListener("hashchange", handler)
onUnmount(() => window.removeEventListener("hashchange", handler))
const hook = h("div", { class: "router-hook" })
let currentView = null
watch([path], () => {
const cur = path()
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]
})
router.params(params)
currentView = render(() => isFunc(route.component) ? route.component(params) : route.component)
hook.replaceChildren(currentView.container)
}
})
return hook
}
router.params = $({})
router.to = p => window.location.hash = p.replace(/^#?\/?/, "#/")
router.back = () => window.history.back()
router.path = () => window.location.hash.replace(/^#/, "") || "/"
const req = ({ url, method = 'GET', headers = {} }) => {
const loading = $(false);
const error = $(null);
const data = $(null);
let controller = null;
let timeoutId = null;
const run = async (body = null) => {
controller?.abort();
clearTimeout(timeoutId);
controller = new AbortController();
timeoutId = setTimeout(() => controller.abort(), 10000);
loading(true);
error(null);
try {
const isFormData = body instanceof FormData;
const res = await fetch(url, {
method,
headers: isFormData ? headers : { 'Content-Type': 'application/json', ...headers },
body: isFormData ? body : (body ? JSON.stringify(body) : undefined),
signal: controller.signal
});
const text = await res.text();
const json = text ? JSON.parse(text) : null;
if (!res.ok) throw new Error(json?.message || res.statusText);
data(json);
return json;
} catch (e) {
if (e.name !== 'AbortError') error(e.message);
throw e;
} finally {
loading(false);
clearTimeout(timeoutId);
controller = null;
timeoutId = null;
}
};
const abort = () => controller?.abort();
return { run, abort, loading, error, data };
};
const mount = (comp, target) => {
const t = typeof target === "string" ? doc.querySelector(target) : target
if (!t) return
if (MOUNTED_NODES.has(t)) MOUNTED_NODES.get(t).destroy()
const inst = render(isFunc(comp) ? comp : () => comp)
t.replaceChildren(inst.container)
MOUNTED_NODES.set(t, inst)
return inst
}
const SigPro = Object.freeze({ $, $$, watch, h, when, each, fx, router, req, mount, batch })
if (typeof window !== "undefined") {
Object.assign(window, SigPro)
"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(tag => {
window[tag] = (props, children) => h(tag, props, children)
})
}
export { $, $$, watch, h, when, each, fx, router, req, mount, batch }

308
sigpro.ui.d.ts vendored Normal file
View 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;
}
}

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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View File

@@ -1,15 +1,8 @@
/** // sigproRouter for Vite
* SigPro Vite Plugin - File-based Routing export function sigproRouter() {
* @module sigpro/vite
*/
import fs from 'node:fs';
import path from 'node:path';
export default function sigproRouter() {
const virtualModuleId = 'virtual:sigpro-routes'; const virtualModuleId = 'virtual:sigpro-routes';
const resolvedVirtualModuleId = '\0' + virtualModuleId; const resolvedVirtualModuleId = '\0' + virtualModuleId;
// Helper para escanear archivos
const getFiles = (dir) => { const getFiles = (dir) => {
if (!fs.existsSync(dir)) return []; if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir, { recursive: true }) return fs.readdirSync(dir, { recursive: true })
@@ -17,14 +10,12 @@ export default function sigproRouter() {
.map(file => path.resolve(dir, file)); .map(file => path.resolve(dir, file));
}; };
// Transformador de ruta de archivo a URL de router
const pathToUrl = (pagesDir, filePath) => { const pathToUrl = (pagesDir, filePath) => {
let relative = path.relative(pagesDir, filePath) let relative = path.relative(pagesDir, filePath)
.replace(/\\/g, '/') .replace(/\\/g, '/')
.replace(/\.(js|jsx)$/, '') .replace(/\.(js|jsx)$/, '')
.replace(/\/index$/, '') .replace(/\/index$/, '')
.replace(/^index$/, ''); .replace(/^index$/, '');
return ('/' + relative) return ('/' + relative)
.replace(/\/+/g, '/') .replace(/\/+/g, '/')
.replace(/\[\.\.\.([^\]]+)\]/g, '*') .replace(/\[\.\.\.([^\]]+)\]/g, '*')
@@ -34,18 +25,13 @@ export default function sigproRouter() {
return { return {
name: 'sigpro-router', name: 'sigpro-router',
resolveId(id) { resolveId(id) {
if (id === virtualModuleId) return resolvedVirtualModuleId; if (id === virtualModuleId) return resolvedVirtualModuleId;
}, },
load(id) { load(id) {
if (id !== resolvedVirtualModuleId) return; if (id !== resolvedVirtualModuleId) return;
const root = process.cwd(); const root = process.cwd();
const pagesDir = path.resolve(root, 'src/pages'); 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 files = getFiles(pagesDir).sort((a, b) => {
const urlA = pathToUrl(pagesDir, a); const urlA = pathToUrl(pagesDir, a);
const urlB = pathToUrl(pagesDir, b); const urlB = pathToUrl(pagesDir, b);
@@ -55,23 +41,15 @@ export default function sigproRouter() {
}); });
let routeEntries = ''; let routeEntries = '';
files.forEach((fullPath) => { files.forEach((fullPath) => {
const urlPath = pathToUrl(pagesDir, 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, '/'); const relativeImport = './' + path.relative(root, fullPath).replace(/\\/g, '/');
routeEntries += ` { path: '${urlPath}', component: () => import('/${relativeImport}') },\n`;
routeEntries += ` { path: '${urlPath}', component: async () => (await import('/${relativeImport}')).default },\n`;
}); });
// Fallback 404 si no existe una ruta comodín
if (!routeEntries.includes("path: '*'")) { 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}];`; return `export const routes = [\n${routeEntries}];`;
} }
}; };
} }
export { sigproRouter };

117
src/tailwind Normal file
View 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'
]