Compare commits
109 Commits
af5bd1a537
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| eb1c81ec26 | |||
| 8481e339cc | |||
| d83aff6229 | |||
| 8df06d9c12 | |||
| 72bfc2b5c1 | |||
| 1db7b81eb7 | |||
| 1607b41ebc | |||
| 27d9474610 | |||
| 3dea037697 | |||
| 9fc4eaebbb | |||
| c86c37aec2 | |||
| 26464d2161 | |||
| dc9af3181f | |||
| 00ad6d7f9f | |||
| 5efb9e0f96 | |||
| 651d9587c2 | |||
| 3fe05d40e6 | |||
| 0b3eb0159f | |||
| 1349d431e9 | |||
| 6f538b8613 | |||
| 7d8db0192a | |||
| 1d71340552 | |||
| 06c7763b34 | |||
| d48241a9d9 | |||
| 2a482f2340 | |||
| 1800b16940 | |||
| 8b2e67b3b0 | |||
| 0a790de054 | |||
| c01b41d892 | |||
| 5a2cefa115 | |||
| a0701422f5 | |||
| 645f9b42b0 | |||
| 8796b9f94d | |||
| afa2817118 | |||
| 369a35d92a | |||
| 610c9a9586 | |||
| cd58b97d09 | |||
| e4b08a0aad | |||
| 439809b1e7 | |||
| ab0e6e0697 | |||
| 39a67b94fc | |||
| 820d55b012 | |||
| f3fb26354c | |||
| 8a9805b79a | |||
| bef6c20231 | |||
| b1fa97afc3 | |||
| a35ea1e38e | |||
| 69e277d726 | |||
| 9da5bd74f9 | |||
| 7251573e28 | |||
| 1ca67dd4a0 | |||
| db9bd39679 | |||
| 2b303fc55c | |||
| f98cb19ee1 | |||
| f28594348e | |||
| 7d4340a987 | |||
| f11dd340ff | |||
| 6d7ac2d2e9 | |||
| 0df4b3912d | |||
| 771f4a9f83 | |||
| d46c5ca3af | |||
| 4526726b1b | |||
| 2a0ce8c68f | |||
| 995f1557bf | |||
| 6a33b7df07 | |||
| 99780e8399 | |||
| b931434edc | |||
| dc2f6f8736 | |||
| 7cce0e5e59 | |||
| 03da2b7cd3 | |||
| 496ad150ce | |||
| a65219759d | |||
| 25975eb89a | |||
| 04052ef7b4 | |||
| 3faf1fe5a6 | |||
| 76a97fe2a2 | |||
| fb7ebe5fec | |||
| 9b9284d3d1 | |||
| 83c5279ab9 | |||
| ab36557c8c | |||
| 1c45dc5466 | |||
| ee5e6e5207 | |||
| 29dda4c07e | |||
| ab1413ca5a | |||
| a5ecc17166 | |||
| fdffac2a72 | |||
| 5b0cfad9b8 | |||
| ccdbeb1b16 | |||
| 2f1cfae0b2 | |||
| 6e0c21eddc | |||
| 20f7242e83 | |||
| 73d5c12f13 | |||
| c653d361d6 | |||
| 1006a42284 | |||
| ba6d731377 | |||
| 91225e185d | |||
| c28b1860e7 | |||
| 7287e9c094 | |||
| 14f06c3a88 | |||
| 5bf1ecd4f0 | |||
| 2d97d7d117 | |||
| 0d59518a80 | |||
| 4690aa5013 | |||
| 0837030da8 | |||
| e659aa940f | |||
| fb1ac8c9c3 | |||
| 69b2dd723c | |||
| 60ff7f4e99 | |||
| becc4b8227 |
@@ -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
|
||||||
5
.github/workflows/publish-gitea.yml
vendored
5
.github/workflows/publish-gitea.yml
vendored
@@ -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
38
.github/workflows/publish-npm.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Publish to NPM (Manual)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Versión a publicar (ej: 1.2.20)'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: node:20-bullseye
|
||||||
|
options: >-
|
||||||
|
--dns 192.168.1.1
|
||||||
|
--add-host git.natxocc.com:host-gateway
|
||||||
|
--add-host gitea:host-gateway
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Clone source from Gitea
|
||||||
|
run: |
|
||||||
|
git clone https://git.natxocc.com/natxocc/sigpro.git source
|
||||||
|
cd source
|
||||||
|
git checkout main
|
||||||
|
|
||||||
|
- uses: oven-sh/setup-bun@v1
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Install and publish
|
||||||
|
run: |
|
||||||
|
cd source
|
||||||
|
bun install
|
||||||
|
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
|
||||||
|
npm publish --access public
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
43
.github/workflows/publish.yml
vendored
43
.github/workflows/publish.yml
vendored
@@ -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
22
.github/workflows/unpublish-npm.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Delete npm version
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Versión a eliminar'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
delete:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
- name: Unpublish version
|
||||||
|
run: npm unpublish sigpro@${{ github.event.inputs.version }} --force
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
53
Readme.md
53
Readme.md
@@ -1,15 +1,12 @@
|
|||||||
# `SigPro` ⚛️
|
Blazing fast, zero-overhead, vanilla JS renderer with atomic reactivity.
|
||||||
|
|
||||||
|
# `SigPro`
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/sigpro)
|
[](https://www.npmjs.com/package/sigpro)
|
||||||
[](https://bundlephobia.com/package/sigpro)
|

|
||||||
[](https://github.com/natxocc/sigpro)
|
|
||||||
[](https://github.com/natxocc/sigpro/blob/main/LICENSE)
|
[](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
1
dist/sigpro.db.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
var o=async(e,n={},t=null)=>{if(t)t(!0);try{let r=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n),credentials:"include"});if(!r.ok){let s=await r.text();throw Error(`Error ${r.status}: ${s}`)}return await r.json()}finally{if(t)t(!1)}};export{o as db};
|
||||||
1
dist/sigpro.editor.js
vendored
Normal file
1
dist/sigpro.editor.js
vendored
Normal file
File diff suppressed because one or more lines are too long
602
dist/sigpro.esm.js
vendored
602
dist/sigpro.esm.js
vendored
@@ -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,
|
|
||||||
$$,
|
|
||||||
$
|
|
||||||
};
|
|
||||||
1
dist/sigpro.esm.min.js
vendored
1
dist/sigpro.esm.min.js
vendored
File diff suppressed because one or more lines are too long
78
dist/sigpro.grid.js
vendored
Normal file
78
dist/sigpro.grid.js
vendored
Normal file
File diff suppressed because one or more lines are too long
646
dist/sigpro.js
vendored
646
dist/sigpro.js
vendored
File diff suppressed because one or more lines are too long
1
dist/sigpro.locale.js
vendored
Normal file
1
dist/sigpro.locale.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
var{$:c}=window.SigPro,s=c("en"),n={},e=(t)=>{for(let o of Object.keys(t)){if(!n[o])n[o]={};Object.assign(n[o],t[o])}},r=(t)=>{if(t&&n[t])s(t)},a=(t)=>{return()=>n[s()]?.[t]??t};export{a as t,r as setLocale,e as addLang};
|
||||||
1
dist/sigpro.min.js
vendored
1
dist/sigpro.min.js
vendored
File diff suppressed because one or more lines are too long
1
dist/sigpro.router.js
vendored
Normal file
1
dist/sigpro.router.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
var{$:p,h:u,watch:m,render:g,isF:y}=window.SigPro,d=()=>window.location.hash.slice(1)||"/",s=p(d());window.addEventListener("hashchange",()=>s(d()));var w=p({}),c=(n)=>{let i=u("div",{class:"router-hook"}),r=null;return m([s],()=>{let l=s(),t=n.find((o)=>{let e=o.path.split("/").filter(Boolean),a=l.split("/").filter(Boolean);return e.length===a.length&&e.every((h,f)=>h[0]===":"||h===a[f])})||n.find((o)=>o.path==="*");if(t){r?.destroy();let o={};t.path.split("/").filter(Boolean).forEach((e,a)=>{if(e[0]===":")o[e.slice(1)]=l.split("/").filter(Boolean)[a]}),w(o),r=g(()=>y(t.component)?t.component(o):t.component),i.replaceChildren(r.container)}}),i.destroy=()=>{r?.destroy()},i};c.params=w;c.to=(n)=>window.location.hash=n.replace(/^#?\/?/,"#/");c.back=()=>window.history.back();c.path=()=>s();export{w as routerParams,c as router};
|
||||||
2
dist/sigpro.ui.css
vendored
Normal file
2
dist/sigpro.ui.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/sigpro.ui.js
vendored
Normal file
1
dist/sigpro.ui.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/sigpro.vite.js
vendored
Normal file
4
dist/sigpro.vite.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
function g(){let u="\x00virtual:sigpro-routes",i=(e)=>{if(!fs.existsSync(e))return[];return fs.readdirSync(e,{recursive:!0}).filter((r)=>/\.(js|jsx)$/.test(r)&&!path.basename(r).startsWith("_")).map((r)=>path.resolve(e,r))},l=(e,r)=>{return("/"+path.relative(e,r).replace(/\\/g,"/").replace(/\.(js|jsx)$/,"").replace(/\/index$/,"").replace(/^index$/,"")).replace(/\/+/g,"/").replace(/\[\.\.\.([^\]]+)\]/g,"*").replace(/\[([^\]]+)\]/g,":$1").replace(/\/$/,"")||"/"};return{name:"sigpro-router",resolveId(e){if(e==="virtual:sigpro-routes")return u},load(e){if(e!==u)return;let r=process.cwd(),t=path.resolve(r,"src/pages"),p=i(t).sort((n,a)=>{let o=l(t,n),c=l(t,a);if(o.includes(":")&&!c.includes(":"))return 1;if(!o.includes(":")&&c.includes(":"))return-1;return c.length-o.length}),s="";if(p.forEach((n)=>{let a=l(t,n),o="./"+path.relative(r,n).replace(/\\/g,"/");s+=` { path: '${a}', component: () => import('/${o}') },
|
||||||
|
`}),!s.includes("path: '*'"))s+=` { path: '*', component: () => ({ default: () => document.createTextNode('404 - Not Found') }) },
|
||||||
|
`;return`export const routes = [
|
||||||
|
${s}];`}}}export{g as sigproRouter};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<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>
|
<section class="max-w-6xl mx-auto px-6 py-16"><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 items-stretch"><div class="card bg-base-200/30 border border-base-300 hover:border-primary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-primary italic">FUNCTIONAL</h3><p class="text-sm opacity-70">No strings. No templates. Pure JS function calls for instant DOM mounting.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-secondary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-secondary italic">ATOMIC</h3><p class="text-sm opacity-70">Fine‑grained signals update exactly what changes. No V‑DOM diffing overhead.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-accent/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-accent italic">ULTRA‑THIN</h3><p class="text-sm opacity-70">less than 3KB runtime. Infinitely smaller bundle than React, Vue or even Svelte.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-base-content/20 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black italic text-base-content">COMPILER‑FREE</h3><p class="text-sm opacity-70">Standard Vanilla JS. What you write is what the browser executes. Period.</p></div></div></div></section>
|
||||||
|
|
||||||
<div class="max-w-6xl mx-auto px-6 py-8"><h2 class="text-4xl font-black mb-6">Functional DOM Construction</h2><p class="text-lg opacity-80 mb-6">SigPro replaces slow "Template Parsing" with <strong>High‑Efficiency Function Calls</strong>. While other frameworks force the browser to parse strings of HTML or execute complex JSX transformations, SigPro uses a direct functional approach.</p>
|
<div class="max-w-6xl mx-auto px-6 py-8"><h2 class="text-4xl font-black mb-6">Functional DOM Construction</h2><p class="text-lg opacity-80 mb-6">SigPro replaces slow "Template Parsing" with <strong>High‑Efficiency Function Calls</strong>. While other frameworks force the browser to parse strings of HTML or execute complex JSX transformations, SigPro uses a direct functional approach.</p>
|
||||||
|
|
||||||
@@ -22,23 +22,6 @@
|
|||||||
<li><strong>SigPro:</strong> You ship <strong>Pure Vanilla JS</strong>. The runtime is so small that it often costs less than a single high‑res icon.</li>
|
<li><strong>SigPro:</strong> You ship <strong>Pure Vanilla JS</strong>. The runtime is so small that it often costs less than a single high‑res icon.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 class="text-2xl font-bold mt-8 mb-4">Two Ways to Use SigPro (v1.2.19+)</h3>
|
|
||||||
<p class="mb-2"><strong>🎯 Modern ESM (recommended):</strong> Import only the functions you need – zero global pollution.</p>
|
|
||||||
<pre class="bg-base-300/30 p-4 rounded-lg mb-4"><code>import { $, div, button, mount } from 'sigpro';
|
|
||||||
const count = $(0);
|
|
||||||
mount(() => div([ button({ onclick: () => count(count()+1) }, count) ]), '#app');</code></pre>
|
|
||||||
<p class="mb-2"><strong>🌍 Classic Global (IIFE):</strong> Load the script and everything is automatically on <code>window</code> – no imports needed.</p>
|
|
||||||
<pre class="bg-base-300/30 p-4 rounded-lg mb-4"><code><script src="https://cdn.jsdelivr.net/npm/sigpro@1.2.19/dist/sigpro.js"></script>
|
|
||||||
<script>
|
|
||||||
const count = $(0);
|
|
||||||
mount(() => div([ button({ onclick: () => count(count()+1) }, count) ]), '#app');
|
|
||||||
</script></code></pre>
|
|
||||||
<p class="mb-2"><strong>🔧 ESM + Global injection:</strong> If you want the global helpers but still use <code>type="module"</code>, call <code>sigpro()</code>.</p>
|
|
||||||
<pre class="bg-base-300/30 p-4 rounded-lg"><code><script type="module">
|
|
||||||
import { sigpro } from 'https://cdn.jsdelivr.net/npm/sigpro@1.2.19/+esm';
|
|
||||||
sigpro(); // now $, div, button, etc. are global
|
|
||||||
</script></code></pre>
|
|
||||||
|
|
||||||
<h3 class="text-2xl font-bold mt-10 mb-4">Precision Engineering</h3>
|
<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>
|
<h4 class="text-xl font-semibold mt-6 mb-2">1. Functional Efficiency</h4>
|
||||||
<p><code>div()</code>, <code>button()</code>, <code>span()</code>… These aren't just wrappers; they are pre‑optimized constructors. When you call <code>div({ class: 'btn' }, "Click")</code>, SigPro creates the element and attaches its reactive listeners in a single, surgical operation.</p>
|
<p><code>div()</code>, <code>button()</code>, <code>span()</code>… These aren't just wrappers; they are pre‑optimized constructors. When you call <code>div({ class: 'btn' }, "Click")</code>, SigPro creates the element and attaches its reactive listeners in a single, surgical operation.</p>
|
||||||
|
|||||||
@@ -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]
|
||||||
184
docs/api/fx.md
184
docs/api/fx.md
@@ -1,184 +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.
|
|
||||||
|
|
||||||
> **Availability:** `fx` is exported from the SigPro module. In **ESM** you must import it (`import { fx } from 'sigpro'`) or inject all globals via `sigpro()`. In the **IIFE** classic script, it is automatically available on `window`. The examples below assume the function is already in scope.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage Patterns
|
|
||||||
|
|
||||||
### 1. 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 built‑in 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.
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
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'`) or inject all globals via `sigpro()`. In the **IIFE** classic script, `$` is automatically available on `window`. The examples below assume `$` is already in scope (via import or global).
|
> **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)
|
||||||
|
|
||||||
@@ -111,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 = () =>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
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'`) or inject all globals via `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.
|
> **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
|
||||||
|
|
||||||
@@ -131,14 +131,14 @@ h('svg', { width: 100, height: 100 }, [
|
|||||||
| **Availability** | Import or global | Import or global (same) |
|
| **Availability** | Import or global | Import or global (same) |
|
||||||
| **Performance** | Identical | Identical (helpers call `h` internally) |
|
| **Performance** | Identical | Identical (helpers call `h` internally) |
|
||||||
|
|
||||||
> **Recommendation:** Use tag helpers (`div()`, `button()`, etc.) for most cases – they are shorter and more readable. Use `h` directly only when the tag name is dynamic (e.g., `h(props.tag, ...)`). Remember that in ESM you need to import the helpers or call `sigpro()` to make them global.
|
> **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');
|
||||||
|
|
||||||
|
|||||||
131
docs/api/jsx.md
131
docs/api/jsx.md
@@ -1,4 +1,4 @@
|
|||||||
# Hyperscript & Tag Helpers
|
# Hyperscript & Tag Helpers & JSX Style
|
||||||
|
|
||||||
SigPro provides two complementary ways to create DOM elements:
|
SigPro provides two complementary ways to create DOM elements:
|
||||||
|
|
||||||
@@ -9,135 +9,6 @@ Both are reactive, auto‑cleanup, and support SVG, events, two‑way 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 – auto‑removed on cleanup. |
|
|
||||||
| **`value` / `checked`** | When a signal is passed, creates two‑way 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 compile‑time syntax sugar for `h` calls.
|
SigPro works seamlessly with JSX. You can use JSX as a compile‑time syntax sugar for `h` calls.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ 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'`) or inject all globals via `sigpro()`. In the **IIFE** classic script, it is automatically available on `window`. The examples below assume the function is already in scope.
|
> **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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -119,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);
|
||||||
|
|||||||
@@ -1,17 +1,6 @@
|
|||||||
# ⚡ SigPro – Complete API Reference
|
# SigPro – Complete API Reference
|
||||||
|
|
||||||
SigPro is a **Real‑DOM first** reactive micro‑framework. No virtual DOM, no diffing overhead – it updates the DOM directly with surgical precision. Built‑in 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, if you prefer the global style in an ESM environment:
|
|
||||||
// import { sigpro } from 'sigpro'; sigpro(); // then $, h, div... become window globals
|
|
||||||
// In a classic IIFE script (<script src="sigpro.js">), everything is available globally automatically.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔁 Core Reactivity
|
|
||||||
|
|
||||||
### `$(value, localStorageKey?)` – Signal & Computed
|
### `$(value, localStorageKey?)` – Signal & Computed
|
||||||
|
|
||||||
@@ -37,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:
|
||||||
@@ -82,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
|
||||||
|
|
||||||
@@ -96,7 +67,7 @@ The universal builder. `props` can be omitted. Children can be strings, numbers,
|
|||||||
| Two‑way binding | `value: mySignal` (works on `input`, `textarea`, `select`) |
|
| Two‑way 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 re‑executed and the DOM patched automatically:
|
**Dynamic children** – pass a function as a child, it will be re‑executed and the DOM patched automatically:
|
||||||
|
|
||||||
@@ -108,28 +79,13 @@ h('div', {}, [
|
|||||||
|
|
||||||
### Tag shortcuts
|
### Tag shortcuts
|
||||||
|
|
||||||
When using the **ESM module with named imports**, you can import the tag helpers individually:
|
Tag helpers **are exported** from the core.
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { div, h1, button } from 'sigpro'
|
|
||||||
|
|
||||||
div({ class: 'container' }, [
|
|
||||||
h1({}, 'Title'),
|
|
||||||
button({ onClick: () => alert('hi') }, 'Click me')
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|
||||||
If you prefer the **global style** (all tags and core functions on `window`), either:
|
|
||||||
- Use the classic IIFE script: `<script src="sigpro.js"></script>`
|
|
||||||
- Or in ESM, call `sigpro()` after importing: `import { sigpro } from 'sigpro'; sigpro();`
|
|
||||||
|
|
||||||
After that, you can write `div()`, `span()`, etc. directly, without any import.
|
|
||||||
|
|
||||||
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?)`
|
||||||
|
|
||||||
@@ -162,7 +118,7 @@ When the array changes, elements are added, removed, or reordered with minimal D
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 💥 Batch
|
## Batch
|
||||||
|
|
||||||
### `batch(fn)`
|
### `batch(fn)`
|
||||||
|
|
||||||
@@ -176,97 +132,7 @@ batch(() => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Mounting – `mount(component, target)`
|
||||||
|
|
||||||
## ✨ 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)`
|
|
||||||
|
|
||||||
Hash‑based 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 built‑in 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).
|
||||||
|
|
||||||
@@ -280,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.
|
||||||
@@ -292,10 +158,10 @@ 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
|
||||||
|
|
||||||
@@ -309,19 +175,3 @@ const App = () =>
|
|||||||
|
|
||||||
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 (after calling `sigpro()` or using the classic script):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
window.myReactive = $
|
|
||||||
```
|
|
||||||
184
docs/api/req.md
184
docs/api/req.md
@@ -1,184 +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.
|
|
||||||
|
|
||||||
> **Availability:** `req` is exported from the SigPro module. In **ESM** you must import it (`import { req } from 'sigpro'`) or inject all globals via `sigpro()`. In the **IIFE** classic script, it is automatically available on `window`. The examples below assume the function is already in scope.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage Patterns
|
|
||||||
|
|
||||||
### 1. 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 10‑second timeout is set (auto‑abort).
|
|
||||||
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 human‑readable 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`. |
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
```javascript
|
```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>
|
||||||
|
|
||||||
```javascript
|
```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>
|
||||||
|
|
||||||
```javascript
|
```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>
|
||||||
|
|
||||||
```javascript
|
```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 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## When to Use Each
|
|
||||||
|
|
||||||
### Use `$()` when:
|
|
||||||
|
|
||||||
<div id="demo-use-dollar"></div>
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{
|
// Clear, predictable, and memory-safe
|
||||||
const count = $(0);
|
const user = {
|
||||||
const firstName = $("John");
|
name: $("Juan"),
|
||||||
const lastName = $("Doe");
|
email: $("juan@example.com"),
|
||||||
const fullName = $(() => `${firstName()} ${lastName()}`);
|
preferences: {
|
||||||
|
theme: $("dark"),
|
||||||
const App = () => div([
|
notifications: $(true)
|
||||||
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
|
```javascript
|
||||||
{
|
const createUserStore = () => {
|
||||||
const form = $$({ email: "", password: "" });
|
const name = $("")
|
||||||
const settings = $$({ theme: "dark", notifications: true });
|
const email = $("")
|
||||||
|
|
||||||
const App = () => div([
|
const isValid = $(() => name().length > 0 && email().includes("@"))
|
||||||
input({ placeholder: "Email", onInput: e => form.email = e.target.value }),
|
|
||||||
input({ placeholder: "Password", type: "password", onInput: e => form.password = e.target.value }),
|
const actions = {
|
||||||
p(() => `Email: ${form.email}, Password: ${form.password}`),
|
setName: (value) => name(value),
|
||||||
button({ onClick: () => settings.theme = settings.theme === "dark" ? "light" : "dark" }, "Toggle theme"),
|
setEmail: (value) => email(value),
|
||||||
p(() => `Current theme: ${settings.theme}`)
|
reset: () => {
|
||||||
]);
|
name("")
|
||||||
setTimeout(() => mount(App, '#demo-use-dollar-dollar'), 50);
|
email("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name, email, isValid, ...actions }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userStore = createUserStore()
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ DON'T: Try to wrap objects with signals
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Wrong - loses reactivity on nested properties
|
||||||
|
const user = $({ name: "Juan", email: "..." })
|
||||||
|
user().name = "Ana" // ❌ Not reactive!
|
||||||
|
|
||||||
|
// Correct - each property its own signal
|
||||||
|
const userName = $("Juan")
|
||||||
|
const userEmail = $("...")
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ DON'T: Destructure signals in reactive contexts
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Wrong - breaks tracking
|
||||||
|
const { name, email } = user
|
||||||
|
watch(() => name(), ...) // ❌ 'name' is not tracked properly
|
||||||
|
|
||||||
|
// Correct - use the original signal
|
||||||
|
watch(() => user.name(), ...) // ✅
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -276,115 +282,121 @@ $$<T extends object>(obj: T): T
|
|||||||
|
|
||||||
### ✅ DO:
|
### ✅ DO:
|
||||||
```javascript
|
```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:
|
||||||
```javascript
|
```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>
|
||||||
|
|
||||||
```javascript
|
```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:
|
||||||
|
|
||||||
```javascript
|
- ✅ **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.
|
||||||
160
docs/api/tags.md
160
docs/api/tags.md
@@ -4,54 +4,32 @@ In **SigPro**, you don't need to manually type `h('div', ...)` for every element
|
|||||||
|
|
||||||
## 1. How it Works
|
## 1. How it Works
|
||||||
|
|
||||||
SigPro iterates through a list of standard HTML tags and creates a wrapper function for each one.
|
SigPro creates a wrapper function for each standard HTML tag.
|
||||||
- **Under the hood:** `h('button', { onclick: ... }, 'Click')`
|
- **Under the hood:** `h('button', { onclick: ... }, 'Click')`
|
||||||
- **SigPro Style:** `button({ onclick: ... }, 'Click')`
|
- **SigPro Style:** `button({ onclick: ... }, 'Click')`
|
||||||
|
|
||||||
> **Note:** All tag helpers are **lowercase** (e.g., `div`, `span`, `button`). This keeps the syntax close to raw HTML.
|
> **Note:** All tag helpers are **lowercase** (e.g., `div`, `span`, `button`) and can be used directly once globally enabled.
|
||||||
|
|
||||||
These helpers can be used in two ways, depending on your environment:
|
> If you prefer to avoid globals, you can always use `h('div', ...)` directly—it’s perfectly fine.
|
||||||
|
|
||||||
### Mode A: Classic (IIFE) – Auto‑global
|
> **Auto‑cleanup:** All tag helpers and `h` automatically dispose effects, event listeners, and nested components when removed from the DOM.
|
||||||
When you load the **IIFE bundle** (`sigpro.js`) with a traditional `<script>` tag (no `type="module"`), all tag helpers are automatically injected into the `window` object.
|
|
||||||
```html
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sigpro@1.2.19/dist/sigpro.js"></script>
|
|
||||||
<script>
|
|
||||||
// div, span, button, ... are already global
|
|
||||||
const App = () => div({ class: "card" }, "Hello");
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mode B: ESM (Modern) – Explicit or Imported
|
|
||||||
When you import the **ES module** (via `import` or CDN with `type="module"`), nothing is added to `window` by default. You have two options:
|
|
||||||
|
|
||||||
1. **Manual global injection** – import `sigpro` and call it:
|
|
||||||
```javascript
|
|
||||||
import { sigpro } from 'sigpro';
|
|
||||||
sigpro(); // now div, span, button, etc. become global
|
|
||||||
```
|
|
||||||
2. **Named imports** (recommended) – import the helpers you need directly:
|
|
||||||
```javascript
|
|
||||||
import { div, span, button } from 'sigpro';
|
|
||||||
// use them directly
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. The Complete List of Tag Helpers
|
## 2. The Complete List of Tag Helpers
|
||||||
|
|
||||||
All helpers are **lowercase** and follow HTML5 tag names. You can use them globally (after injection) or import them individually.
|
All helpers are **lowercase** and follow HTML5 tag names.
|
||||||
|
|
||||||
| Category | Available 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 all standard 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`.
|
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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -79,54 +57,7 @@ section([
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Reactive Power
|
## 4. Custom Components with `h()` or Tag Helpers
|
||||||
|
|
||||||
These helpers are natively wired into SigPro's reactivity system.
|
|
||||||
|
|
||||||
### Reactive Attributes (One‑Way)
|
|
||||||
|
|
||||||
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");
|
|
||||||
```
|
|
||||||
|
|
||||||
### Two‑Way Binding (Automatic)
|
|
||||||
|
|
||||||
Assign a **signal** directly to `value` or `checked` on form inputs – SigPro automatically bridges the signal and the input element bidirectionally.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const search = $("");
|
|
||||||
|
|
||||||
input({
|
|
||||||
type: "text",
|
|
||||||
placeholder: "Search...",
|
|
||||||
value: search
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Pro Tip:** To make an input **read‑only** but still reactive, wrap the signal in a function: `value: () => search()` – this prevents backward synchronization.
|
|
||||||
|
|
||||||
### Dynamic Children
|
|
||||||
|
|
||||||
You can pass a **function as a child** – it will be re‑executed 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()` or Tag Helpers
|
|
||||||
|
|
||||||
While the tag helpers cover all standard HTML tags, you can create reusable components using them directly.
|
While the tag helpers cover all standard HTML tags, you can create reusable components using them directly.
|
||||||
|
|
||||||
@@ -167,78 +98,3 @@ const Timer = () => {
|
|||||||
return el;
|
return el;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Comparison with `h()`
|
|
||||||
|
|
||||||
| Use case | Recommendation |
|
|
||||||
| :--- | :--- |
|
|
||||||
| Standard tags (`div`, `span`, `button`) | Use tag 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`) |
|
|
||||||
|
|
||||||
> **Auto‑cleanup:** All tag helpers and `h` automatically dispose effects, event listeners, and nested components when removed from the DOM.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Complete Example
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// In a modern ESM environment (recommended)
|
|
||||||
import { div, h1, input, p, button, mount, $ } from 'sigpro';
|
|
||||||
|
|
||||||
const nameSignal = $('');
|
|
||||||
|
|
||||||
const App = () =>
|
|
||||||
div({ class: "app" }, [
|
|
||||||
h1("Welcome"),
|
|
||||||
input({
|
|
||||||
placeholder: "Your name",
|
|
||||||
value: nameSignal
|
|
||||||
}),
|
|
||||||
p(() => `Hello, ${nameSignal() || "stranger"}!`),
|
|
||||||
button({ onClick: () => alert("Clicked") }, "Click me")
|
|
||||||
]);
|
|
||||||
|
|
||||||
mount(App, '#app');
|
|
||||||
```
|
|
||||||
|
|
||||||
Or using the classic script (auto‑global):
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sigpro@1.2.19/dist/sigpro.js"></script>
|
|
||||||
<script>
|
|
||||||
const nameSignal = $('');
|
|
||||||
const App = () => div({ class: "app" }, [
|
|
||||||
h1("Welcome"),
|
|
||||||
input({ placeholder: "Your name", value: nameSignal }),
|
|
||||||
p(() => `Hello, ${nameSignal() || "stranger"}!`),
|
|
||||||
button({ onClick: () => alert("Clicked") }, "Click me")
|
|
||||||
]);
|
|
||||||
mount(App, '#app');
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Important Notes
|
|
||||||
|
|
||||||
- **Naming:** All tag helpers are **lowercase** – no PascalCase helpers (`Div`, `Button`).
|
|
||||||
- **Global availability:**
|
|
||||||
- **IIFE script** → automatically on `window`.
|
|
||||||
- **ESM module** → not global by default; use `import { div } from 'sigpro'` or call `sigpro()` to inject all globals.
|
|
||||||
- **Custom components:** Use **PascalCase** for your own component functions (e.g., `UserCard`) to visually distinguish them from built‑in tags.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Summary
|
|
||||||
|
|
||||||
| Feature | Description |
|
|
||||||
| :--- | :--- |
|
|
||||||
| **Tag helpers** | Lowercase functions for every HTML element (e.g., `div()`, `button()`). |
|
|
||||||
| **Availability** | Auto‑global in IIFE; in ESM use named imports or `sigpro()`. |
|
|
||||||
| **Reactive attributes** | Pass a function to any attribute to keep it synced. |
|
|
||||||
| **Two‑way binding** | Assign a signal directly to `value` or `checked` on form elements. |
|
|
||||||
| **Dynamic children** | Pass a function as a child for live updating content. |
|
|
||||||
| **Auto‑cleanup** | All effects, events, and children are disposed when the element is removed. |
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ 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'`) or inject all globals via `sigpro()`. In the **IIFE** classic script, it is automatically available on `window`. The examples below assume the function is already in scope.
|
> **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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
> **Availability:** `when` is exported from the SigPro module. In **ESM** you must import it (`import { when } from 'sigpro'`) or inject all globals via `sigpro()`. In the **IIFE** classic script, it is automatically available on `window`. The examples below assume the function is already in scope.
|
> **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
121
docs/convert.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// src/convert.js
|
||||||
|
import { $ } from "./sigpro.js";
|
||||||
|
function html2sigpro(h, advanced = false) {
|
||||||
|
const B = new Set(["allowfullscreen", "async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "formnovalidate", "hidden", "ismap", "itemscope", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected", "truespeed"]), esc = (v) => v.replace(/"/g, "\\\""), bP = (el) => {
|
||||||
|
let a = [...el.attributes].map(({ name: n, value: v }) => /^on/i.test(n) ? `${n}: (e) => { ${v.replace(/\s+/g, " ").trim()} }` : B.has(n.toLowerCase()) && (!v || v == n) ? `${n}: true` : `${n}: "${esc(v)}"`);
|
||||||
|
return a.length ? `{ ${a.join(", ")} }` : "";
|
||||||
|
}, cN = (n, d = 0) => {
|
||||||
|
let s = " ".repeat(d);
|
||||||
|
if (n.nodeType == 3) {
|
||||||
|
let t = n.textContent;
|
||||||
|
return t.trim() ? `${s}"${esc(t)}"` : "";
|
||||||
|
}
|
||||||
|
if (n.nodeType == 1) {
|
||||||
|
let t = n.tagName.toLowerCase();
|
||||||
|
let classes = [];
|
||||||
|
let otherAttrs = [];
|
||||||
|
if (advanced) {
|
||||||
|
const classAttribute = Array.from(n.attributes).find((attr) => attr.name === "class");
|
||||||
|
if (classAttribute) {
|
||||||
|
classes = classAttribute.value.trim().split(/\s+/).filter((c2) => c2);
|
||||||
|
}
|
||||||
|
otherAttrs = [...n.attributes].filter((attr) => attr.name !== "class");
|
||||||
|
}
|
||||||
|
let p = "";
|
||||||
|
if (advanced && classes.length > 0) {
|
||||||
|
const classChain = classes.map((c2) => `.${c2.replace(/-/g, "_")}`).join("");
|
||||||
|
if (otherAttrs.length > 0) {
|
||||||
|
const otherProps = otherAttrs.map(({ name: n2, value: v }) => /^on/i.test(n2) ? `${n2}: (e) => { ${v.replace(/\s+/g, " ").trim()} }` : B.has(n2.toLowerCase()) && (!v || v == n2) ? `${n2}: true` : `${n2}: "${esc(v)}"`);
|
||||||
|
p = `${classChain}({ ${otherProps.join(", ")} })`;
|
||||||
|
} else {
|
||||||
|
p = classChain;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p = bP(n);
|
||||||
|
}
|
||||||
|
let c = [...n.childNodes].map((i) => cN(i, d + 1)).filter(Boolean);
|
||||||
|
if (!c.length) {
|
||||||
|
if (advanced && classes.length > 0 && otherAttrs.length === 0) {
|
||||||
|
return `${s}${t}${p}`;
|
||||||
|
}
|
||||||
|
return `${s}${t}(${p})`;
|
||||||
|
}
|
||||||
|
if (c.length == 1 && !c[0].includes(`
|
||||||
|
`)) {
|
||||||
|
if (advanced && classes.length > 0 && otherAttrs.length === 0 && !p.includes("{")) {
|
||||||
|
return `${s}${t}${p}(${c[0].trim()})`;
|
||||||
|
}
|
||||||
|
return `${s}${t}(${p ? p + ", " : ""}${c[0].trim()})`;
|
||||||
|
}
|
||||||
|
if (advanced && classes.length > 0 && otherAttrs.length === 0 && !p.includes("{")) {
|
||||||
|
return `${s}${t}${p}([
|
||||||
|
${c.join(`,
|
||||||
|
`)}
|
||||||
|
${s}])`;
|
||||||
|
}
|
||||||
|
return `${s}${t}(${p ? p + ", " : ""}[
|
||||||
|
${c.join(`,
|
||||||
|
`)}
|
||||||
|
${s}])`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}, r = [...new DOMParser().parseFromString(h, "text/html").body.childNodes].map((n) => cN(n)).filter(Boolean);
|
||||||
|
return r.length == 1 ? r[0].trim() : `[
|
||||||
|
${r.join(`,
|
||||||
|
`)}
|
||||||
|
]`;
|
||||||
|
}
|
||||||
|
var converter = () => {
|
||||||
|
const inH = $("");
|
||||||
|
const setInH = (v) => inH(v);
|
||||||
|
const outS = $("");
|
||||||
|
const setOutS = (v) => outS(v);
|
||||||
|
const advanced = $(false);
|
||||||
|
const setAdvanced = (v) => advanced(v);
|
||||||
|
const cnv = () => {
|
||||||
|
try {
|
||||||
|
setOutS(html2sigpro(inH(), advanced()));
|
||||||
|
} catch (e) {
|
||||||
|
setOutS("Error: " + e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const txS = "width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical", btS = "padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px";
|
||||||
|
return div({ style: "max-width:900px;margin:20px auto;font-family:sans-serif" }, [
|
||||||
|
h1("HTML → SigPro"),
|
||||||
|
label({ style: "display:block;font-weight:700" }, "HTML:"),
|
||||||
|
textarea({
|
||||||
|
style: txS,
|
||||||
|
placeholder: "HTML...",
|
||||||
|
value: inH,
|
||||||
|
oninput: (e) => {
|
||||||
|
setInH(e.target.value);
|
||||||
|
cnv();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
div({ style: "margin:10px 0;display:flex;align-items:center;gap:10px" }, [
|
||||||
|
button({ style: btS + ";background:#3b82f6;color:#fff", onclick: cnv }, "Convert"),
|
||||||
|
button({ style: btS + ";background:#d1d5db", onclick: () => {
|
||||||
|
setInH("");
|
||||||
|
setOutS("");
|
||||||
|
setAdvanced(false);
|
||||||
|
} }, "Clear"),
|
||||||
|
label({ style: "display:flex;align-items:center;gap:5px;cursor:pointer" }, [
|
||||||
|
input({ type: "checkbox", checked: advanced, onchange: (e) => {
|
||||||
|
setAdvanced(e.target.checked);
|
||||||
|
cnv();
|
||||||
|
} }),
|
||||||
|
span("Advanced (dot notation for classes)")
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
div({ style: "display:flex;justify-content:space-between;align-items:center;margin-bottom:5px" }, [
|
||||||
|
span({ style: "font-weight:700" }, "Out:"),
|
||||||
|
button({ style: btS + ";background:#10b981;color:#fff", onclick: () => {
|
||||||
|
navigator.clipboard.writeText(outS());
|
||||||
|
alert("Copied!");
|
||||||
|
} }, "Copy")
|
||||||
|
]),
|
||||||
|
textarea({ style: txS + ";background:#f9fafb", readonly: true, value: outS, placeholder: "Result..." })
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
window.html2sigpro = html2sigpro;
|
||||||
|
window.converter = converter;
|
||||||
11
docs/convert.md
Normal file
11
docs/convert.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# HTML to SigPro Converter
|
||||||
|
|
||||||
|
Convert your existing HTML markup directly into SigPro Tag Helper syntax. Paste your HTML in the left panel and get clean, ready-to-use SigPro code on the right.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
<div id="sigpro-converter"></div>
|
||||||
|
|
||||||
|
```js
|
||||||
|
mount(window.converter, '#sigpro-converter');
|
||||||
|
```
|
||||||
@@ -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,7 +64,18 @@
|
|||||||
};
|
};
|
||||||
</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>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
156
docs/install.md
156
docs/install.md
@@ -12,70 +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 module – no automatic global injection
|
|
||||||
import { sigpro, $, h, mount } from 'https://cdn.jsdelivr.net/npm/sigpro@1.2.19/+esm';
|
|
||||||
|
|
||||||
// Option A: manually inject all globals (like the classic script)
|
import { $, h, mount } from "https://cdn.jsdelivr.net/npm/sigpro@latest/dist/sigpro.esm.min.js";
|
||||||
sigpro(); // now $, h, div, watch, etc. are on window
|
|
||||||
|
|
||||||
// Option B: use named imports (no global pollution)
|
|
||||||
const count = $(0);
|
const count = $(0);
|
||||||
mount(() => h1(() => `Count: ${count()}`), '#app');
|
mount(() => h("h1", {}, () => `Count: ${count()}`), "#app");
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input type="radio" name="install_method" class="tab border-base-300 whitespace-nowrap" aria-label="CDN (IIFE)" />
|
|
||||||
<div class="tab-content bg-base-100 border-base-300 rounded-box p-6">
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- Classic script: auto‑installs everything on window -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sigpro@1.2.19/dist/sigpro.js"></script>
|
|
||||||
<script>
|
|
||||||
// $, h, div, button, watch, ... are already global
|
|
||||||
const count = $(0);
|
|
||||||
const App = () => div({ class: "card" }, [
|
|
||||||
h1(() => `Count: ${count()}`),
|
|
||||||
button({ onclick: () => count(count() + 1) }, "Increment")
|
|
||||||
]);
|
|
||||||
mount(App, '#app');
|
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -89,34 +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="Bundlers (ESM)" 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
|
||||||
// App.js – Using named imports (recommended)
|
// App.js – Use named imports for the core, activate helpers if needed
|
||||||
import { $, h1, button, div, mount } from 'sigpro';
|
import { $, mount } from "sigpro";
|
||||||
|
|
||||||
export const App = () => {
|
const App = () => {
|
||||||
const count = $(0);
|
const count = $(0);
|
||||||
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", onclick: () => count(count() + 1) },
|
||||||
"Increment"
|
"Increment",
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// main.js
|
mount(App, "#app");
|
||||||
import { mount } from '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
|
||||||
@@ -125,20 +76,21 @@ mount(App, '#app');
|
|||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sigpro@1.2.19/dist/sigpro.js"></script>
|
<script type="module">
|
||||||
<script>
|
// Import the core
|
||||||
// Everything is already global – no import needed
|
import { $, h, mount } from "https://cdn.jsdelivr.net/npm/sigpro@latest/dist/sigpro.esm.min.js";
|
||||||
const name = $('Developer');
|
const name = $("Developer");
|
||||||
const App = () => section({ class: "container" }, [
|
const App = () =>
|
||||||
h2(() => `Welcome, ${name()}`),
|
section({ class: "container" }, [
|
||||||
input({
|
h2(() => `Welcome, ${name()}`),
|
||||||
type: "text",
|
input({
|
||||||
class: "input input-bordered",
|
type: "text",
|
||||||
value: name,
|
class: "input input-bordered",
|
||||||
placeholder: "Type your name...",
|
value: name,
|
||||||
}),
|
placeholder: "Type your name...",
|
||||||
]);
|
}),
|
||||||
mount(App, '#app');
|
]);
|
||||||
|
mount(App, "#app");
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -149,62 +101,14 @@ mount(App, '#app');
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Global by Design (Two Modes)
|
## 3. Why no build step?
|
||||||
|
|
||||||
SigPro gives you full control over global pollution.
|
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.
|
||||||
|
|
||||||
### Mode A: Classic (IIFE) – Auto‑injection
|
|
||||||
When you load the **IIFE bundle** (`sigpro.js`) with a traditional `<script>` tag (no `type="module"`), the library automatically injects:
|
|
||||||
- All core functions (`$`, `$$`, `watch`, `h`, `when`, `each`, `fx`, `router`, `req`, `mount`, `batch`) into `window`.
|
|
||||||
- Lowercase tag helpers (`div`, `span`, `button`, etc.) also become global functions.
|
|
||||||
|
|
||||||
✅ Zero configuration – just drop the script and start coding.
|
|
||||||
|
|
||||||
### Mode B: ESM (Modern) – Explicit Injection
|
|
||||||
When you import the **ESM module** (from CDN or via `import`), **nothing** is added to `window` by default. You have two clean options:
|
|
||||||
|
|
||||||
1. **Named imports** (recommended for most apps):
|
|
||||||
```javascript
|
|
||||||
import { $, h, mount } from 'sigpro';
|
|
||||||
```
|
|
||||||
No global pollution, perfect for bundlers and large projects.
|
|
||||||
|
|
||||||
2. **Manual global injection** (similar to classic mode but controlled):
|
|
||||||
```javascript
|
|
||||||
import { sigpro } from 'sigpro';
|
|
||||||
sigpro(); // now $, h, div, button, ... are on window
|
|
||||||
```
|
|
||||||
Useful for quick prototyping or when you prefer the global style.
|
|
||||||
|
|
||||||
### Why two modes?
|
|
||||||
- **Legacy / no‑build**: Use the IIFE script and get everything automatically.
|
|
||||||
- **Modern ESM**: Keep your global namespace clean, leverage tree‑shaking, or inject only when you need it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
- **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.
|
||||||
@@ -214,7 +118,7 @@ SigPro stands out by removing the "Build Step" tax and the "Virtual DOM" overhea
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ 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.
|
||||||
|
|
||||||
> **Availability:** `router` and its helper methods (`router.to`, `router.back`, `router.path`, `router.params`) are exported from the SigPro module. In **ESM** you must import them (`import { router } from 'sigpro'`) or inject all globals via `sigpro()`. In the **IIFE** classic script, they are automatically available on `window`. The examples below assume the functions are already in scope.
|
> You must import them (`import { router } from 'sigpro/router'`). The examples below assume the functions are already in scope.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -28,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");
|
||||||
@@ -152,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");
|
||||||
@@ -187,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
34
docs/sigpro.convert.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
var{$:x}=window.SigPro;function f(s,l="tags"){let a=new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected","truespeed"]),p=(o)=>o.replace(/"/g,"\\\""),c=(o)=>{let t=[...o.attributes].map(({name:e,value:n})=>/^on/i.test(e)?`${e}: (e) => { ${n.replace(/\s+/g," ").trim()} }`:a.has(e.toLowerCase())&&(!n||n==e)?`${e}: true`:`${e}: "${p(n)}"`);return t.length?`{ ${t.join(", ")} }`:""},m=(o,t=0)=>{let e=" ".repeat(t);if(o.nodeType==3){let n=o.textContent;return n.trim()?`${e}"${p(n)}"`:""}if(o.nodeType==1){let n=o.tagName.toLowerCase(),d=c(o),i=l==="core"?`h('${n}'`:n,r=[...o.childNodes].map((h)=>m(h,t+1)).filter(Boolean),g=!!d;if(l==="core"){if(!r.length)return g?`${e}${i}, ${d})`:`${e}${i})`;if(r.length===1&&!r[0].includes(`
|
||||||
|
`))return g?`${e}${i}, ${d}, ${r[0].trim()})`:`${e}${i}, ${r[0].trim()})`;return g?`${e}${i}, ${d}, [
|
||||||
|
${r.join(`,
|
||||||
|
`)}
|
||||||
|
${e}])`:`${e}${i}, [
|
||||||
|
${r.join(`,
|
||||||
|
`)}
|
||||||
|
${e}])`}else{if(!r.length)return g?`${e}${i}(${d})`:`${e}${i}`;if(r.length===1&&!r[0].includes(`
|
||||||
|
`))return g?`${e}${i}(${d}, ${r[0].trim()})`:`${e}${i}(${r[0].trim()})`;return g?`${e}${i}(${d}, [
|
||||||
|
${r.join(`,
|
||||||
|
`)}
|
||||||
|
${e}])`:`${e}${i}([
|
||||||
|
${r.join(`,
|
||||||
|
`)}
|
||||||
|
${e}])`}}return""},u=[...new DOMParser().parseFromString(s,"text/html").body.childNodes].map((o)=>m(o)).filter(Boolean);return u.length==1?u[0].trim():`[
|
||||||
|
${u.join(`,
|
||||||
|
`)}
|
||||||
|
]`}var b=()=>{let s=x(""),l=x(""),a=x("tags"),p=x(""),c=()=>{try{l(f(s(),a()))}catch(t){l("Error: "+t.message)}p(s())},m=()=>{s(""),l(""),a("tags"),p("")},u="width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical",o="padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px";return div({style:"margin:20px auto;font-family:sans-serif"},[h1("HTML → SigPro"),div({style:"margin-bottom:10px"},[div({style:"display:flex;gap:20px;flex-wrap:wrap;margin-top:5px"},[label({style:"display:flex;align-items:center;gap:6px"},["Core",input({type:"radio",name:"mode",value:"core",checked:a()==="core",onchange:(t)=>{if(t.target.checked)a("core"),c()}}),span("core — h('tag', props, ...)")]),label({style:"display:flex;align-items:center;gap:6px"},["Tags",input({type:"radio",name:"mode",value:"tags",checked:a()==="tags",onchange:(t)=>{if(t.target.checked)a("tags"),c()}}),span("tags — tag({ props }, ...)")])])]),div({style:"margin-top:15px;display:flex;gap:10px"},[button({style:"padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px;background:#3b82f6;color:#fff",onclick:c},"Convert"),button({style:"padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px;background:#d1d5db",onclick:m},"Clear")]),div({style:"display:grid;grid-template-columns:1fr;gap:15px;margin-top:15px;width:100%"},[div({style:"border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column"},[label({style:"font-weight:bold;margin-bottom:8px"},"HTML Input"),textarea({style:"width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical",placeholder:"Paste your HTML here...",value:s,oninput:(t)=>{s(t.target.value),c()}})]),div({style:"border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column"},[div({style:"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px"},[span({style:"font-weight:bold"},"SigPro Output"),button({style:"padding:4px 8px;background:#10b981;color:white;border:none;border-radius:4px;cursor:pointer;font-size:12px",onclick:()=>{navigator.clipboard.writeText(l()),alert("Copied!")}},"Copy")]),textarea({style:"width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical;background:#f9fafb",readonly:!0,value:l,placeholder:"Converted code will appear here..."})]),div({style:"border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column"},[label({style:"font-weight:bold;margin-bottom:8px"},"Live Preview"),iframe({style:"width:100%;height:200px;border:1px solid #e2e8f0;border-radius:4px;background:white;",srcdoc:()=>{return`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||||
|
<style>
|
||||||
|
body { padding: 10px; margin: 0; font-family: sans-serif; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${p()||""}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`},sandbox:"allow-same-origin allow-scripts allow-popups allow-forms allow-modals"})])])])};window.html2sigpro=f;window.converter=b;
|
||||||
1
docs/sigpro.db.js
Normal file
1
docs/sigpro.db.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
var o=async(e,n={},t=null)=>{if(t)t(!0);try{let r=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n),credentials:"include"});if(!r.ok){let s=await r.text();throw Error(`Error ${r.status}: ${s}`)}return await r.json()}finally{if(t)t(!1)}};export{o as db};
|
||||||
1
docs/sigpro.editor.js
Normal file
1
docs/sigpro.editor.js
Normal file
File diff suppressed because one or more lines are too long
78
docs/sigpro.grid.js
Normal file
78
docs/sigpro.grid.js
Normal file
File diff suppressed because one or more lines are too long
646
docs/sigpro.js
646
docs/sigpro.js
File diff suppressed because one or more lines are too long
1
docs/sigpro.locale.js
Normal file
1
docs/sigpro.locale.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
var{$:c}=window.SigPro,s=c("en"),n={},e=(t)=>{for(let o of Object.keys(t)){if(!n[o])n[o]={};Object.assign(n[o],t[o])}},r=(t)=>{if(t&&n[t])s(t)},a=(t)=>{return()=>n[s()]?.[t]??t};export{a as t,r as setLocale,e as addLang};
|
||||||
1
docs/sigpro.router.js
Normal file
1
docs/sigpro.router.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
var{$:p,h:u,watch:m,render:g,isF:y}=window.SigPro,d=()=>window.location.hash.slice(1)||"/",s=p(d());window.addEventListener("hashchange",()=>s(d()));var w=p({}),c=(n)=>{let i=u("div",{class:"router-hook"}),r=null;return m([s],()=>{let l=s(),t=n.find((o)=>{let e=o.path.split("/").filter(Boolean),a=l.split("/").filter(Boolean);return e.length===a.length&&e.every((h,f)=>h[0]===":"||h===a[f])})||n.find((o)=>o.path==="*");if(t){r?.destroy();let o={};t.path.split("/").filter(Boolean).forEach((e,a)=>{if(e[0]===":")o[e.slice(1)]=l.split("/").filter(Boolean)[a]}),w(o),r=g(()=>y(t.component)?t.component(o):t.component),i.replaceChildren(r.container)}}),i.destroy=()=>{r?.destroy()},i};c.params=w;c.to=(n)=>window.location.hash=n.replace(/^#?\/?/,"#/");c.back=()=>window.history.back();c.path=()=>s();export{w as routerParams,c as router};
|
||||||
1
docs/sigpro.tags.js
Normal file
1
docs/sigpro.tags.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
var{h:a}=window.SigPro;if(typeof window<"u")"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((e)=>{window[e]=(t,s)=>a(e,t,s)});
|
||||||
2
docs/sigpro.ui.css
Normal file
2
docs/sigpro.ui.css
Normal file
File diff suppressed because one or more lines are too long
1
docs/sigpro.ui.js
Normal file
1
docs/sigpro.ui.js
Normal file
File diff suppressed because one or more lines are too long
1
docs/sigpro.utils.js
Normal file
1
docs/sigpro.utils.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
var{$:d,h:m,watch:g,render:x,isF:b}=window.SigPro,l=(t)=>{let e=()=>window.location.hash.slice(1)||"/",o=d(e()),n=()=>o(e());window.addEventListener("hashchange",n);let s=m("div",{class:"router-hook"}),h=null;return g([o],()=>{let f=o(),a=t.find((r)=>{let c=r.path.split("/").filter(Boolean),p=f.split("/").filter(Boolean);return c.length===p.length&&c.every((w,y)=>w[0]===":"||w===p[y])})||t.find((r)=>r.path==="*");if(a){h?.destroy();let r={};a.path.split("/").filter(Boolean).forEach((c,p)=>{if(c[0]===":")r[c.slice(1)]=f.split("/").filter(Boolean)[p]}),l.params(r),h=x(()=>b(a.component)?a.component(r):a.component),s.replaceChildren(h.container)}}),s.destroy=()=>{window.removeEventListener("hashchange",n),h?.destroy()},s};l.params=d({});l.to=(t)=>window.location.hash=t.replace(/^#?\/?/,"#/");l.back=()=>window.history.back();l.path=()=>window.location.hash.replace(/^#/,"")||"/";var k=async(t,e={},o=null)=>{if(o)o(!0);try{let n=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e),credentials:"include"});if(!n.ok){let s=await n.text();throw Error(`Error ${n.status}: ${s}`)}return await n.json()}finally{if(o)o(!1)}},u=d("en"),i={},v=(t)=>{for(let e of Object.keys(t)){if(!i[e])i[e]={};Object.assign(i[e],t[e])}},E=(t)=>{if(t&&i[t])u(t)},L=(t)=>{return()=>i[u()]?.[t]??t};export{L as t,E as setLocale,l as router,k as db,v as addLang};
|
||||||
4
docs/sigpro.vite.js
Normal file
4
docs/sigpro.vite.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
function g(){let u="\x00virtual:sigpro-routes",i=(e)=>{if(!fs.existsSync(e))return[];return fs.readdirSync(e,{recursive:!0}).filter((r)=>/\.(js|jsx)$/.test(r)&&!path.basename(r).startsWith("_")).map((r)=>path.resolve(e,r))},l=(e,r)=>{return("/"+path.relative(e,r).replace(/\\/g,"/").replace(/\.(js|jsx)$/,"").replace(/\/index$/,"").replace(/^index$/,"")).replace(/\/+/g,"/").replace(/\[\.\.\.([^\]]+)\]/g,"*").replace(/\[([^\]]+)\]/g,":$1").replace(/\/$/,"")||"/"};return{name:"sigpro-router",resolveId(e){if(e==="virtual:sigpro-routes")return u},load(e){if(e!==u)return;let r=process.cwd(),t=path.resolve(r,"src/pages"),p=i(t).sort((n,a)=>{let o=l(t,n),c=l(t,a);if(o.includes(":")&&!c.includes(":"))return 1;if(!o.includes(":")&&c.includes(":"))return-1;return c.length-o.length}),s="";if(p.forEach((n)=>{let a=l(t,n),o="./"+path.relative(r,n).replace(/\\/g,"/");s+=` { path: '${a}', component: () => import('/${o}') },
|
||||||
|
`}),!s.includes("path: '*'"))s+=` { path: '*', component: () => ({ default: () => document.createTextNode('404 - Not Found') }) },
|
||||||
|
`;return`export const routes = [
|
||||||
|
${s}];`}}}export{g as sigproRouter};
|
||||||
388
docs/ui.md
Normal file
388
docs/ui.md
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
<div id="ui"></div>
|
||||||
|
<div id="tab"></div>
|
||||||
|
<div id="file"></div>
|
||||||
|
<div id="demo-toast"></div>
|
||||||
|
|
||||||
|
```js
|
||||||
|
const fruta = $("");
|
||||||
|
const color = $("#3b82f6");
|
||||||
|
const fecha = $("");
|
||||||
|
const rango = $({ start: null, end: null });
|
||||||
|
const pais = $("");
|
||||||
|
|
||||||
|
const frutas = [
|
||||||
|
"Manzana",
|
||||||
|
"Pera",
|
||||||
|
"Plátano",
|
||||||
|
"Fresa",
|
||||||
|
"Mango",
|
||||||
|
"Sandía",
|
||||||
|
"Melón",
|
||||||
|
"Uva",
|
||||||
|
];
|
||||||
|
const paises = [
|
||||||
|
{ label: "🇪🇸 España", value: "ES" },
|
||||||
|
{ label: "🇲🇽 México", value: "MX" },
|
||||||
|
{ label: "🇦🇷 Argentina", value: "AR" },
|
||||||
|
{ label: "🇨🇴 Colombia", value: "CO" },
|
||||||
|
{ label: "🇨🇱 Chile", value: "CL" },
|
||||||
|
];
|
||||||
|
|
||||||
|
mount(
|
||||||
|
() =>
|
||||||
|
div({ class: "p-8 max-w-md mx-auto flex flex-col gap-4" }, [
|
||||||
|
h1({ class: "text-2xl font-bold" }, "Field Components"),
|
||||||
|
|
||||||
|
|
||||||
|
ui.autocomplete({
|
||||||
|
label: "Fruta favorita",
|
||||||
|
items: frutas,
|
||||||
|
value: fruta,
|
||||||
|
placeholder: "Buscar fruta...",
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
ui.autocomplete({
|
||||||
|
label: "País",
|
||||||
|
items: paises,
|
||||||
|
value: pais,
|
||||||
|
placeholder: "Elige un país...",
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
ui.datepicker({
|
||||||
|
label: "Fecha de nacimiento",
|
||||||
|
value: fecha,
|
||||||
|
placeholder: "Selecciona fecha...",
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
ui.datepicker({
|
||||||
|
label: "Estancia",
|
||||||
|
range: true,
|
||||||
|
value: rango,
|
||||||
|
placeholder: "Check-in → Check-out",
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
ui.colorpicker({
|
||||||
|
label: "Color favorito",
|
||||||
|
value: color,
|
||||||
|
placeholder: "Elige un color...",
|
||||||
|
}),
|
||||||
|
|
||||||
|
ui.password({}),
|
||||||
|
ui.theme(),
|
||||||
|
|
||||||
|
div(
|
||||||
|
{ class: "bg-base-200 rounded-box p-4 flex flex-col gap-2 text-sm" },
|
||||||
|
[
|
||||||
|
div({}, () => `🍎 Fruta: ${val(fruta) || "—"}`),
|
||||||
|
div({}, () => `🌍 País: ${val(pais) || "—"}`),
|
||||||
|
div({}, () => `📅 Fecha: ${val(fecha) || "—"}`),
|
||||||
|
div({}, () => {
|
||||||
|
const r = val(rango);
|
||||||
|
return r.start && r.end
|
||||||
|
? `🏨 Estancia: ${r.start} → ${r.end}`
|
||||||
|
: "🏨 Estancia: —";
|
||||||
|
}),
|
||||||
|
div({ class: "flex items-center gap-2" }, [
|
||||||
|
span({}, "🎨 Color:"),
|
||||||
|
div({
|
||||||
|
class: "w-6 h-6 rounded border border-base-300",
|
||||||
|
style: () => `background:${val(color)}`,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
"#ui",
|
||||||
|
);
|
||||||
|
|
||||||
|
const archivos = $([]);
|
||||||
|
const drag = $(false);
|
||||||
|
const error = $("");
|
||||||
|
const subiendo = $(false);
|
||||||
|
const progreso = $(0);
|
||||||
|
|
||||||
|
const MAX_SIZE = 5 * 1024 * 1024;
|
||||||
|
const MAX_FILES = 3;
|
||||||
|
|
||||||
|
const handleFiles = (files) => {
|
||||||
|
const arr = Array.from(files);
|
||||||
|
if (arr.length > MAX_FILES) {
|
||||||
|
error(`Máximo ${MAX_FILES} archivos`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const big = arr.find(f => f.size > MAX_SIZE);
|
||||||
|
if (big) {
|
||||||
|
error(`"${big.name}" supera los 5MB`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
error("");
|
||||||
|
archivos(arr);
|
||||||
|
};
|
||||||
|
|
||||||
|
const subirArchivos = async () => {
|
||||||
|
const files = archivos();
|
||||||
|
if (!files.length) return toast("Selecciona archivos primero", "alert-warning");
|
||||||
|
|
||||||
|
subiendo(true);
|
||||||
|
progreso(0);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
files.forEach(f => formData.append('files', f));
|
||||||
|
formData.append('carpeta', 'demo');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
xhr.upload.onprogress = (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
progreso(Math.round((e.loaded / e.total) * 100));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
resolve(JSON.parse(xhr.responseText));
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Error ${xhr.status}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = () => reject(new Error('Error de conexión'));
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.open('POST', '/api/upload');
|
||||||
|
xhr.send(formData);
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
toast(`✅ ${files.length} archivo(s) subidos`, "alert-success");
|
||||||
|
archivos([]);
|
||||||
|
progreso(0);
|
||||||
|
} catch (err) {
|
||||||
|
toast(`❌ ${err.message}`, "alert-error");
|
||||||
|
} finally {
|
||||||
|
subiendo(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileInputProps = {
|
||||||
|
type: "file",
|
||||||
|
class: "hidden",
|
||||||
|
multiple: true,
|
||||||
|
accept: "image/*,.pdf,.doc,.docx",
|
||||||
|
onchange: (e) => { handleFiles(e.target.files); e.target.value = ''; }
|
||||||
|
};
|
||||||
|
|
||||||
|
mount(
|
||||||
|
() => div({ class: "p-8 max-w-md mx-auto flex flex-col gap-4" }, [
|
||||||
|
h1({ class: "text-2xl font-bold" }, "📁 Upload Files"),
|
||||||
|
|
||||||
|
// Zona drag & drop
|
||||||
|
h("label", {
|
||||||
|
class: () => `relative flex items-center justify-between h-14 px-4 border-2 border-dashed rounded-lg cursor-pointer transition-all ${
|
||||||
|
drag() ? 'border-primary bg-primary/10' : 'border-base-content/20 bg-base-100'
|
||||||
|
} ${subiendo() ? 'pointer-events-none opacity-50' : ''}`,
|
||||||
|
ondragover: (e) => { e.preventDefault(); if (!subiendo()) drag(true); },
|
||||||
|
ondragleave: () => drag(false),
|
||||||
|
ondrop: (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
drag(false);
|
||||||
|
if (subiendo()) return;
|
||||||
|
handleFiles(e.dataTransfer.files);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
h("div", { class: "flex items-center gap-3" }, [
|
||||||
|
h("span", { class: "icon-[lucide--upload] w-5 h-5 text-base-content/60" }),
|
||||||
|
h("div", {}, [
|
||||||
|
h("div", { class: "text-sm font-medium" }, "Arrastra archivos aquí"),
|
||||||
|
h("div", { class: "text-xs text-base-content/50" }, `Máx ${MAX_FILES} archivos · ${MAX_SIZE / 1024 / 1024}MB c/u`),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
h("span", { class: "text-xs text-base-content/40" }, "o haz clic"),
|
||||||
|
h("input", fileInputProps),
|
||||||
|
]),
|
||||||
|
|
||||||
|
|
||||||
|
() => error() ? ui.fileError({ message: error() }) : null,
|
||||||
|
|
||||||
|
|
||||||
|
() => archivos().length > 0 ? h("div", { class: "space-y-3" }, [
|
||||||
|
h("div", { class: "flex flex-wrap gap-2" },
|
||||||
|
archivos().map((f, i) => {
|
||||||
|
const isImage = f.type?.startsWith('image/');
|
||||||
|
const url = isImage ? URL.createObjectURL(f) : null;
|
||||||
|
|
||||||
|
return h("div", {
|
||||||
|
class: "relative group rounded-lg overflow-hidden border border-base-300 bg-base-200 w-20"
|
||||||
|
}, [
|
||||||
|
isImage ? h("img", {
|
||||||
|
src: url,
|
||||||
|
class: "w-20 h-20 object-cover",
|
||||||
|
onload: () => url && URL.revokeObjectURL(url)
|
||||||
|
}) : h("div", {
|
||||||
|
class: "w-20 h-20 flex flex-col items-center justify-center gap-1"
|
||||||
|
}, [
|
||||||
|
h("span", { class: "text-2xl" }, f.type?.includes('pdf') ? "📕" : "📄"),
|
||||||
|
h("span", { class: "text-[8px] uppercase opacity-50" }, f.name?.split('.').pop()),
|
||||||
|
]),
|
||||||
|
h("div", { class: "p-1" }, [
|
||||||
|
h("div", { class: "text-[9px] truncate font-medium leading-tight" }, f.name),
|
||||||
|
h("div", { class: "text-[8px] opacity-50" }, `${~~(f.size / 1024)} KB`),
|
||||||
|
]),
|
||||||
|
!subiendo() ? h("button", {
|
||||||
|
class: "absolute top-0.5 right-0.5 btn btn-circle btn-ghost btn-xs opacity-0 group-hover:opacity-100 bg-base-100/80",
|
||||||
|
onclick: () => archivos(archivos().filter((_, idx) => idx !== i))
|
||||||
|
}, h("span", { class: "icon-[lucide--x] w-3 h-3" })) : null,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
() => subiendo() ? h("div", { class: "space-y-1" }, [
|
||||||
|
h("div", { class: "flex justify-between text-xs" }, [
|
||||||
|
h("span", {}, "Subiendo..."),
|
||||||
|
h("span", {}, () => `${progreso()}%`),
|
||||||
|
]),
|
||||||
|
h("progress", { class: "progress progress-primary w-full", value: progreso, max: "100" }),
|
||||||
|
]) : null,
|
||||||
|
|
||||||
|
|
||||||
|
h("div", { class: "flex gap-2" }, [
|
||||||
|
h("button", {
|
||||||
|
class: "btn btn-ghost btn-sm",
|
||||||
|
onclick: () => { archivos([]); error(""); }
|
||||||
|
}, "🗑 Limpiar"),
|
||||||
|
h("button", {
|
||||||
|
class: "btn btn-primary btn-sm",
|
||||||
|
disabled: subiendo,
|
||||||
|
onclick: subirArchivos
|
||||||
|
}, () => subiendo() ? "⏳ Subiendo..." : "☁️ Subir al servidor"),
|
||||||
|
]),
|
||||||
|
]) : null,
|
||||||
|
]),
|
||||||
|
"#file",
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const tabsSignal = $([
|
||||||
|
{ id: "a", label: "Tab A", content: "Content of tab A", open: true },
|
||||||
|
{ id: "b", label: "Tab B", content: "Content of tab B", closable: true },
|
||||||
|
{ id: "c", label: "Tab C", content: "Content of tab C" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
mount(
|
||||||
|
() => ui.tabs(
|
||||||
|
{ class: "tabs-box" },
|
||||||
|
() => tabsSignal().flatMap((tab, i) =>
|
||||||
|
ui.tab({
|
||||||
|
name: "demo-tabs",
|
||||||
|
classContent: "bg-base-100 border-base-300 p-6",
|
||||||
|
label: tab.label,
|
||||||
|
content: tab.content,
|
||||||
|
checked: tab.open || false,
|
||||||
|
tabs: tabsSignal,
|
||||||
|
index: i,
|
||||||
|
closable: tab.closable || false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"#tab",
|
||||||
|
);
|
||||||
|
|
||||||
|
mount(
|
||||||
|
div({ class: "flex flex-wrap gap-2" }, [
|
||||||
|
button({ class: "btn", onclick: () => toast("File saved!") }, "Simple"),
|
||||||
|
button(
|
||||||
|
{ class: "btn", onclick: () => toast("Error!", "alert-error", 5000) },
|
||||||
|
"Error (5s)",
|
||||||
|
),
|
||||||
|
button(
|
||||||
|
{
|
||||||
|
class: "btn",
|
||||||
|
onclick: () =>
|
||||||
|
toast(
|
||||||
|
div({ class: "flex items-center gap-2" }, [
|
||||||
|
span({ class: "icon-[lucide--check] text-lg" }),
|
||||||
|
span({}, "Report generated"),
|
||||||
|
]),
|
||||||
|
"alert-success",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"With icon",
|
||||||
|
),
|
||||||
|
button(
|
||||||
|
{
|
||||||
|
class: "btn",
|
||||||
|
onclick: () =>
|
||||||
|
toast(
|
||||||
|
div({ class: "flex flex-col" }, [
|
||||||
|
strong({}, "ATTENTION!"),
|
||||||
|
span({}, "Error saving!"),
|
||||||
|
button(
|
||||||
|
{
|
||||||
|
class: "btn btn-xs mt-1",
|
||||||
|
onclick: () => console.log("Retry"),
|
||||||
|
},
|
||||||
|
"Retry",
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
"alert-warning",
|
||||||
|
7000,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"Complex",
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
"#toast",
|
||||||
|
);
|
||||||
|
|
||||||
|
mount(
|
||||||
|
div({ class: "flex flex-wrap gap-2" }, [
|
||||||
|
button({ class: "btn", onclick: () => toast("File saved!") }, "Simple"),
|
||||||
|
button(
|
||||||
|
{ class: "btn", onclick: () => toast("Error!", "alert-error", 5000) },
|
||||||
|
"Error (5s)",
|
||||||
|
),
|
||||||
|
button(
|
||||||
|
{
|
||||||
|
class: "btn",
|
||||||
|
onclick: () =>
|
||||||
|
toast(
|
||||||
|
div({ class: "flex items-center gap-2" }, [
|
||||||
|
ui.icon("icon-[lucide--check]"),
|
||||||
|
span({}, "Report generated"),
|
||||||
|
]),
|
||||||
|
"alert-success",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"With icon",
|
||||||
|
),
|
||||||
|
button(
|
||||||
|
{
|
||||||
|
class: "btn",
|
||||||
|
onclick: () =>
|
||||||
|
toast(
|
||||||
|
div({ class: "flex flex-col" }, [
|
||||||
|
strong({}, "ATTENTION!"),
|
||||||
|
span({}, "Error saving!"),
|
||||||
|
button(
|
||||||
|
{
|
||||||
|
class: "btn btn-xs mt-1",
|
||||||
|
onclick: () => console.log("Retry"),
|
||||||
|
},
|
||||||
|
"Retry",
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
"alert-warning",
|
||||||
|
7000,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"Complex",
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
"#demo-toast",
|
||||||
|
);
|
||||||
|
|
||||||
|
```
|
||||||
@@ -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.
|
|
||||||
118
package.json
118
package.json
@@ -1,60 +1,112 @@
|
|||||||
{
|
{
|
||||||
"name": "sigpro",
|
"name": "sigpro",
|
||||||
"version": "1.2.20",
|
"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
40
sigpro.d.ts
vendored
@@ -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;
|
||||||
|
|||||||
593
sigpro.js
593
sigpro.js
@@ -1,593 +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 sigproFn = Object.freeze({ $, $$, watch, h, when, each, fx, router, req, mount, batch })
|
|
||||||
|
|
||||||
const sigpro = () => {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
Object.assign(window, sigproFn)
|
|
||||||
"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) })
|
|
||||||
console.log("SigPro DX installed.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof import.meta === 'undefined' && typeof window !== 'undefined') sigpro()
|
|
||||||
|
|
||||||
export { sigpro, $, $$, watch, h, when, each, fx, router, req, mount, batch }
|
|
||||||
308
sigpro.ui.d.ts
vendored
Normal file
308
sigpro.ui.d.ts
vendored
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
// sigpro.ui.d.ts
|
||||||
|
|
||||||
|
declare module "sigpro.ui" {
|
||||||
|
import type { Signal } from "./sigpro";
|
||||||
|
|
||||||
|
// Utilidades
|
||||||
|
function hide(): void;
|
||||||
|
|
||||||
|
// Toast
|
||||||
|
function toast(
|
||||||
|
message: string | (() => any) | any,
|
||||||
|
type?: "alert-success" | "alert-error" | "alert-warning" | "alert-info",
|
||||||
|
duration?: number
|
||||||
|
): () => void;
|
||||||
|
|
||||||
|
// Calendar
|
||||||
|
interface CalendarProps {
|
||||||
|
class?: string;
|
||||||
|
value?: Signal<string | { start?: string; end?: string; startHour?: number; endHour?: number }> | any;
|
||||||
|
range?: boolean | (() => boolean);
|
||||||
|
hour?: boolean;
|
||||||
|
onChange?: (value: any) => void;
|
||||||
|
}
|
||||||
|
function calendar(p: CalendarProps): HTMLElement;
|
||||||
|
|
||||||
|
// Pallete
|
||||||
|
interface PalleteProps {
|
||||||
|
class?: string;
|
||||||
|
value?: Signal<string> | ((v: string) => void);
|
||||||
|
onchange?: (color: string) => void;
|
||||||
|
}
|
||||||
|
function pallete(p: PalleteProps): HTMLElement;
|
||||||
|
|
||||||
|
// UI Components
|
||||||
|
namespace ui {
|
||||||
|
// Accordion
|
||||||
|
function accordion(p: { class?: string; name?: string; checked?: boolean }, ...c: any[]): HTMLElement;
|
||||||
|
function accordion_title(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function accordion_content(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Alert
|
||||||
|
function alert(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Autocomplete
|
||||||
|
interface AutocompleteProps {
|
||||||
|
label?: string;
|
||||||
|
items: string[] | { label: string; value: any }[] | Signal<any[]>;
|
||||||
|
value?: Signal<string> | ((v: any) => void);
|
||||||
|
placeholder?: string;
|
||||||
|
class?: string;
|
||||||
|
icon?: string;
|
||||||
|
onChange?: (v: any) => void;
|
||||||
|
}
|
||||||
|
function autocomplete(p: AutocompleteProps): HTMLElement;
|
||||||
|
|
||||||
|
// Avatar
|
||||||
|
function avatar(p: { class?: string; innerClass?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function avatar_group(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Badge
|
||||||
|
function badge(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Breadcrumbs
|
||||||
|
function breadcrumbs(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Button
|
||||||
|
function button(p: { class?: string; onclick?: (e: Event) => void; disabled?: boolean | Signal<boolean> }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Card
|
||||||
|
function card(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function card_title(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function card_body(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function card_actions(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Carousel
|
||||||
|
function carousel(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function carousel_item(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Chat
|
||||||
|
function chat(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function chat_image(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function chat_header(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function chat_bubble(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function chat_footer(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Checkbox
|
||||||
|
function checkbox(p: { class?: string; checked?: boolean | Signal<boolean>; [key: string]: any }): HTMLElement;
|
||||||
|
|
||||||
|
// Colorpicker
|
||||||
|
function colorpicker(p: AutocompleteProps): HTMLElement;
|
||||||
|
|
||||||
|
// Combo
|
||||||
|
interface ComboProps {
|
||||||
|
label?: string;
|
||||||
|
value?: Signal<string> | string | ((v: string) => void);
|
||||||
|
placeholder?: string;
|
||||||
|
class?: string;
|
||||||
|
icon?: string;
|
||||||
|
custom?: any;
|
||||||
|
disabled?: boolean | (() => boolean);
|
||||||
|
readonly?: string;
|
||||||
|
}
|
||||||
|
function combo(p: ComboProps, c?: Function): HTMLElement;
|
||||||
|
|
||||||
|
// Datepicker
|
||||||
|
interface DatepickerProps {
|
||||||
|
label?: string;
|
||||||
|
value?: Signal<string | { start?: string; end?: string }>;
|
||||||
|
range?: boolean | (() => boolean);
|
||||||
|
placeholder?: string;
|
||||||
|
class?: string;
|
||||||
|
fromPlaceholder?: string;
|
||||||
|
toPlaceholder?: string;
|
||||||
|
onChange?: (v: any) => void;
|
||||||
|
}
|
||||||
|
function datepicker(p: DatepickerProps): HTMLElement;
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
function divider(p: { class?: string }): HTMLElement;
|
||||||
|
|
||||||
|
// Drawer
|
||||||
|
function drawer(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function drawer_toggle(p: { class?: string }): HTMLElement;
|
||||||
|
function drawer_content(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function drawer_side(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function drawer_overlay(p: { class?: string }): HTMLElement;
|
||||||
|
|
||||||
|
// Dropdown
|
||||||
|
function dropdown(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function dropdown_button(p: { class?: string; tabindex?: string; role?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function dropdown_content(p: { class?: string; tabindex?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// FAB
|
||||||
|
function fab(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function fab_button(p: { class?: string; tabindex?: string; role?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Fieldset
|
||||||
|
function fieldset(p: { class?: string; label?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// File
|
||||||
|
function file(p: { class?: string; multiple?: boolean; accept?: string; onchange?: (e: Event) => void }): HTMLElement;
|
||||||
|
interface FileDragProps {
|
||||||
|
class?: string;
|
||||||
|
drag?: boolean | Signal<boolean>;
|
||||||
|
ondrag?: (v: boolean) => void;
|
||||||
|
ondrop?: (files: FileList) => void;
|
||||||
|
}
|
||||||
|
function file_drag(p: FileDragProps, ...c: any[]): HTMLElement;
|
||||||
|
interface FilePreviewProps {
|
||||||
|
class?: string;
|
||||||
|
files?: File[] | Signal<File[]>;
|
||||||
|
onremove?: (index: number) => void;
|
||||||
|
}
|
||||||
|
function file_preview(p: FilePreviewProps): HTMLElement;
|
||||||
|
function file_error(p: { class?: string; message?: string }): HTMLElement;
|
||||||
|
|
||||||
|
// Float
|
||||||
|
function float(p: { label?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Indicator
|
||||||
|
function indicator(p: { class?: string; value?: any; badgeClass?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Input
|
||||||
|
function input(p: {
|
||||||
|
label?: string;
|
||||||
|
class?: string;
|
||||||
|
icon?: string;
|
||||||
|
right?: any;
|
||||||
|
type?: string | (() => string);
|
||||||
|
placeholder?: string;
|
||||||
|
value?: Signal<string> | string;
|
||||||
|
[key: string]: any
|
||||||
|
}): HTMLElement;
|
||||||
|
|
||||||
|
// Kbd
|
||||||
|
function kbd(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Label
|
||||||
|
function label(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Loading
|
||||||
|
function loading(p: { class?: string }): HTMLElement;
|
||||||
|
|
||||||
|
// Menu
|
||||||
|
function menu(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
interface MenuItem {
|
||||||
|
label?: string;
|
||||||
|
href?: string;
|
||||||
|
items?: MenuItem[];
|
||||||
|
open?: boolean;
|
||||||
|
submenuClass?: string;
|
||||||
|
}
|
||||||
|
function menu_items(p: { items?: MenuItem[] }): HTMLElement[];
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
function modal(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function modal_box(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function modal_action(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Navbar
|
||||||
|
function navbar(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Option
|
||||||
|
function option(p: { [key: string]: any }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Password
|
||||||
|
function password(p: { label?: string; class?: string; value?: Signal<string> }): HTMLElement;
|
||||||
|
|
||||||
|
// Progress
|
||||||
|
function progress(p: { class?: string; value?: number | Signal<number>; max?: number | string }): HTMLElement;
|
||||||
|
|
||||||
|
// Radial
|
||||||
|
function radial(p: { class?: string; value?: number | Signal<number>; role?: string }): HTMLElement;
|
||||||
|
|
||||||
|
// Radio
|
||||||
|
function radio(p: { class?: string; name?: string; checked?: boolean | Signal<boolean>; [key: string]: any }): HTMLElement;
|
||||||
|
|
||||||
|
// Range
|
||||||
|
function range(p: { class?: string; min?: number; max?: number; value?: number | Signal<number>; oninput?: (e: Event) => void }): HTMLElement;
|
||||||
|
|
||||||
|
// Rating
|
||||||
|
interface RatingProps {
|
||||||
|
class?: string;
|
||||||
|
count?: number;
|
||||||
|
mask?: string;
|
||||||
|
itemClass?: string;
|
||||||
|
name?: string;
|
||||||
|
value?: Signal<number> | number;
|
||||||
|
offset?: number;
|
||||||
|
onChange?: (i: number) => void;
|
||||||
|
}
|
||||||
|
function rating(p: RatingProps): HTMLElement;
|
||||||
|
|
||||||
|
// Search
|
||||||
|
function search(p: { label?: string; class?: string; icon?: string; value?: Signal<string>; placeholder?: string }): HTMLElement;
|
||||||
|
|
||||||
|
// Select
|
||||||
|
function select(p: { class?: string; [key: string]: any }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Stack
|
||||||
|
function stack(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Stat
|
||||||
|
function stat(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function stat_figure(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function stat_title(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function stat_value(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function stat_desc(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Steps
|
||||||
|
function steps(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function step(p: { class?: string; dataContent?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Swap
|
||||||
|
function swap(p: { class?: string; value?: Signal<boolean> | boolean }, ...c: any[]): HTMLElement;
|
||||||
|
function swap_on(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function swap_off(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Table
|
||||||
|
function table(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function table_head(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function table_body(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function table_foot(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function table_row(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function table_th(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function table_td(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Tabs
|
||||||
|
function tabs(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
interface TabProps {
|
||||||
|
class?: string;
|
||||||
|
classContent?: string;
|
||||||
|
name?: string;
|
||||||
|
label?: string;
|
||||||
|
content?: any;
|
||||||
|
checked?: boolean | (() => boolean);
|
||||||
|
tabs?: Signal<any[]> | ((v: any[]) => void);
|
||||||
|
index?: number;
|
||||||
|
closable?: boolean;
|
||||||
|
onclick?: () => void;
|
||||||
|
}
|
||||||
|
function tab(p: TabProps): any[];
|
||||||
|
|
||||||
|
// Textarea
|
||||||
|
function textarea(p: { class?: string; [key: string]: any }): HTMLElement;
|
||||||
|
|
||||||
|
// Textrotate
|
||||||
|
function textrotate(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Theme
|
||||||
|
function theme(p?: { value?: string; class?: string }): HTMLElement;
|
||||||
|
|
||||||
|
// Timeline
|
||||||
|
function timeline(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function timeline_start(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function timeline_middle(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
function timeline_end(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Toggle
|
||||||
|
function toggle(p: { class?: string; value?: string | Signal<string>; checked?: boolean | Signal<boolean>; [key: string]: any }): HTMLElement;
|
||||||
|
|
||||||
|
// Tooltip
|
||||||
|
function tooltip(p: { class?: string; tip?: string }, ...c: any[]): HTMLElement;
|
||||||
|
|
||||||
|
// Validator
|
||||||
|
function validator(p: { class?: string }, ...c: any[]): HTMLElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
66843
src/grid-e/main.esm.mjs
Normal file
66843
src/grid-e/main.esm.mjs
Normal file
File diff suppressed because one or more lines are too long
131
src/grid-e/package.json
Normal file
131
src/grid-e/package.json
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"name": "ag-grid-enterprise",
|
||||||
|
"version": "35.2.0",
|
||||||
|
"description": "Advanced Data Grid / Data Table supporting Javascript / Typescript / React / Angular / Vue",
|
||||||
|
"main": "./dist/package/main.cjs.js",
|
||||||
|
"types": "./dist/types/src/main.d.ts",
|
||||||
|
"module": "./main.esm.mjs",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/package/main.esm.mjs",
|
||||||
|
"types": "./dist/types/src/main.d.ts",
|
||||||
|
"require": "./dist/package/main.cjs.js",
|
||||||
|
"default": "./dist/package/main.cjs.js"
|
||||||
|
},
|
||||||
|
"./styles/ag-grid-no-native-widgets.css": "./styles/ag-grid-no-native-widgets.css",
|
||||||
|
"./styles/ag-grid-no-native-widgets.min.css": "./styles/ag-grid-no-native-widgets.min.css",
|
||||||
|
"./styles/ag-grid.css": "./styles/ag-grid.css",
|
||||||
|
"./styles/ag-grid.min.css": "./styles/ag-grid.min.css",
|
||||||
|
"./styles/ag-theme-alpine-no-font.css": "./styles/ag-theme-alpine-no-font.css",
|
||||||
|
"./styles/ag-theme-alpine-no-font.min.css": "./styles/ag-theme-alpine-no-font.min.css",
|
||||||
|
"./styles/ag-theme-alpine.css": "./styles/ag-theme-alpine.css",
|
||||||
|
"./styles/ag-theme-alpine.min.css": "./styles/ag-theme-alpine.min.css",
|
||||||
|
"./styles/ag-theme-balham-no-font.css": "./styles/ag-theme-balham-no-font.css",
|
||||||
|
"./styles/ag-theme-balham-no-font.min.css": "./styles/ag-theme-balham-no-font.min.css",
|
||||||
|
"./styles/ag-theme-balham.css": "./styles/ag-theme-balham.css",
|
||||||
|
"./styles/ag-theme-balham.min.css": "./styles/ag-theme-balham.min.css",
|
||||||
|
"./styles/ag-theme-material-no-font.css": "./styles/ag-theme-material-no-font.css",
|
||||||
|
"./styles/ag-theme-material-no-font.min.css": "./styles/ag-theme-material-no-font.min.css",
|
||||||
|
"./styles/ag-theme-material.css": "./styles/ag-theme-material.css",
|
||||||
|
"./styles/ag-theme-material.min.css": "./styles/ag-theme-material.min.css",
|
||||||
|
"./styles/ag-theme-quartz-no-font.css": "./styles/ag-theme-quartz-no-font.css",
|
||||||
|
"./styles/ag-theme-quartz-no-font.min.css": "./styles/ag-theme-quartz-no-font.min.css",
|
||||||
|
"./styles/ag-theme-quartz.css": "./styles/ag-theme-quartz.css",
|
||||||
|
"./styles/ag-theme-quartz.min.css": "./styles/ag-theme-quartz.min.css",
|
||||||
|
"./styles/agGridAlpineFont.css": "./styles/agGridAlpineFont.css",
|
||||||
|
"./styles/agGridAlpineFont.min.css": "./styles/agGridAlpineFont.min.css",
|
||||||
|
"./styles/agGridBalhamFont.css": "./styles/agGridBalhamFont.css",
|
||||||
|
"./styles/agGridBalhamFont.min.css": "./styles/agGridBalhamFont.min.css",
|
||||||
|
"./styles/agGridClassicFont.css": "./styles/agGridClassicFont.css",
|
||||||
|
"./styles/agGridClassicFont.min.css": "./styles/agGridClassicFont.min.css",
|
||||||
|
"./styles/agGridMaterialFont.css": "./styles/agGridMaterialFont.css",
|
||||||
|
"./styles/agGridMaterialFont.min.css": "./styles/agGridMaterialFont.min.css",
|
||||||
|
"./styles/agGridQuartzFont.css": "./styles/agGridQuartzFont.css",
|
||||||
|
"./styles/agGridQuartzFont.min.css": "./styles/agGridQuartzFont.min.css",
|
||||||
|
"./styles": "./styles/_index.scss"
|
||||||
|
},
|
||||||
|
"sideEffects": [
|
||||||
|
"./styles/ag-grid-no-native-widgets.css",
|
||||||
|
"./styles/ag-grid-no-native-widgets.min.css",
|
||||||
|
"./styles/ag-grid.css",
|
||||||
|
"./styles/ag-grid.min.css",
|
||||||
|
"./styles/ag-theme-alpine-no-font.css",
|
||||||
|
"./styles/ag-theme-alpine-no-font.min.css",
|
||||||
|
"./styles/ag-theme-alpine.css",
|
||||||
|
"./styles/ag-theme-alpine.min.css",
|
||||||
|
"./styles/ag-theme-balham-no-font.css",
|
||||||
|
"./styles/ag-theme-balham-no-font.min.css",
|
||||||
|
"./styles/ag-theme-balham.css",
|
||||||
|
"./styles/ag-theme-balham.min.css",
|
||||||
|
"./styles/ag-theme-material-no-font.css",
|
||||||
|
"./styles/ag-theme-material-no-font.min.css",
|
||||||
|
"./styles/ag-theme-material.css",
|
||||||
|
"./styles/ag-theme-material.min.css",
|
||||||
|
"./styles/ag-theme-quartz-no-font.css",
|
||||||
|
"./styles/ag-theme-quartz-no-font.min.css",
|
||||||
|
"./styles/ag-theme-quartz.css",
|
||||||
|
"./styles/ag-theme-quartz.min.css",
|
||||||
|
"./styles/agGridAlpineFont.css",
|
||||||
|
"./styles/agGridAlpineFont.min.css",
|
||||||
|
"./styles/agGridBalhamFont.css",
|
||||||
|
"./styles/agGridBalhamFont.min.css",
|
||||||
|
"./styles/agGridClassicFont.css",
|
||||||
|
"./styles/agGridClassicFont.min.css",
|
||||||
|
"./styles/agGridMaterialFont.css",
|
||||||
|
"./styles/agGridMaterialFont.min.css",
|
||||||
|
"./styles/agGridQuartzFont.css",
|
||||||
|
"./styles/agGridQuartzFont.min.css"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/ag-grid/ag-grid.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"ag",
|
||||||
|
"ag-grid",
|
||||||
|
"datagrid",
|
||||||
|
"data-grid",
|
||||||
|
"datatable",
|
||||||
|
"data-table",
|
||||||
|
"grid",
|
||||||
|
"table",
|
||||||
|
"react",
|
||||||
|
"table",
|
||||||
|
"angular",
|
||||||
|
"angular-component",
|
||||||
|
"react",
|
||||||
|
"react-component",
|
||||||
|
"reactjs",
|
||||||
|
"vue",
|
||||||
|
"vuejs"
|
||||||
|
],
|
||||||
|
"author": "Sean Landsman <sean@thelandsmans.com>",
|
||||||
|
"license": "Commercial",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/ag-grid/ag-grid/issues"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not ie >= 0",
|
||||||
|
"not ie_mob >= 0",
|
||||||
|
"not blackberry > 0"
|
||||||
|
],
|
||||||
|
"homepage": "https://www.ag-grid.com/",
|
||||||
|
"dependencies": {
|
||||||
|
"ag-grid-community": "35.2.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"ag-charts-community": "13.2.0",
|
||||||
|
"ag-charts-enterprise": "13.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"ag-charts-community": "13.2.0",
|
||||||
|
"ag-charts-enterprise": "13.2.0",
|
||||||
|
"@types/jest": "^29.5.0",
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
"jest-canvas-mock": "2.5.2",
|
||||||
|
"canvas": "^3.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
147
src/sigpro.convert.js
Normal file
147
src/sigpro.convert.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
/// <reference path="../sigpro.d.ts" />
|
||||||
|
|
||||||
|
var { $ } = window.SigPro;
|
||||||
|
|
||||||
|
function html2sigpro(h, mode = "tags") {
|
||||||
|
const B = new Set(["allowfullscreen", "async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "formnovalidate", "hidden", "ismap", "itemscope", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected", "truespeed"]);
|
||||||
|
const esc = v => v.replace(/"/g, '\\"');
|
||||||
|
|
||||||
|
const bP = el => {
|
||||||
|
let a = [...el.attributes].map(({ name: n, value: v }) =>
|
||||||
|
/^on/i.test(n) ? `${n}: (e) => { ${v.replace(/\s+/g, " ").trim()} }` :
|
||||||
|
(B.has(n.toLowerCase()) && (!v || v == n)) ? `${n}: true` : `${n}: "${esc(v)}"`
|
||||||
|
);
|
||||||
|
return a.length ? `{ ${a.join(", ")} }` : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const cN = (n, d = 0) => {
|
||||||
|
let s = " ".repeat(d);
|
||||||
|
if (n.nodeType == 3) {
|
||||||
|
let t = n.textContent;
|
||||||
|
return t.trim() ? `${s}"${esc(t)}"` : "";
|
||||||
|
}
|
||||||
|
if (n.nodeType == 1) {
|
||||||
|
let tag = n.tagName.toLowerCase();
|
||||||
|
let props = bP(n);
|
||||||
|
let prefix = mode === "core" ? `h('${tag}'` : tag;
|
||||||
|
|
||||||
|
let children = [...n.childNodes].map(i => cN(i, d + 1)).filter(Boolean);
|
||||||
|
const hasProps = !!props;
|
||||||
|
|
||||||
|
if (mode === "core") {
|
||||||
|
if (!children.length) return hasProps ? `${s}${prefix}, ${props})` : `${s}${prefix})`;
|
||||||
|
if (children.length === 1 && !children[0].includes("\n"))
|
||||||
|
return hasProps ? `${s}${prefix}, ${props}, ${children[0].trim()})` : `${s}${prefix}, ${children[0].trim()})`;
|
||||||
|
return hasProps ? `${s}${prefix}, ${props}, [\n${children.join(",\n")}\n${s}])` : `${s}${prefix}, [\n${children.join(",\n")}\n${s}])`;
|
||||||
|
} else {
|
||||||
|
if (!children.length) return hasProps ? `${s}${prefix}(${props})` : `${s}${prefix}`;
|
||||||
|
if (children.length === 1 && !children[0].includes("\n"))
|
||||||
|
return hasProps ? `${s}${prefix}(${props}, ${children[0].trim()})` : `${s}${prefix}(${children[0].trim()})`;
|
||||||
|
return hasProps ? `${s}${prefix}(${props}, [\n${children.join(",\n")}\n${s}])` : `${s}${prefix}([\n${children.join(",\n")}\n${s}])`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const r = [...new DOMParser().parseFromString(h, "text/html").body.childNodes].map(n => cN(n)).filter(Boolean);
|
||||||
|
return r.length == 1 ? r[0].trim() : `[\n${r.join(",\n")}\n]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const converter = () => {
|
||||||
|
const inH = $("");
|
||||||
|
const outS = $("");
|
||||||
|
const mode = $("tags");
|
||||||
|
const previewHtml = $("");
|
||||||
|
|
||||||
|
const cnv = () => {
|
||||||
|
try {
|
||||||
|
outS(html2sigpro(inH(), mode()));
|
||||||
|
} catch (e) {
|
||||||
|
outS("Error: " + e.message);
|
||||||
|
}
|
||||||
|
previewHtml(inH());
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAll = () => {
|
||||||
|
inH("");
|
||||||
|
outS("");
|
||||||
|
mode("tags");
|
||||||
|
previewHtml("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const txS = "width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical";
|
||||||
|
const btS = "padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px";
|
||||||
|
|
||||||
|
return div({ style: "margin:20px auto;font-family:sans-serif" }, [
|
||||||
|
h1("HTML → SigPro"),
|
||||||
|
div({ style: "margin-bottom:10px" }, [
|
||||||
|
div({ style: "display:flex;gap:20px;flex-wrap:wrap;margin-top:5px" }, [
|
||||||
|
label({ style: "display:flex;align-items:center;gap:6px" }, [
|
||||||
|
"Core",
|
||||||
|
input({ type: "radio", name: "mode", value: "core", checked: mode() === "core", onchange: e => { if (e.target.checked) { mode("core"); cnv(); } } }),
|
||||||
|
span("core — h('tag', props, ...)")
|
||||||
|
]),
|
||||||
|
label({ style: "display:flex;align-items:center;gap:6px" }, [
|
||||||
|
"Tags",
|
||||||
|
input({ type: "radio", name: "mode", value: "tags", checked: mode() === "tags", onchange: e => { if (e.target.checked) { mode("tags"); cnv(); } } }),
|
||||||
|
span("tags — tag({ props }, ...)")
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
div({ style: "margin-top:15px;display:flex;gap:10px" }, [
|
||||||
|
button({ style: btS + ";background:#3b82f6;color:#fff", onclick: cnv }, "Convert"),
|
||||||
|
button({ style: btS + ";background:#d1d5db", onclick: clearAll }, "Clear")
|
||||||
|
]),
|
||||||
|
div({ style: "display:grid;grid-template-columns:1fr;gap:15px;margin-top:15px;width:100%" }, [
|
||||||
|
div({ style: "border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column" }, [
|
||||||
|
label({ style: "font-weight:bold;margin-bottom:8px" }, "HTML Input"),
|
||||||
|
textarea({
|
||||||
|
style: txS,
|
||||||
|
placeholder: "Paste your HTML here...",
|
||||||
|
value: inH,
|
||||||
|
oninput: e => { inH(e.target.value); cnv(); }
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
div({ style: "border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column" }, [
|
||||||
|
div({ style: "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px" }, [
|
||||||
|
span({ style: "font-weight:bold" }, "SigPro Output"),
|
||||||
|
button({
|
||||||
|
style: "padding:4px 8px;background:#10b981;color:white;border:none;border-radius:4px;cursor:pointer;font-size:12px",
|
||||||
|
onclick: () => { navigator.clipboard.writeText(outS()); alert("Copied!"); }
|
||||||
|
}, "Copy")
|
||||||
|
]),
|
||||||
|
textarea({ style: txS + ";background:#f9fafb", readonly: true, value: outS, placeholder: "Converted code will appear here..." })
|
||||||
|
]),
|
||||||
|
div({ style: "border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column" }, [
|
||||||
|
label({ style: "font-weight:bold;margin-bottom:8px" }, "Live Preview"),
|
||||||
|
iframe({
|
||||||
|
style: "width:100%;height:200px;border:1px solid #e2e8f0;border-radius:4px;background:white;",
|
||||||
|
srcdoc: () => {
|
||||||
|
const html = previewHtml() || "";
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||||
|
<style>
|
||||||
|
body { padding: 10px; margin: 0; font-family: sans-serif; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${html}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
sandbox: "allow-same-origin allow-scripts allow-popups allow-forms allow-modals"
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.html2sigpro = html2sigpro;
|
||||||
|
window.converter = converter;
|
||||||
18
src/sigpro.db.js
Normal file
18
src/sigpro.db.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export const db = async (url, data = {}, loading = null) => {
|
||||||
|
if (loading) loading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
throw new Error(`Error ${res.status}: ${errorText}`);
|
||||||
|
}
|
||||||
|
return await res.json();
|
||||||
|
} finally {
|
||||||
|
if (loading) loading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
21
src/sigpro.editor.js
Normal file
21
src/sigpro.editor.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/// <reference path="../sigpro.d.ts" />
|
||||||
|
|
||||||
|
const { $, isF } = window.SigPro
|
||||||
|
|
||||||
|
export const Editor = p => {
|
||||||
|
let { value: v, class: x } = p, i = $(0), s = $(""), c = $(0), t = $(0), o = $(0), S = () => window.getSelection().rangeCount ? window.getSelection().getRangeAt(0) : 0, R, r, E = "😀 😊 😉 🧐 😮 🤔 😅 😂 😍 😘 🥰 👍 👎 👌 🤝 🤞 👋 👏 🙌 🙏 💪 ☝️ 👇 👈 👉 🖕 ✅ ⚠️ 🚀 📢 ✉️ ❤️".split(" "),
|
||||||
|
U = () => { t(t() + 1); R && c(R.innerText.length) }, N = () => { if (R) { let H = R.innerHTML; isF(v) ? v(H) : p.onchange?.(H); U() } },
|
||||||
|
X = (m, k = null) => { R?.focus(); if (r) { let w = window.getSelection(); w.removeAllRanges(); w.addRange(r) } document.execCommand(m, 0, k); r = 0; N() },
|
||||||
|
q = (k, Z = null) => { t(); if (!R || i()) return 0; try { if (k == 'formatBlock') { let n = window.getSelection().getRangeAt(0).commonAncestorContainer; while (n && n !== R) { if (n.tagName === Z) return 1; n = n.parentNode } return 0 } return document.queryCommandState(k) } catch (e) { return 0 } },
|
||||||
|
hU = f => { if (f) { let d = new FileReader; d.onload = e => { let I = f.type.startsWith('image/'), z = e.target.result; X("insertHTML", I ? div({ style: "display:inline-block;resize:both;overflow:hidden;vertical-align:bottom;width:200px;border:1px dashed #ccc;padding:2px;cursor:pointer", class: "resizable-img-container" }, [img({ src: z, style: "width:100%;height:100%;object-fit:contain;pointer-events:none" })]) : a({ href: z, download: f.name, contenteditable: "false", style: "display:inline-flex;align-items:center;gap:5px;padding:4px 8px;border:1px solid #ccc;border-radius:4px;background:#f9f9f9;text-decoration:none;color:#333;font-size:12px;margin:2px;cursor:pointer" }, [span({ class: "icon-[lucide--paperclip] w-3 h-3" }), f.name])) }; d.readAsDataURL(f) } },
|
||||||
|
B = (I, m, k) => button({ type: "button", class: () => `btn btn-ghost btn-xs ${q(m, k) ? 'btn-active bg-primary/20' : ''}`, onclick: () => typeof m == 'function' ? m() : X(m, k) }, [span({ class: `icon-[lucide--${I}]` })]);
|
||||||
|
|
||||||
|
return div({ class: `border border-base-300 rounded-box bg-base-100 overflow-hidden flex flex-col ${x||""}` }, [
|
||||||
|
div({ class: "flex flex-wrap items-center gap-1 p-2 border-b border-base-300 bg-base-200 sticky top-0 z-20" }, [
|
||||||
|
div({ class: "flex flex-wrap gap-1 flex-1" }, [B("bold", "bold"), B("italic", "italic"), B("underline", "underline"), input({ type: "color", class: "w-5 h-5 p-0 bg-transparent cursor-pointer", oninput: e => X("foreColor", e.target.value) }), span({ class: "w-px h-5 bg-base-300 mx-1" }), B("align-left", "justifyLeft"), B("align-center", "justifyCenter"), B("align-right", "justifyRight"), span({ class: "w-px h-5 bg-base-300 mx-1" }), B("list", "insertUnorderedList"), B("list-ordered", "insertOrderedList"), B("indent-decrease", "outdent"), B("indent-increase", "indent"), B("quote", () => X("formatBlock", q('formatBlock', 'BLOCKQUOTE') ? 'P' : 'BLOCKQUOTE'), 'BLOCKQUOTE'), span({ class: "w-px h-5 bg-base-300 mx-1" }), B("link", () => { let u = prompt('URL:'); u && X("createLink", u) }), B("paperclip", () => { let I = document.createElement('input'); I.type = 'file'; I.onchange = e => hU(e.target.files[0]); I.click() }), div({ class: "relative" }, [B("smile", () => { r = S(); o(!o()) }), div({ class: "absolute top-full left-0 mt-1 p-2 bg-base-100 border shadow-xl rounded-box w-52 z-50 flex flex-wrap gap-1", style: () => o() ? "" : "display:none" }, E.map(e => span({ class: "cursor-pointer p-1 text-lg", onclick: () => { X("insertText", e); o(0) } }, e)))]), B("undo-2", "undo"), B("redo-2", "redo")]), B("code-2", () => { if (!i()) s(R?.innerHTML || ""); else if (R) { R.innerHTML = s(); N() } i(!i()) })]),
|
||||||
|
div({ class: "relative flex-1 flex flex-col", onclick: () => o(0) }, [
|
||||||
|
div({ ref: l => { if (l && !R) { R = l; l.innerHTML = (isF(v) ? v() : v) || ""; document.execCommand("defaultParagraphSeparator", 0, "br"); l.onclick = e => { let c = e.target.closest('.resizable-img-container'); if (c) { let I = c.querySelector('img'); I && (k => { let O = document.createElement('div'); O.style = "position:fixed;top:0;left:0;width:100%;height:100%;background:#000e;z-index:9999;display:flex;align-items:center;justify-content:center;cursor:zoom-out"; O.onclick = () => O.remove(); let M = document.createElement('img'); M.src = k; M.style = "max-width:95%;max-height:95%"; O.appendChild(M); document.body.appendChild(O) })(I.src) } } } }, style: () => `min-height:22rem;${i() ? 'display:none' : ''}`, class: "p-4 outline-none text-base-content leading-relaxed [&>div]:m-0 [&>p]:m-0 [&>div]:min-h-[1em] [&_.resizable-img-container]:hover:border-primary [&_blockquote]:border-l-4 [&_blockquote]:border-base-300 [&_blockquote]:pl-4 [&_blockquote]:italic [&_ul]:list-disc [&_ul]:pl-8 [&_ol]:list-decimal [&_ol]:pl-8", contenteditable: "true", oninput: N, onkeydown: e => e.key === 'Tab' && (e.preventDefault(), X("indent")), onkeyup: () => { U(); r = S() }, onmouseup: N, onpaste: e => { e.preventDefault(); X('insertText', e.clipboardData.getData('text/plain')) }, ondrop: e => { e.preventDefault(); hU(e.dataTransfer.files[0]) }, ondragover: e => e.preventDefault() }),
|
||||||
|
textarea({ class: "w-full flex-1 min-h-[22rem] p-4 font-mono text-sm bg-base-200 border-0", style: () => i() ? '' : 'display:none', value: s, oninput: e => { s(e.target.value); if (R) R.innerHTML = e.target.value; p.onchange?.(e.target.value) } })]),
|
||||||
|
div({ class: "px-3 py-1 border-t text-[10px] text-right opacity-60" }, [span(() => c())])
|
||||||
|
])
|
||||||
|
}
|
||||||
207
src/sigpro.grid.js
Normal file
207
src/sigpro.grid.js
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
/// <reference path="../sigpro.d.ts" />
|
||||||
|
|
||||||
|
const { h, watch, onUnmount } = window.SigPro
|
||||||
|
|
||||||
|
import {
|
||||||
|
ModuleRegistry,
|
||||||
|
ValidationModule,
|
||||||
|
ColumnAutoSizeModule,
|
||||||
|
CellStyleModule,
|
||||||
|
QuickFilterModule,
|
||||||
|
RowSelectionModule,
|
||||||
|
TextEditorModule,
|
||||||
|
ClientSideRowModelModule,
|
||||||
|
themeQuartz,
|
||||||
|
createGrid,
|
||||||
|
NumberFilterModule,
|
||||||
|
TextFilterModule,
|
||||||
|
DateFilterModule
|
||||||
|
} from "ag-grid-community";
|
||||||
|
import {
|
||||||
|
MultiFilterModule,
|
||||||
|
SetFilterModule,
|
||||||
|
CellSelectionModule,
|
||||||
|
PivotModule,
|
||||||
|
MasterDetailModule,
|
||||||
|
SideBarModule,
|
||||||
|
ColumnsToolPanelModule,
|
||||||
|
ColumnMenuModule,
|
||||||
|
StatusBarModule,
|
||||||
|
ExcelExportModule,
|
||||||
|
ClipboardModule,
|
||||||
|
ContextMenuModule
|
||||||
|
} from "./grid-e";
|
||||||
|
|
||||||
|
ModuleRegistry.registerModules([
|
||||||
|
ValidationModule,
|
||||||
|
ColumnAutoSizeModule,
|
||||||
|
CellStyleModule,
|
||||||
|
QuickFilterModule,
|
||||||
|
RowSelectionModule,
|
||||||
|
TextEditorModule,
|
||||||
|
ClientSideRowModelModule,
|
||||||
|
MultiFilterModule,
|
||||||
|
CellSelectionModule,
|
||||||
|
PivotModule,
|
||||||
|
MasterDetailModule,
|
||||||
|
SideBarModule,
|
||||||
|
ColumnsToolPanelModule,
|
||||||
|
ColumnMenuModule,
|
||||||
|
StatusBarModule,
|
||||||
|
ExcelExportModule,
|
||||||
|
ClipboardModule,
|
||||||
|
NumberFilterModule,
|
||||||
|
TextFilterModule,
|
||||||
|
SetFilterModule,
|
||||||
|
DateFilterModule,
|
||||||
|
ContextMenuModule
|
||||||
|
]);
|
||||||
|
|
||||||
|
const Grid = (props) => {
|
||||||
|
const { data, options, api, on, class: className, style = "height: 100%; width: 100%", dark } = props;
|
||||||
|
let gridApi = null;
|
||||||
|
let cleanupFn = null;
|
||||||
|
|
||||||
|
const getDark = () =>
|
||||||
|
dark !== undefined
|
||||||
|
? (typeof dark === 'function' ? dark() : dark)
|
||||||
|
: document.documentElement.getAttribute('data-theme') === 'dark' ||
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
const getTheme = () => {
|
||||||
|
const isDark = getDark();
|
||||||
|
|
||||||
|
if (isDark) {
|
||||||
|
return themeQuartz.withParams({
|
||||||
|
headerFontSize: 14,
|
||||||
|
headerVerticalPaddingScale: 0.4,
|
||||||
|
rowVerticalPaddingScale: 0.4,
|
||||||
|
backgroundColor: "#1d1d1d",
|
||||||
|
foregroundColor: "#ffffff",
|
||||||
|
headerBackgroundColor: "#2a2a2a",
|
||||||
|
headerForegroundColor: "#ffffff",
|
||||||
|
oddRowBackgroundColor: "#262626",
|
||||||
|
borderColor: "#404040",
|
||||||
|
browserColorScheme: "dark"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return themeQuartz.withParams({
|
||||||
|
browserColorScheme: "light",
|
||||||
|
headerFontSize: 14,
|
||||||
|
headerVerticalPaddingScale: 0.4,
|
||||||
|
rowVerticalPaddingScale: 0.4
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initGrid = (container) => {
|
||||||
|
if (cleanupFn) {
|
||||||
|
cleanupFn();
|
||||||
|
cleanupFn = null;
|
||||||
|
}
|
||||||
|
if (gridApi && !gridApi.isDestroyed()) {
|
||||||
|
gridApi.destroy();
|
||||||
|
if (api) api.current = null;
|
||||||
|
gridApi = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const initialData = typeof data === "function" ? data() : data;
|
||||||
|
const initialOptions = typeof options === "function" ? options() : options;
|
||||||
|
|
||||||
|
const commonEvents = [
|
||||||
|
'onFilterChanged', 'onModelUpdated', 'onGridSizeChanged',
|
||||||
|
'onFirstDataRendered', 'onRowValueChanged', 'onSelectionChanged',
|
||||||
|
'onCellClicked', 'onCellDoubleClicked', 'onCellValueChanged',
|
||||||
|
'onRowClicked', 'onSortChanged', 'onContextMenu',
|
||||||
|
'onColumnResized', 'onColumnMoved', 'onRowDataUpdated',
|
||||||
|
'onCellEditingStarted', 'onCellEditingStopped',
|
||||||
|
'onPaginationChanged', 'onBodyScroll'
|
||||||
|
];
|
||||||
|
|
||||||
|
const eventHandlers = {};
|
||||||
|
commonEvents.forEach(eventName => {
|
||||||
|
if (on?.[eventName]) {
|
||||||
|
eventHandlers[eventName] = (params) => on[eventName](params);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const gridOptions = {
|
||||||
|
...initialOptions,
|
||||||
|
theme: getTheme(),
|
||||||
|
rowData: initialData || [],
|
||||||
|
onGridReady: (params) => {
|
||||||
|
gridApi = params.api;
|
||||||
|
if (api) api.current = gridApi;
|
||||||
|
if (on?.onGridReady) on.onGridReady(params);
|
||||||
|
|
||||||
|
if (initialOptions?.autoSizeColumns) {
|
||||||
|
params.api.autoSizeAllColumns();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...eventHandlers
|
||||||
|
};
|
||||||
|
|
||||||
|
gridApi = createGrid(container, gridOptions);
|
||||||
|
|
||||||
|
const stopData = watch(() => {
|
||||||
|
if (!gridApi || gridApi.isDestroyed()) return;
|
||||||
|
const newData = typeof data === "function" ? data() : data;
|
||||||
|
if (Array.isArray(newData)) {
|
||||||
|
const currentData = gridApi.getGridOption("rowData");
|
||||||
|
if (newData !== currentData) {
|
||||||
|
gridApi.setGridOption("rowData", newData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const stopTheme = watch(() => {
|
||||||
|
if (!gridApi || gridApi.isDestroyed()) return;
|
||||||
|
getDark();
|
||||||
|
const newTheme = getTheme();
|
||||||
|
const currentTheme = gridApi.getGridOption("theme");
|
||||||
|
if (JSON.stringify(newTheme) !== JSON.stringify(currentTheme)) {
|
||||||
|
gridApi.setGridOption("theme", newTheme);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const stopOptions = watch(() => {
|
||||||
|
if (!gridApi || gridApi.isDestroyed() || !options) return;
|
||||||
|
const newOptions = typeof options === "function" ? options() : options;
|
||||||
|
if (newOptions) {
|
||||||
|
Object.entries(newOptions).forEach(([key, val]) => {
|
||||||
|
try {
|
||||||
|
gridApi.setGridOption(key, val);
|
||||||
|
} catch (e) { }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cleanupFn = () => {
|
||||||
|
stopData();
|
||||||
|
stopTheme();
|
||||||
|
stopOptions();
|
||||||
|
if (gridApi && !gridApi.isDestroyed()) {
|
||||||
|
gridApi.destroy();
|
||||||
|
if (api) api.current = null;
|
||||||
|
gridApi = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmount(() => {
|
||||||
|
if (cleanupFn) {
|
||||||
|
cleanupFn();
|
||||||
|
cleanupFn = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return h("div", {
|
||||||
|
class: className,
|
||||||
|
style: style,
|
||||||
|
ref: initGrid
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Grid };
|
||||||
254
src/sigpro.js
Normal file
254
src/sigpro.js
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
export const isF = f => typeof f == "function";
|
||||||
|
export const isO = o => o && typeof o == "object";
|
||||||
|
export const isA = Array.isArray;
|
||||||
|
const doc = typeof document < "u" ? document : null;
|
||||||
|
const txt = s => doc.createTextNode(s == null ? "" : String(s));
|
||||||
|
const toNd = n => n?._rt ? n._cnt : (n instanceof Node ? n : txt(n));
|
||||||
|
export const fragment = p => p.children;
|
||||||
|
export const val = v => isF(v) ? v() : v;
|
||||||
|
|
||||||
|
let aEff = null, aOwn = null, isFlushing = 0, bDepth = 0;
|
||||||
|
const eQ = new Set(), MOUNTED = new WeakMap();
|
||||||
|
|
||||||
|
const SVG_NS = "http://www.w3.org/2000/svg", XLINK = "http://www.w3.org/1999/xlink";
|
||||||
|
const SVG_TAGS = new Set("svg,path,circle,rect,line,polyline,polygon,g,defs,text,textPath,tspan,use,symbol,image,marker,ellipse".split(","));
|
||||||
|
const DANG_ATTR = new Set(["src", "href", "formaction", "action", "background", "code", "archive"]);
|
||||||
|
|
||||||
|
const clr = s => { if (s) { s.forEach(f => f()); s.clear(); } };
|
||||||
|
const dispose = e => {
|
||||||
|
if (!e || e._x) return;
|
||||||
|
e._x = 1;
|
||||||
|
let st = [e], c;
|
||||||
|
while ((c = st.pop())) {
|
||||||
|
clr(c._c);
|
||||||
|
if (c._ch) { c._ch.forEach(x => st.push(x)); c._ch.clear(); }
|
||||||
|
if (c._d) { c._d.forEach(d => d.delete(c)); c._d.clear(); }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onUnmount = f => aOwn && (aOwn._c ||= new Set()).add(f);
|
||||||
|
const untrack = f => { let p = aEff; aEff = null; try { return f() } finally { aEff = p } };
|
||||||
|
|
||||||
|
const createEffect = (f, isC = 0) => {
|
||||||
|
const e = () => {
|
||||||
|
if (e._x) return;
|
||||||
|
if (e._d) e._d.forEach(s => s.delete(e));
|
||||||
|
clr(e._c);
|
||||||
|
let pE = aEff, pO = aOwn;
|
||||||
|
aEff = aOwn = e;
|
||||||
|
try { return e._res = f(); }
|
||||||
|
catch (err) { console.error("[SigPro]", err); }
|
||||||
|
finally { aEff = pE; aOwn = pO; }
|
||||||
|
};
|
||||||
|
e._d = e._c = e._ch = null;
|
||||||
|
e._x = 0; e._iC = isC;
|
||||||
|
e._dp = aEff ? aEff._dp + 1 : 0;
|
||||||
|
e._m = []; e._p = aOwn;
|
||||||
|
if (aOwn) (aOwn._ch ||= new Set()).add(e);
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
const flush = () => {
|
||||||
|
if (isFlushing) return;
|
||||||
|
isFlushing = 1;
|
||||||
|
let q = [...eQ].sort((a, b) => a._dp - b._dp);
|
||||||
|
eQ.clear();
|
||||||
|
for (let e of q) if (!e._x) e();
|
||||||
|
isFlushing = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const batch = f => {
|
||||||
|
bDepth++;
|
||||||
|
try { return f() } finally { if (!--bDepth && eQ.size && !isFlushing) flush() }
|
||||||
|
};
|
||||||
|
|
||||||
|
const trkUpd = (s, trg = 0) => {
|
||||||
|
if (!trg && aEff && !aEff._x) {
|
||||||
|
s.add(aEff);
|
||||||
|
(aEff._d ||= new Set()).add(s);
|
||||||
|
} else if (trg && s.size) {
|
||||||
|
let q = 0;
|
||||||
|
for (let e of s) {
|
||||||
|
if (e === aEff || e._x) continue;
|
||||||
|
if (e._iC) { e._dt = 1; if (e._sb) trkUpd(e._sb, 1); }
|
||||||
|
else { eQ.add(e); q = 1; }
|
||||||
|
}
|
||||||
|
if (q && !isFlushing && !bDepth) queueMicrotask(flush);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const $ = (v, k = null) => {
|
||||||
|
let s = new Set();
|
||||||
|
if (isF(v)) {
|
||||||
|
let c, cp = () => {
|
||||||
|
if (cp._dt) {
|
||||||
|
let p = aEff; aEff = cp;
|
||||||
|
try { let n = v(); if (!Object.is(c, n)) { c = n; trkUpd(s, 1); } }
|
||||||
|
finally { aEff = p; }
|
||||||
|
cp._dt = 0;
|
||||||
|
}
|
||||||
|
trkUpd(s); return c;
|
||||||
|
};
|
||||||
|
cp._iC = cp._dt = 1; cp._sb = s; cp._d = null; cp._x = 0;
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
if (k) try { v = JSON.parse(localStorage.getItem(k)) ?? v } catch (e) { }
|
||||||
|
return (...a) => {
|
||||||
|
if (a.length) {
|
||||||
|
let n = isF(a[0]) ? a[0](v) : a[0];
|
||||||
|
if (!Object.is(v, n)) { v = n; if (k) localStorage.setItem(k, JSON.stringify(v)); trkUpd(s, 1); }
|
||||||
|
}
|
||||||
|
trkUpd(s); return v;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const watch = (src, cb) => {
|
||||||
|
let e = createEffect(cb ? () => { let v = isA(src) ? src.map(s => s()) : src(); untrack(() => cb(v)) } : src);
|
||||||
|
e(); return () => dispose(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clnNd = n => {
|
||||||
|
if (!n) return;
|
||||||
|
clr(n._c);
|
||||||
|
if (n._oE) dispose(n._oE);
|
||||||
|
if (n.childNodes) n.childNodes.forEach(clnNd);
|
||||||
|
};
|
||||||
|
|
||||||
|
const valAtt = (k, v) => (v == null || v === false) ? null :
|
||||||
|
(DANG_ATTR.has(k) || k.startsWith("on")) && /^\s*(javascript|data|vbscript):/i.test(String(v)) ? '#' : v;
|
||||||
|
|
||||||
|
export const h = (tag, prp = {}, ch = []) => {
|
||||||
|
if (prp instanceof Node || isA(prp) || !isO(prp)) { ch = prp; prp = {}; }
|
||||||
|
|
||||||
|
if (isF(tag)) {
|
||||||
|
let e = createEffect(() => e._res = tag(prp, { children: ch, emit: (ev, ...a) => prp[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...a) }));
|
||||||
|
e();
|
||||||
|
if (e._res == null) return null;
|
||||||
|
let nd = e._res instanceof Node || (isA(e._res) && e._res.every(n => n instanceof Node)) ? e._res : txt(e._res);
|
||||||
|
let att = n => { if (isO(n) && !n._rt) { n._m = e._m || []; n._c = e._c || new Set(); n._oE = e; } };
|
||||||
|
isA(nd) ? nd.forEach(att) : att(nd);
|
||||||
|
return nd;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isS = SVG_TAGS.has(tag), el = isS ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag);
|
||||||
|
el._c = new Set();
|
||||||
|
|
||||||
|
for (let k in prp) {
|
||||||
|
let v = prp[k];
|
||||||
|
if (k === "ref") { isF(v) ? v(el) : (v.current = el); continue; }
|
||||||
|
if (isS && k.startsWith("xlink:")) {
|
||||||
|
let cv = valAtt(k.slice(6), v);
|
||||||
|
cv == null ? el.removeAttributeNS(XLINK, k.slice(6)) : el.setAttributeNS(XLINK, k.slice(6), cv);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (k.startsWith("on")) {
|
||||||
|
let ev = k.slice(2).toLowerCase(); el.addEventListener(ev, v);
|
||||||
|
let off = () => el.removeEventListener(ev, v); el._c.add(off); onUnmount(off);
|
||||||
|
} else if (isF(v)) {
|
||||||
|
let e = createEffect(() => {
|
||||||
|
let r = valAtt(k, v());
|
||||||
|
if (k === "class") el.className = r || "";
|
||||||
|
else if (r == null) el.removeAttribute(k);
|
||||||
|
else if (k === "style" && typeof r == "string") el.setAttribute("style", r);
|
||||||
|
else if (k in el && !isS) el[k] = r;
|
||||||
|
else el.setAttribute(k, r === true ? "" : r);
|
||||||
|
});
|
||||||
|
e(); el._c.add(() => dispose(e)); onUnmount(() => dispose(e));
|
||||||
|
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
|
||||||
|
el.addEventListener(k === "checked" ? "change" : "input", ev => v(ev.target[k]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let r = valAtt(k, v);
|
||||||
|
if (r != null) {
|
||||||
|
if (k === "style" && typeof r == "string") el.setAttribute("style", r);
|
||||||
|
else if (k in el && !isS) el[k] = r;
|
||||||
|
else el.setAttribute(k, r === true ? "" : r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = c => {
|
||||||
|
if (isA(c)) return c.forEach(app);
|
||||||
|
if (isF(c)) {
|
||||||
|
let anc = txt(""), cur = []; el.appendChild(anc);
|
||||||
|
let e = createEffect(() => {
|
||||||
|
let r = c(), nxt = (isA(r) ? r : [r]).map(toNd), ref = anc;
|
||||||
|
cur.forEach(n => { n._rt ? n.destroy() : clnNd(n); if (n.parentNode) n.remove(); });
|
||||||
|
for (let i = nxt.length - 1; i >= 0; i--) {
|
||||||
|
let nd = nxt[i];
|
||||||
|
if (nd.parentNode !== ref.parentNode) ref.parentNode?.insertBefore(nd, ref);
|
||||||
|
if (nd._m) nd._m.forEach(f => f()); ref = nd;
|
||||||
|
}
|
||||||
|
cur = nxt;
|
||||||
|
});
|
||||||
|
e(); el._c.add(() => dispose(e)); onUnmount(() => dispose(e));
|
||||||
|
} else {
|
||||||
|
let nd = toNd(c); el.appendChild(nd);
|
||||||
|
if (nd._m) nd._m.forEach(f => f());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
app(ch); return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const render = rFn => {
|
||||||
|
let c = new Set(), pO = aOwn, pE = aEff, cnt = doc.createElement("div");
|
||||||
|
cnt.style.display = "contents"; cnt.setAttribute("role", "presentation");
|
||||||
|
aOwn = { _c: c }; aEff = null;
|
||||||
|
const pRes = r => {
|
||||||
|
if (!r) return;
|
||||||
|
if (r._rt) { c.add(r.destroy); cnt.appendChild(r._cnt); }
|
||||||
|
else if (isA(r)) r.forEach(pRes);
|
||||||
|
else cnt.appendChild(r instanceof Node ? r : txt(r));
|
||||||
|
};
|
||||||
|
try { pRes(rFn({ onCleanup: f => c.add(f) })); } finally { aOwn = pO; aEff = pE; }
|
||||||
|
return { _rt: 1, _cnt: cnt, destroy: () => { clr(c); clnNd(cnt); cnt.remove(); } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const when = (c, Y, N = null) => {
|
||||||
|
let anc = txt(""), rt = h("div", { style: "display:contents" }, [anc]), v;
|
||||||
|
watch(() => !!val(c), s => {
|
||||||
|
if (v) { v.destroy(); v = null; }
|
||||||
|
let ct = s ? Y : N;
|
||||||
|
if (ct) { v = render(() => val(ct)); rt.insertBefore(v._cnt, anc); }
|
||||||
|
});
|
||||||
|
onUnmount(() => v?.destroy()); return rt;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const each = (s, fn, kF) => {
|
||||||
|
let anc = txt(""), rt = h("div", { style: "display:contents" }, [anc]), cch = new Map();
|
||||||
|
watch(() => val(s) || [], it => {
|
||||||
|
let nCc = new Map(), nOd = [];
|
||||||
|
for (let i = 0, l = (it || []).length; i < l; i++) {
|
||||||
|
let t = it[i], k = kF ? (t?.[kF] ?? i) : (t?.id ?? i), v = cch.get(k);
|
||||||
|
if (!v) v = render(() => fn(t, i)); else cch.delete(k);
|
||||||
|
nCc.set(k, v); nOd.push(v);
|
||||||
|
}
|
||||||
|
cch.forEach(v => v.destroy());
|
||||||
|
let ref = anc;
|
||||||
|
for (let i = nOd.length - 1; i >= 0; i--) {
|
||||||
|
let nd = nOd[i]._cnt;
|
||||||
|
if (nd.nextSibling !== ref) rt.insertBefore(nd, ref); ref = nd;
|
||||||
|
}
|
||||||
|
cch = nCc;
|
||||||
|
});
|
||||||
|
return rt;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mount = (c, tgt) => {
|
||||||
|
let t = typeof tgt == "string" ? doc.querySelector(tgt) : tgt;
|
||||||
|
if (!t) return;
|
||||||
|
if (MOUNTED.has(t)) MOUNTED.get(t).destroy();
|
||||||
|
let i = render(isF(c) ? c : () => c);
|
||||||
|
t.replaceChildren(i._cnt); MOUNTED.set(t, i); return i;
|
||||||
|
};
|
||||||
|
|
||||||
|
// const htmlTags = "a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video";
|
||||||
|
|
||||||
|
export const SigPro = { $, watch, batch, h, fragment, render, mount, when, each, onUnmount, val, isA, isF, isO };
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.SigPro = SigPro;
|
||||||
|
// htmlTags.split(" ").forEach(tag => {
|
||||||
|
// window[tag] = (props, children) => h(tag, props, children);
|
||||||
|
// });
|
||||||
|
}
|
||||||
21
src/sigpro.locale.js
Normal file
21
src/sigpro.locale.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const { $ } = window.SigPro;
|
||||||
|
|
||||||
|
const currentLocale = $("en");
|
||||||
|
const translations = {};
|
||||||
|
|
||||||
|
export const addLang = obj => {
|
||||||
|
for (const locale of Object.keys(obj)) {
|
||||||
|
if (!translations[locale]) translations[locale] = {};
|
||||||
|
Object.assign(translations[locale], obj[locale]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setLocale = locale => {
|
||||||
|
if (locale && translations[locale]) {
|
||||||
|
currentLocale(locale);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const t = key => {
|
||||||
|
return () => translations[currentLocale()]?.[key] ?? key;
|
||||||
|
};
|
||||||
49
src/sigpro.router.js
Normal file
49
src/sigpro.router.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const { $, h, watch, render, isF } = window.SigPro;
|
||||||
|
|
||||||
|
const getHash = () => window.location.hash.slice(1) || "/";
|
||||||
|
const currentPath = $(getHash());
|
||||||
|
|
||||||
|
window.addEventListener("hashchange", () => currentPath(getHash()));
|
||||||
|
|
||||||
|
export const routerParams = $({});
|
||||||
|
|
||||||
|
export const router = routes => {
|
||||||
|
const hook = h("div", { class: "router-hook" });
|
||||||
|
let currentView = null;
|
||||||
|
|
||||||
|
watch([currentPath], () => {
|
||||||
|
const cur = currentPath();
|
||||||
|
|
||||||
|
const route = routes.find(r => {
|
||||||
|
const p1 = r.path.split("/").filter(Boolean);
|
||||||
|
const p2 = cur.split("/").filter(Boolean);
|
||||||
|
return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i]);
|
||||||
|
}) || routes.find(r => r.path === "*");
|
||||||
|
|
||||||
|
if (route) {
|
||||||
|
currentView?.destroy();
|
||||||
|
|
||||||
|
const params = {};
|
||||||
|
route.path.split("/").filter(Boolean).forEach((p, i) => {
|
||||||
|
if (p[0] === ":") params[p.slice(1)] = cur.split("/").filter(Boolean)[i];
|
||||||
|
});
|
||||||
|
|
||||||
|
routerParams(params);
|
||||||
|
|
||||||
|
currentView = render(() => isF(route.component) ? route.component(params) : route.component);
|
||||||
|
|
||||||
|
hook.replaceChildren(currentView.container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
hook.destroy = () => {
|
||||||
|
currentView?.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
return hook;
|
||||||
|
};
|
||||||
|
|
||||||
|
router.params = routerParams;
|
||||||
|
router.to = p => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||||
|
router.back = () => window.history.back();
|
||||||
|
router.path = () => currentPath();
|
||||||
108
src/sigpro.ui.css
Normal file
108
src/sigpro.ui.css
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "daisyui";
|
||||||
|
@plugin "@iconify/tailwind4";
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "splight";
|
||||||
|
default: true;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "light";
|
||||||
|
--color-base-100: oklch(100% 0 0);
|
||||||
|
--color-base-200: oklch(98% 0 0);
|
||||||
|
--color-base-300: oklch(92% 0 0);
|
||||||
|
--color-base-content: oklch(25% 0.006 285);
|
||||||
|
--color-primary: oklch(25% 0.006 285);
|
||||||
|
--color-primary-content: oklch(98% 0 0);
|
||||||
|
--color-secondary: oklch(55% 0.046 257.417);
|
||||||
|
--color-secondary-content: oklch(98% 0 0);
|
||||||
|
--color-accent: oklch(96% 0 0);
|
||||||
|
--color-accent-content: oklch(25% 0.006 285);
|
||||||
|
--color-neutral: oklch(14% 0.005 285.823);
|
||||||
|
--color-neutral-content: oklch(92% 0.004 286.32);
|
||||||
|
--color-info: oklch(74% 0.16 232);
|
||||||
|
--color-success: oklch(62% 0.17 163);
|
||||||
|
--color-warning: oklch(82% 0.18 84);
|
||||||
|
--color-error: oklch(60% 0.25 27);
|
||||||
|
--radius-selector: 0.5rem;
|
||||||
|
--radius-field: 0.5rem;
|
||||||
|
--radius-box: 0.5rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 1;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "spdark";
|
||||||
|
default: false;
|
||||||
|
prefersdark: true;
|
||||||
|
color-scheme: "dark";
|
||||||
|
--color-base-100: oklch(15% 0.005 285.823);
|
||||||
|
--color-base-200: oklch(20% 0.005 285.823);
|
||||||
|
--color-base-300: oklch(30% 0.005 285.823);
|
||||||
|
--color-base-content: oklch(92% 0.004 286.32);
|
||||||
|
--color-primary: oklch(98% 0 0);
|
||||||
|
--color-primary-content: oklch(15% 0 0);
|
||||||
|
--color-secondary: oklch(65% 0.046 257.417);
|
||||||
|
--color-secondary-content: oklch(15% 0.005 285.823);
|
||||||
|
--color-accent: oklch(25% 0 0);
|
||||||
|
--color-accent-content: oklch(98% 0 0);
|
||||||
|
--color-neutral: oklch(92% 0.004 286.32);
|
||||||
|
--color-neutral-content: oklch(14% 0.005 285.823);
|
||||||
|
--color-info: oklch(70% 0.1 230);
|
||||||
|
--color-success: oklch(65% 0.15 160);
|
||||||
|
--color-warning: oklch(85% 0.15 90);
|
||||||
|
--color-error: oklch(55% 0.2 27);
|
||||||
|
--radius-selector: 0.5rem;
|
||||||
|
--radius-field: 0.5rem;
|
||||||
|
--radius-box: 0.5rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 1;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input,
|
||||||
|
.label,
|
||||||
|
.select,
|
||||||
|
.textarea {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-within {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: 0 0 4px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:focus) {
|
||||||
|
background-color: oklch(from var(--color-base-100) calc(l - 0.03) c h);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-label span {
|
||||||
|
color: oklch(30% 0.01 260);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-label:focus-within span {
|
||||||
|
color: oklch(25% 0.02 260);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-label:has(input:not(:placeholder-shown)) span {
|
||||||
|
color: oklch(28% 0.01 260);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Solo para la Demo de docsify */
|
||||||
|
.markdown-section progress.progress {
|
||||||
|
all: revert-layer;
|
||||||
|
}
|
||||||
367
src/sigpro.ui.js
Normal file
367
src/sigpro.ui.js
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
/// <reference path="../sigpro.d.ts" />
|
||||||
|
|
||||||
|
const { $, h, mount, watch, val, isF, isA, isO } = window.SigPro;
|
||||||
|
|
||||||
|
export const hide = () => document.activeElement?.blur();
|
||||||
|
|
||||||
|
export const ui = {
|
||||||
|
accordion: (p, c) => h("div", { ...p, class: `collapse ${p.class || ''}` }, [h("input", { type: "radio", name: p.name, checked: p.checked }), c]),
|
||||||
|
accordion_title: (p, c) => h("div", { ...p, class: `collapse-title ${p.class || ''}` }, c),
|
||||||
|
accordion_content: (p, c) => h("div", { ...p, class: `collapse-content ${p.class || ''}` }, c),
|
||||||
|
alert: (p, c) => h("div", { ...p, class: `alert ${p.class || ''}` }, c),
|
||||||
|
autocomplete: (p) => ui.combo(p, ({ query, close, setValue }) =>
|
||||||
|
h("ul", { class: "menu bg-base-100 w-full" }, () => {
|
||||||
|
const q = String(val(query)).toLowerCase();
|
||||||
|
const list = (val(p.items) || []).filter(i =>
|
||||||
|
(isO(i) ? (i.label ?? i.value) : String(i)).toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
return list.length
|
||||||
|
? list.map((item, idx) =>
|
||||||
|
h("li", { key: item.value ?? idx },
|
||||||
|
h("a", {
|
||||||
|
onclick: e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const v = item?.value ?? item;
|
||||||
|
setValue(isO(item) ? (item.label ?? item.value) : String(item));
|
||||||
|
if (isF(p.value)) p.value(v); else p.onChange?.(v);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}, isO(item) ? (item.label ?? item.value) : item)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: [h("li", { class: "disabled" }, h("a", {}, "Sin resultados"))];
|
||||||
|
})
|
||||||
|
),
|
||||||
|
avatar: (p, c) => h("div", { ...p, class: `avatar ${p.class || ''}` }, h("div", { class: p.innerClass || '' }, c)),
|
||||||
|
avatar_group: (p, c) => h("div", { ...p, class: `avatar-group -space-x-6 ${p.class || ''}` }, c),
|
||||||
|
badge: (p, c) => h("span", { ...p, class: `badge ${p.class || ''}` }, c),
|
||||||
|
breadcrumbs: (p, c) => h("div", { ...p, class: `breadcrumbs ${p.class || ''}` }, c),
|
||||||
|
button: (p, c) => h("button", { ...p, class: `btn ${p.class || ''}` }, c),
|
||||||
|
card: (p, c) => h("div", { ...p, class: `card ${p.class || ''}` }, c),
|
||||||
|
card_title: (p, c) => h("div", { ...p, class: `card-title ${p.class || ''}` }, c),
|
||||||
|
card_body: (p, c) => h("div", { ...p, class: `card-body ${p.class || ''}` }, c),
|
||||||
|
card_actions: (p, c) => h("div", { ...p, class: `card-actions ${p.class || ''}` }, c),
|
||||||
|
carousel: (p, c) => h("div", { ...p, class: `carousel ${p.class || ''}` }, c),
|
||||||
|
carousel_item: (p, c) => h("div", { ...p, class: `carousel-item ${p.class || ''}` }, c),
|
||||||
|
chat: (p, c) => h("div", { ...p, class: `chat ${p.class || ''}` }, c),
|
||||||
|
chat_image: (p, c) => h("div", { ...p, class: `chat-image avatar ${p.class || ''}` }, c),
|
||||||
|
chat_header: (p, c) => h("div", { ...p, class: `chat-header ${p.class || ''}` }, c),
|
||||||
|
chat_bubble: (p, c) => h("div", { ...p, class: `chat-bubble ${p.class || ''}` }, c),
|
||||||
|
chat_footer: (p, c) => h("div", { ...p, class: `chat-footer ${p.class || ''}` }, c),
|
||||||
|
checkbox: (p) => h("input", { ...p, type: "checkbox", class: `checkbox ${p.class || ''}` }),
|
||||||
|
colorpicker: (p) => ui.combo({
|
||||||
|
...p, custom: () => h("span", {
|
||||||
|
class: "w-4 h-4 rounded border border-base-300",
|
||||||
|
style: `background:${val(p.value) || '#000'}`
|
||||||
|
})
|
||||||
|
}, ({ close, setValue }) =>
|
||||||
|
pallete({ ...p, onchange: (c) => { setValue(c); close(); } })
|
||||||
|
),
|
||||||
|
combo: (p, c) => {
|
||||||
|
const { placeholder = "", class: cls = "" } = p;
|
||||||
|
const query = isF(p.value) ? p.value : $(p.value ?? "");
|
||||||
|
let inputEl, open = $(false);
|
||||||
|
|
||||||
|
return ui.float({ label: p.label }, [
|
||||||
|
h("div", { class: `dropdown w-full ${cls} ${val(open) ? "dropdown-open" : ""}` }, [
|
||||||
|
h("label", { class: "input w-full" }, [
|
||||||
|
h("span", { class: p.icon ?? "icon-[lucide--search]" }),
|
||||||
|
p.custom ?? null,
|
||||||
|
h("input", {
|
||||||
|
type: "search", placeholder, tabindex: "0",
|
||||||
|
value: query,
|
||||||
|
onfocus: () => open(true),
|
||||||
|
ref: el => inputEl = el
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
h("div", {
|
||||||
|
class: "dropdown-content bg-base-100 rounded-box z-50 max-w-80 shadow-sm",
|
||||||
|
onmousedown: e => e.preventDefault()
|
||||||
|
}, () => val(open) && typeof c === "function"
|
||||||
|
? c({ query, open, close: () => { open(false); inputEl?.blur(); }, setValue: v => query(v) })
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
datepicker: (p) => {
|
||||||
|
const range = isF(p.range) ? p.range() : p.range;
|
||||||
|
if (!range) return ui.combo({ value: (isF(p.value) ? p.value() : p.value) || '', ...p },
|
||||||
|
({ close, setValue }) => h("div", { class: "w-80" },
|
||||||
|
calendar({ ...p, class: "w-full", onChange: v => { setValue(v); close(); if (isF(p.value)) p.value(v) } })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const v = $(isF(p.value) ? p.value() : p.value || { start: null, end: null });
|
||||||
|
const start = $((v() || {}).start || ''), end = $((v() || {}).end || '');
|
||||||
|
const sync = () => { v({ start: start(), end: end() }); if (isF(p.value)) p.value(v()); };
|
||||||
|
const cal = (key, sig, ph, dis) => ui.combo({ value: sig, placeholder: ph, class: "flex-1", disabled: dis },
|
||||||
|
({ close, setValue }) => h("div", { class: "w-72" },
|
||||||
|
calendar({
|
||||||
|
...p, class: "w-full", value: v, range: true, onChange: r => {
|
||||||
|
v(r); start(r?.start || ''); end(r?.end || ''); setValue(r?.[key] || ''); if (r?.end) close(); if (isF(p.value)) p.value(r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return h("div", { class: `flex gap-1 ${p.class || ''}`, onchange: sync }, [
|
||||||
|
cal('start', start, p.fromPlaceholder || "Inicio"),
|
||||||
|
cal('end', end, p.toPlaceholder || "Fin", () => !v()?.start)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
dialog: (p, c) => {
|
||||||
|
const pos = $(p.pos || { x: 100, y: 100 });
|
||||||
|
const show = $(p.show || false);
|
||||||
|
const anim = $(false);
|
||||||
|
|
||||||
|
watch(show, s => s ? setTimeout(() => anim(true), 10) : anim(false));
|
||||||
|
|
||||||
|
return h("div", {
|
||||||
|
class: () => `fixed z-50 transition-opacity duration-300 ${show() && anim() ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'}`,
|
||||||
|
style: () => `left: ${pos().x}px; top: ${pos().y}px; transition: left 50ms, top 50ms;`
|
||||||
|
}, [
|
||||||
|
h("div", { class: `bg-base-100 rounded-box shadow-2xl border ${p.class || ''}` }, [
|
||||||
|
p.title && h("div", {
|
||||||
|
class: "flex justify-between items-center cursor-move p-2 border-b select-none bg-base-200 rounded-t-box",
|
||||||
|
onmousedown: e => {
|
||||||
|
const s = { x: e.clientX - pos().x, y: e.clientY - pos().y };
|
||||||
|
const m = ev => pos({ x: ev.clientX - s.x, y: ev.clientY - s.y });
|
||||||
|
const u = () => { document.removeEventListener('mousemove', m); document.removeEventListener('mouseup', u); };
|
||||||
|
document.addEventListener('mousemove', m); document.addEventListener('mouseup', u);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}, [h("span", { class: "font-bold" }, p.title), h("button", { class: "btn btn-sm btn-circle btn-ghost", onclick: () => show(false) }, "✕")]),
|
||||||
|
h("div", { class: "p-4" }, c),
|
||||||
|
p.footer && h("div", { class: "p-2 border-t flex justify-end gap-2" }, p.footer)
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
divider: (p) => h("div", { ...p, class: `divider ${p.class || ''}` }),
|
||||||
|
drawer: (p, c) => h("div", { ...p, class: `drawer ${p.class || ''}` }, c),
|
||||||
|
drawer_toggle: (p) => h("input", { ...p, type: "checkbox", class: `drawer-toggle ${p.class || ''}` }),
|
||||||
|
drawer_content: (p, c) => h("div", { ...p, class: `drawer-content ${p.class || ''}` }, c),
|
||||||
|
drawer_side: (p, c) => h("div", { ...p, class: `drawer-side ${p.class || ''}` }, c),
|
||||||
|
drawer_overlay: (p) => h("label", { ...p, class: `drawer-overlay ${p.class || ''}` }),
|
||||||
|
dropdown: (p, c) => h("div", { ...p, class: `dropdown ${p.class || ''}` }, c),
|
||||||
|
dropdown_button: (p, c) => h("div", { ...p, tabindex: "0", role: "button", class: `btn ${p.class || ''}` }, c),
|
||||||
|
dropdown_content: (p, c) => h("div", { ...p, tabindex: "0", class: `dropdown-content ${p.class || ''}` }, c),
|
||||||
|
fab: (p, c) => h("div", { ...p, class: `fab ${p.class || ''}` }, c),
|
||||||
|
fab_button: (p, c) => h("div", { ...p, tabindex: "0", role: "button", class: `btn ${p.class || ''}` }, c),
|
||||||
|
fieldset: (p, c) => h("fieldset", { class: `fieldset ${p.class || ''}` }, [h("legend", { class: "fieldset-legend" }, p.label), c]),
|
||||||
|
file: (p) => h("input", { ...p, type: "file", class: `file-input ${p.class || ''}` }),
|
||||||
|
file_drag: (p, c) => h("label", {
|
||||||
|
class: () => `relative flex items-center justify-between h-12 px-4 border-2 border-dashed rounded-lg cursor-pointer transition-all ${p.drag ? 'border-primary bg-primary/10' : 'border-base-content/20 bg-base-100'} ${p.class || ''}`,
|
||||||
|
ondragover: (e) => { e.preventDefault(); p.ondrag?.(true); },
|
||||||
|
ondragleave: () => p.ondrag?.(false),
|
||||||
|
ondrop: (e) => { e.preventDefault(); p.ondrag?.(false); p.ondrop?.(e.dataTransfer.files); }
|
||||||
|
}, c),
|
||||||
|
file_preview: (p) => h("ul", { class: `mt-2 space-y-1 ${p.class || ''}` },
|
||||||
|
(p.files || []).map((f, i) =>
|
||||||
|
h("li", { class: "flex items-center justify-between p-1.5 pl-3 text-xs bg-base-200/50 rounded-md border" }, [
|
||||||
|
h("div", { class: "flex items-center gap-2 truncate opacity-70" }, [
|
||||||
|
h("span", {}, "📄"),
|
||||||
|
h("span", { class: "truncate max-w-[180px]" }, f.name),
|
||||||
|
h("span", { class: "text-[9px] opacity-50" }, `(${~~(f.size / 1024)}KB)`)
|
||||||
|
]),
|
||||||
|
h("button", { class: "btn btn-ghost btn-xs btn-circle", onclick: () => p.onremove?.(i) }, h("span", { class: "icon-[lucide--x]" }))
|
||||||
|
])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
file_error: (p) => h("div", { class: `text-[10px] text-error mt-1 px-1 ${p.class || ''}` }, p.message),
|
||||||
|
float: (p, c) => h("label", { class: "floating-label" }, [h("span", {}, p.label ?? null), c]),
|
||||||
|
indicator: (p, c) => h("div", { ...p, class: `indicator ${p.class || ''}` }, [p.value && h("span", { class: `indicator-item badge ${p.badgeClass || ''}` }, p.value), c]),
|
||||||
|
input: (p) => ui.float({ label: p.label }, [
|
||||||
|
h("label", { class: "input w-full" }, [
|
||||||
|
span({ class: `${p.icon ?? ''}` }),
|
||||||
|
h("input", { ...p, class: `w-full ${p.class || ''}` }),
|
||||||
|
p.right || null
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
kbd: (p, c) => h("kbd", { ...p, class: `kbd ${p.class || ''}` }, c),
|
||||||
|
label: (p, c) => h("span", { ...p, class: `label ${p.class || ''}` }, c),
|
||||||
|
loading: (p) => h("span", { ...p, class: `loading loading-spinner ${p.class || ''}` }),
|
||||||
|
menu: (p, c) => h("ul", { ...p, class: `menu ${p.class || ''}` }, c),
|
||||||
|
menu_title: (p, c) => h('li', { ...p, class: "menu-title" }, c),
|
||||||
|
menu_item: (p) =>
|
||||||
|
p.items
|
||||||
|
? h('li', {}, [h('details', { open: p.open || false }, [h('summary', {}, p.label), h('ul', { class: p.submenuClass || '' }, p.items.map(i => ui.menu_item(i)))])])
|
||||||
|
: h('li', {}, p.href || p.onclick
|
||||||
|
? h('a', { ...(p.href ? { href: p.href } : {}), onclick: p.onclick }, p.label)
|
||||||
|
: p.label),
|
||||||
|
modal: (p, c) => h("dialog", { ...p, class: `modal ${p.class || ''}` }, [c, h("form", { method: "dialog", class: "modal-backdrop" }, h("button", {}, "close"))]),
|
||||||
|
modal_box: (p, c) => h("div", { ...p, class: `modal-box ${p.class || ''}` }, [h("form", { method: "dialog" }, h("button", { class: "btn btn-sm btn-circle btn-ghost absolute right-2 top-2" }, "✕")), c]),
|
||||||
|
modal_action: (p, c) => h("div", { ...p, class: `modal-action ${p.class || ''}` }, c),
|
||||||
|
navbar: (p, c) => h("div", { ...p, class: `navbar ${p.class || ''}` }, c),
|
||||||
|
option: (p, c) => h("option", { ...p }, c),
|
||||||
|
password: (p) => {
|
||||||
|
const show = $(false);
|
||||||
|
const { right, ...rest } = p;
|
||||||
|
return ui.input({
|
||||||
|
...rest,
|
||||||
|
type: () => val(show) ? "text" : "password",
|
||||||
|
icon: "icon-[lucide--lock]",
|
||||||
|
right: ui.swap({ value: show, class: "swap-rotate" }, [
|
||||||
|
ui.swap_on({}, span({ class: "icon-[lucide--eye]" })),
|
||||||
|
ui.swap_off({}, span({ class: "icon-[lucide--eye-off]" }))
|
||||||
|
])
|
||||||
|
});
|
||||||
|
},
|
||||||
|
progress: (p) => h("progress", { ...p, class: `progress ${p.class || ''}` }),
|
||||||
|
radial: (p) => h("div", { ...p, class: `radial-progress ${p.class || ''}`, style: `--value:${val(p.value) ?? 0}`, role: "progressbar" }, p.value ?? ""),
|
||||||
|
radio: (p) => h("input", { ...p, type: "radio", class: `radio ${p.class || ''}` }),
|
||||||
|
range: (p) => h("input", { ...p, type: "range", class: `range ${p.class || ''}` }),
|
||||||
|
rating: (p) => h("div", { class: `rating ${p.class || ''}` },
|
||||||
|
[...Array(p.count || 5)].map((_, i) =>
|
||||||
|
h("input", {
|
||||||
|
class: `mask ${p.mask || 'mask-star'} ${p.itemClass || ''}`,
|
||||||
|
name: p.name,
|
||||||
|
type: "radio",
|
||||||
|
checked: () => val(p.value) === (p.offset ? i + p.offset : i),
|
||||||
|
onclick: () => isF(p.value) ? p.value(i) : p.onChange?.(i)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
search: (p) => ui.input({ ...p, type: "search", icon: p.icon ?? "icon-[lucide--search]" }),
|
||||||
|
select: (p, c) => h("select", { ...p, class: `select ${p.class || ''}` }, c),
|
||||||
|
stack: (p, c) => h("div", { ...p, class: `stack ${p.class || ''}` }, c),
|
||||||
|
stat: (p, c) => h("div", { ...p, class: `stat ${p.class || ''}` }, c),
|
||||||
|
stat_figure: (p, c) => h("div", { ...p, class: `stat-figure ${p.class || ''}` }, c),
|
||||||
|
stat_title: (p, c) => h("div", { ...p, class: `stat-title ${p.class || ''}` }, c),
|
||||||
|
stat_value: (p, c) => h("div", { ...p, class: `stat-value ${p.class || ''}` }, c),
|
||||||
|
stat_desc: (p, c) => h("div", { ...p, class: `stat-desc ${p.class || ''}` }, c),
|
||||||
|
steps: (p, c) => h("ul", { ...p, class: `steps ${p.class || ''}` }, c),
|
||||||
|
step: (p, c) => h("li", { ...p, class: `step ${p.class || ''}`, "data-content": p.dataContent }, c),
|
||||||
|
swap: (p, c) => h("label", { class: `swap ${p.class || ''}` }, [h("input", { type: "checkbox", checked: p.value }), ...(isA(c) ? c : [c])]),
|
||||||
|
swap_on: (p, c) => h("div", { ...p, class: `swap-on ${p.class || ''}` }, c),
|
||||||
|
swap_off: (p, c) => h("div", { ...p, class: `swap-off ${p.class || ''}` }, c),
|
||||||
|
table: (p, c) => h("table", { ...p, class: `table ${p.class || ''}` }, c),
|
||||||
|
thead: (p, c) => h("thead", { ...p, class: p.class || '' }, c),
|
||||||
|
tbody: (p, c) => h("tbody", { ...p, class: p.class || '' }, c),
|
||||||
|
tfoot: (p, c) => h("tfoot", { ...p, class: p.class || '' }, c),
|
||||||
|
tr: (p, c) => h("tr", { ...p, class: p.class || '' }, c),
|
||||||
|
th: (p, c) => h("th", { ...p, class: p.class || '' }, c),
|
||||||
|
td: (p, c) => h("td", { ...p, class: p.class || '' }, c),
|
||||||
|
tabs: (p, c) => div({ ...p, class: `tabs ${p.class || ''}` }, c),
|
||||||
|
tab: (p) => {
|
||||||
|
const close = () => p.tabs?.(p.tabs().filter((_, idx) => idx !== p.index))
|
||||||
|
return [
|
||||||
|
h('label', { class: `tab ${p.class || ''}` }, [
|
||||||
|
h('input', { type: 'radio', name: p.name, checked: p.checked || undefined }),
|
||||||
|
p.label,
|
||||||
|
p.closable ? h('span', {
|
||||||
|
class: 'ml-1 inline-flex items-center justify-center w-4 h-4 rounded-full hover:bg-base-300 text-base-content/60 hover:text-base-content cursor-pointer',
|
||||||
|
onclick: (e) => { e.stopPropagation(); close() }
|
||||||
|
}, h('span', { class: 'icon-[lucide--x] w-3 h-3' })) : null,
|
||||||
|
]),
|
||||||
|
div({ class: `tab-content bg-base-100 border-base-300 p-6 ${p?.classContent || ''}` }, p.content),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
textarea: (p) => h("textarea", { ...p, class: `textarea ${p.class || ''}` }),
|
||||||
|
textrotate: (p, c) => h("span", { ...p, class: `text-rotate ${p.class || ''}` }, h("span", {}, c)),
|
||||||
|
theme: (p) => ui.swap({ class: `text-xl ${p.class || ''}`, value: p.value }, [
|
||||||
|
ui.swap_on({}, span({ class: "icon-[lucide--moon]" })),
|
||||||
|
ui.swap_off({}, span({ class: "icon-[lucide--sun]" }))
|
||||||
|
]),
|
||||||
|
timeline: (p, c) => h("ul", { ...p, class: `timeline ${p.class || ''}` }, c),
|
||||||
|
timeline_start: (p, c) => h("div", { ...p, class: `timeline-start ${p.class || ''}` }, c),
|
||||||
|
timeline_middle: (p, c) => h("div", { ...p, class: `timeline-middle ${p.class || ''}` }, c),
|
||||||
|
timeline_end: (p, c) => h("div", { ...p, class: `timeline-end ${p.class || ''}` }, c),
|
||||||
|
toggle: (p) => h("input", { ...p, type: "checkbox", class: `toggle ${p.class || ''}` }),
|
||||||
|
tooltip: (p, c) => h('div', { class: `tooltip ${p.class || ''}`, "data-tip": p.tip }, c),
|
||||||
|
validator: (p, c) => h("div", { ...p, class: `validator-hint ${p.class || ''}` }, c),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calendar = p => {
|
||||||
|
let [d, hv, sh, eh] = [$(new Date()), $(0), $(0), $(0)], now = new Date(),
|
||||||
|
F = v => v ? `${v.getFullYear()}-${String(v.getMonth() + 1).padStart(2, '0')}-${String(v.getDate()).padStart(2, '0')}` : '',
|
||||||
|
P = n => (n < 10 ? '0' : '') + n,
|
||||||
|
M = (m, y = 0) => d(new Date(d().getFullYear() + y, d().getMonth() + m, 1)),
|
||||||
|
V = () => typeof p.value == 'function' ? p.value() : p.value,
|
||||||
|
G = () => typeof p.range == 'function' ? p.range() : p.range,
|
||||||
|
L = dt => {
|
||||||
|
let s = F(dt), v = V(), r = G();
|
||||||
|
if (!r) return p.onChange?.(p.hour ? `${s}T${P(sh())}:00:00` : s);
|
||||||
|
if (!v?.start || v.end) return p.onChange?.({ start: s, end: null, ...(p.hour && { startHour: sh() }) });
|
||||||
|
let nv = s < v.start ? { start: s, end: v.start } : { start: v.start, end: s };
|
||||||
|
p.onChange?.({ ...nv, ...(p.hour && { startHour: v.startHour ?? sh(), endHour: eh() }) });
|
||||||
|
},
|
||||||
|
I = ({ v, on }) => h('div', { class: 'flex-1 flex gap-2 items-center' }, [
|
||||||
|
h('input', { type: 'range', min: 0, max: 23, value: v, class: 'range range-xs', oninput: e => on(+e.target.value) }),
|
||||||
|
h('span', { class: 'text-sm font-mono' }, () => P(v()) + ':00')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return h('div', { class: `p-4 bg-base-100 rounded-box w-80 select-none ${p.class || ''}` }, [
|
||||||
|
h('div', { class: 'flex justify-between items-center mb-4' }, [
|
||||||
|
h('div', { class: 'flex gap-1' }, [
|
||||||
|
h('button', { class: 'btn btn-ghost btn-xs', onclick: () => M(0, -1) }, h('span', { class: 'icon-[lucide--chevrons-left]' })),
|
||||||
|
h('button', { class: 'btn btn-ghost btn-xs', onclick: () => M(-1, 0) }, h('span', { class: 'icon-[lucide--chevron-left]' }))
|
||||||
|
]),
|
||||||
|
h('span', { class: 'font-bold uppercase' }, () => d().toLocaleString('es', { month: 'short', year: 'numeric' })),
|
||||||
|
h('div', { class: 'flex gap-1' }, [
|
||||||
|
h('button', { class: 'btn btn-ghost btn-xs', onclick: () => M(1, 0) }, h('span', { class: 'icon-[lucide--chevron-right]' })),
|
||||||
|
h('button', { class: 'btn btn-ghost btn-xs', onclick: () => M(0, 1) }, h('span', { class: 'icon-[lucide--chevrons-right]' }))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
h('div', { class: 'grid grid-cols-7 gap-1', onmouseleave: () => hv(null) }, [
|
||||||
|
...'LMXJVSD'.split('').map(l => h('div', { class: 'text-[10px] opacity-40 font-bold text-center' }, l)),
|
||||||
|
() => {
|
||||||
|
let y = d().getFullYear(), m = d().getMonth(), first = (new Date(y, m, 1).getDay() + 6) % 7;
|
||||||
|
return [...Array(first).fill(h('div')), ...Array(new Date(y, m + 1, 0).getDate()).keys()].map(i => {
|
||||||
|
if (typeof i != 'number') return i;
|
||||||
|
let day = i + 1, ds = F(new Date(y, m, day)), today = F(now) == ds;
|
||||||
|
return h('button', {
|
||||||
|
type: 'button', onclick: () => L(new Date(y, m, day)), onmouseenter: () => G() && hv(ds),
|
||||||
|
class: () => {
|
||||||
|
let v = V(), hov = hv(), s = v?.start || (typeof v == 'string' ? v.slice(0, 10) : 0),
|
||||||
|
isE = v?.end == ds, isS = s == ds,
|
||||||
|
inR = G() && v?.start && (v.end ? (ds > v.start && ds < v.end) : (hov && ((ds > s && ds <= hov) || (ds < s && ds >= hov))));
|
||||||
|
return `btn btn-xs p-0 aspect-square min-h-0 h-auto font-normal relative ${isS || isE ? 'btn-primary z-10' : inR ? 'bg-primary/20 border-none rounded-none' : 'btn-ghost'} ${today ? 'ring-1 ring-primary font-black' : ''}`
|
||||||
|
}
|
||||||
|
}, day)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
p.hour && h('div', { class: 'mt-3 pt-2 border-t flex gap-4' }, G() ? [I({ v: sh, on: sh }), I({ v: eh, on: eh })] : [I({ v: sh, on: sh })])
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pallete = p => {
|
||||||
|
let L = s => (s || '').toLowerCase(),
|
||||||
|
C = ['#000', '#1A1A1A', '#333', '#4D4D4D', '#666', '#808080', '#B3B3B3', '#FFF', '#450a0a', '#7f1d1d', '#991b1b', '#b91c1c', '#dc2626', '#ef4444', '#f87171', '#fca5a5', '#431407', '#7c2d12', '#9a3412', '#c2410c', '#ea580c', '#f97316', '#fb923c', '#ffedd5', '#713f12', '#a16207', '#ca8a04', '#eab308', '#facc15', '#fde047', '#fef08a', '#fff9c4', '#064e3b', '#065f46', '#059669', '#10b981', '#34d399', '#4ade80', '#84cc16', '#d9f99d', '#082f49', '#075985', '#0284c7', '#0ea5e9', '#38bdf8', '#7dd3fc', '#22d3ee', '#cffafe', '#1e1b4b', '#312e81', '#4338ca', '#4f46e5', '#6366f1', '#818cf8', '#a5b4fc', '#e0e7ff', '#2e1065', '#4c1d95', '#6d28d9', '#7c3aed', '#8b5cf6', '#a855f7', '#d946ef', '#fae8ff'];
|
||||||
|
|
||||||
|
return h('div', { class: `p-3 bg-base-100 rounded-box shadow w-64 ${p.class || ''}` },
|
||||||
|
h('div', { class: 'grid grid-cols-8 gap-1' },
|
||||||
|
C.map(c => h('button', {
|
||||||
|
type: 'button',
|
||||||
|
style: `background:${c}`,
|
||||||
|
onclick: () => (isF(p.value) ? p.value(c) : p.onchange?.(c), hide()),
|
||||||
|
class: () => `size-6 rounded-sm transition-all hover:scale-125 hover:z-10 active:scale-95 border border-black/5 p-0 min-h-0 ${L(val(p.value)) == L(c) ? 'ring-2 ring-offset-1 ring-primary z-10 scale-110' : ''}`
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toast = (m, t = "alert-success", d = 3500) => {
|
||||||
|
let C = document.getElementById("stc"), T, E, w = h("div", { style: "display:contents" });
|
||||||
|
if (!C) document.body.append(C = h("div", { id: "stc", class: "fixed top-0 right-0 z-[9999] p-4 flex flex-col items-end gap-2 pointer-events-none" }));
|
||||||
|
C.append(w);
|
||||||
|
|
||||||
|
const i = mount(() => {
|
||||||
|
let v = $(0), l = $(0);
|
||||||
|
E = () => l() || (l(1), clearTimeout(T), setTimeout(() => (i.destroy(), w.remove(), C.firstChild || C.remove()), 300));
|
||||||
|
setTimeout(() => v(1));
|
||||||
|
return h("div", {
|
||||||
|
class: () => `alert alert-soft ${t} shadow-lg transition-all duration-300 inline-flex w-auto pointer-events-auto ${l() ? 'translate-x-full opacity-0' : v() ? 'translate-x-0 opacity-100' : 'translate-x-10 opacity-0'}`
|
||||||
|
}, [
|
||||||
|
typeof m == 'function' ? m() : typeof m == 'string' ? h("span", m) : m,
|
||||||
|
h("button", { class: "btn btn-xs btn-circle btn-ghost", onclick: E }, h("span", { class: "icon-[lucide--x]" }))
|
||||||
|
])
|
||||||
|
}, w);
|
||||||
|
|
||||||
|
if (d > 0) T = setTimeout(E, d);
|
||||||
|
return E;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.ui = ui;
|
||||||
|
window.toast = toast;
|
||||||
|
window.calendar = calendar;
|
||||||
|
window.pallete = pallete
|
||||||
@@ -1,15 +1,8 @@
|
|||||||
/**
|
// 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
117
src/tailwind
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
const layout = [
|
||||||
|
'join', 'join-vertical', 'lg:join-horizontal',
|
||||||
|
'divider', 'divider-horizontal',
|
||||||
|
'validator', 'validator-hint',
|
||||||
|
'glass'
|
||||||
|
]
|
||||||
|
|
||||||
|
const icons = [
|
||||||
|
'icon-[lucide--calendar]', 'icon-[lucide--chevrons-left]',
|
||||||
|
'icon-[lucide--chevron-left]', 'icon-[lucide--chevron-right]',
|
||||||
|
'icon-[lucide--chevrons-right]', 'icon-[lucide--info]',
|
||||||
|
'icon-[lucide--check-circle]', 'icon-[lucide--alert-triangle]',
|
||||||
|
'icon-[lucide--alert-circle]', 'icon-[lucide--heart]',
|
||||||
|
'icon-[lucide--upload]', 'icon-[lucide--x]', 'icon-[lucide--text]',
|
||||||
|
'icon-[lucide--lock]', 'icon-[lucide--hash]', 'icon-[lucide--mail]',
|
||||||
|
'icon-[lucide--search]', 'icon-[lucide--phone]', 'icon-[lucide--link]',
|
||||||
|
'icon-[lucide--eye-off]', 'icon-[lucide--eye]'
|
||||||
|
]
|
||||||
|
|
||||||
|
const inputs = [
|
||||||
|
'input', 'input-bordered', 'input-ghost',
|
||||||
|
'input-primary', 'input-secondary', 'input-accent',
|
||||||
|
'input-info', 'input-success', 'input-warning', 'input-error',
|
||||||
|
'input-xs', 'input-sm', 'input-md', 'input-lg',
|
||||||
|
'floating-label'
|
||||||
|
]
|
||||||
|
|
||||||
|
const alerts = [
|
||||||
|
'alert', 'alert-info', 'alert-success', 'alert-warning', 'alert-error',
|
||||||
|
'alert-soft', 'alert-outline', 'alert-dash'
|
||||||
|
]
|
||||||
|
|
||||||
|
const avatars = [
|
||||||
|
'avatar', 'avatar-group', 'avatar-online', 'avatar-offline', 'avatar-placeholder'
|
||||||
|
]
|
||||||
|
|
||||||
|
const badges = [
|
||||||
|
'badge', 'badge-primary', 'badge-secondary', 'badge-accent',
|
||||||
|
'badge-info', 'badge-success', 'badge-warning', 'badge-error',
|
||||||
|
'badge-outline', 'badge-soft', 'badge-dash',
|
||||||
|
'badge-xs', 'badge-sm', 'badge-md', 'badge-lg'
|
||||||
|
]
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
'btn', 'btn-primary', 'btn-secondary', 'btn-accent',
|
||||||
|
'btn-ghost', 'btn-info', 'btn-success', 'btn-warning',
|
||||||
|
'btn-error', 'btn-neutral',
|
||||||
|
'btn-xs', 'btn-sm', 'btn-md', 'btn-lg', 'btn-xl',
|
||||||
|
'btn-outline', 'btn-soft', 'btn-dash', 'btn-link',
|
||||||
|
'btn-circle', 'btn-square', 'btn-wide', 'btn-block',
|
||||||
|
'btn-active', 'btn-disabled'
|
||||||
|
]
|
||||||
|
|
||||||
|
const checkboxes = [
|
||||||
|
'checkbox', 'checkbox-primary', 'checkbox-secondary', 'checkbox-accent',
|
||||||
|
'checkbox-info', 'checkbox-success', 'checkbox-warning', 'checkbox-error',
|
||||||
|
'checkbox-xs', 'checkbox-sm', 'checkbox-md', 'checkbox-lg'
|
||||||
|
]
|
||||||
|
|
||||||
|
const toggles = [
|
||||||
|
'toggle', 'toggle-primary', 'toggle-secondary', 'toggle-accent',
|
||||||
|
'toggle-xs', 'toggle-sm', 'toggle-md', 'toggle-lg'
|
||||||
|
]
|
||||||
|
|
||||||
|
const chats = [
|
||||||
|
'chat', 'chat-end', 'chat-start', 'chat-image',
|
||||||
|
'chat-header', 'chat-footer', 'chat-bubble'
|
||||||
|
]
|
||||||
|
|
||||||
|
const drawers = [
|
||||||
|
'drawer', 'drawer-end', 'drawer-toggle', 'drawer-content',
|
||||||
|
'drawer-side', 'drawer-overlay'
|
||||||
|
]
|
||||||
|
|
||||||
|
const dropdowns = [
|
||||||
|
'dropdown', 'dropdown-content', 'dropdown-end',
|
||||||
|
'dropdown-top', 'dropdown-bottom', 'dropdown-left', 'dropdown-right'
|
||||||
|
]
|
||||||
|
|
||||||
|
const misc = [
|
||||||
|
'breadcrumbs', 'fab', 'fieldset', 'fieldset-legend',
|
||||||
|
'indicator', 'indicator-item', 'menu', 'menu-dropdown', 'menu-dropdown-show',
|
||||||
|
'kbd', 'kbd-xs', 'kbd-sm', 'kbd-md', 'kbd-lg', 'kbd-xl',
|
||||||
|
'list', 'list-row', 'list-bullet', 'list-image', 'list-none',
|
||||||
|
'mask', 'mask-star', 'mask-star-2', 'mask-heart', 'mask-circle',
|
||||||
|
'modal', 'modal-box', 'modal-action', 'modal-backdrop',
|
||||||
|
'modal-open', 'modal-middle', 'modal-top', 'modal-bottom',
|
||||||
|
'navbar', 'navbar-start', 'navbar-center', 'navbar-end',
|
||||||
|
'progress', 'progress-neutral', 'progress-primary', 'progress-secondary',
|
||||||
|
'progress-accent', 'progress-info', 'progress-success',
|
||||||
|
'progress-warning', 'progress-error', 'radial-progress',
|
||||||
|
'radio', 'radio-primary', 'radio-secondary', 'radio-accent',
|
||||||
|
'radio-info', 'radio-success', 'radio-warning', 'radio-error',
|
||||||
|
'radio-xs', 'radio-sm', 'radio-md', 'radio-lg',
|
||||||
|
'range', 'range-primary', 'range-secondary', 'range-accent',
|
||||||
|
'range-info', 'range-success', 'range-warning', 'range-error',
|
||||||
|
'range-xs', 'range-sm', 'range-md', 'range-lg',
|
||||||
|
'rating', 'rating-half', 'rating-hidden',
|
||||||
|
'select', 'select-bordered', 'select-primary', 'select-secondary',
|
||||||
|
'select-accent', 'select-info', 'select-success',
|
||||||
|
'select-warning', 'select-error',
|
||||||
|
'select-xs', 'select-sm', 'select-md', 'select-lg',
|
||||||
|
'stack', 'stack-top', 'stack-bottom', 'stack-start', 'stack-end',
|
||||||
|
'stat', 'stat-figure', 'stat-title', 'stat-value', 'stat-desc',
|
||||||
|
'swap', 'swap-on', 'swap-off', 'swap-active',
|
||||||
|
'swap-rotate', 'swap-flip', 'swap-indeterminate',
|
||||||
|
'table', 'table-zebra', 'table-pin-rows', 'table-pin-cols',
|
||||||
|
'table-xs', 'table-sm', 'table-md', 'table-lg',
|
||||||
|
'tabs', 'tabs-box', 'tabs-lift', 'tabs-border', 'tab', 'tab-content',
|
||||||
|
'timeline', 'timeline-vertical', 'timeline-horizontal',
|
||||||
|
'timeline-compact', 'timeline-start', 'timeline-middle',
|
||||||
|
'timeline-end', 'timeline-box',
|
||||||
|
'tooltip', 'tooltip-top', 'tooltip-bottom', 'tooltip-left', 'tooltip-right',
|
||||||
|
'tooltip-primary', 'tooltip-secondary', 'tooltip-accent',
|
||||||
|
'tooltip-info', 'tooltip-success', 'tooltip-warning', 'tooltip-error',
|
||||||
|
'tooltip-open'
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user