Compare commits

..

293 Commits

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

Remove auto assign to window
2026-04-16 13:59:13 +02:00
4c2ef02e95 revert dad4fc0aca
revert 1.2.13
2026-04-16 13:59:00 +02:00
1a78879197 revert 8bdc86faf3
revert 1.2.13

Remove auto assign to window
2026-04-16 13:18:44 +02:00
d7bdfccb03 revert dad4fc0aca
revert 1.2.13
2026-04-16 13:18:29 +02:00
dad4fc0aca 1.2.13 2026-04-16 12:46:51 +02:00
8bdc86faf3 1.2.13
Remove auto assign to window
2026-04-16 12:46:35 +02:00
728abe0aa3 Eliminar .github/workflows/build.yaml 2026-04-16 00:13:57 +02:00
7d01ff13be Actualizar .github/workflows/build.yaml
Some checks are pending
Build Project / build-job (push) Waiting to run
2026-04-16 00:09:39 +02:00
49029b5eae Añadir .github/workflows/build.yaml
Some checks failed
Build Project / build-job (push) Failing after 37s
2026-04-16 00:07:04 +02:00
91c72b6b9f Actualizar .github/workflows/publish-gitea.yml 2026-04-15 17:55:01 +02:00
b472cb8921 Update to 1.2.12 2026-04-15 17:54:23 +02:00
5dbff9d499 Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 17:15:52 +02:00
1aee6a297d Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 17:14:33 +02:00
77b73cceef Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 17:07:49 +02:00
50d0d1319e Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 17:05:29 +02:00
e203168ac1 Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 17:04:23 +02:00
b21af833ac Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 17:02:50 +02:00
b9207b1ddc Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 17:02:22 +02:00
51abd36724 Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 16:59:10 +02:00
ef45d5f057 Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 16:57:02 +02:00
f3f774fda1 Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 16:54:57 +02:00
ac9527f9a3 Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 16:53:20 +02:00
9856eefa64 Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 16:50:42 +02:00
70e3c33bc8 Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 16:47:21 +02:00
e06d5da447 Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 16:42:25 +02:00
9db278ea9b Actualizar .github/workflows/publicar-paquete.yml 2026-04-15 16:41:28 +02:00
0aa57fb944 Actualizar package.json 2026-04-15 16:39:39 +02:00
2e2a0b56f0 Añadir .github/workflows/publicar-paquete.yml 2026-04-15 16:37:04 +02:00
bedd4ae225 1.2.12 Fix computed problem
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 9s
Publish to NPM / build (release) Successful in 26s
2026-04-15 14:57:21 +02:00
1e470179ef Actualizar package.json 2026-04-15 14:27:31 +02:00
4826892582 Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 14:10:37 +02:00
c654c6ff1a Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 14:07:15 +02:00
dc0dd14fd5 Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 14:06:15 +02:00
f3b536e00e Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 14:04:06 +02:00
0fb71372ac Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 13:58:08 +02:00
4a8461903c Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 13:49:36 +02:00
9dd231fbc3 Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 13:46:52 +02:00
4d4f423ed7 Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 13:43:51 +02:00
b835cd1992 Actualizar .github/workflows/deploy-docs.yaml
Some checks failed
Deploy Docs to Synology / deploy (push) Has been cancelled
2026-04-15 13:40:40 +02:00
9e469c975d Actualizar .github/workflows/deploy-docs.yaml
Some checks failed
Deploy Docs to Synology / deploy (push) Has been cancelled
2026-04-15 13:37:27 +02:00
873b3624c6 Actualizar .github/workflows/deploy-docs.yaml
Some checks failed
Deploy Docs to Synology / deploy (push) Failing after 4s
2026-04-15 13:36:16 +02:00
f8fd9b4ae7 Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 13:24:12 +02:00
3ee77d4e9b Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 13:20:56 +02:00
32ab220ade Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 13:17:38 +02:00
f50823c3cd Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 13:13:00 +02:00
71f2b2340b Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 13:10:05 +02:00
26d33c94bf Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 13:08:36 +02:00
5d3ab87940 Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 13:05:18 +02:00
5d3a9cdea4 Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 12:19:22 +02:00
80fa361da3 Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 12:18:02 +02:00
34c0b16595 Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 12:14:27 +02:00
abeab4a8b6 Actualizar .github/workflows/deploy-docs.yaml 2026-04-15 12:13:28 +02:00
5a0bec8837 Añadir .github/workflows/deploy-docs.yaml 2026-04-15 12:12:50 +02:00
0b124871fb Actualizar package.json 2026-04-15 10:48:06 +02:00
0d119bcc35 Actualizar package.json
All checks were successful
Publish to NPM / build (release) Successful in 10s
2026-04-15 10:45:29 +02:00
8a71b86895 Actualizar .github/workflows/publish.yml 2026-04-15 10:45:11 +02:00
c8e352ea96 Actualizar package.json
Some checks failed
Publish to NPM / build (release) Failing after 29s
2026-04-15 10:35:03 +02:00
73e019a582 Actualizar .github/workflows/publish.yml 2026-04-15 10:34:44 +02:00
f14346cf39 Actualizar package.json
Some checks failed
Publish to NPM / build (release) Failing after 58s
2026-04-15 10:28:59 +02:00
14e4955acd Actualizar .github/workflows/publish.yml 2026-04-15 10:28:48 +02:00
23451f04df Actualizar .github/workflows/publish.yml
Some checks failed
Publish to NPM / publish (release) Has been cancelled
2026-04-15 10:25:27 +02:00
9f36e5e3fc Actualizar package.json 2026-04-15 10:25:13 +02:00
de607e3dda Actualizar package.json
Some checks failed
Publish to NPM / publish (release) Failing after 37s
2026-04-15 10:22:53 +02:00
3000212961 Actualizar package.json
Some checks failed
Publish to NPM / publish (release) Has been cancelled
2026-04-15 10:20:28 +02:00
541a3526a1 Actualizar .github/workflows/publish.yml 2026-04-15 10:19:21 +02:00
25188e7473 Actualizar package.json
Some checks failed
Publish to NPM / publish (release) Has been cancelled
2026-04-15 10:15:21 +02:00
876d4c01f0 Actualizar .github/workflows/publish.yml 2026-04-15 10:15:03 +02:00
3c04a9433f Actualizar package.json
Some checks failed
Publish to NPM / publish (release) Failing after 36s
2026-04-15 10:09:50 +02:00
5e90f62168 Actualizar .github/workflows/publish.yml 2026-04-15 10:09:02 +02:00
4a8959162c Actualizar package.json
Some checks failed
Publish to NPM / publish (release) Failing after 40s
2026-04-15 09:04:47 +02:00
5a10974d0b Actualizar .github/workflows/publish.yml 2026-04-15 09:04:37 +02:00
b8b42652d9 Actualizar package.json
Some checks failed
Publish to NPM / publish (release) Failing after 52s
2026-04-15 09:02:11 +02:00
6f03422d64 Actualizar .github/workflows/publish.yml 2026-04-15 09:01:52 +02:00
dc8568ae3a Actualizar package.json
Some checks failed
Publish to NPM / publish (release) Failing after 40s
2026-04-15 00:44:18 +02:00
f5bb657018 Actualizar sigpro.js 2026-04-15 00:44:08 +02:00
52ec5f46c3 Actualizar sigpro.js 2026-04-15 00:43:56 +02:00
64ead15875 Actualizar .github/workflows/publish.yml 2026-04-15 00:40:58 +02:00
43b8f4034c Actualizar .github/workflows/publish.yml 2026-04-15 00:39:00 +02:00
a443d099ac Actualizar .github/workflows/publish.yml 2026-04-15 00:36:39 +02:00
8e4f071f52 Actualizar .github/workflows/publish.yml 2026-04-15 00:35:12 +02:00
fb9b752cce Actualizar .github/workflows/publish.yml 2026-04-15 00:29:34 +02:00
bc272e3d3c Actualizar .github/workflows/publish.yml 2026-04-15 00:28:13 +02:00
9adaeb5cae Actualizar .github/workflows/publish.yml
Some checks failed
Publish to NPM / publish (release) Failing after 40s
2026-04-14 22:55:38 +02:00
8b310805c3 Eliminar sigpro2.js 2026-04-14 22:45:00 +02:00
e40d39a26c 1.2.2 2026-04-14 22:44:48 +02:00
59bf869686 1.2.2 2026-04-14 22:44:23 +02:00
1a8a33dd47 Improve SVG 2026-04-14 16:56:59 +02:00
0ae2d56c37 SigPro 1.2.1 2026-04-14 15:01:26 +02:00
45b34c9668 Include Anim() 2026-04-14 09:49:22 +02:00
6d6c4ce703 Return to more defined variables and little improve, Compressed 3.2 Kb 2026-04-14 08:44:04 +02:00
a43b624e00 Compacted Size 2026-04-14 08:38:00 +02:00
b7dbbd7add Update to new 1.2.0 2026-04-14 00:08:41 +02:00
6d1539cf20 Add $$ proxies 2026-04-13 23:54:30 +02:00
e6b1f65055 add batch() 2026-04-13 23:29:27 +02:00
205e5f5f06 Remove animations IF 2026-04-13 23:16:09 +02:00
1018c0bf9f add router-hook 2026-04-13 20:41:58 +02:00
0729f00fe2 fix set 2026-04-13 19:08:54 +02:00
1d09e8b382 Fix Tag 2026-04-13 18:10:32 +02:00
7aac307af5 createEffect compacted 2026-04-13 18:08:56 +02:00
bc23716280 Solve 1 bug more 2026-04-13 18:00:52 +02:00
cf0e5b2913 Solved 2 bugs 2026-04-13 17:52:26 +02:00
627cfcd5d5 ok + set 2026-04-13 08:01:28 +02:00
d8ab8c75d8 Actualizar sigworkPro.js 2026-04-12 23:54:56 +02:00
dcc669d6fe Actualizar sigpro2.js 2026-04-12 21:35:49 +02:00
f4032777a1 Actualizar sigpro2.js 2026-04-12 19:43:52 +02:00
6ea0dcd3d7 Actualizar sigworkPro.js 2026-04-12 19:21:19 +02:00
5593c2e701 Actualizar sigworkPro.js 2026-04-12 19:18:50 +02:00
6813283c45 Actualizar sigworkPro.js 2026-04-12 18:54:39 +02:00
f411c2e9bb Actualizar sigworkPro.js 2026-04-12 18:47:52 +02:00
f38b3b4b46 Actualizar sigworkPro.js 2026-04-12 15:46:42 +02:00
9f524feb98 Actualizar sigworkPro.js 2026-04-12 01:37:30 +02:00
2e5142f073 Actualizar sigworkPro.js 2026-04-12 01:03:22 +02:00
8903be50ea Actualizar sigworkPro.js 2026-04-12 00:57:56 +02:00
3243f67431 Actualizar sigpro2.js 2026-04-11 02:10:20 +02:00
e9e0828206 Actualizar sigpro2.js 2026-04-11 02:08:20 +02:00
fbd9ddba47 Actualizar sigpro2.js 2026-04-11 01:15:10 +02:00
03fe2a1b80 Actualizar sigpro2.js 2026-04-11 00:32:44 +02:00
b168761127 Actualizar sigpro2.js 2026-04-11 00:20:16 +02:00
53fdaaa212 Actualizar sigpro-lite.js 2026-04-11 00:07:37 +02:00
02ee84f8e5 Añadir sigpro-lite.js 2026-04-10 23:56:08 +02:00
9b9b345e48 Reparado para sigpro ui 2026-04-10 23:11:27 +02:00
9588bcbce8 up 2026-04-10 18:37:58 +02:00
52af01e128 ok 2026-04-10 09:08:52 +02:00
adafb8eb11 last 2026-04-09 23:47:15 +02:00
689febdbf4 p 2026-04-09 23:24:05 +02:00
98789b438b 1.2 1.3 2026-04-09 21:59:59 +02:00
7a9c138b65 kk1 2026-04-09 14:55:08 +02:00
d0c6663112 clean 2026-04-08 22:01:24 +02:00
ad96380e6f Router no Async 2026-04-08 19:29:58 +02:00
38b833e5c2 Actualizar sigpro2.js 2026-04-08 19:20:50 +02:00
272c086e4f Vualvo al contenido sin _sub 2026-04-08 19:20:11 +02:00
7350633a63 onMOunt on UnMount 2026-04-08 15:31:16 +02:00
d586af52b6 quito IIFE 2026-04-08 15:07:20 +02:00
d9cbbae40d _sub 2026-04-08 15:02:51 +02:00
c4929e26e2 Add share Use 2026-04-08 14:24:14 +02:00
7879aa463b Fase2 2026-04-08 14:15:34 +02:00
d053ba39a1 Fase1 2026-04-08 14:08:56 +02:00
215a3375b5 If con transicion enter / exit 2026-04-08 12:47:29 +02:00
9c0acb83f8 Correct small errors 2026-04-08 12:27:00 +02:00
557bd1428a mount mas minimalista pero eficiente 2026-04-08 12:20:15 +02:00
4b5243052b Posibilidad de usar value: count y suscribir automaticamente 2026-04-08 12:17:41 +02:00
b2ba771b9c Add get 2026-04-08 12:10:23 +02:00
925a673775 Añadimos Router con .to.path.params.back 2026-04-08 12:06:57 +02:00
2ac429c3f8 revert 20022a361c
revert Agrupacion de funciones en linea (visual)
2026-04-08 12:04:27 +02:00
20022a361c Agrupacion de funciones en linea (visual) 2026-04-08 11:58:00 +02:00
150cad34d5 Refactor con nombres de variables descriptivos y comentarios en cabeza 2026-04-08 11:54:36 +02:00
fc148fdbcc Add mount 2026-04-08 11:51:40 +02:00
b75d1175a5 Add signal persist 2026-04-08 11:48:04 +02:00
1e8b3ad803 New Router 2026-04-08 11:43:56 +02:00
bfd79f8491 Elimino el titulo 2026-04-08 11:13:27 +02:00
a39d85ff89 Inicial con seguridad base reforzada 2026-04-08 11:12:52 +02:00
9e36bb8041 Eliminar sigpro2.js 2026-04-08 11:09:31 +02:00
75520a8499 Eliminar sigpro_work.js 2026-04-08 11:09:15 +02:00
e9ca8d397b Eliminar sigpro.ts 2026-04-08 11:08:58 +02:00
35bfbde6a7 Eliminar sigwork.js 2026-04-08 11:08:49 +02:00
c56bdd4ba9 Router SVG y signal persistente Mejorado 2026-04-08 10:51:21 +02:00
5f34c79fca comment remove 2026-04-08 01:58:07 +02:00
d15251c808 Add sanitize 2026-04-08 01:54:03 +02:00
80ab4baf87 clean comments 2026-04-08 01:41:54 +02:00
4e59bcb460 remove transition 2026-04-08 01:40:42 +02:00
d508a99290 add value: signal effect auto in h 2026-04-08 01:33:41 +02:00
b2c6b8d398 Add on-off in router 2026-04-08 01:30:23 +02:00
4a9707819d Add Mount 2026-04-08 01:27:23 +02:00
80 changed files with 71737 additions and 6364 deletions

28
.github/workflows/docs.yaml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Deploy Docs to Synology
on:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
container:
image: node:20-bullseye
options: >-
--dns 192.168.1.1
--add-host git.natxocc.com:host-gateway
--add-host gitea:host-gateway
-v /volume1/webdocs/sigpro:/mnt/nas_docs
steps:
- name: Checkout código
uses: actions/checkout@v4
with:
fetch-depth: 1
env:
GIT_CONFIG_PARAMETERS: "'url.https://git.natxocc.com/.insteadOf=http://gitea:3000/'"
- name: Copiar archivos
run: |
cp -r docs/. /mnt/nas_docs/
ls -la /mnt/nas_docs

43
.github/workflows/publish-gitea.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Publish to SigPro (NPM)
on:
workflow_dispatch:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
container:
image: node:20-bullseye
options: >-
--dns 192.168.1.1
--add-host git.natxocc.com:host-gateway
--add-host gitea:host-gateway
steps:
- name: Checkout código
uses: actions/checkout@v4
env:
GIT_CONFIG_PARAMETERS: "'url.https://git.natxocc.com/.insteadOf=http://gitea:3000/'"
- name: Instalar Bun
run: |
curl -fsSL https://bun.sh/install | bash
echo "$HOME/.bun/bin" >> $GITHUB_PATH
- name: Instalar y Build
run: |
export PATH="$HOME/.bun/bin:$PATH"
bun install
bun run build
- name: Configurar Registro y Publicar
run: |
# 1. Definimos la URL del registro
REGISTRY="git.natxocc.com/api/packages/natxocc/npm/"
echo "//${REGISTRY}:_authToken=${{ secrets.PACK_TOKEN }}" > ~/.npmrc
npm publish --registry "https://${REGISTRY}" --userconfig ~/.npmrc

38
.github/workflows/publish-npm.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Publish to NPM (Manual)
on:
workflow_dispatch:
inputs:
version:
description: 'Versión a publicar (ej: 1.2.20)'
required: true
jobs:
publish:
runs-on: ubuntu-latest
container:
image: node:20-bullseye
options: >-
--dns 192.168.1.1
--add-host git.natxocc.com:host-gateway
--add-host gitea:host-gateway
steps:
- name: Clone source from Gitea
run: |
git clone https://git.natxocc.com/natxocc/sigpro.git source
cd source
git checkout main
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install and publish
run: |
cd source
bun install
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
npm publish --access public
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,25 +0,0 @@
name: Publish to NPM
on:
release:
types: [published] # Se dispara cuando creas una "Release" en GitHub
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Usamos Bun ya que tu proyecto lo usa
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Authenticate with NPM
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
- name: Publish to NPM
run: npm publish --access public

22
.github/workflows/unpublish-npm.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Delete npm version
on:
workflow_dispatch:
inputs:
version:
description: 'Versión a eliminar'
required: true
jobs:
delete:
runs-on: ubuntu-latest
steps:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Unpublish version
run: npm unpublish sigpro@${{ github.event.inputs.version }} --force
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,15 +1,12 @@
# `SigPro` ⚛️ Blazing fast, zero-overhead, vanilla JS renderer with atomic reactivity.
# `SigPro`
[![npm version](https://img.shields.io/npm/v/sigpro.svg)](https://www.npmjs.com/package/sigpro) [![npm version](https://img.shields.io/npm/v/sigpro.svg)](https://www.npmjs.com/package/sigpro)
[![bundle size](https://img.shields.io/bundlephobia/minzip/sigpro)](https://bundlephobia.com/package/sigpro) ![js size](https://img.shields.io/badge/js_size-2.8_kB_brotli-blue)
[![size](https://img.badgesize.io/natxocc/sigpro/main/sigpro/index.js?compression=gzip)](https://github.com/natxocc/sigpro)
[![license](https://img.shields.io/npm/l/sigpro)](https://github.com/natxocc/sigpro/blob/main/LICENSE) [![license](https://img.shields.io/npm/l/sigpro)](https://github.com/natxocc/sigpro/blob/main/LICENSE)
### **The Atomic Reactivity Engine for the Modern Web.** [**Explore the Docs →**](https://sigpro.natxocc.com/#/)
**SigPro** is an ultra-lightweight rendering engine designed for extreme performance. By eliminating the Virtual DOM and heavy compilers, it achieves **surgical reactivity** in less than 2KB.
[**Explore the Docs →**](https://natxocc.github.io/sigpro/) | [**View on GitHub**](https://github.com/natxocc/sigpro)
--- ---
@@ -19,23 +16,23 @@ After years of building within closed ecosystems like **React, Vue, or Svelte**
That extra development time and the cognitive load of "learning the framework" instead of "learning the language" is exactly what **SigPro** eliminates. If the final destination is always JS, why not use a Pure JS-based system that drastically simplifies coding with a readable, vanilla, and remarkably fast architecture? That extra development time and the cognitive load of "learning the framework" instead of "learning the language" is exactly what **SigPro** eliminates. If the final destination is always JS, why not use a Pure JS-based system that drastically simplifies coding with a readable, vanilla, and remarkably fast architecture?
* 🎯 **Atomic Precision:** Powered by a *Signal-based* architecture. State is bound directly to DOM nodes—when a value changes, **only that specific node updates**. * **Atomic Precision:** Powered by a *Signal-based* architecture. State is bound directly to DOM nodes—when a value changes, **only that specific node updates**.
* 🚀 **Zero-Hydration Bottlenecks:** No 100KB bundles or complex build steps. SigPro is pure, optimized JavaScript tailored for the browser's native engine. * **Zero-Hydration Bottlenecks:** No 100KB bundles or complex build steps. SigPro is pure, optimized JavaScript tailored for the browser's native engine.
* 🍦 **Pure Vanilla JS Performance:** High-octane performance without the need for transpilers or heavy transformations. It runs natively in the browser just as well as it does in complex build pipelines. * **Pure Vanilla JS Performance:** High-octane performance without the need for transpilers or heavy transformations. It runs natively in the browser just as well as it does in complex build pipelines.
* 🛠️ **Build-Tool Agnostic:** Total freedom. Use it with **Vite, Webpack, or Rollup** for enterprise projects, or simply import it via a **`<script>` tag** for rapid prototyping. No tooling required. * **Build-Tool Agnostic:** Total freedom. Use it with **Vite, Webpack, or Rollup** for enterprise projects, or simply import it via a **`<script>` tag** for rapid prototyping. No tooling required.
* 🚀 **Vite-Powered DX:** First-class Vite support with **file-based routing** out of the box. The official `sigpro/vite` plugin automatically scans your `src/pages` directory and generates reactive routes—no manual route configuration needed. * **Vite-Powered DX:** First-class Vite support with **file-based routing** out of the box. The official `sigpro/vite` plugin automatically scans your `src/pages` directory and generates reactive routes—no manual route configuration needed.
* 📈 **Zero-Scale Bloat:** Unlike other frameworks where the bundle grows exponentially, SigPro's footprint remains **flat and predictable**. You only pay for the code you write. * **Zero-Scale Bloat:** Unlike other frameworks where the bundle grows exponentially, SigPro's footprint remains **flat and predictable**. You only pay for the code you write.
* 💎 **Premium DX (Developer Experience):** Forget boilerplate imports. SigPro injects an elegant, functional syntax (`Div()`, `Button()`, `Span()`) directly into your scope for a **"Zero-Import"** workflow. * **Premium DX (Developer Experience):** Forget boilerplate imports. SigPro injects an elegant, functional syntax (`div()`, `button()`, `span()`) directly into your scope for a **"Zero-Import"** workflow.
* 📦 **Fully Loaded:** Built-in Hash Routing, native **`localStorage` persistence**, and automatic lifecycle management (cleanups) included in less than 2KB. * **Fully Loaded:** Built-in Hash Routing, native **`localStorage` persistence**, and automatic lifecycle management (cleanups) included in less than 2KB.
* 🌳 **Tree-Shakable:** Optimized for modern bundlers. Import only what you use, or load the full engine for rapid prototyping. * **Tree-Shakable:** Optimized for modern bundlers. Import only what you use, or load the full engine for rapid prototyping.
----- -----
## Real-World Benchmarks ## Real-World Benchmarks
SigPro isn't just "fast on paper." In the industry-standard **JS Framework Benchmark**, it consistently outperforms the most popular libraries by operating at near-native speeds with almost zero memory overhead. SigPro isn't just "fast on paper." In the industry-standard **JS Framework Benchmark**, it consistently outperforms the most popular libraries by operating at near-native speeds with almost zero memory overhead.
### 🚀 Execution Speed (CPU) ### Execution Speed (CPU)
*Lower is better. Measured in milliseconds (ms).* *Lower is better. Measured in milliseconds (ms).*
| Benchmark Test | **SigPro** | SolidJS | Vue 3 | React 18 | | Benchmark Test | **SigPro** | SolidJS | Vue 3 | React 18 |
@@ -44,7 +41,7 @@ SigPro isn't just "fast on paper." In the industry-standard **JS Framework Bench
| **Direct Selection** (on click) | **17.5ms** | ~18ms | ~32ms | ~65ms | | **Direct Selection** (on click) | **17.5ms** | ~18ms | ~32ms | ~65ms |
| **Initial Render** (1k rows) | **~35ms** | ~32ms | ~45ms | ~70ms | | **Initial Render** (1k rows) | **~35ms** | ~32ms | ~45ms | ~70ms |
### 🧠 Memory Footprint ### Memory Footprint
*Lower is better. Measured in Megabytes (MB) after 1k rows.* *Lower is better. Measured in Megabytes (MB) after 1k rows.*
| Metric | **SigPro** | Vanilla JS | Svelte | React | | Metric | **SigPro** | Vanilla JS | Svelte | React |
@@ -65,6 +62,8 @@ Create reactive, persistent components with a syntax that feels like Vanilla JS,
``` ```
```javascript ```javascript
import { $, mount } from "sigpro";
const Counter = () => { const Counter = () => {
// Simple signal // Simple signal
const value = $(100); const value = $(100);
@@ -74,14 +73,14 @@ const Counter = () => {
const doubleValue = $(()=> value() * count()); const doubleValue = $(()=> value() * count());
// Create fast HTML with pure JS // Create fast HTML with pure JS
return Div({ class: "card" }, [ return div({ class: "card" }, [
H1(`Count: ${count()}, Reference: ${value()}, Double x Ref: ${doubleValue()}`), h1(() => `Count: ${count()}, Reference: ${value()}, Double x Ref: ${doubleValue()}`),
P("Atomic updates. Zero re-renders of the parent tree."), p("Atomic updates. Zero re-renders of the parent tree."),
Button({ onclick: () => count(c => c + 1)}, "Increment +1") button({ onclick: () => count(c => c + 1)}, "Increment +1")
]); ]);
}; };
Mount(Counter, "#app"); mount(Counter, "#app");
``` ```
----- -----
@@ -90,7 +89,7 @@ Mount(Counter, "#app");
| Feature | **SigPro** | React / Vue | Svelte | | Feature | **SigPro** | React / Vue | Svelte |
| :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- |
| **Payload (Gzipped)** | **< 1.8KB** | ~30KB - 50KB | ~5KB (Compiled Runtime) | | **Payload (Gzipped)** | **<3KB** | ~30KB - 50KB | ~5KB (Compiled Runtime) |
| **State Logic** | **Atomic Signals** | Virtual DOM Diffing | Compiler Dirty Bits | | **State Logic** | **Atomic Signals** | Virtual DOM Diffing | Compiler Dirty Bits |
| **Update Speed** | **Direct Node Access** | Component Re-render | Block Reconciliation | | **Update Speed** | **Direct Node Access** | Component Re-render | Block Reconciliation |
| **Native Persistence** | **Included ($)** | Requires Plugins | Manual | | **Native Persistence** | **Included ($)** | Requires Plugins | Manual |
@@ -123,7 +122,7 @@ src/
```javascript ```javascript
// vite.config.js // vite.config.js
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { sigproRouter } from 'sigpro/vite'; import { sigproRouter } from 'sigpro/router';
export default defineConfig({ export default defineConfig({
plugins: [sigproRouter()] plugins: [sigproRouter()]

1
dist/sigpro.db.js vendored Normal file
View File

@@ -0,0 +1 @@
var o=async(e,n={},t=null)=>{if(t)t(!0);try{let r=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n),credentials:"include"});if(!r.ok){let s=await r.text();throw Error(`Error ${r.status}: ${s}`)}return await r.json()}finally{if(t)t(!1)}};export{o as db};

1
dist/sigpro.editor.js vendored Normal file

File diff suppressed because one or more lines are too long

439
dist/sigpro.esm.js vendored
View File

@@ -1,439 +0,0 @@
// sigpro.js
var activeEffect = null;
var currentOwner = null;
var effectQueue = new Set;
var isFlushing = false;
var MOUNTED_NODES = new WeakMap;
var doc = document;
var isArr = Array.isArray;
var assign = Object.assign;
var createEl = (t) => doc.createElement(t);
var createText = (t) => doc.createTextNode(String(t ?? ""));
var isFunc = (f) => typeof f === "function";
var isObj = (o) => typeof o === "object" && o !== null;
var runWithContext = (effect, callback) => {
const previousEffect = activeEffect;
activeEffect = effect;
try {
return callback();
} finally {
activeEffect = previousEffect;
}
};
var cleanupNode = (node) => {
if (node._cleanups) {
node._cleanups.forEach((dispose) => dispose());
node._cleanups.clear();
}
node.childNodes?.forEach(cleanupNode);
};
var flushEffects = () => {
if (isFlushing)
return;
isFlushing = true;
while (effectQueue.size > 0) {
const sortedEffects = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
effectQueue.clear();
for (const effect of sortedEffects) {
if (!effect._deleted)
effect();
}
}
isFlushing = false;
};
var trackSubscription = (subscribers) => {
if (activeEffect && !activeEffect._deleted) {
subscribers.add(activeEffect);
activeEffect._deps.add(subscribers);
}
};
var triggerUpdate = (subscribers) => {
subscribers.forEach((effect) => {
if (effect === activeEffect || effect._deleted)
return;
if (effect._isComputed) {
effect.markDirty();
if (effect._subs)
triggerUpdate(effect._subs);
} else {
effectQueue.add(effect);
}
});
if (!isFlushing)
queueMicrotask(flushEffects);
};
var Render = (renderFn) => {
const cleanups = new Set;
const previousOwner = currentOwner;
const container = createEl("div");
container.style.display = "contents";
currentOwner = { cleanups };
const processResult = (result) => {
if (!result)
return;
if (result._isRuntime) {
cleanups.add(result.destroy);
container.appendChild(result.container);
} else if (isArr(result)) {
result.forEach(processResult);
} else {
container.appendChild(result instanceof Node ? result : createText(result));
}
};
try {
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
} finally {
currentOwner = previousOwner;
}
return {
_isRuntime: true,
container,
destroy: () => {
cleanups.forEach((fn) => fn());
cleanupNode(container);
container.remove();
}
};
};
var $ = (initialValue, storageKey = null) => {
const subscribers = new Set;
if (isFunc(initialValue)) {
let cachedValue, isDirty = true;
const effect = () => {
if (effect._deleted)
return;
effect._deps.forEach((dep) => dep.delete(effect));
effect._deps.clear();
runWithContext(effect, () => {
const newValue = initialValue();
if (!Object.is(cachedValue, newValue) || isDirty) {
cachedValue = newValue;
isDirty = false;
triggerUpdate(subscribers);
}
});
};
assign(effect, {
_deps: new Set,
_isComputed: true,
_subs: subscribers,
_deleted: false,
markDirty: () => isDirty = true,
stop: () => {
effect._deleted = true;
effect._deps.forEach((dep) => dep.delete(effect));
subscribers.clear();
}
});
if (currentOwner)
currentOwner.cleanups.add(effect.stop);
return () => {
if (isDirty)
effect();
trackSubscription(subscribers);
return cachedValue;
};
}
let value = initialValue;
if (storageKey) {
try {
const saved = localStorage.getItem(storageKey);
if (saved !== null)
value = JSON.parse(saved);
} catch (e) {
console.warn("SigPro Storage Lock", e);
}
}
return (...args) => {
if (args.length) {
const nextValue = isFunc(args[0]) ? args[0](value) : args[0];
if (!Object.is(value, nextValue)) {
value = nextValue;
if (storageKey)
localStorage.setItem(storageKey, JSON.stringify(value));
triggerUpdate(subscribers);
}
}
trackSubscription(subscribers);
return value;
};
};
var $$ = (object, cache = new WeakMap) => {
if (!isObj(object))
return object;
if (cache.has(object))
return cache.get(object);
const keySubscribers = {};
const proxy = new Proxy(object, {
get(target, key) {
if (activeEffect)
trackSubscription(keySubscribers[key] ??= new Set);
const value = Reflect.get(target, key);
return isObj(value) ? $$(value, cache) : value;
},
set(target, key, value) {
if (Object.is(target[key], value))
return true;
const success = Reflect.set(target, key, value);
if (keySubscribers[key])
triggerUpdate(keySubscribers[key]);
return success;
}
});
cache.set(object, proxy);
return proxy;
};
var Watch = (target, callbackFn) => {
const isExplicit = isArr(target);
const callback = isExplicit ? callbackFn : target;
if (!isFunc(callback))
return () => {};
const owner = currentOwner;
const runner = () => {
if (runner._deleted)
return;
runner._deps.forEach((dep) => dep.delete(runner));
runner._deps.clear();
runner._cleanups.forEach((cleanup) => cleanup());
runner._cleanups.clear();
const previousOwner = currentOwner;
runner.depth = activeEffect ? activeEffect.depth + 1 : 0;
runWithContext(runner, () => {
currentOwner = { cleanups: runner._cleanups };
if (isExplicit) {
runWithContext(null, callback);
target.forEach((dep) => isFunc(dep) && dep());
} else {
callback();
}
currentOwner = previousOwner;
});
};
assign(runner, {
_deps: new Set,
_cleanups: new Set,
_deleted: false,
stop: () => {
if (runner._deleted)
return;
runner._deleted = true;
effectQueue.delete(runner);
runner._deps.forEach((dep) => dep.delete(runner));
runner._cleanups.forEach((cleanup) => cleanup());
if (owner)
owner.cleanups.delete(runner.stop);
}
});
if (owner)
owner.cleanups.add(runner.stop);
runner();
return runner.stop;
};
var Tag = (tag, props = {}, children = []) => {
if (props instanceof Node || isArr(props) || !isObj(props)) {
children = props;
props = {};
}
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
const element = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : createEl(tag);
element._cleanups = new Set;
element.onUnmount = (fn) => element._cleanups.add(fn);
const booleanAttributes = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
const updateAttribute = (name, value) => {
const sanitized = (name === "src" || name === "href") && String(value).toLowerCase().includes("javascript:") ? "#" : value;
if (booleanAttributes.includes(name)) {
element[name] = !!sanitized;
sanitized ? element.setAttribute(name, "") : element.removeAttribute(name);
} else {
sanitized == null ? element.removeAttribute(name) : element.setAttribute(name, sanitized);
}
};
for (let [key, value] of Object.entries(props)) {
if (key === "ref") {
isFunc(value) ? value(element) : value.current = element;
continue;
}
const isSignal = isFunc(value);
if (key.startsWith("on")) {
const eventName = key.slice(2).toLowerCase().split(".")[0];
element.addEventListener(eventName, value);
element._cleanups.add(() => element.removeEventListener(eventName, value));
} else if (isSignal) {
element._cleanups.add(Watch(() => {
const currentVal = value();
key === "class" ? element.className = currentVal || "" : updateAttribute(key, currentVal);
}));
if (["INPUT", "TEXTAREA", "SELECT"].includes(element.tagName) && (key === "value" || key === "checked")) {
const event = key === "checked" ? "change" : "input";
const handler = (e) => value(e.target[key]);
element.addEventListener(event, handler);
element._cleanups.add(() => element.removeEventListener(event, handler));
}
} else {
updateAttribute(key, value);
}
}
const appendChildNode = (child) => {
if (isArr(child))
return child.forEach(appendChildNode);
if (isFunc(child)) {
const marker = createText("");
element.appendChild(marker);
let currentNodes = [];
element._cleanups.add(Watch(() => {
const result = child();
const nextNodes = (isArr(result) ? result : [result]).map((node) => node?._isRuntime ? node.container : node instanceof Node ? node : createText(node));
currentNodes.forEach((node) => {
cleanupNode(node);
node.remove();
});
nextNodes.forEach((node) => marker.parentNode?.insertBefore(node, marker));
currentNodes = nextNodes;
}));
} else {
element.appendChild(child instanceof Node ? child : createText(child));
}
};
appendChildNode(children);
return element;
};
var If = (condition, thenVal, otherwiseVal = null, transition = null) => {
const marker = createText("");
const container = Tag("div", { style: "display:contents" }, [marker]);
let currentView = null, lastState = null;
Watch(() => {
const state = !!(isFunc(condition) ? condition() : condition);
if (state === lastState)
return;
lastState = state;
const dispose = () => {
if (currentView)
currentView.destroy();
currentView = null;
};
if (currentView && !state && transition?.out) {
transition.out(currentView.container, dispose);
} else {
dispose();
}
const branch = state ? thenVal : otherwiseVal;
if (branch) {
currentView = Render(() => isFunc(branch) ? branch() : branch);
container.insertBefore(currentView.container, marker);
if (state && transition?.in)
transition.in(currentView.container);
}
});
return container;
};
var For = (source, renderFn, keyFn, tag = "div", props = { style: "display:contents" }) => {
const marker = createText("");
const container = Tag(tag, props, [marker]);
let viewCache = new Map;
Watch(() => {
const items = (isFunc(source) ? source() : source) || [];
const nextCache = new Map;
const order = [];
for (let i = 0;i < items.length; i++) {
const item = items[i];
const key = keyFn ? keyFn(item, i) : i;
let view = viewCache.get(key);
if (!view) {
const result = renderFn(item, i);
view = result instanceof Node ? { container: result, destroy: () => {
cleanupNode(result);
result.remove();
} } : Render(() => result);
}
viewCache.delete(key);
nextCache.set(key, view);
order.push(key);
}
viewCache.forEach((v) => v.destroy());
let anchor = marker;
for (let i = order.length - 1;i >= 0; i--) {
const view = nextCache.get(order[i]);
if (view.container.nextSibling !== anchor) {
container.insertBefore(view.container, anchor);
}
anchor = view.container;
}
viewCache = nextCache;
});
return container;
};
var Router = (routes) => {
const currentPath = $(window.location.hash.replace(/^#/, "") || "/");
window.addEventListener("hashchange", () => currentPath(window.location.hash.replace(/^#/, "") || "/"));
const outlet = Tag("div", { class: "router-transition" });
let currentView = null;
Watch([currentPath], async () => {
const path = currentPath();
const route = routes.find((r) => {
const routeParts = r.path.split("/").filter(Boolean);
const pathParts = path.split("/").filter(Boolean);
return routeParts.length === pathParts.length && routeParts.every((part, i) => part.startsWith(":") || part === pathParts[i]);
}) || routes.find((r) => r.path === "*");
if (route) {
let component = route.component;
if (isFunc(component) && component.toString().includes("import")) {
component = (await component()).default || await component();
}
const params = {};
route.path.split("/").filter(Boolean).forEach((part, i) => {
if (part.startsWith(":"))
params[part.slice(1)] = path.split("/").filter(Boolean)[i];
});
if (currentView)
currentView.destroy();
if (Router.params)
Router.params(params);
currentView = Render(() => {
try {
return isFunc(component) ? component(params) : component;
} catch (e) {
return Tag("div", { class: "p-4 text-error" }, "Error loading view");
}
});
outlet.appendChild(currentView.container);
}
});
return outlet;
};
Router.params = $({});
Router.to = (path) => window.location.hash = path.replace(/^#?\/?/, "#/");
Router.back = () => window.history.back();
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
var Mount = (component, target) => {
const targetEl = typeof target === "string" ? doc.querySelector(target) : target;
if (!targetEl)
return;
if (MOUNTED_NODES.has(targetEl))
MOUNTED_NODES.get(targetEl).destroy();
const instance = Render(isFunc(component) ? component : () => component);
targetEl.replaceChildren(instance.container);
MOUNTED_NODES.set(targetEl, instance);
return instance;
};
var SigPro = { $, $$, Render, Watch, Tag, If, For, Router, Mount };
if (typeof window !== "undefined") {
assign(window, SigPro);
const tags = `div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption img video audio canvas svg iframe picture source progress meter`.split(" ");
tags.forEach((tag) => {
const helper = tag[0].toUpperCase() + tag.slice(1);
if (!(helper in window))
window[helper] = (p, c) => Tag(tag, p, c);
});
window.SigPro = Object.freeze(SigPro);
}
export {
Watch,
Tag,
Router,
Render,
Mount,
If,
For,
$$,
$
};

File diff suppressed because one or more lines are too long

78
dist/sigpro.grid.js vendored Normal file

File diff suppressed because one or more lines are too long

473
dist/sigpro.js vendored

File diff suppressed because one or more lines are too long

1
dist/sigpro.locale.js vendored Normal file
View File

@@ -0,0 +1 @@
var{$:c}=window.SigPro,s=c("en"),n={},e=(t)=>{for(let o of Object.keys(t)){if(!n[o])n[o]={};Object.assign(n[o],t[o])}},r=(t)=>{if(t&&n[t])s(t)},a=(t)=>{return()=>n[s()]?.[t]??t};export{a as t,r as setLocale,e as addLang};

1
dist/sigpro.min.js vendored

File diff suppressed because one or more lines are too long

1
dist/sigpro.router.js vendored Normal file
View File

@@ -0,0 +1 @@
var{$:p,h:u,watch:m,render:g,isF:y}=window.SigPro,d=()=>window.location.hash.slice(1)||"/",s=p(d());window.addEventListener("hashchange",()=>s(d()));var w=p({}),c=(n)=>{let i=u("div",{class:"router-hook"}),r=null;return m([s],()=>{let l=s(),t=n.find((o)=>{let e=o.path.split("/").filter(Boolean),a=l.split("/").filter(Boolean);return e.length===a.length&&e.every((h,f)=>h[0]===":"||h===a[f])})||n.find((o)=>o.path==="*");if(t){r?.destroy();let o={};t.path.split("/").filter(Boolean).forEach((e,a)=>{if(e[0]===":")o[e.slice(1)]=l.split("/").filter(Boolean)[a]}),w(o),r=g(()=>y(t.component)?t.component(o):t.component),i.replaceChildren(r.container)}}),i.destroy=()=>{r?.destroy()},i};c.params=w;c.to=(n)=>window.location.hash=n.replace(/^#?\/?/,"#/");c.back=()=>window.history.back();c.path=()=>s();export{w as routerParams,c as router};

2
dist/sigpro.ui.css vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/sigpro.ui.js vendored Normal file

File diff suppressed because one or more lines are too long

4
dist/sigpro.vite.js vendored Normal file
View File

@@ -0,0 +1,4 @@
function g(){let u="\x00virtual:sigpro-routes",i=(e)=>{if(!fs.existsSync(e))return[];return fs.readdirSync(e,{recursive:!0}).filter((r)=>/\.(js|jsx)$/.test(r)&&!path.basename(r).startsWith("_")).map((r)=>path.resolve(e,r))},l=(e,r)=>{return("/"+path.relative(e,r).replace(/\\/g,"/").replace(/\.(js|jsx)$/,"").replace(/\/index$/,"").replace(/^index$/,"")).replace(/\/+/g,"/").replace(/\[\.\.\.([^\]]+)\]/g,"*").replace(/\[([^\]]+)\]/g,":$1").replace(/\/$/,"")||"/"};return{name:"sigpro-router",resolveId(e){if(e==="virtual:sigpro-routes")return u},load(e){if(e!==u)return;let r=process.cwd(),t=path.resolve(r,"src/pages"),p=i(t).sort((n,a)=>{let o=l(t,n),c=l(t,a);if(o.includes(":")&&!c.includes(":"))return 1;if(!o.includes(":")&&c.includes(":"))return-1;return c.length-o.length}),s="";if(p.forEach((n)=>{let a=l(t,n),o="./"+path.relative(r,n).replace(/\\/g,"/");s+=` { path: '${a}', component: () => import('/${o}') },
`}),!s.includes("path: '*'"))s+=` { path: '*', component: () => ({ default: () => document.createTextNode('404 - Not Found') }) },
`;return`export const routes = [
${s}];`}}}export{g as sigproRouter};

View File

View File

@@ -1,42 +1,38 @@
<div class="w-full -mt-10"><section class="relative py-20 overflow-hidden border-b border-base-200/30 text-center flex flex-col items-center"><div class="relative z-10 max-w-5xl mx-auto px-6 flex flex-col items-center"><div class="flex justify-center mb-10"><img src="logo.svg" alt="SigPro Logo" class="w-48 h-48 md:w-64 md:h-64 object-contain drop-shadow-2xl"></div><h1 class="text-7xl md:text-9xl font-black tracking-tighter mb-4 bg-clip-text text-transparent bg-linear-to-r from-primary via-secondary to-accent !text-center w-full">SigPro</h1><div class="text-2xl md:text-3xl font-bold text-base-content/90 mb-4 !text-center w-full">Atomic Unified Reactive Engine</div><div class="text-xl text-base-content/60 max-w-3xl mx-auto mb-10 leading-relaxed italic text-balance font-light !text-center w-full">"The efficiency of direct DOM manipulation with the elegance of functional reactivity."</div><div class="flex flex-wrap justify-center gap-4 w-full"><a href="#/install" class="btn btn-primary btn-lg shadow-xl shadow-primary/20 group px-10 border-none">Get Started <span class="group-hover:translate-x-1 transition-transform inline-block">→</span></a><button onclick="window.open('https://github.com/natxocc/sigpro')" class="btn btn-outline btn-lg border-base-300 hover:bg-base-300 hover:text-base-content">GitHub</button></div></div><div class="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full -z-0 opacity-10 pointer-events-none"><div class="absolute top-10 left-1/4 w-96 h-96 bg-primary filter blur-3xl rounded-full animate-pulse"></div><div class="absolute bottom-10 right-1/4 w-96 h-96 bg-accent filter blur-3xl rounded-full animate-pulse" style="animation-delay: 2.5s"></div></div></section></div> <div class="w-full -mt-10"><section class="relative py-20 overflow-hidden border-b border-base-200/30 text-center flex flex-col items-center"><div class="relative z-10 max-w-5xl mx-auto px-6 flex flex-col items-center"><div class="flex justify-center mb-10"><img src="logo.svg" alt="SigPro Logo" class="w-48 h-48 md:w-64 md:h-64 object-contain drop-shadow-2xl"></div><h1 class="text-7xl md:text-9xl font-black tracking-tighter mb-4 bg-clip-text text-transparent bg-linear-to-r from-primary via-secondary to-accent !text-center w-full">SigPro</h1><div class="text-2xl md:text-3xl font-bold text-base-content/90 mb-4 !text-center w-full">Atomic Unified Reactive Engine</div><div class="text-xl text-base-content/60 max-w-3xl mx-auto mb-10 leading-relaxed italic text-balance font-light !text-center w-full">"The efficiency of direct DOM manipulation with the elegance of functional reactivity."</div><div class="flex flex-wrap justify-center gap-4 w-full"><a href="#/install" class="btn btn-primary btn-lg shadow-xl shadow-primary/20 group px-10 border-none">Get Started <span class="group-hover:translate-x-1 transition-transform inline-block">→</span></a><button onclick="window.open('https://git.natxocc.com/natxocc/sigpro')" class="btn btn-outline btn-lg border-base-300 hover:bg-base-300 hover:text-base-content">Gitea</button></div></div><div class="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full -z-0 opacity-10 pointer-events-none"><div class="absolute top-10 left-1/4 w-96 h-96 bg-primary filter blur-3xl rounded-full animate-pulse"></div><div class="absolute bottom-10 right-1/4 w-96 h-96 bg-accent filter blur-3xl rounded-full animate-pulse" style="animation-delay: 2.5s"></div></div></section></div>
<section class="max-w-6xl mx-auto px-6 py-16"><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 items-stretch"><div class="card bg-base-200/30 border border-base-300 hover:border-primary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-primary italic">FUNCTIONAL</h3><p class="text-sm opacity-70">No strings. No templates. Pure JS function calls for instant DOM mounting.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-secondary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-secondary italic">ATOMIC</h3><p class="text-sm opacity-70">Fine-grained signals update exactly what changes. No V-DOM diffing overhead.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-accent/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-accent italic">ULTRA-THIN</h3><p class="text-sm opacity-70">Sub-2KB runtime. Infinitely smaller bundle than React, Vue or even Svelte.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-base-content/20 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black italic text-base-content">COMPILER-FREE</h3><p class="text-sm opacity-70">Standard Vanilla JS. What you write is what the browser executes. Period.</p></div></div></div></section></div> <section class="max-w-6xl mx-auto px-6 py-16"><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 items-stretch"><div class="card bg-base-200/30 border border-base-300 hover:border-primary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-primary italic">FUNCTIONAL</h3><p class="text-sm opacity-70">No strings. No templates. Pure JS function calls for instant DOM mounting.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-secondary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-secondary italic">ATOMIC</h3><p class="text-sm opacity-70">Finegrained signals update exactly what changes. No VDOM diffing overhead.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-accent/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-accent italic">ULTRATHIN</h3><p class="text-sm opacity-70">less than 3KB runtime. Infinitely smaller bundle than React, Vue or even Svelte.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-base-content/20 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black italic text-base-content">COMPILERFREE</h3><p class="text-sm opacity-70">Standard Vanilla JS. What you write is what the browser executes. Period.</p></div></div></div></section>
## Functional DOM Construction <div class="max-w-6xl mx-auto px-6 py-8"><h2 class="text-4xl font-black mb-6">Functional DOM Construction</h2><p class="text-lg opacity-80 mb-6">SigPro replaces slow "Template Parsing" with <strong>HighEfficiency Function Calls</strong>. While other frameworks force the browser to parse strings of HTML or execute complex JSX transformations, SigPro uses a direct functional approach.</p>
SigPro replaces the slow "Template Parsing" with **High-Efficiency Function Calls**. <table class="table w-full mb-12">
<thead><tr><th>Feature</th><th>Standard HTML / JSX</th><th>SigPro Functional</th></tr></thead>
<tbody>
<tr><td>Syntax</td><td><code>&lt;div&gt;Hello&lt;/div&gt;</code></td><td><code>div("Hello")</code> (or <code>h('div', "Hello")</code>)</td></tr>
<tr><td>Processing</td><td>Parse → Diff → Patch</td><td>Direct API Call</td></tr>
<tr><td>Overhead</td><td>High (VDOM / Parser)</td><td><strong>Zero</strong></td></tr>
<tr><td>Reactivity</td><td>Componentwide</td><td><strong>Atomic (Nodelevel)</strong></td></tr>
</tbody>
</table>
While other frameworks force the browser to parse strings of HTML or execute complex JSX transformations, SigPro uses a direct functional approach: <h3 class="text-2xl font-bold mt-8 mb-4">Less Code, More Power</h3>
<p class="mb-4">By sharing a minuscule runtime, your final application bundle is <strong>infinitely smaller</strong>.</p>
<ul class="list-disc pl-6 space-y-2 mb-8">
<li><strong>React/Vue:</strong> You ship a heavy orchestrator (~3090KB) just to say "Hello World".</li>
<li><strong>Solid/Svelte:</strong> You still depend on a compilation step that generates extra boilerplate.</li>
<li><strong>SigPro:</strong> You ship <strong>Pure Vanilla JS</strong>. The runtime is so small that it often costs less than a single highres icon.</li>
</ul>
| Feature | Standard HTML / JSX | SigPro Functional | <h3 class="text-2xl font-bold mt-10 mb-4">Precision Engineering</h3>
| :--- | :--- | :--- | <h4 class="text-xl font-semibold mt-6 mb-2">1. Functional Efficiency</h4>
| **Syntax** | `<div>Hello</div>` | `Div("Hello")` | <p><code>div()</code>, <code>button()</code>, <code>span()</code>… These aren't just wrappers; they are preoptimized constructors. When you call <code>div({ class: 'btn' }, "Click")</code>, SigPro creates the element and attaches its reactive listeners in a single, surgical operation.</p>
| **Processing** | Parse → Diff → Patch | Direct API Call |
| **Overhead** | High (V-DOM / Parser) | **Zero** |
| **Reactivity** | Component-wide | **Atomic (Node-level)** |
### Less Code, More Power <h4 class="text-xl font-semibold mt-6 mb-2">2. The "NoBundle" Bundle</h4>
By sharing a miniscule runtime, your final application bundle is **infinitely smaller**. <p>Because SigPro is so small, it is the only engine where <strong>the more code you write, the more the efficiency gap grows</strong>. While others grow linearly with components and framework overhead, SigPro stays flat, leveraging the native power of modern browser engines.</p>
* **React/Vue:** You ship a heavy orchestrator (~30-90KB) just to say "Hello World". <h4 class="text-xl font-semibold mt-6 mb-2">3. Shared Runtime</h4>
* **Solid/Svelte:** You still depend on a compilation step that generates extra boilerplate. <p>All components share the same atomic engine. One signal can update a single character in a paragraph across 100 components without ever reevaluating the component functions themselves.</p>
* **SigPro:** You ship **Pure Vanilla JS**. The runtime is so small that it often costs less than a single high-res icon. </div>
--- <div class="bg-base-200/50 rounded-3xl p-10 my-16 border border-base-300 shadow-inner max-w-6xl mx-auto"><div class="grid grid-cols-1 lg:grid-cols-3 gap-8 items-center"><div class="lg:col-span-2"><h2 class="text-4xl font-black mb-4 mt-0 tracking-tight italic text-primary">Kill the Bloat.</h2><p class="text-xl opacity-80 leading-relaxed">Stop shipping 100KB of "Framework" for 2KB of business logic. SigPro gives you the tools to build ultrafast, modern apps with <strong>True Vanilla Performance</strong>.</p></div></div></div>
## Precision Engineering
### 1. Functional Efficiency
`Div()`, `Button()`, `Span()`... These aren't just wrappers; they are pre-optimized constructors. When you call `Div({ class: 'btn' }, "Click")`, SigPro creates the element and attaches its reactive listeners in a single, surgical operation.
### 2. The "No-Bundle" Bundle
Because SigPro is so small, it is the only engine where **the more code you write, the more the efficiency gap grows**. While others grow linearly with components and framework overhead, SigPro stays flat, leveraging the native power of modern browser engines.
### 3. Shared Runtime
All components share the same atomic engine. One signal can update a single character in a paragraph across 100 components without ever re-evaluating the component functions themselves.
---
<div class="bg-base-200/50 rounded-3xl p-10 my-16 border border-base-300 shadow-inner"><div class="grid grid-cols-1 lg:grid-cols-3 gap-8 items-center"><div class="lg:col-span-2"><h2 class="text-4xl font-black mb-4 mt-0 tracking-tight italic text-primary">Kill the Bloat.</h2><p class="text-xl opacity-80 leading-relaxed">Stop shipping 100KB of "Framework" for 2KB of business logic. SigPro gives you the tools to build ultra-fast, modern apps with <strong>True Vanilla Performance</strong>.</p></div></div></div>
<div class="text-center py-10 opacity-30 font-mono text-xs tracking-widest uppercase">Precision Reactive Engine — NatxoCC</div> <div class="text-center py-10 opacity-30 font-mono text-xs tracking-widest uppercase">Precision Reactive Engine — NatxoCC</div>

View File

@@ -2,21 +2,23 @@
* **Introduction** * **Introduction**
* [Installation](install.md) * [Installation](install.md)
* [Examples](examples.md) * [Router](router.md)
* [Vite Plugin](vite/plugin.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)
* [If](api/if.md) * [when](api/when.md)
* [For](api/for.md) * [each](api/each.md)
* [Router](api/router.md) * [mount](api/mount.md)
* [Mount](api/mount.md) * [h](api/h.md)
* [Tag](api/html.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 Components** * **UI**
* [Quick Start](ui/quick.md) * [WIP]

173
docs/api/each.md Normal file
View File

@@ -0,0 +1,173 @@
# Reactive Lists: `each( )`
The `each` function is a highperformance keyed list renderer. It maps a reactive array to DOM nodes and surgically updates only the items that changed (added, removed, or reordered). Unlike a simple `.map()`, `each` reuses DOM nodes and preserves internal state.
## Function Signature
```typescript
each(
source: Signal<any[]> | (() => any[]) | any[],
itemFn: (item: any, index: number) => Node | (() => Node),
keyField?: string
): HTMLElement
```
| Parameter | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **`source`** | `Signal`, `() => any[]`, or `any[]` | Yes | The reactive array to iterate over. |
| **`itemFn`** | `(item, index) => Node` | Yes | Returns a DOM node (or a function that returns a node) for each item. |
| **`keyField`** | `string` | No | Name of the property to use as unique key (e.g., `"id"`). Default: `item?.id ?? index`. |
**Returns:** A `div` with `style="display: contents"` that contains the live list.
---
## Usage Patterns
### 1. Basic Keyed List (Recommended)
Pass the name of the property that contains the unique identifier (e.g., `"id"`). This allows SigPro to reuse DOM nodes when the list is reordered or filtered.
```javascript
const users = $([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
]);
ul({ class: "list" }, [
each(users,
(user) => li({ class: "p-2" }, user.name),
"id" // ← use property "id" as stable key
)
]);
```
### 2. Automatic Key (Simple Lists)
If you omit the `keyField`, `each` defaults to `item?.id ?? index`. For primitive arrays or objects without an `id`, the index is used.
```javascript
const tags = $(["Tech", "JS", "Web"]);
div({ class: "flex gap-1" }, [
each(tags, (tag) => span({ class: "badge" }, tag))
// key defaults to index (0,1,2) fine for static order
]);
```
### 3. Using a Different Property Name
If your unique identifier is not called `id` (e.g., `_id`, `userId`, `slug`), just pass the property name as the third parameter:
```javascript
const products = $([
{ _id: 101, name: "Laptop" },
{ _id: 102, name: "Mouse" }
]);
each(products, (item) => li(item.name), "_id");
```
### 4. Dynamic Content Using Functions
If your `itemFn` returns a **function**, that function is reexecuted every time the items data changes (but the node is reused).
```javascript
const todos = $([
{ id: 1, text: "Learn SigPro", done: false }
]);
each(todos,
(todo) => div([
input({ type: "checkbox", checked: () => todo.done, onInput: e => todo.done = e.target.checked }),
span(() => todo.done ? s(todo.text) : todo.text)
]),
"id"
);
```
### 5. Source as a Plain Array or Function
`source` can be a plain array (nonreactive) or a function that returns an array it will still react to changes if signals are read inside the function.
```javascript
const filter = $("all");
const filteredTodos = () => {
const all = todos();
if (filter() === "active") return all.filter(t => !t.done);
return all;
};
each(filteredTodos, (todo) => li(todo.text), "id");
```
---
## How It Works (Reconciliation)
When the `source` changes, `each`:
1. **Compares keys** between the old and new items using the specified `keyField` (or `item.id` / index).
2. **Reuses existing DOM nodes** for keys that stay the same.
3. **Moves nodes** if order changed (no recreation).
4. **Creates new nodes** for new keys.
5. **Destroys nodes** for removed keys cleans up all effects, event listeners, and child components.
> This is much more efficient than destroying and rebuilding the whole list on every update.
---
## Performance Tips
- **Stable keys** Use a property that never changes (like a database primary key). Avoid `Math.random()` or array `index` for lists that can be reordered.
- **State preservation** If a list item contains an input or local state, using a stable key ensures that state is preserved even when the list is filtered or sorted.
- **Lazy item functions** If an item is expensive to render, wrap it in a function: `() => ExpensiveComponent(item)`. The component is only created when the item actually appears in the DOM.
---
## Summary Comparison
| Feature | Standard `Array.map` | SigPro `each` |
| :--- | :--- | :--- |
| **Rerenders on change** | Recreates entire list | Only adds/removes/moves changed items |
| **DOM nodes** | New nodes every time | **Reused via keys** |
| **Memory cleanup** | Manual (or leak) | **Automatic** (destroy on removal) |
| **Internal state per item** | Lost on every update | **Preserved** (if key stable) |
| **Reactivity** | None (manual rerender) | Builtin, finegrained |
---
## Complete Example
```javascript
const items = $([
{ id: 1, name: "Apple", price: 1.2 },
{ id: 2, name: "Banana", price: 0.8 }
]);
const addItem = () => {
const newId = Date.now();
items([...items(), { id: newId, name: `Item ${newId}`, price: 1.0 }]);
};
const removeItem = (id) => {
items(items().filter(i => i.id !== id));
};
const App = () =>
div([
button({ onClick: addItem }, "Add item"),
ul(
each(items,
(item) => li([
span(`${item.name} $${item.price}`),
button({ onClick: () => removeItem(item.id) }, "X")
]),
"id"
)
)
]);
mount(App, '#app');
```

View File

@@ -1,83 +0,0 @@
# Reactive Lists: `For( )`
The `For` function is a high-performance list renderer. It maps an array (or a Signal containing an array) to DOM nodes. Unlike a simple `.map()`, `For` is **keyed**, meaning it only updates, moves, or deletes the specific items that changed.
## Function Signature
```typescript
For(
source: Signal<any[]> | Function | any[],
render: (item: any, index: number) => HTMLElement,
keyFn?: (item: any, index: number) => string | number
): HTMLElement
```
| Parameter | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **`source`** | `Signal` | Yes | The reactive array to iterate over. |
| **`render`** | `Function` | Yes | A function that returns a component or Node for each item. |
| **`keyFn`** | `Function` | **No** | A function to extract a **unique ID**. If omitted, it defaults to the `index`. |
**Returns:** A `div` element with `display: contents` containing the live list.
---
## Usage Patterns
### 1. Basic Keyed List (Recommended)
Always use a unique property (like an `id`) as a key to ensure SigPro doesn't recreate nodes unnecessarily when reordering or filtering.
```javascript
const users = $([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
]);
Ul({ class: "list" }, [
For(users,
(user) => Li({ class: "p-2" }, user.name),
(user) => user.id // Stable and unique key
)
]);
```
### 2. Simplified Usage (Automatic Key)
If you omit the third parameter, `For` will automatically use the array index as the key. This is ideal for simple lists that don't change order frequently.
```javascript
const tags = $(["Tech", "JS", "Web"]);
// No need to pass keyFn if the index is sufficient
Div({ class: "flex gap-1" }, [
For(tags, (tag) => Badge(tag))
]);
```
---
## How it Works (The Reconciliation)
When the `source` signal changes, `For` performs the following steps:
1. **Key Diffing**: It compares the new keys with the previous ones stored in an internal `Map`.
2. **Node Reuse**: If a key already exists, the DOM node is **reused** and moved to its new position. No new elements are created.
3. **Physical Cleanup**: If a key disappears from the list, SigPro calls `.destroy()` to stop reactivity and physically removes the node from the DOM to prevent memory leaks.
---
## Performance Tips
* **Stable Keys**: Never use `Math.random()` as a key. This will force SigPro to destroy and recreate the entire list on every update, killing performance.
* **State Preservation**: If your list items have internal state (like an input with text), `For` ensures that state is preserved even if the list is reordered, as long as the key (`id`) remains the same.
---
## Summary Comparison
| Feature | Standard `Array.map` | SigPro `For` |
| :--- | :--- | :--- |
| **Re-render** | Re-renders everything | Only updates changes |
| **DOM Nodes** | Re-created every time | **Reused via Keys** |
| **Memory** | Potential leaks | **Automatic Cleanup** |
| **State** | Lost on re-render | **Preserved per item** |
| **Ease of Use** | Manual logic required | **Optional (fallback to index)** |

View File

@@ -1,64 +1,69 @@
# Global State Management: Atomic & Modular # Global State Management: Atomic & Modular
SigPro leverages the native power and efficiency of **Signals** to create robust global stores with **0% complexity**. While other frameworks force you into heavy libraries and rigid boilerplate (Redux, Pinia, or Svelte Stores), SigPro treats "The Store" as a simple architectural choice: **defining a Signal outside of a component.** SigPro leverages the native power and efficiency of **signals** to create robust global stores with **zero complexity**. While other frameworks force you into heavy libraries and rigid boilerplate (Redux, Pinia, or Svelte stores), SigPro treats “the store as a simple architectural choice: **defining a signal outside of a component.**
> **Availability:** `$` (and other core functions) are exported from the SigPro module. In **ESM** you must import them (`import { $ } from 'sigpro'`). In the **IIFE** classic script, `$` is automatically available on `window`. The examples below assume `$` is already in scope (via import or global).
## Modular Organization (Zero Constraints) ## Modular Organization (Zero Constraints)
You are not restricted to a single `store.js`. You can organize your state by **feature**, **domain**, or **page**. Since a SigPro store is just a standard JavaScript module exporting Signals, you can name your files whatever you like (`auth.js`, `cart.js`, `settings.js`) to keep your logic clean. You are not restricted to a single `store.js`. You can organize your state by **feature**, **domain**, or **page**. Since a SigPro store is just a standard JavaScript module exporting signals, you can name your files whatever you like (`auth.js`, `cart.js`, `settings.js`) to keep your logic clean.
### 1. File-Based Stores (`<any-name>.js`) ### 1. FileBased Stores (`<any-name>.js`)
Creating a dedicated file allows you to export only what you need. This modularity ensures **Tree Shaking** works perfectly—you never load state that isn't imported.
Creating a dedicated file allows you to export only what you need. This modularity ensures **tree shaking** works perfectly you never load state that isnt imported.
```javascript ```javascript
// auth.js // auth.js
import SigPro from "sigpro"; import { $ } from 'sigpro';
// A simple global signal // A simple global signal
export const user = $({ name: "Guest", loggedIn: false }); export const user = $({ name: "Guest", loggedIn: false });
// A persistent global signal (auto-syncs with localStorage via native key) // A persistent global signal (autosyncs with localStorage)
export const theme = $("light", "app-theme-pref"); export const theme = $("light", "app-theme-pref");
// A computed global signal that reacts to the 'user' signal // A computed global signal that reacts to the 'user' signal
export const welcomeMessage = $(() => `Welcome back, ${user().name}!`); export const welcomeMessage = $(() => `Welcome back, ${user().name}!`);
``` ```
### 2. Cross-Component Consumption ### 2. CrossComponent Consumption
Once exported, these signals act as a **Single Source of Truth**. SigPro ensures that if a signal changes in one file, every component importing it across the entire app updates **atomically** without a full re-render.
Once exported, these signals act as a **single source of truth**. SigPro ensures that if a signal changes in one file, every component importing it across the entire app updates **atomically** without a full rerender.
```javascript ```javascript
// Profile.js // Profile.js
import { user } from "./auth.js"; import { user } from "./auth.js";
const Profile = () => Div([ const Profile = () => div([
H2(user().name), h2(() => user().name),
Button({ onclick: () => user({ name: "John Doe", loggedIn: true }) }, "Log In") button({ onclick: () => user({ name: "John Doe", loggedIn: true }) }, "Log In")
]); ]);
// Navbar.js // Navbar.js
import { welcomeMessage, theme } from "./auth.js"; import { welcomeMessage, theme } from "./auth.js";
const Navbar = () => Nav({ class: theme }, [ const Navbar = () => nav({ class: () => theme() }, [
Span(welcomeMessage) span(() => welcomeMessage())
]); ]);
``` ```
--- ---
## Why SigPro Stores are Superior ## Why SigPro Stores Are Superior
| Feature | SigPro | Redux / Pinia / Svelte | | Feature | SigPro | Redux / Pinia / Svelte |
| :--- | :--- | :--- | | :-------------------- | :---------------------------- | :------------------------------ |
| **Boilerplate** | **0%** (Just a variable) | High (Actions, Reducers, Wrappers) | | **Boilerplate** | **0%** (just a variable) | High (actions, reducers, stores)|
| **Organization** | **Unlimited** (Any filename) | Often strictly "Store" or "Actions" | | **Organization** | **Unlimited** (any filename) | Often strictly “store” files |
| **Persistence** | **Native** (Just add a key) | Requires Middleware / Plugins | | **Persistence** | **Native** (just add a key) | Requires middleware / plugins |
| **Learning Curve** | **Instant** | Steep / Complex | | **Learning Curve** | **Instant** | Steep / complex |
| **Bundle Size** | **0KB** (Part of the core) | 10KB - 30KB+ | | **Bundle Size** | **0KB** (part of core) | 10KB 30KB+ |
--- ---
## The "Persistence" Advantage ## The Persistence Advantage
The magic of SigPros `$(value, "key")` is that it works identically for local and global states. By simply adding a second argument, your Modular Store survives browser refreshes automatically. No manual `localStorage.getItem` or `JSON.parse` logic is ever required.
The magic of SigPros `$(value, "key")` is that it works identically for local and global states. By simply adding a second argument, your modular store survives browser refreshes automatically. No manual `localStorage.getItem` or `JSON.parse` logic is ever required.
```javascript ```javascript
// This single line creates a global, reactive, // This single line creates a global, reactive,
@@ -69,7 +74,69 @@ export const cart = $([], "session-cart");
--- ---
## Summary of Scopes ## Summary of Scopes
* **Local Scope:** Signal defined **inside** a component. Unique to every instance created.
* **Module Scope:** Signal defined **outside** a component (same file). Shared by all instances within that specific file. | Scope | Definition | Behaviour |
* **Global Scope:** Signal defined in a **separate file**. Shared across the entire application by any importing module. | :-------------- | :-------------------------------------------------------------- | :-------------------------------------------- |
* **Persistent Scope:** Any Signal defined with a **key**. Shared globally and remembered after a page reload. | **Local** | Signal defined **inside** a component | Unique to every component instance |
| **Module** | Signal defined **outside** a component (same file) | Shared by all instances within that file |
| **Global** | Signal defined in a **separate file** and imported | Shared across the entire application |
| **Persistent** | Any Signal defined with a **key** (e.g., `$([], "cart")`) | Shared globally and persisted in `localStorage` |
---
## Complete Example Todo Store
```javascript
// store/todos.js
import { $ } from 'sigpro';
export const todos = $([], "todos");
export const filter = $("all");
export const addTodo = (text) => {
todos([...todos(), { id: Date.now(), text, done: false }]);
};
export const toggleTodo = (id) => {
todos(todos().map(t => t.id === id ? { ...t, done: !t.done } : t));
};
export const filteredTodos = $(() => {
const all = todos();
if (filter() === "active") return all.filter(t => !t.done);
if (filter() === "completed") return all.filter(t => t.done);
return all;
});
```
```javascript
// components/TodoApp.js
import 'sigpro';
import { todos, filter, addTodo, toggleTodo, filteredTodos } from "../store/todos.js";
const TodoApp = () =>
div({ class: "todo-app" }, [
input({ placeholder: "Add todo...", onKeyDown: (e) => {
if (e.key === "Enter" && e.target.value) {
addTodo(e.target.value);
e.target.value = "";
}
}}),
div({ class: "filters" }, [
button({ onClick: () => filter("all") }, "All"),
button({ onClick: () => filter("active") }, "Active"),
button({ onClick: () => filter("completed") }, "Completed")
]),
ul(
each(filteredTodos,
(todo) => li([
input({ type: "checkbox", checked: () => todo.done, onInput: () => toggleTodo(todo.id) }),
span(() => todo.done ? s(todo.text) : todo.text)
]),
(todo) => todo.id
)
)
]);
mount(TodoApp, "#app");
```

161
docs/api/h.md Normal file
View File

@@ -0,0 +1,161 @@
# Hyperscript Function: `h( )`
The `h` function is the **core DOM builder** of SigPro. It creates DOM elements from a tag name, props, and children. While the global tag helpers (`div()`, `button()`, etc.) are built on top of `h`, you may need `h` directly for dynamic tag names or when you prefer an explicit function style.
> **Availability:** `h` and all tag helpers (`div`, `button`, etc.) are exported from the SigPro module. In **ESM** you must import them (`import { h, div, button } from 'sigpro'`). In the **IIFE** classic script, `h` and all tag helpers are automatically available on `window`. The examples below assume the functions are already in scope.
## Function Signature
```typescript
h(
tag: string | Function,
props?: object | Node | any[],
children?: any
): Node
```
| Parameter | Type | Description |
| :--- | :--- | :--- |
| **`tag`** | `string` or `Function` | HTML tag name (e.g., `"div"`) or a component function. |
| **`props`** | `object` | Optional. Attributes, event handlers, refs, etc. If not an object, it becomes `children`. |
| **`children`** | `any` | Optional. Text, nodes, arrays, or reactive functions. |
**Returns:** A DOM node (or an array of nodes when the tag is a component that returns an array).
---
## Usage Patterns
### 1. Basic Element Creation
```javascript
// Simple div with text
h('div', {}, 'Hello world');
// With attributes
h('button', { class: 'btn', onclick: () => alert('clicked') }, 'Click me');
```
### 2. Nested Children
Children can be a single node, an array, or a function.
```javascript
h('div', { class: 'container' }, [
h('h1', {}, 'Title'),
h('p', {}, 'Paragraph text')
]);
```
### 3. Reactive Children
Pass a **function** as a child it will be reevaluated whenever any signal inside changes, and the DOM will be patched surgically.
```javascript
const count = $(0);
h('div', {}, [
h('p', {}, () => `Count: ${count()}`),
h('button', { onclick: () => count(count() + 1) }, '+1')
]);
```
### 4. Reactive Attributes
Pass a function as an attribute value to keep it dynamic.
```javascript
const theme = $('dark');
h('div', { class: () => `box ${theme()}` }, 'Themed box');
```
### 5. TwoWay Binding
Assign a signal directly to `value` or `checked` on form elements SigPro automatically syncs both ways.
```javascript
const name = $('');
h('input', {
type: 'text',
value: name, // two-way binding
placeholder: 'Your name'
});
h('p', {}, () => `Hello, ${name()}`);
```
### 6. Component Functions as `tag`
You can pass a component function directly to `h`. SigPro will execute it with the provided props and an `emit` helper for custom events.
```javascript
const Button = (props, { children }) =>
h('button', { class: 'btn', onclick: props.onClick }, children);
const App = () =>
h('div', {}, [
h(Button, { onClick: () => alert('clicked') }, 'Custom button')
]);
```
### 7. SVG Elements
Use `h` with SVG tag names SigPro automatically applies the correct namespace.
```javascript
h('svg', { width: 100, height: 100 }, [
h('circle', { cx: 50, cy: 50, r: 40, fill: 'red' })
]);
```
---
## Special Props
| Prop | Behaviour |
| :--- | :--- |
| **`ref`** | `ref: (el) => ...` or `ref: { current: null }` provides direct access to the DOM node after creation. |
| **`onEvent`** | Any prop starting with `on` (e.g., `onClick`, `onInput`) is treated as an event listener. Automatically removed on cleanup. |
| **`value` / `checked`** | When a signal is passed, creates twoway binding for inputs, textareas, and selects. |
| **`class`** | You can use `class` (not `className`). Accepts a string or a reactive function. |
---
## `h` vs Tag Helpers
| Feature | `h('div', ...)` | `div(...)` (tag helper) |
| :--- | :--- | :--- |
| **Dynamic tag names** | ✅ `h(tagName, ...)` | ❌ Must know tag name at write time |
| **Explicit style** | More verbose | Cleaner, DSLlike |
| **Availability** | Import or global | Import or global (same) |
| **Performance** | Identical | Identical (helpers call `h` internally) |
> **Recommendation:** Use tag helpers (`div()`, `button()`, etc.) for most cases they are shorter and more readable. Use `h` directly only when the tag name is dynamic (e.g., `h(props.tag, ...)`).
---
## Complete Example
```javascript
import 'sigpro';
const dynamicTag = $('h1');
const App = () =>
h('div', { class: 'demo' }, [
h(dynamicTag(), {}, () => `Current tag: ${dynamicTag()}`),
h('button', { onclick: () => dynamicTag(dynamicTag() === 'h1' ? 'h2' : 'h1') }, 'Toggle heading size')
]);
mount(App, '#app');
```
---
## Summary
- `h` is the lowlevel DOM builder used internally by all tag helpers.
- It supports reactive attributes, reactive children, twoway binding, event listeners, and SVG.
- Use `h` directly when you need a **dynamic tag name**; otherwise, prefer the convenient tag helpers (import them or inject globally).
- Components written with `h` are fully reactive and automatically cleaned up.

View File

@@ -1,104 +0,0 @@
# The DOM Factory: `Tag( )`
`Tag` is the internal engine that creates, attributes, and attaches reactivity to DOM elements. It uses `Watch` to maintain a live, high-performance link between your Signals and the Document Object Model.
## Function Signature
```typescript
Tag(tagName: string, props?: Object, children?: any[] | any): HTMLElement
```
| Parameter | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **`tagName`** | `string` | Yes | Valid HTML tag name (e.g., `"div"`, `"button"`). |
| **`props`** | `Object` | No | Attributes, Events, Two-way bindings, and **Refs**. |
| **`children`** | `any` | No | Nested elements, text strings, or reactive functions. |
---
## Key Features
### 1. Manual DOM Access: `ref`
The `ref` property allows you to capture the underlying `HTMLElement` as soon as it is created. This is essential for integrating 3rd-party libraries (like AG Grid or Chart.js) or managing focus and measurements.
* **Callback Ref**: A function that receives the `HTMLElement` immediately.
* **Object Ref**: An object with a `.current` property assigned to the `HTMLElement`.
```javascript
// Auto-focus on mount
Input({ ref: (el) => el.focus() });
// Capturing a node for an external library
const gridDiv = { current: null };
Div({ ref: gridDiv, class: "ag-theme-quartz" });
```
### 2. Attribute Handling
SigPro intelligently decides how to apply each property:
* **Standard Props**: Applied via `setAttribute` or direct property assignment.
* **Class Names**: Supports `class` or `className` interchangeably.
* **Boolean Props**: Automatic handling for `checked`, `disabled`, `hidden`, etc.
* **Note**: The `ref` property is intercepted and **never** rendered as an attribute in the HTML.
### 3. Event Listeners
Events are defined by the `on` prefix. SigPro automatically registers the listener and ensures it is cleaned up when the element is destroyed.
```javascript
Button({
onclick: (e) => console.log("Clicked!", e),
}, "Click Me");
```
### 4. Reactive Attributes (One-Way)
If an attribute value is a **function** (like a Signal), `Tag` creates an internal **`Watch`** to keep the DOM in sync with the state.
```javascript
Div({
// Updates the class whenever 'theme()' changes
class: () => theme() === "dark" ? "bg-black" : "bg-white"
});
```
### 5. Smart Two-Way Binding (Automatic)
SigPro automatically enables **bidirectional synchronization** when it detects a **Signal** assigned to a form-capable attribute (`value` or `checked`) on an input element (`input`, `textarea`, `select`).
```javascript
// Syncs input value <-> signal automatically
Input({
type: "text",
value: username // No special symbols needed!
})
```
> **Note:** To use a Signal as **read-only** in an input, wrap it in an anonymous function: `value: () => username()`.
### 6. Reactive Children
Children can be static or dynamic. When a child is a function, SigPro creates a reactive boundary using `Watch` for that specific part of the DOM.
```javascript
Div({}, [
H1("Static Title"),
// Only this text node re-renders when 'count' changes
() => `Current count: ${count()}`
]);
```
---
## Memory Management (Internal)
Every element created with `Tag` is "self-aware" regarding its reactive dependencies.
* **`._cleanups`**: A hidden `Set` attached to the element that stores all `stop()` functions from its internal `Watch` calls and event listeners.
* **Lifecycle**: When an element is removed by a Controller (`If`, `For`, or `Router`), SigPro performs a recursive **"sweep"** to execute these cleanups, ensuring **zero memory leaks**.
---
## Tag Constructors (The Shortcuts)
Instead of writing `Tag("div", ...)` every time, SigPro provides PascalCase global functions for all standard HTML tags. These are direct mappings to the `Tag` factory.
```javascript
// This:
Div({ class: "wrapper" }, [ Span("Hello") ])
// Is exactly equivalent to:
Tag("div", { class: "wrapper" }, [ Tag("span", {}, "Hello") ])
```

View File

@@ -1,180 +0,0 @@
# Reactive Branching: `If( )`
The `If` function is a reactive control flow operator. It manages the conditional rendering of components with optional smooth transitions, ensuring that only the active branch exists in the DOM and in memory.
## Function Signature
```typescript
If(
condition: Signal<boolean> | Function,
thenVal: Component | Node,
otherwiseVal?: Component | Node,
transition?: Transition
): HTMLElement
```
| Parameter | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **`condition`** | `Signal` | Yes | A reactive source that determines which branch to render. |
| **`thenVal`** | `any` | Yes | The content to show when the condition is **truthy**. |
| **`otherwiseVal`** | `any` | No | The content to show when the condition is **falsy** (defaults to null). |
| **`transition`** | `Transition` | No | Optional animation hooks for enter/exit transitions. |
**Returns:** A `div` element with `display: contents` that acts as a reactive portal for the branches.
---
## Transition Interface
```typescript
interface Transition {
/** Called when branch enters. Use for fade-in, slide-in, etc. */
in: (el: HTMLElement) => void;
/** Called when branch leaves. Call `done()` when animation completes. */
out: (el: HTMLElement, done: () => void) => void;
}
```
### Example: Fade Transition
```javascript
const fade = {
in: (el) => {
el.style.opacity = "0";
el.style.transition = "opacity 0.3s";
requestAnimationFrame(() => {
el.style.opacity = "1";
});
},
out: (el, done) => {
el.style.transition = "opacity 0.3s";
el.style.opacity = "0";
setTimeout(done, 300);
}
};
If(show, Modal, null, fade);
```
---
## Usage Patterns
### 1. Simple Toggle
```javascript
const isVisible = $(false);
Div([
Button({ onclick: () => isVisible(!isVisible()) }, "Toggle Message"),
If(isVisible,
P("Now you see me!"),
P("Now you don't...")
)
]);
```
### 2. With Smooth Animation
```javascript
const showModal = $(false);
Div([
Button({ onclick: () => showModal(true) }, "Open Modal"),
If(showModal,
() => Modal({ onClose: () => showModal(false) }),
null,
fade // ← Smooth enter/exit animation
)
]);
```
### 3. Lazy Component Loading
Unlike CSS `display: none`, `If` is **lazy**. The inactive branch is never created, saving memory.
```javascript
If(() => user.isLogged(),
() => Dashboard(), // Only executed if logged in
() => LoginGate() // Only executed if guest
)
```
### 4. Complex Conditions
```javascript
If(() => count() > 10 && status() === 'ready',
Span("Threshold reached!")
)
```
### 5. If.not Helper
```javascript
If.not(loading,
() => Content(), // Shows when loading is FALSE
() => Spinner() // Shows when loading is TRUE
)
```
---
## Automatic Cleanup
One of the core strengths of `If` is its integrated **Cleanup** logic. SigPro ensures that when a branch is swapped out, it is completely purged.
1. **Stop Watchers**: All `Watch` calls inside the inactive branch are permanently stopped.
2. **Unbind Events**: Event listeners attached via `Tag` are removed.
3. **Recursive Sweep**: SigPro performs a deep "sweep" of the removed branch.
4. **Transition Respect**: When using transitions, destruction only happens AFTER the `out` animation completes.
---
## Best Practices
- **Function Wrappers**: For heavy components, use `() => MyComponent()` to prevent initialization until needed.
- **Reusable Transitions**: Define common transitions (fade, slide, scale) in a shared module.
- **Cleanup**: No manual cleanup needed. SigPro handles everything automatically.
---
## Technical Comparison
| Feature | Standard CSS `hidden` | SigPro `If` |
| :--- | :--- | :--- |
| **DOM Presence** | Always present | Only if active |
| **Reactivity** | Still processing | **Paused/Destroyed** |
| **Memory usage** | Higher | **Optimized** |
| **Cleanup** | Manual | **Automatic** |
| **Smooth Transitions** | Manual | **Built-in hook** |
| **Animation Timing** | You manage | **Respected by core** |
---
## Complete Transition Examples
### Fade
```javascript
const fade = {
in: (el) => { el.style.opacity = "0"; requestAnimationFrame(() => { el.style.transition = "opacity 0.3s"; el.style.opacity = "1"; }); },
out: (el, done) => { el.style.transition = "opacity 0.3s"; el.style.opacity = "0"; setTimeout(done, 300); }
};
```
### Slide
```javascript
const slide = {
in: (el) => { el.style.transform = "translateX(-100%)"; requestAnimationFrame(() => { el.style.transition = "transform 0.3s"; el.style.transform = "translateX(0)"; }); },
out: (el, done) => { el.style.transition = "transform 0.3s"; el.style.transform = "translateX(-100%)"; setTimeout(done, 300); }
};
```
### Scale
```javascript
const scale = {
in: (el) => { el.style.transform = "scale(0)"; requestAnimationFrame(() => { el.style.transition = "transform 0.2s"; el.style.transform = "scale(1)"; }); },
out: (el, done) => { el.style.transition = "transform 0.2s"; el.style.transform = "scale(0)"; setTimeout(done, 200); }
};
```

View File

@@ -1,23 +1,34 @@
# JSX with SigPro # Hyperscript & Tag Helpers & JSX Style
SigPro works seamlessly with JSX. SigPro provides two complementary ways to create DOM elements:
## Configuration 1. **The `h` function** the lowlevel DOM builder.
2. **Global Tag Helpers** (e.g., `div()`, `button()`, `span()`) a convenient DSL built on top of `h`.
### TypeScript Both are reactive, autocleanup, and support SVG, events, twoway binding, and dynamic children.
---
## JSX with SigPro
SigPro works seamlessly with JSX. You can use JSX as a compiletime syntax sugar for `h` calls.
### Configuration
#### TypeScript
```json ```json
// tsconfig.json // tsconfig.json
{ {
"compilerOptions": { "compilerOptions": {
"jsx": "react", "jsx": "react",
"jsxFactory": "Tag", "jsxFactory": "h",
"jsxFragmentFactory": "Fragment" "jsxFragmentFactory": "Fragment"
} }
} }
``` ```
### Vite #### Vite
```js ```js
// vite.config.js // vite.config.js
@@ -25,31 +36,33 @@ import { defineConfig } from 'vite'
export default defineConfig({ export default defineConfig({
esbuild: { esbuild: {
jsxFactory: 'Tag', jsxFactory: 'h',
jsxFragmentFactory: 'Fragment' jsxFragmentFactory: 'Fragment'
} }
}) })
``` ```
### Babel #### Babel
```js ```js
// babel.config.js // babel.config.js
export default { export default {
plugins: [ plugins: [
['@babel/plugin-transform-react-jsx', { ['@babel/plugin-transform-react-jsx', {
pragma: 'Tag', pragma: 'h',
pragmaFrag: 'Fragment' pragmaFrag: 'Fragment'
}] }]
] ]
} }
``` ```
## Usage Example > **Note:** You need to import `h` and `Fragment` from SigPro in every JSX file, or make them global.
### JSX Example
```jsx ```jsx
// App.jsx // App.jsx
import { $, Mount, Fragment } from 'sigpro'; import { $, h, Fragment, mount } from 'sigpro';
const Button = ({ onClick, children }) => ( const Button = ({ onClick, children }) => (
<button class="btn btn-primary" onclick={onClick}> <button class="btn btn-primary" onclick={onClick}>
@@ -76,10 +89,10 @@ const App = () => {
); );
}; };
Mount(App, '#app'); mount(App, '#app');
``` ```
## What Gets Compiled ### What Gets Compiled
Your JSX: Your JSX:
```jsx ```jsx
@@ -89,59 +102,53 @@ Your JSX:
``` ```
Compiles to: Compiles to:
```javascript ```js
Tag('div', { class: "container" }, h('div', { class: "container" },
Tag(Button, {}, "Click") h(Button, {}, "Click")
) )
``` ```
## Without Build Step (CDN) ---
SigPro automatically injects `Div()`, `Button()`, `Span()`, and all other HTML tag helpers globally when loaded via CDN. `Fragment` is also available. ## Without a Build Step (CDN + Tag Helpers)
If you dont want to configure a JSX compiler, you can use the global tag helpers directly. They are available after loading SigPro via CDN.
```html ```html
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<script src="https://unpkg.com/sigpro"></script> <script type="module">
import 'https://cdn.jsdelivr.net/npm/sigpro@1.2.18/+esm';
// Now $, $$, watch, h, mount, div, button, etc. are global
const count = $(0);
const App = () =>
div({ class: 'container' }, [
h1(() => `Count: ${count()}`),
button({ onClick: () => count(count() + 1) }, 'Increment')
]);
mount(App, '#app');
</script>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script>
const { $, Mount, Fragment } = SigPro;
const App = () => {
const count = $(0);
return Div({ class: "container p-8" }, [
H1({ class: "text-2xl font-bold" }, "SigPro Demo"),
Button({
class: "btn-primary",
onclick: () => count(count() + 1)
}, () => `Clicks: ${count()}`),
Fragment({}, [
P({}, "Multiple elements"),
P({}, "Without wrapper")
])
]);
};
Mount(App, '#app');
</script>
</body> </body>
</html> </html>
``` ```
## Template Literals Alternative (htm) ---
For a JSX-like syntax without a build step, use `htm`: ## Template Literals Alternative (`htm`)
```javascript For a JSXlike syntax without a build step, you can combine SigPro with [`htm`](https://github.com/developit/htm).
import { $, Mount } from 'https://unpkg.com/sigpro';
```js
import { $, h, mount } from 'https://cdn.jsdelivr.net/npm/sigpro@1.2.18/+esm';
import htm from 'https://esm.sh/htm'; import htm from 'https://esm.sh/htm';
const html = htm.bind(Tag); const html = htm.bind(h); // bind to SigPro's h function
const App = () => { const App = () => {
const count = $(0); const count = $(0);
@@ -156,16 +163,18 @@ const App = () => {
`; `;
}; };
Mount(App, '#app'); mount(App, '#app');
``` ```
## Summary ---
| Method | Build Step | Syntax | ## Summary Comparison
|--------|------------|--------|
| JSX | Required | `<div>...</div>` |
| CDN (Tag Helpers) | Optional | `Div({}, ...)` |
| htm | Optional | `` html`<div>...</div>` `` |
> [!TIP] | Method | Build Step | Syntax | Recommended for |
> **Recommendation:** Use JSX for large projects, CDN tag helpers (`Div()`, `Button()`) for simple projects, or htm for buildless projects that want HTML-like syntax. | :--- | :--- | :--- | :--- |
| **`h` function** | Optional | `h('div', ...)` | Dynamic tag names, lowlevel control |
| **Tag Helpers** | Optional | `div(...)` | Most cases clean, simple, no build step |
| **JSX** | Required | `<div>...</div>` | Large projects, teams familiar with React syntax |
| **`htm`** | Optional | `` html`<div>...</div>` `` | Buildless but HTMLlike syntax |
> **Tip:** All approaches are fully reactive, support twoway binding, events, SVG, and automatic cleanup. Choose the one that fits your workflow.

View File

@@ -1,85 +1,151 @@
# Application Mounter: `Mount( )` # Application Mounter: `mount( )`
The `Mount` function is the entry point of your reactive world. It bridges the gap between your SigPro logic and the browser's Real DOM by injecting a component into the document and initializing its reactive lifecycle. The `mount` function is the entry point of your reactive world. It bridges the gap between your SigPro logic and the browser's real DOM by rendering a component into a target element and managing its full reactive lifecycle.
## Function Signature ## Function Signature
```typescript ```typescript
Mount(node: Function | HTMLElement, target?: string | HTMLElement): RuntimeObject mount(component: Function | Node, target: string | HTMLElement): RuntimeObject
``` ```
| Parameter | Type | Default | Description | | Parameter | Type | Required | Description |
| :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- |
| **`node`** | `Function` or `Node` | **Required** | The component function or DOM element to render. | | **`component`** | `Function` or `Node` | Yes | A component function (returns a Node) or a direct DOM node. |
| **`target`** | `string` or `Node` | `document.body` | CSS selector or DOM element where the app will live. | | **`target`** | `string` or `HTMLElement` | Yes | CSS selector (e.g., `"#app"`) or DOM element where the app will be mounted. |
**Returns:** A `Runtime` object containing the `container` and a `destroy()` method to wipe all reactivity and DOM nodes. **Returns:** A `Runtime` object with:
- `container`: The actual DOM element created by the renderer.
- `destroy()`: A method to completely unmount and clean up the application.
> **Availability:** `mount` is exported from the SigPro module. In **ESM** you must import it (`import { mount } from 'sigpro'`). In the **IIFE** classic script, it is automatically available on `window`. The examples below assume the function is already in scope.
--- ---
## Common Usage Scenarios ## Usage Patterns
### 1. The SPA Entry Point ### 1. Main Application Entry Point
In a Single Page Application, you typically mount your main component to the body or a root div. SigPro manages the entire view from that point.
```javascript ```javascript
import SigPro from 'sigpro'; import { mount } from 'sigpro';
import App from './App.js';
// Mounts your main App component const App = () => div({ class: "app" }, [
Mount(App, '#app-root'); h1("Hello SigPro"),
button("Click me")
]);
mount(App, '#app');
``` ```
### 2. Reactive "Islands" ### 2. Reactive Widget (Island Architecture)
SigPro is perfect for adding reactivity to static pages. You can mount small widgets into specific parts of an existing HTML layout.
Mount small reactive components into static HTML pages.
```javascript ```javascript
const Counter = () => { const Counter = () => {
const count = $(0); const count = $(0);
return Button({ onclick: () => count(c => c + 1) }, [ return button({ onclick: () => count(count() + 1) }, () => `Clicks: ${count()}`);
"Clicks: ", count
]);
}; };
// Mount only the counter into a specific sidebar div mount(Counter, '#sidebar-widget');
Mount(Counter, '#sidebar-widget'); ```
### 3. Direct Node Mounting
You can also mount an already existing DOM node.
```javascript
const myDiv = div("I am already a node");
mount(myDiv, '#container');
``` ```
--- ---
## How it Works (Lifecycle & Cleanup) ## How It Works (Lifecycle & Cleanup)
When `Mount` is executed, it performs these critical steps to ensure a leak-free environment: When you call `mount`, SigPro performs these steps:
1. **Duplicate Detection**: If you call `Mount` on a target that already has a SigPro instance, it automatically calls `.destroy()` on the previous instance. This prevents "Zombie Effects" from stacking in memory. 1. **Duplicate Detection**
2. **Internal Scoping**: It executes the component function inside an internal **Reactive Owner**. This captures every `Watch` and event listener created during the render. SigPro keeps a `WeakMap` (`MOUNTED_NODES`) that tracks which DOM target already has a mounted runtime. If you mount a new component to the same target, the previous instance is **automatically destroyed** before the new one is rendered. This prevents memory leaks and “zombie effects”.
3. **Target Injection**: It clears the target using `replaceChildren()` and appends the new component.
4. **Runtime Creation**: It returns a control object: 2. **Render Phase**
* `container`: The actual DOM element created. The `render` function creates a **cleanup container** (a `div` with `style="display: contents"`), and executes the component inside a fresh reactive owner. All effects (`watch`), event listeners, and child components created during this render are captured.
* `destroy()`: The "kill switch" that runs all cleanups, stops all watchers, and removes the element from the DOM.
3. **DOM Injection**
The target element is cleared using `replaceChildren()`, and the container (which holds the rendered content) is appended.
4. **Runtime Object**
Returns an object `{ _isRuntime: true, container, destroy }`. The `destroy` function recursively disposes all effects, cleans up DOM nodes, and removes the container from the parent.
--- ---
## Manual Unmounting ## Manual Unmounting
While SigPro handles most cleanups automatically (via `If`, `For`, and `Router`), you can manually destroy any mounted instance. This is vital for imperatively managed UI like **Toasts** or **Modals**. You can call `destroy()` at any time to tear down the application. This is essential for imperatively managed UI like **modals**, **toasts**, or **dynamic panels**.
```javascript ```javascript
const instance = Mount(MyToast, '#toast-container'); const widget = mount(MyToast, '#toast-container');
// Later, to remove the toast and kill its reactivity: // Later, remove it completely:
instance.destroy(); widget.destroy();
```
---
## Automatic Remount on Same Target
If you call `mount` a second time on the same target, SigPro automatically destroys the previous instance and replaces it with the new one. No manual cleanup required.
```javascript
mount(LoginScreen, '#app');
// ... later, after login
mount(Dashboard, '#app'); // LoginScreen is destroyed automatically
```
---
## What is Automatically Cleaned Up
When `destroy()` is called (or when a new mount replaces an old one), everything is purged:
- All `watch` effects
- All event listeners added via SigPro (`onClick`, `onInput`, etc.)
- All child components created with `when`, `each`, or nested `mount` calls
- Any custom cleanups registered with `onUnmount`
> **You only need manual cleanup** for external resources not managed by SigPro (e.g., `setInterval`, thirdparty libraries, WebSocket connections). Use `onUnmount` for that.
---
## Complete Example
```javascript
import { $, mount } from 'sigpro';
const App = () => {
const count = $(0);
return div({ class: "demo" }, [
h1(() => `Count: ${count()}`),
button({ onClick: () => count(count() + 1) }, "Increment")
]);
};
const runtime = mount(App, '#app');
// Destroy after 10 seconds
setTimeout(() => runtime.destroy(), 10000);
``` ```
--- ---
## Summary Cheat Sheet ## Summary Cheat Sheet
| Goal | Code Pattern | | Goal | Code |
| :--- | :--- | | :--- | :--- |
| **Mount to body** | `Mount(App)` | | Mount to a CSS selector | `mount(App, '#root')` |
| **Mount to CSS Selector** | `Mount(App, '#root')` | | Mount to a DOM element | `mount(App, document.getElementById('root'))` |
| **Mount to DOM Node** | `Mount(App, myElement)` | | Mount a static node | `mount(div("Hello"), '#target')` |
| **Clean & Re-mount** | Calling `Mount` again on the same target | | Manual destruction | `const app = mount(App, '#app'); app.destroy();` |
| **Total Cleanup** | `const app = Mount(App); app.destroy();` | | Autoreplace on same target | Just call `mount` again SigPro handles cleanup. |
> **Note:** The target must exist in the DOM at the time of mounting.

View File

@@ -1,238 +1,177 @@
# ⚡ Quick API Reference # SigPro Complete API Reference
SigPro is a high-performance micro-framework that updates the **Real DOM** surgically. No Virtual DOM, no unnecessary re-renders, and built-in **Cleanup** (memory cleanup). ## Core Reactivity
<div class="text-center my-8"> ### `$(value, localStorageKey?)` Signal & Computed
<div class="flex justify-center gap-2 flex-wrap mb-4">
<span class="badge badge-primary badge-lg font-mono text-lg">$-$$</span>
<span class="badge badge-secondary badge-lg font-mono text-lg">Watch</span>
<span class="badge badge-accent badge-lg font-mono text-lg">Tag</span>
<span class="badge badge-info badge-lg font-mono text-lg">If</span>
<span class="badge badge-success badge-lg font-mono text-lg">For</span>
<span class="badge badge-warning badge-lg font-mono text-lg">Router</span>
<span class="badge badge-error badge-lg font-mono text-lg">Mount</span>
</div>
<h1 class="text-5xl font-black bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent">
⚡ All the power! ⚡
</h1>
</div>
## Core Functions Creates a reactive signal. If a function is passed, it becomes a **computed** signal that caches its result until dependencies change.
Explore the reactive building blocks of SigPro. | Usage | Description |
|-------|-------------|
| `const count = $(0)` | Basic signal, returns a getter/setter: `count()` reads, `count(5)` writes. |
| `const double = $( () => count() * 2 )` | Computed signal updates automatically when `count` changes. |
| `const stored = $('hello', 'myKey')` | Persisted signal reads/writes to `localStorage`. |
<div class="overflow-x-auto my-8 border border-base-300 rounded-xl shadow-sm"> **Example**
<table class="table table-zebra w-full"> ```javascript
<thead class="bg-base-200 text-base-content"> const count = $(0)
<tr> const double = $( () => count() * 2 )
<th>Function</th>
<th>Signature</th> watch(() => {
<th>Description</th> console.log(`count = ${count()}, double = ${double()}`)
</tr> }) // logs on every change
</thead>
<tbody> count(5) // triggers log: count=5, double=10
<tr> ```
<td><code class="text-primary font-bold">$(val, key?)</code></td>
<td class="font-mono text-xs opacity-70">(any, string?) => Signal</td>
<td>Creates a <b>Signal</b>. If <code>key</code> is provided, it persists in <code>localStorage</code>.</td>
</tr>
<tr>
<td><code class="text-primary font-bold">$(fn)</code></td>
<td class="font-mono text-xs opacity-70">(function) => Computed</td>
<td>Creates a <b>Computed Signal</b> that auto-updates when dependencies change.</td>
</tr>
<tr>
<td><code class="text-primary font-bold">$$(obj)</code></td>
<td class="font-mono text-xs opacity-70">(object) => Proxy</td>
<td>Creates a <b>Deep Reactive Proxy</b>. Track nested property access automatically. No need for manual signals.</td>
</tr>
<tr>
<td><code class="text-secondary font-bold">Watch(fn)</code></td>
<td class="font-mono text-xs opacity-70">(function) => stopFn</td>
<td><b>Auto Mode:</b> Tracks any signal touched inside. Returns a stop function.</td>
</tr>
<tr>
<td><code class="text-secondary font-bold">Watch(deps, fn)</code></td>
<td class="font-mono text-xs opacity-70">(Array, function) => stopFn</td>
<td><b>Explicit Mode:</b> Only runs when signals in <code>deps</code> change.</td>
</tr>
<tr>
<td><code class="text-accent font-bold">If(cond, then, else?)</code></td>
<td class="font-mono text-xs opacity-70">(Signal|bool, fn, fn?) => Node</td>
<td>Reactive conditional. Automatically destroys "else" branch memory.</td>
</tr>
<tr>
<td><code class="text-accent font-bold">For(src, render, key)</code></td>
<td class="font-mono text-xs opacity-70">(Signal, fn, fn) => Node</td>
<td><b>Keyed Loop:</b> Optimized list renderer. Uses the key function for surgical DOM moves.</td>
</tr>
<tr>
<td><code class="text-info font-bold">Router(routes)</code></td>
<td class="font-mono text-xs opacity-70">(Array) => Node</td>
<td><b>SPA Router:</b> Hash-based routing with dynamic params (<code>:id</code>) and auto-cleanup.</td>
</tr>
<tr>
<td><code class="font-bold">Mount(node, target)</code></td>
<td class="font-mono text-xs opacity-70">(any, string|Node) => Runtime</td>
<td>Entry point. Cleans the target and mounts the app with full lifecycle management.</td>
</tr>
</tbody>
</table>
</div>
--- ---
## Element Constructors (Tags) ### `watch(source, callback?)` Reactive Effect
SigPro provides **PascalCase** wrappers for all standard HTML5 tags (e.g., `Div`, `Span`, `Button`). Two modes:
### Syntax Pattern 1. **Autotrack mode** pass a function: `watch(() => { /* reads signals */ })`
Automatically reruns whenever any signal read inside changes.
<div class="mockup-code bg-base-300 text-base-content"> 2. **Explicit mode** pass an array of signals and a callback:
<pre data-prefix=""><code>Tag({ attributes }, [children])</code></pre> `watch([count, double], () => { ... })`
</div> Runs the callback when any of the listed signals change. The callback receives the new values.
### Special Attributes & Routing Both modes return a `stop` function that disposes the effect.
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 my-10">
<div class="card bg-base-200 border border-base-300 shadow-sm">
<div class="card-body p-5">
<h3 class="text-xs font-black uppercase tracking-widest opacity-60">Two-way Binding</h3>
<code class="text-primary font-bold text-sm bg-base-300/50 p-2 rounded-lg">value: mySignal</code>
<p class="text-xs mt-3 leading-relaxed">Automatic sync for <code>Input</code>, <code>Textarea</code>, and <code>Select</code>. Updates the signal on 'input' or 'change'.</p>
</div>
</div>
<div class="card bg-base-200 border border-base-300 shadow-sm">
<div class="card-body p-5">
<h3 class="text-xs font-black uppercase tracking-widest opacity-60">Dynamic Routing</h3>
<code class="text-info font-bold text-sm bg-base-300/50 p-2 rounded-lg">Router.to('/user/1')</code>
<p class="text-xs mt-3 leading-relaxed">Navigate programmatically. Access params via <code>Router.params().id</code>.</p>
</div>
</div>
<div class="card bg-base-200 border border-base-300 shadow-sm">
<div class="card-body p-5">
<h3 class="text-xs font-black uppercase tracking-widest opacity-60">Refs & DOM</h3>
<code class="text-accent font-bold text-sm bg-base-300/50 p-2 rounded-lg">ref: (el) => ...</code>
<p class="text-xs mt-3 leading-relaxed">Get direct access to the DOM node once it is created.</p>
</div>
</div>
<div class="card bg-base-200 border border-base-300 shadow-sm">
<div class="card-body p-5">
<h3 class="text-xs font-black uppercase tracking-widest opacity-60">Event Handling</h3>
<code class="text-secondary font-bold text-sm bg-base-300/50 p-2 rounded-lg">onClick: (e) => ...</code>
<p class="text-xs mt-3 leading-relaxed">Standard events with automatic <code>removeEventListener</code> on destruction.</p>
</div>
</div>
</div>
---
## Custom API (Bring Your Own Syntax)
SigPro's core functions are intentionally simple and can be easily renamed in **one line** to match your preferred coding style.
### One-Line Renaming
```javascript ```javascript
import { $ as signal, Mount as render, Tag as tag, If as when, For as each, Watch as effect } from 'sigpro'; // auto mode
const stop = watch(() => console.log(count()))
// Now use your custom names // explicit mode
const count = signal(0); watch([count, double], ([newCount, newDouble]) => {
effect(() => console.log(count())); console.log(newCount, newDouble)
})
```
render(() => > **Important**: Effects are depthaware they run in topological order, parents before children.
tag('div', {}, [
when(count, ---
() => tag('span', {}, 'Positive'),
() => tag('span', {}, 'Zero or negative') ## Components & DOM (Hyperscript)
### `h(tag, props, children)` Create DOM Nodes
The universal builder. `props` can be omitted. Children can be strings, numbers, nodes, arrays, or **dynamic functions**.
| Feature | Example |
|---------|---------|
| Standard attributes | `h('div', { class: 'box', id: 'main' })` |
| Events | `onClick: (e) => ...` (automatically cleaned up) |
| Reactive attributes | `class: () => count() > 0 ? 'positive' : 'negative'` |
| Twoway binding | `value: mySignal` (works on `input`, `textarea`, `select`) |
| Refs | `ref: (el) => ...` or `ref: { current: null }` |
| SVG support | tag names like `svg`, `circle`, `path` sets correct namespace |
| Dangerous URL sanitising | `href` / `src` with `javascript:` or `data:` are blocked → `'#'` (when XSS shield is active) |
**Dynamic children** pass a function as a child, it will be reexecuted and the DOM patched automatically:
```javascript
h('div', {}, [
() => count() > 0 ? h('span', {}, 'positive') : h('span', {}, 'zero or negative')
])
```
### Tag shortcuts
Tag helpers **are exported** from the core.
Available tags: `a`, `abbr`, `article`, `aside`, `audio`, `b`, `blockquote`, `br`, `button`, `canvas`, `caption`, `cite`, `code`, `col`, `colgroup`, `datalist`, `dd`, `del`, `details`, `dfn`, `dialog`, `div`, `dl`, `dt`, `em`, `embed`, `fieldset`, `figcaption`, `figure`, `footer`, `form`, `h1``h6`, `header`, `hr`, `i`, `iframe`, `img`, `input`, `ins`, `kbd`, `label`, `legend`, `li`, `main`, `mark`, `meter`, `nav`, `object`, `ol`, `optgroup`, `option`, `output`, `p`, `picture`, `pre`, `progress`, `section`, `select`, `slot`, `small`, `source`, `span`, `strong`, `sub`, `summary`, `sup`, `svg`, `table`, `tbody`, `td`, `template`, `textarea`, `tfoot`, `th`, `thead`, `time`, `tr`, `u`, `ul`, `video`.
---
## Flow Control Components
### `when(condition, thenComponent, elseComponent?)`
Reactive conditional rendering. `condition` can be a boolean, a signal, or any function that returns a boolean. Both branches can be `Node`, `() => Node`, or `null`. Automatically disposes the unmounted branch.
```javascript
when(
() => user.loggedIn(),
() => div({}, 'Welcome back!'),
() => button({ onClick: () => login() }, 'Login')
) )
]),
'#app'
);
``` ```
### Create React-like Hooks ---
### `each(source, itemRenderer, keyFn)`
Optimised keyed list rendering. `source` can be an array or a signal/function returning an array. `itemRenderer(item, index)` returns a Node (or a function that returns Nodes). `keyFn(item, index)` returns a unique identifier **required** for efficient DOM reuse.
```javascript ```javascript
import * as SigPro from 'sigpro'; const items = $([{ id: 1, text: 'a' }, { id: 2, text: 'b' }])
const useState = (initial) => { each(items,
const signal = SigPro.$(initial); (item) => Li({}, item.text),
return [signal, (value) => signal(value)]; (item) => item.id
}; )
const useEffect = (fn, deps) => {
deps ? SigPro.Watch(deps, fn) : SigPro.Watch(fn);
};
// Usage
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => console.log(count()), [count]);
return SigPro.Tag('button', { onClick: () => setCount(count() + 1) }, count);
};
``` ```
### Create Vue-like API When the array changes, elements are added, removed, or reordered with minimal DOM operations.
---
## Batch
### `batch(fn)`
Batch multiple reactive updates into a single flush, improving performance.
```javascript ```javascript
import { $ as ref, Watch as watch, Mount as mount } from 'sigpro'; batch(() => {
count(1)
const computed = (fn) => ref(fn); name('John')
const createApp = (component) => ({ mount: (selector) => mount(component, selector) }); // effects run only once after the batch ends
})
// Usage
const count = ref(0);
const double = computed(() => count() * 2);
watch([count], () => console.log(count()));
``` ```
### Global Custom API with sigpro.config.js ## Mounting `mount(component, target)`
Create a central configuration file to reuse your custom naming across the entire project: Clears the target element and mounts the application. Returns the runtime instance (which has a `.destroy()` method).
```javascript ```javascript
// config/sigpro.config.js mount(() => App(), '#app')
import { $ as signal, Mount as render, Tag as tag, If as when, For as each, Watch as effect } from 'sigpro'; // or
mount(App, document.body)
// Re-export everything with your custom names
export { signal, render, tag, when, each, effect };
// Also re-export the original functions if needed
export * from 'sigpro';
``` ```
If you mount again on the same target, the previous instance is automatically destroyed.
---
## Global Cleanup & Memory
SigPro tracks every effect, DOM event listener, and nested component. When a component is unmounted:
- All its effects are disposed.
- All DOM event listeners are removed.
- All `onUnmount` callbacks run.
- Child components are recursively destroyed.
You never need to manually clean up just write reactive code.
---
## Full Example Counter with Persistence
```javascript ```javascript
// app.js - Import your custom API globally import { $, mount } from 'sigpro';
import { signal, render, tag, when, each, effect } from './config/sigpro.config.js';
// Use your preferred syntax everywhere const count = $(0, 'counter') // persists in localStorage
const count = signal(0);
const double = signal(() => count() * 2);
effect(() => console.log(`Count: ${count()}, Double: ${double()}`));
const App = () => const App = () =>
tag('div', { class: 'p-4' }, [ div({ class: 'counter' }, [
tag('h1', {}, () => `Count: ${count()}`), h1({}, () => `Count: ${count()}`),
tag('button', { onclick: () => count(count() + 1) }, 'Increment') button({ onClick: () => count(count() + 1) }, '+'),
]); button({ onClick: () => count(count() - 1) }, '-'),
button({ onClick: () => count(0) }, 'Reset')
])
render(App, '#app'); mount(App, '#app')
```
> [!TIP]
> **Why rename?** Team preferences, framework migration, or just personal taste. SigPro adapts to you, not the other way around.
> [!IMPORTANT]
> **Performance Hint:** For lists (`For`), always provide a unique key function `(item) => item.id` to prevent unnecessary node creation and enable reordering.
> [!TIP]
> **Pro Tip:** Use `$$()` for complex nested state objects instead of multiple `$()` signals. It's cleaner and automatically tracks deep properties.
> [!TIP]
> **Performance Hint:** Always use functions `() => signal()` for dynamic children to ensure SigPro only updates the specific node and not the whole container.
``` ```

View File

@@ -1,96 +0,0 @@
# Routing: `Router()` & Utilities
SigPro includes a built-in, lightweight **Hash Router** to create Single Page Applications (SPA). It manages the URL state, matches components to paths, and handles the lifecycle of your pages automatically.
## Router Signature
```typescript
Router(routes: Route[]): HTMLElement
```
### Route Object
| Property | Type | Description |
| :--- | :--- | :--- |
| **`path`** | `string` | The URL fragment (e.g., `"/"`, `"/user/:id"`, or `"*"`). |
| **`component`** | `Function` | A function that returns a Node, a String, or a reactive View. |
---
## Usage Patterns
### 1. Defining Routes
The `Router` returns a `div` element with the class `.router-transition`. When the hash changes, the router destroys the previous view and mounts the new one inside this container.
```javascript
const App = () => Div({ class: "app-layout" }, [
Navbar(),
// The router outlet is placed here
Router([
{ path: "/", component: Home },
{ path: "/profile/:id", component: (params) => UserProfile(params.id) },
{ path: "*", component: () => H1("404 Not Found") }
])
]);
```
### 2. Dynamic Segments (`:id`)
The router automatically parses URL parameters (like `:id`) and passes them as an object to the component function. You can also access them globally via `Router.params()`.
```javascript
// If the URL is #/profile/42
const UserProfile = (params) => {
return H1(`User ID is: ${params.id}`); // Displays "User ID is: 42"
};
```
---
## Navigation Utilities
SigPro provides a set of programmatic methods to control the history and read the state.
### `Router.to(path)`
Navigates to a specific path. It automatically formats the hash (e.g., `/home` becomes `#/home`).
```javascript
Button({ onclick: () => Router.to("/dashboard") }, "Go to Dashboard")
```
### `Router.back()`
Goes back to the previous page in the browser history.
```javascript
Button({ onclick: () => Router.back() }, "Back")
```
### `Router.path()`
Returns the current path (without the `#`).
```javascript
Watch(() => {
console.log("Current path is:", Router.path());
});
```
---
## Technical Behavior
* **Automatic Cleanup**: Every time you navigate, the router calls `.destroy()` on the previous view. This ensures that all **signals, effects, and event listeners** from the old page are purged from memory.
* **Reactive Params**: `Router.params` is a signal (`$`). You can react to parameter changes without re-mounting the entire router outlet.
* **Hash-Based**: By using `window.location.hash`, SigPro works out-of-the-box on any static hosting (like GitHub Pages or Vercel) without needing server-side redirects.
---
## Styling the transition of Router
The router returns a standard `div` with the `.router-transition` class. You can easily style it or add transitions:
```css
.router-transition {
display: block;
min-height: 100vh;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
```

View File

@@ -1,6 +1,6 @@
# The Signal Function: `$()` # The Signal Function: `$()`
The `$( )` function is the core constructor of SigPro. It defines how data is stored, computed, and persisted. The `$()` function is the **only** reactive primitive in SigPro. It defines how data is stored, computed, and persisted. For complex nested objects, you compose signals naturally.
## Function Signature ## Function Signature
@@ -25,21 +25,34 @@ $(computation: Function): ComputedSignal
**`$(value)`** **`$(value)`**
Creates a writable signal. It returns a function that acts as both **getter** and **setter**. Creates a writable signal. It returns a function that acts as both **getter** and **setter**.
```javascript <div id="demo-signal-simple"></div>
const count = $(0);
count(); // Read (0) ```javascript
count(10); // Write (10) {
const count = $(0);
const App = () => div({ class: "example" }, [
p(() => `Count: ${count()}`),
button({ onClick: () => count(count() + 1) }, "+1")
]);
setTimeout(() => mount(App, '#demo-signal-simple'), 50);
}
``` ```
### 2. Persistent State ### 2. Persistent State
**`$(value, key)`** **`$(value, key)`**
Creates a writable signal that syncs with the browser's storage. Creates a writable signal that syncs with the browser's storage.
```javascript <div id="demo-signal-persist"></div>
const theme = $("light", "app-theme");
theme("dark"); // Automatically calls localStorage.setItem("app-theme", '"dark"') ```javascript
{
const theme = $("light", "theme-persist-demo");
const App = () => div([
p(() => `Current theme: ${theme()}`),
button({ onClick: () => theme(theme() === "light" ? "dark" : "light") }, "Toggle theme")
]);
setTimeout(() => mount(App, '#demo-signal-persist'), 50);
}
``` ```
*Note: On page load, SigPro will prioritize the value found in `localStorage` over the `initialValue`.* *Note: On page load, SigPro will prioritize the value found in `localStorage` over the `initialValue`.*
@@ -47,12 +60,23 @@ theme("dark"); // Automatically calls localStorage.setItem("app-theme", '"dark"'
**`$(function)`** **`$(function)`**
Creates a read-only signal that updates automatically when any signal used inside it changes. Creates a read-only signal that updates automatically when any signal used inside it changes.
<div id="demo-signal-computed"></div>
```javascript ```javascript
{
const price = $(100); const price = $(100);
const tax = $(0.21); const tax = $(0.21);
// This tracks both 'price' and 'tax' automatically
const total = $(() => price() * (1 + tax())); const total = $(() => price() * (1 + tax()));
const App = () => div([
p(() => `Price: €${price()}`),
p(() => `Tax rate: ${tax() * 100}%`),
p(() => `Total: €${total().toFixed(2)}`),
button({ onClick: () => price(price() + 10) }, "+€10"),
button({ onClick: () => price(price() - 10) }, "-€10")
]);
setTimeout(() => mount(App, '#demo-signal-computed'), 50);
}
``` ```
--- ---
@@ -60,156 +84,196 @@ const total = $(() => price() * (1 + tax()));
## Updating with Logic ## Updating with Logic
When calling the setter, you can pass an **updater function** to access the current value safely. When calling the setter, you can pass an **updater function** to access the current value safely.
<div id="demo-signal-updater"></div>
```javascript ```javascript
{
const list = $(["A", "B"]); const list = $(["A", "B"]);
const App = () => div([
// Adds "C" using the previous state ul(() => list().map(item => li(item))),
list(prev => [...prev, "C"]); button({ onClick: () => list(prev => [...prev, "C"]) }, "Add C")
]);
setTimeout(() => mount(App, '#demo-signal-updater'), 50);
}
``` ```
--- ---
# The Reactive Object: `$$( )` ## Composing Signals for Complex State
The `$$( )` function creates a reactive proxy for complex nested objects. Unlike `$()`, which tracks a single value, `$$()` tracks **every property access** automatically. 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-compose-simple"></div>
```javascript ```javascript
const state = $$({ count: 0, name: "Juan" }); {
const count = $(0);
const name = $("Juan");
Watch(() => state.count, () => { // Optionally create a derived combined state
console.log(`Count is now ${state.count}`); const state = $(() => ({ count: count(), name: name() }));
});
state.count++; // ✅ Triggers update const App = () => div([
state.name = "Ana"; // ✅ Also reactive p(() => `Count: ${count()}, Name: ${name()}`),
button({ onClick: () => count(count() + 1) }, "Increment count"),
button({ onClick: () => name(name() === "Juan" ? "Ana" : "Juan") }, "Toggle name")
]);
setTimeout(() => mount(App, '#demo-compose-simple'), 50);
}
``` ```
### 2. Deep Reactivity ### 2. Deeply Nested State
Unlike `$()`, `$$()` tracks nested properties automatically. <div id="demo-compose-deep"></div>
```javascript ```javascript
const user = $$({ {
profile: { const profileName = $("Juan");
name: "Juan", const profileCity = $("Madrid");
address: { const profileZip = $("28001");
city: "Madrid",
zip: "28001"
}
}
});
// This works! Tracks deep property access // Computed derived values
Watch(() => user.profile.address.city, () => { const fullAddress = $(() => `${profileCity()}, ${profileZip()}`);
console.log("City changed");
});
user.profile.address.city = "Barcelona"; // ✅ Triggers update watch(profileCity, () => console.log("City changed to:", profileCity()));
const App = () => div([
p(() => `Name: ${profileName()}`),
p(() => `City: ${profileCity()}`),
p(() => `Full address: ${fullAddress()}`),
button({ onClick: () => profileCity("Barcelona") }, "Change to Barcelona")
]);
setTimeout(() => mount(App, '#demo-compose-deep'), 50);
}
``` ```
### 3. Arrays ### 3. Arrays
`$$()` works with arrays and array methods. <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, () => { const todoCount = $(() => todos().length);
console.log(`You have ${todos.length} todos`);
});
// Array methods are reactive watch(todoCount, () => console.log(`You have ${todoCount()} todos`));
todos.push({ id: 3, text: "Deploy", done: false }); // ✅ Triggers
todos[0].done = true; // ✅ Deep reactivity works const App = () => div([
todos.splice(1, 1); // ✅ Triggers ul(() => todos().map(todo => li(todo.text + (todo.done ? " ✓" : "")))),
button({ onClick: () => todos(prev => [...prev, { id: Date.now(), text: "New todo", done: false }]) }, "Add todo"),
button({ onClick: () => {
const updated = [...todos()];
updated[0] = { ...updated[0], done: !updated[0].done };
todos(updated);
}}, "Toggle first todo")
]);
setTimeout(() => mount(App, '#demo-compose-array'), 50);
}
``` ```
### 4. Mixed with Signals ### 4. Complete Form Example
`$$()` works seamlessly with `$()` signals. <div id="demo-compose-form"></div>
```javascript ```javascript
const form = $$({ {
fields: { const email = $("");
email: "", const password = $("");
password: "" const isValid = $(() => email().includes("@") && password().length > 6);
},
isValid: $(false) // Signal inside reactive object
});
// Computed using both watch(isValid, valid => console.log("Form valid:", valid));
const canSubmit = $(() =>
form.fields.email.includes("@") &&
form.fields.password.length > 6
);
Watch(canSubmit, (valid) => { const App = () => div([
form.isValid(valid); // Update signal inside reactive object input({
}); type: "email",
placeholder: "Email",
value: email,
onInput: e => email(e.target.value)
}),
input({
type: "password",
placeholder: "Password",
value: password,
onInput: e => password(e.target.value)
}),
p(() => `Form valid: ${isValid() ? "Yes" : "No"}`)
]);
setTimeout(() => mount(App, '#demo-compose-form'), 50);
}
``` ```
--- ---
## Key Differences: `$()` vs `$$()` ## Best Practices for Complex State
| Feature | `$()` Signal | `$$()` Reactive | ### ✅ 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:
- Working with primitives (numbers, strings, booleans)
- Need localStorage persistence
- Creating computed values
- Want explicit control over updates
```javascript ```javascript
const count = $(0); // Clear, predictable, and memory-safe
const user = $(null); const user = {
const fullName = $(() => `${firstName()} ${lastName()}`); name: $("Juan"),
email: $("juan@example.com"),
preferences: {
theme: $("dark"),
notifications: $(true)
}
}
// Computed values derived from composition
const userDisplay = $(() => `${user.name()} <${user.email()}>`)
``` ```
### Use `$$()` when: ### ✅ DO: Create store patterns
- Working with complex nested objects
- Managing forms with multiple fields
- Using arrays with mutations (push, pop, splice)
- Want natural object syntax (no function calls)
```javascript ```javascript
const form = $$({ email: "", password: "" }); const createUserStore = () => {
const settings = $$({ theme: "dark", notifications: true }); const name = $("")
const store = $$({ users: [], filters: {}, pagination: { page: 1 } }); const email = $("")
const isValid = $(() => name().length > 0 && email().includes("@"))
const actions = {
setName: (value) => name(value),
setEmail: (value) => email(value),
reset: () => {
name("")
email("")
}
}
return { name, email, isValid, ...actions }
}
const userStore = createUserStore()
```
### ❌ DON'T: Try to wrap objects with signals
```javascript
// Wrong - loses reactivity on nested properties
const user = $({ name: "Juan", email: "..." })
user().name = "Ana" // ❌ Not reactive!
// Correct - each property its own signal
const userName = $("Juan")
const userEmail = $("...")
```
### ❌ DON'T: Destructure signals in reactive contexts
```javascript
// Wrong - breaks tracking
const { name, email } = user
watch(() => name(), ...) // ❌ 'name' is not tracked properly
// Correct - use the original signal
watch(() => user.name(), ...) // ✅
``` ```
--- ---
@@ -218,124 +282,121 @@ const store = $$({ users: [], filters: {}, pagination: { page: 1 } });
### ✅ DO: ### ✅ 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`, `If`, and `For` 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-final"></div>
```javascript ```javascript
// Combining both approaches {
const app = { // All state as explicit signals
// Simple primitives with persistence const theme = $("dark", "theme_complete")
theme: $("dark", "theme"), const sidebarOpen = $(true)
sidebarOpen: $(true), const userName = $("")
const userEmail = $("")
const notifications = $(true)
const language = $("es")
// Complex state with $$() // Computed signals
user: $$({ const isLoggedIn = $(() => !!userName() && !!userEmail())
name: "",
email: "", // Actions as plain functions
preferences: { const login = (name, email) => {
notifications: true, userName(name)
language: "es" userEmail(email)
} }
const logout = () => {
userName("")
userEmail("")
notifications(true) // Reset on logout
}
// Components using signals directly
const LoginForm = () => div([
input({
placeholder: "Name",
onInput: e => userName(e.target.value)
}), }),
input({
placeholder: "Email",
onInput: e => userEmail(e.target.value)
}),
button({
onClick: () => login(userName(), userEmail())
}, "Login")
])
// Computed values const UserProfile = () => div([
isLoggedIn: $(() => !!app.user.name), h2(() => `Welcome ${userName()}`),
p(() => `Email: ${userEmail()}`),
p(() => `Notifications: ${notifications() ? "ON" : "OFF"}`),
p(() => `Language: ${language()}`),
button({
onClick: () => notifications(!notifications())
}, "Toggle Notifications"),
button({ onClick: logout }, "Logout")
])
// Actions const App = () => div({ class: "complete-example" }, [
login(name, email) { when(() => isLoggedIn(), () => UserProfile(), () => LoginForm())
app.user.name = name; ])
app.user.email = email;
},
logout() { setTimeout(() => mount(App, '#demo-complete-final'), 50)
app.user.name = "";
app.user.email = "";
app.user.preferences.notifications = true;
} }
};
// UI component
const UserProfile = () => {
return Div({}, [
If(() => app.isLoggedIn(),
() => Div({}, [
H2(`Welcome ${app.user.name}`),
P(`Email: ${app.user.email}`),
P(`Notifications: ${app.user.preferences.notifications ? "ON" : "OFF"}`),
Button({ onclick: () => app.user.preferences.notifications = !app.user.preferences.notifications },
"Toggle Notifications"
),
Button({ onclick: app.logout }, "Logout")
]),
() => LoginForm()
)
]);
};
``` ```
--- ---
## 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 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.
const user = $$({
name: "",
email: ""
});
user.name = "Juan"; // Direct assignment
```

View File

@@ -1,189 +1,100 @@
# Global Tag Helpers # Global Tag Helpers
In **SigPro**, you don't need to manually type `Tag('div', ...)` for every element. To keep your code declarative and readable, the engine automatically generates **Global Helper Functions** for all standard HTML5 tags upon initialization. In **SigPro**, you don't need to manually type `h('div', ...)` for every element. To keep your code declarative and readable, the engine provides **helper functions** for all standard HTML5 tags.
## 1. How it Works ## 1. How it Works
SigPro iterates through a manifest of standard HTML tags and attaches a wrapper function for each one directly to the `window` object. This creates a specialized **DSL** (Domain Specific Language) that looks like a template engine but is **100% standard JavaScript**. SigPro creates a wrapper function for each standard HTML tag.
- **Under the hood:** `h('button', { onclick: ... }, 'Click')`
- **SigPro Style:** `button({ onclick: ... }, 'Click')`
* **Under the hood:** `Tag('button', { onclick: ... }, 'Click')` > **Note:** All tag helpers are **lowercase** (e.g., `div`, `span`, `button`) and can be used directly once globally enabled.
* **SigPro Style:** `Button({ onclick: ... }, 'Click')`
> If you prefer to avoid globals, you can always use `h('div', ...)` directly—its perfectly fine.
> **Autocleanup:** All tag helpers and `h` automatically dispose effects, event listeners, and nested components when removed from the DOM.
--- ---
## 2. The Complete Global Registry ## 2. The Complete List of Tag Helpers
The following functions are injected into the global scope using **PascalCase** to prevent naming collisions with common JS variables: All helpers are **lowercase** and follow HTML5 tag names.
| Category | Available Global Functions | | Category | Available functions |
| :--- | :--- | | :--- | :--- |
| **Structure** | `Div`, `Span`, `P`, `Section`, `Nav`, `Main`, `Header`, `Footer`, `Article`, `Aside` | | **Structure** | `div`, `span`, `p`, `section`, `nav`, `main`, `header`, `footer`, `article`, `aside` |
| **Typography** | `H1` to `H6`, `Ul`, `Ol`, `Li`, `Dl`, `Dt`, `Dd`, `Strong`, `Em`, `Code`, `Pre`, `Small`, `B`, `U`, `Mark` | | **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: `a`, `abbr`, `article`, `aside`, `audio`, `b`, `blockquote`, `br`, `button`, `canvas`, `caption`, `cite`, `code`, `col`, `colgroup`, `datalist`, `dd`, `del`, `details`, `dfn`, `dialog`, `div`, `dl`, `dt`, `em`, `embed`, `fieldset`, `figcaption`, `figure`, `footer`, `form`, `h1``h6`, `header`, `hr`, `i`, `iframe`, `img`, `input`, `ins`, `kbd`, `label`, `legend`, `li`, `main`, `mark`, `meter`, `nav`, `object`, `ol`, `optgroup`, `option`, `output`, `p`, `picture`, `pre`, `progress`, `section`, `select`, `slot`, `small`, `source`, `span`, `strong`, `sub`, `summary`, `sup`, `svg`, `table`, `tbody`, `td`, `template`, `textarea`, `tfoot`, `th`, `thead`, `time`, `tr`, `u`, `ul`, `video`.
--- ---
## 3. Usage Patterns (Smart Arguments) ## 3. Usage Patterns
SigPro tag helpers are flexible. They automatically detect if you are passing attributes, children, or both.
### A. Attributes + Children ### A. Attributes + Children
```javascript ```javascript
Div({ class: 'container', id: 'main' }, [ div({ class: 'container', id: 'main' }, [
H1("Welcome to SigPro"), h1("Welcome to SigPro"),
P("The zero-VDOM framework.") p("The zeroVDOM framework.")
]); ]);
``` ```
### B. Children Only (The "Skipper") ### B. Children Only
If you don't need attributes, you can pass the content directly as the first argument.
If you don't need attributes, pass the content directly as the first argument.
```javascript ```javascript
Section([ section([
H2("Clean Syntax"), h2("Clean Syntax"),
Button("I have no props!") button("I have no props!")
]); ]);
``` ```
--- ---
## 4. Reactive Power ## 4. Custom Components with `h()` or Tag Helpers
These helpers are natively wired into SigPro's **`Watch`** engine. While the tag helpers cover all standard HTML tags, you can create reusable components using them directly.
### Reactive Attributes (One-Way) ### Basic Component
Simply pass a Signal (function) to any attribute. SigPro creates an internal `Watch` to keep the DOM in sync.
```javascript
const theme = $("light");
Div({
class: () => `app-box ${theme()}`
}, "Themeable Box");
```
### Smart Two-Way Binding (Automatic)
SigPro automatically bridges the **Signal** and the **Input** element bi-directionally when you assign a Signal to `value` or `checked`. No special operators are required.
```javascript
const search = $("");
// UI updates Signal AND Signal updates UI automatically
Input({
type: "text",
placeholder: "Search...",
value: search
});
```
> **Pro Tip:** If you want an input to be **read-only** but still reactive, wrap the signal in an anonymous function: `value: () => search()`. This prevents the "backwards" synchronization.
### Dynamic Flow & Cleanup
Combine tags with Core controllers. SigPro automatically cleans up the `Watch` instances and event listeners when nodes are removed from the DOM.
```javascript
const items = $(["Apple", "Banana", "Cherry"]);
Ul({ class: "list-disc" }, [
For(items, (item) => Li(item), (item) => item)
]);
```
---
<div class="alert alert-error shadow-lg my-8 border-l-8 border-error bg-error/10 text-error-content">
<div>
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
<div>
<h3 class="font-bold text-lg">Important: Naming Conventions</h3>
<div class="text-sm opacity-90">
<ol class="list-decimal ml-4 mt-2 space-y-2">
<li><b>Avoid Shadowing:</b> Don't name your local variables like the tags (e.g., <code>const Div = ...</code>). This will "hide" the global SigPro helper.</li>
<li><b>Custom Components:</b> Always use <b>PascalCase</b> for your own component functions (e.g., <code>UserCard</code>, <code>NavMenu</code>) to distinguish them from built-in Tag Helpers.</li>
</ol>
</div>
</div>
</div>
</div>
---
## 5. Logic to UI Comparison
Here is how a dynamic **User Status** component translates from SigPro logic to the final DOM structure.
```javascript
const UserStatus = (name, online) => (
Div({ class: 'flex items-center gap-2' }, [
Span({
hidden: () => !online(),
class: 'w-3 h-3 bg-green-500 rounded-full'
}),
P({
class: () => online() ? "text-bold" : "text-gray-400"
}, name)
])
);
```
---
## 6. Custom Tags with `Tag`
Create reusable components using `Tag`. All reactivity auto-cleans itself.
### Basic Example
```javascript ```javascript
const UserCard = (props, children) => const UserCard = (props, children) =>
Tag('div', { class: 'card p-4', 'data-id': props.id }, children); div({ class: 'card p-4', 'data-id': props.id }, children);
UserCard({ id: 123 }, [H3("John Doe"), P("john@example.com")]); UserCard({ id: 123 }, [h3("John Doe"), p("john@example.com")]);
``` ```
### Reactive Component (Auto-Cleaned) ### Reactive Component
```javascript ```javascript
const Counter = (initial = 0) => { const Counter = () => {
const count = $(initial); const count = $(0);
return Tag('div', { class: 'flex gap-2' }, [ return div({ class: 'flex gap-2' }, [
Button({ onclick: () => count(count() - 1) }, '-'), button({ onClick: () => count(count() - 1) }, '-'),
Span(() => count()), span(() => count()),
Button({ onclick: () => count(count() + 1) }, '+') button({ onClick: () => count(count() + 1) }, '+')
]); ]);
}; };
``` ```
### When Manual Cleanup is Needed ### Manual Cleanup for External Resources
Only for external resources (intervals, sockets, third-party libs): Only needed for intervals, sockets, thirdparty libraries:
```javascript ```javascript
const Timer = () => { const Timer = () => {
const time = $(new Date().toLocaleTimeString()); const time = $(new Date().toLocaleTimeString());
const el = Tag('span', {}, () => time()); const el = span(() => time());
const interval = setInterval(() => time(new Date().toLocaleTimeString()), 1000); const interval = setInterval(() => time(new Date().toLocaleTimeString()), 1000);
el._cleanups.add(() => clearInterval(interval)); // Manual cleanup onUnmount(() => clearInterval(interval));
return el; return el;
}; };
``` ```
### `Tag` vs Tag Helpers
| Use | Recommendation |
|:---|:---|
| Standard tags (`div`, `span`) | `Div()`, `Span()` helpers |
| Reusable components | Function returning `Tag` |
| Dynamic tag names | `Tag(tagName, props, children)` |
> **Auto-cleanup**: `Tag` automatically destroys watchers, events, and nested components. Only add to `_cleanups` for external resources.
---
| State (`online`) | Rendered HTML | Memory Management |
| :--- | :--- | :--- |
| **`true`** | `<div class="flex..."><span class="w-3..."></span><p class="text-bold">John</p></div>` | Watcher active |
| **`false`** | `<div class="flex..."><span hidden class="w-3..."></span><p class="text-gray-400">John</p></div>` | Attribute synced |

View File

@@ -1,91 +1,124 @@
# Reactivity Control: `Watch( )` # Reactivity Control: `watch( )`
The `Watch` function is the reactive engine of SigPro. It allows you to execute code automatically when signals change. `Watch` is **polymorphic**: it can track dependencies automatically or follow an explicit list. The `watch` function is the reactive engine of SigPro. It allows you to execute code automatically when signals change. `watch` is **polymorphic**: it can track dependencies automatically or follow an explicit list.
## Function Signature ## Function Signature
```typescript ```typescript
// Automatic Mode (Magic Tracking) // Automatic Mode (Magic Tracking)
Watch(callback: Function): StopFunction watch(callback: Function): StopFunction
// Explicit Mode (Isolated Dependencies) // Explicit Mode (Isolated Dependencies)
Watch(deps: Signal[], callback: Function): StopFunction watch(deps: Signal[], callback: (values: any[]) => void): StopFunction
``` ```
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
| :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- |
| **`target / deps`** | `Function` | `Array` | Yes | Either the code to run (Auto) or an array of signals to watch (Explicit). | | **`callback`** (auto mode) | `Function` | Yes | The code to run. Any signal accessed inside becomes a dependency. |
| **`callback`** | `Function` | Only in Explicit | The code that will run when the `deps` change. | | **`deps`** (explicit mode) | `Signal[]` | Yes | An array of signals to watch explicitly. |
| **`callback`** (explicit mode) | `Function` | Yes | Runs when any of the `deps` change. Receives an array of their current values. |
**Returns:** A `StopFunction` that, when called, destroys the watcher and releases memory. **Returns:** A `StopFunction` that, when called, destroys the watcher and releases memory.
> **Availability:** `watch` is exported from the SigPro module. In **ESM** you must import it (`import { watch } from 'sigpro'`). In the **IIFE** classic script, it is automatically available on `window`. The examples below assume the function is already in scope.
--- ---
## Usage Patterns ## Usage Patterns
### 1. Automatic Mode (Default) ### 1. Automatic Mode (Default)
Any signal you "touch" inside the callback becomes a dependency. SigPro tracks them behind the scenes. Any signal you **touch** inside the callback becomes a dependency. SigPro tracks them behind the scenes.
```javascript ```javascript
const count = $(0); const count = $(0);
Watch(() => { watch(() => {
// Re-runs every time 'count' changes // Reruns every time 'count' changes
console.log(`Count is: ${count()}`); console.log(`Count is: ${count()}`);
}); });
``` ```
### 2. Explicit Mode (Advanced Cleanup) ### 2. Explicit Mode (Isolated)
This mode **isolates** execution. The callback only triggers when the signals in the array change. Any other signal accessed *inside* the callback will NOT trigger a re-run. This is the "gold standard" for Routers and heavy components. This mode **isolates** execution. The callback only triggers when the signals in the array change. Any other signal accessed *inside* the callback will **not** trigger a rerun. This is ideal for routers or performancecritical components.
```javascript ```javascript
const sPath = $("/home"); const path = $("/home");
const user = $("Admin"); const user = $("Admin");
Watch([sPath], () => { watch([path], ([newPath]) => {
// Only triggers when 'sPath' changes. // Only triggers when 'path' changes.
// Changes to 'user' will NOT trigger this, preventing accidental re-renders. // Changes to 'user' will NOT trigger this.
console.log(`Navigating to ${sPath()} as ${user()}`); console.log(`Navigating to ${newPath} as ${user()}`);
}); });
``` ```
### 3. Automatic Cleanup In explicit mode, the callback receives an array of current values corresponding to the `deps` order.
If your logic creates timers, event listeners, or other reactive effects, SigPro tracks them as "children" of the current watch. When the watcher re-runs or stops, it kills everything inside automatically.
### 3. Stopping a Watcher
Call the returned function to kill the watcher manually.
```javascript ```javascript
Watch(() => { const stop = watch(() => console.log(count()));
const timer = setInterval(() => console.log("Tick"), 1000);
// Register a manual cleanup if needed
// Or simply rely on SigPro to kill nested Watch() calls
return () => clearInterval(timer);
});
```
---
## Stopping a Watcher
Call the returned function to manually kill the watcher. This is essential for manual DOM injections (like Toasts) or long-lived background processes.
```javascript
const stop = Watch(() => console.log(count()));
// Later... // Later...
stop(); // The link between the signal and this code is physically severed. stop(); // Disconnects the watcher completely.
```
### 4. Automatic Cleanup Inside Effects
If your watcher creates timers, event listeners, or nested effects, SigPro tracks them as children and cleans them up automatically before rerunning or when stopped.
```javascript
watch(() => {
const timer = setInterval(() => console.log("tick"), 1000);
// No need to manually clear SigPro will dispose it when the watcher reruns or stops.
// (But you can also return a cleanup function if needed)
});
``` ```
--- ---
## Pro Tip: The Microtask Queue ## Batching & Microtask Queue
SigPro batches updates. If you update multiple signals in the same execution block, the watcher will only fire **once** at the end of the task.
SigPro batches reactive updates. If you modify several signals in the same synchronous block, the watcher will fire **only once**, after the task completes.
```javascript ```javascript
const a = $(0); const a = $(0);
const b = $(0); const b = $(0);
Watch(() => console.log(a(), b())); watch(() => console.log(a(), b()));
// This triggers only ONE re-run. // Triggers only ONE log: "1 2"
a(1); a(1);
b(2); b(2);
``` ```
This is achieved via `queueMicrotask`, ensuring optimal performance.
---
## Key Points
- **Function name:** `watch` (lowercase) exported from SigPro and also available globally (depending on environment).
- **Auto mode:** `watch(fn)` automatically tracks any signals read inside `fn`.
- **Explicit mode:** `watch([sig1, sig2], (values) => {...})` only reruns when listed signals change; callback receives an array of their new values.
- **Stop function:** returned by both modes; call it to dispose the effect and its children.
- **Batching:** multiple signal writes in one event loop tick trigger a single execution (microtask).
---
## Complete Example
```javascript
const count = $(0);
const step = $(1);
watch(() => {
console.log(`Count changed to ${count()}`);
});
watch([count, step], ([newCount, newStep]) => {
console.log(`Count=${newCount}, step=${newStep} (explicit)`);
});
count(5); // logs: auto + explicit
step(2); // logs: explicit only (auto does not track step)
```

132
docs/api/when.md Normal file
View File

@@ -0,0 +1,132 @@
# Reactive Branching: `when( )`
The `when` function is the reactive control flow operator in SigPro. It conditionally renders one of two branches (or nothing) based on a reactive condition. The inactive branch is completely removed from the DOM and its effects are destroyed, saving memory and CPU.
## Function Signature
```typescript
when(
condition: boolean | Signal<boolean> | (() => boolean),
thenBranch: Node | (() => Node),
elseBranch?: Node | (() => Node) | null
): HTMLElement
```
| Parameter | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **`condition`** | `boolean` / `Signal` / `() => boolean` | Yes | A value or reactive expression that determines which branch to show. |
| **`thenBranch`** | `Node` / `() => Node` | Yes | Content rendered when the condition is **truthy**. |
| **`elseBranch`** | `Node` / `() => Node` | No | Content rendered when the condition is **falsy**. Defaults to nothing. |
**Returns:** A `div` with `style="display:contents"` that acts as an anchor for the dynamic content. This element is part of the DOM and will be replaced/updated automatically.
> **Availability:** `when` is exported from the SigPro module. In **ESM** you must import it (`import { when } from 'sigpro'`). In the **IIFE** classic script, it is automatically available on `window`. The examples below assume the function is already in scope.
---
## Usage Patterns
### 1. Simple Toggle
```javascript
const isVisible = $(false);
div([
button({ onclick: () => isVisible(!isVisible()) }, "Toggle"),
when(isVisible,
p("Now you see me!"),
p("Now you don't...")
)
]);
```
### 2. With Functions (Lazy Initialization)
To avoid creating heavy components until they are actually needed, pass a function that returns the branch.
```javascript
when(() => user.isLoggedIn(),
() => DashboardComponent(), // Only created when logged in
() => LoginForm() // Only created when guest
);
```
### 3. Complex Conditions
`condition` can be any expression that returns a boolean it can read signals, computed values, or plain booleans.
```javascript
when(() => count() > 10 && status() === 'ready',
span("Threshold reached!")
);
```
### 4. Without an `else` branch
If no `elseBranch` is provided, nothing is rendered when the condition is falsy.
```javascript
when(loading,
div({ class: "spinner" }, "Loading...")
);
```
---
## Automatic Cleanup
`when` automatically manages the lifecycle of each branch:
- When the condition changes, the current branch is destroyed.
- All effects (`watch`), event listeners, and child `when`/`each` inside the destroyed branch are recursively disposed.
- The new branch is created and mounted.
- Memory leaks are prevented without any manual intervention.
---
## Best Practices
- **Use functions for expensive branches** `() => Component()` ensures the component is only created when the branch becomes active.
- **Avoid inline complex logic** keep conditions readable; extract to computed signals if needed.
- **No manual cleanup required** SigPro handles everything.
---
## Technical Comparison
| Feature | CSS `display: none` | `when` |
| :--- | :--- | :--- |
| **DOM presence** | Always present | Only active branch exists |
| **Event listeners** | Still attached | Removed |
| **Effects (`watch`)** | Still running | Destroyed |
| **Memory usage** | Higher | Optimised (only one branch alive) |
| **Cleanup** | Manual | Automatic |
---
## Complete Example
```javascript
const loggedIn = $(false);
const username = $("Guest");
const Profile = () => div([
h2(`Welcome, ${username()}`),
button({ onclick: () => loggedIn(false) }, "Logout")
]);
const LoginForm = () => div([
input({ placeholder: "Name", onInput: e => username(e.target.value) }),
button({ onclick: () => loggedIn(true) }, "Login")
]);
const App = () =>
div([
when(loggedIn,
() => Profile(),
() => LoginForm()
)
]);
mount(App, '#app');
```

121
docs/convert.js Normal file
View File

@@ -0,0 +1,121 @@
// src/convert.js
import { $ } from "./sigpro.js";
function html2sigpro(h, advanced = false) {
const B = new Set(["allowfullscreen", "async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "formnovalidate", "hidden", "ismap", "itemscope", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected", "truespeed"]), esc = (v) => v.replace(/"/g, "\\\""), bP = (el) => {
let a = [...el.attributes].map(({ name: n, value: v }) => /^on/i.test(n) ? `${n}: (e) => { ${v.replace(/\s+/g, " ").trim()} }` : B.has(n.toLowerCase()) && (!v || v == n) ? `${n}: true` : `${n}: "${esc(v)}"`);
return a.length ? `{ ${a.join(", ")} }` : "";
}, cN = (n, d = 0) => {
let s = " ".repeat(d);
if (n.nodeType == 3) {
let t = n.textContent;
return t.trim() ? `${s}"${esc(t)}"` : "";
}
if (n.nodeType == 1) {
let t = n.tagName.toLowerCase();
let classes = [];
let otherAttrs = [];
if (advanced) {
const classAttribute = Array.from(n.attributes).find((attr) => attr.name === "class");
if (classAttribute) {
classes = classAttribute.value.trim().split(/\s+/).filter((c2) => c2);
}
otherAttrs = [...n.attributes].filter((attr) => attr.name !== "class");
}
let p = "";
if (advanced && classes.length > 0) {
const classChain = classes.map((c2) => `.${c2.replace(/-/g, "_")}`).join("");
if (otherAttrs.length > 0) {
const otherProps = otherAttrs.map(({ name: n2, value: v }) => /^on/i.test(n2) ? `${n2}: (e) => { ${v.replace(/\s+/g, " ").trim()} }` : B.has(n2.toLowerCase()) && (!v || v == n2) ? `${n2}: true` : `${n2}: "${esc(v)}"`);
p = `${classChain}({ ${otherProps.join(", ")} })`;
} else {
p = classChain;
}
} else {
p = bP(n);
}
let c = [...n.childNodes].map((i) => cN(i, d + 1)).filter(Boolean);
if (!c.length) {
if (advanced && classes.length > 0 && otherAttrs.length === 0) {
return `${s}${t}${p}`;
}
return `${s}${t}(${p})`;
}
if (c.length == 1 && !c[0].includes(`
`)) {
if (advanced && classes.length > 0 && otherAttrs.length === 0 && !p.includes("{")) {
return `${s}${t}${p}(${c[0].trim()})`;
}
return `${s}${t}(${p ? p + ", " : ""}${c[0].trim()})`;
}
if (advanced && classes.length > 0 && otherAttrs.length === 0 && !p.includes("{")) {
return `${s}${t}${p}([
${c.join(`,
`)}
${s}])`;
}
return `${s}${t}(${p ? p + ", " : ""}[
${c.join(`,
`)}
${s}])`;
}
return "";
}, r = [...new DOMParser().parseFromString(h, "text/html").body.childNodes].map((n) => cN(n)).filter(Boolean);
return r.length == 1 ? r[0].trim() : `[
${r.join(`,
`)}
]`;
}
var converter = () => {
const inH = $("");
const setInH = (v) => inH(v);
const outS = $("");
const setOutS = (v) => outS(v);
const advanced = $(false);
const setAdvanced = (v) => advanced(v);
const cnv = () => {
try {
setOutS(html2sigpro(inH(), advanced()));
} catch (e) {
setOutS("Error: " + e.message);
}
};
const txS = "width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical", btS = "padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px";
return div({ style: "max-width:900px;margin:20px auto;font-family:sans-serif" }, [
h1("HTML → SigPro"),
label({ style: "display:block;font-weight:700" }, "HTML:"),
textarea({
style: txS,
placeholder: "HTML...",
value: inH,
oninput: (e) => {
setInH(e.target.value);
cnv();
}
}),
div({ style: "margin:10px 0;display:flex;align-items:center;gap:10px" }, [
button({ style: btS + ";background:#3b82f6;color:#fff", onclick: cnv }, "Convert"),
button({ style: btS + ";background:#d1d5db", onclick: () => {
setInH("");
setOutS("");
setAdvanced(false);
} }, "Clear"),
label({ style: "display:flex;align-items:center;gap:5px;cursor:pointer" }, [
input({ type: "checkbox", checked: advanced, onchange: (e) => {
setAdvanced(e.target.checked);
cnv();
} }),
span("Advanced (dot notation for classes)")
])
]),
div({ style: "display:flex;justify-content:space-between;align-items:center;margin-bottom:5px" }, [
span({ style: "font-weight:700" }, "Out:"),
button({ style: btS + ";background:#10b981;color:#fff", onclick: () => {
navigator.clipboard.writeText(outS());
alert("Copied!");
} }, "Copy")
]),
textarea({ style: txS + ";background:#f9fafb", readonly: true, value: outS, placeholder: "Result..." })
]);
};
window.html2sigpro = html2sigpro;
window.converter = converter;

11
docs/convert.md Normal file
View File

@@ -0,0 +1,11 @@
# HTML to SigPro Converter
Convert your existing HTML markup directly into SigPro Tag Helper syntax. Paste your HTML in the left panel and get clean, ready-to-use SigPro code on the right.
## Usage
<div id="sigpro-converter"></div>
```js
mount(window.converter, '#sigpro-converter');
```

View File

@@ -1,227 +0,0 @@
# Interactive Examples
Explore the power of SigPro through practical patterns. From basic reactivity to advanced composition.
NOTE: Here we use DaisyUI for styles.
---
## 1. Basic Reactivity
The classic counter. Notice how we use `$(0)` to create a signal and the `Button` tag helper to update it.
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
<div class="card-body">
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
<div id="demo-counter" class="bg-base-100 p-6 rounded-xl border border-base-300 flex items-center justify-center"></div>
</div>
</div>
```javascript
const Counter = () => {
const $count = $(0);
return Div({ class: 'flex gap-4 items-center' }, [
Button({ class: 'btn btn-circle btn-outline', onclick: () => $count(c => c - 1) }, "-"),
Span({ class: 'text-2xl font-mono w-12 text-center' }, $count),
Button({ class: 'btn btn-circle btn-primary', onclick: () => $count(c => c + 1) }, "+")
]);
};
Mount(Counter, '#demo-counter');
```
---
## 2. Derived State (Computed)
Signals can depend on other signals. The "Double" value updates automatically.
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
<div class="card-body">
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
<div id="demo-computed" class="bg-base-100 p-6 rounded-xl border border-base-300"></div>
</div>
</div>
```javascript
const ComputedDemo = () => {
const $count = $(10);
const $double = $(() => $count() * 2);
return Div({ class: 'space-y-4' }, [
Input({ type: 'range', min: 1, max: 100, class: 'range range-primary', value: $count }),
P({ class: 'text-center' }, ["Base: ", $count, " ⮕ ", Span({class: 'text-primary font-bold'}, ["Double: ", $double])])
]);
};
Mount(ComputedDemo, '#demo-computed');
```
---
## 3. Lists and Loops (For)
SigPro handles lists efficiently, only updating specific DOM nodes.
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
<div class="card-body">
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
<div id="demo-list" class="bg-base-100 p-6 rounded-xl border border-base-300"></div>
</div>
</div>
```javascript
const ListDemo = () => {
const $todos = $(['Learn SigPro', 'Build an App']);
const $input = $("");
const addTodo = () => {
if ($input()) {
$todos(prev => [...prev, $input()]);
$input("");
}
};
return Div([
Div({ class: 'flex gap-2 mb-4' }, [
Input({ class: 'input input-bordered flex-1', value: $input, placeholder: 'New task...' }),
Button({ class: 'btn btn-primary', onclick: addTodo }, "Add")
]),
Ul({ class: 'menu bg-base-200 rounded-box p-2' },
For($todos, (item) => Li([A(item)]), (item) => item)
)
]);
};
Mount(ListDemo, '#demo-list');
```
---
## 4. Conditional Rendering (If)
Toggle visibility without re-rendering the entire parent.
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
<div class="card-body">
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
<div id="demo-if" class="bg-base-100 p-6 rounded-xl border border-base-300 flex justify-center"></div>
</div>
</div>
```javascript
const ConditionalDemo = () => {
const $isVisible = $(false);
return Div({ class: 'text-center w-full' }, [
Button({ class: 'btn btn-outline mb-4', onclick: () => $isVisible(! $isVisible()) }, "Toggle Secret"),
If($isVisible,
() => Div({ class: 'p-4 bg-warning text-warning-content rounded-lg shadow-inner' }, "🤫 SigPro is Awesome!"),
() => Div({ class: 'p-4 opacity-50' }, "Nothing to see here...")
)
]);
};
Mount(ConditionalDemo, '#demo-if');
```
---
## 5. Persistent State
Pass a string as a second argument to `$(val, key)` to sync with `localStorage`.
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
<div class="card-body">
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
<div id="demo-persist" class="bg-base-100 p-6 rounded-xl border border-base-300"></div>
</div>
</div>
```javascript
const PersistDemo = () => {
const $name = $("Guest", "sigpro-demo-name");
return Div({ class: 'flex flex-col gap-2' }, [
H3({ class: 'text-lg font-bold' }, ["Hello, ", $name]),
Input({ class: 'input input-bordered', value: $name, placeholder: 'Type your name...' }),
P({ class: 'text-xs opacity-50' }, "Refresh the page to see magic!")
]);
};
Mount(PersistDemo, '#demo-persist');
```
<script>
(function() {
const initExamples = () => {
const counterTarget = document.querySelector('#demo-counter');
if (counterTarget && !counterTarget.hasChildNodes()) {
const Counter = () => {
const $count = $(0);
return Div({ class: 'flex gap-4 items-center' }, [
Button({ class: 'btn btn-circle btn-outline', onclick: () => $count(c => c - 1) }, "-"),
Span({ class: 'text-2xl font-mono w-12 text-center' }, $count),
Button({ class: 'btn btn-circle btn-primary', onclick: () => $count(c => c + 1) }, "+")
]);
};
Mount(Counter, counterTarget);
}
// 2. Computed
const computedTarget = document.querySelector('#demo-computed');
if (computedTarget && !computedTarget.hasChildNodes()) {
const ComputedDemo = () => {
const $count = $(10);
const $double = $(() => $count() * 2);
return Div({ class: 'space-y-4 w-full' }, [
Input({ type: 'range', min: 1, max: 100, class: 'range range-primary', value: $count }),
P({ class: 'text-center' }, ["Base: ", $count, " ⮕ ", Span({class: 'text-primary font-bold'}, ["Double: ", $double])])
]);
};
Mount(ComputedDemo, computedTarget);
}
// 3. List
const listTarget = document.querySelector('#demo-list');
if (listTarget && !listTarget.hasChildNodes()) {
const ListDemo = () => {
const $todos = $(['Learn SigPro', 'Build an App']);
const $input = $("");
const addTodo = () => { if ($input()) { $todos(prev => [...prev, $input()]); $input(""); } };
return Div([
Div({ class: 'flex gap-2 mb-4' }, [
Input({ class: 'input input-bordered flex-1', value: $input, placeholder: 'New task...' }),
Button({ class: 'btn btn-primary', onclick: addTodo }, "Add")
]),
Ul({ class: 'menu bg-base-200 rounded-box p-2' }, For($todos, (item) => Li([A(item)]), (item) => item))
]);
};
Mount(ListDemo, listTarget);
}
// 4. If
const ifTarget = document.querySelector('#demo-if');
if (ifTarget && !ifTarget.hasChildNodes()) {
const ConditionalDemo = () => {
const $isVisible = $(false);
return Div({ class: 'text-center w-full' }, [
Button({ class: 'btn btn-outline mb-4', onclick: () => $isVisible(! $isVisible()) }, "Toggle Secret"),
If($isVisible,
() => Div({ class: 'p-4 bg-warning text-warning-content rounded-lg' }, "🤫 SigPro is Awesome!"),
() => Div({ class: 'p-4 opacity-50' }, "Nothing to see here...")
)
]);
};
Mount(ConditionalDemo, ifTarget);
}
// 5. Persist
const persistTarget = document.querySelector('#demo-persist');
if (persistTarget && !persistTarget.hasChildNodes()) {
const PersistDemo = () => {
const $name = $("Guest", "sigpro-demo-name");
return Div({ class: 'flex flex-col gap-2' }, [
H3({ class: 'text-lg font-bold' }, ["Hello, ", $name]),
Input({ class: 'input input-bordered', value: $name }),
P({ class: 'text-xs opacity-50' }, "Refresh the page!")
]);
};
Mount(PersistDemo, persistTarget);
}
};
// Ejecutar inmediatamente y también en cada navegación de Docsify
initExamples();
if (window.$docsify) {
window.$docsify.plugins = [].concat(window.$docsify.plugins || [], (hook) => {
hook.doneEach(initExamples);
});
}
})();
</script>

View File

@@ -1,14 +1,21 @@
<!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>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify/lib/themes/vue.css"> <link
rel="stylesheet"
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" /> href="//cdn.jsdelivr.net/npm/docsify/lib/themes/vue.css"
/>
<link href="./sigpro.ui.css" rel="stylesheet" type="text/css" />
<!-- <link
href="https://cdn.jsdelivr.net/npm/daisyui@5"
rel="stylesheet"
type="text/css"
/> -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head> </head>
@@ -17,21 +24,58 @@
<script> <script>
window.$docsify = { window.$docsify = {
name: 'SigPro', name: "SigPro",
repo: '', repo: "",
loadSidebar: true, loadSidebar: true,
subMaxLevel: 0, subMaxLevel: 0,
sidebarDisplayLevel: 1, sidebarDisplayLevel: 1,
executeScript: true, executeScript: true,
copyCode: { copyCode: {
buttonText: '<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>', buttonText:
errorText: 'Error', '<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>',
successText: '<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><polyline points="20 6 9 17 4 12"></polyline></svg>' errorText: "Error",
} successText:
'<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><polyline points="20 6 9 17 4 12"></polyline></svg>',
},
search: {
placeholder: "Type to search",
noData: "No Results!",
depth: 3,
hideOtherSidebarContent: true,
},
plugins: [
function (hook, vm) {
hook.doneEach(function () {
const codeBlocks = document.querySelectorAll(
'pre[data-lang="js"] code',
);
codeBlocks.forEach((code) => {
try {
const scriptContent = `(function() { ${code.innerText} }).call(window);`;
const runDemo = new Function(scriptContent);
runDemo();
} catch (err) {
console.error("Error ejecutando demo de SigPro:", err);
} }
});
});
},
],
};
</script> </script>
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
<script type="module">
import * as SigPro from "./sigpro.js";
Object.assign(window, SigPro);
import "./sigpro.tags.js";
import "./sigpro.ui.js";
<script src="./sigpro.js"></script> import("./sigpro.convert.js").then(() => {
console.log("SigPro y Convert cargados correctamente.");
});
// document.documentElement.setAttribute("data-theme", "splight");
</script>
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify/lib/docsify.min.js"></script> <script src="//cdn.jsdelivr.net/npm/docsify/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>

View File

@@ -12,106 +12,62 @@ Choose the method that best fits your workflow:
```bash ```bash
npm install sigpro npm install sigpro
```
</div> or
<input type="radio" name="install_method" class="tab border-base-300" aria-label="pnpm" />
<div class="tab-content bg-base-100 border-base-300 rounded-box p-6">
```bash
pnpm add sigpro
```
</div>
<input type="radio" name="install_method" class="tab border-base-300" aria-label="yarn" />
<div class="tab-content bg-base-100 border-base-300 rounded-box p-6">
```bash
yarn add sigpro
```
</div>
<input type="radio" name="install_method" class="tab border-base-300" aria-label="bun" />
<div class="tab-content bg-base-100 border-base-300 rounded-box p-6">
```bash
bun add sigpro bun add sigpro
``` ```
</div> </div>
<input type="radio" name="install_method" class="tab border-base-300 whitespace-nowrap" aria-label="CDN (ESM)" />
<input type="radio" name="install_method" class="tab border-base-300 whitespace-nowrap" aria-label="CDN" />
<div class="tab-content bg-base-100 border-base-300 rounded-box p-6"> <div class="tab-content bg-base-100 border-base-300 rounded-box p-6">
```html ```html
<script type="module"> <script type="module">
// Import the core and UI components
import SigPro from "[https://cdn.jsdelivr.net/npm/sigpro@latest/+esm](https://cdn.jsdelivr.net/npm/sigpro@latest/+esm)";
import { UI } from "[https://cdn.jsdelivr.net/npm/sigpro@latest/ui/+esm](https://cdn.jsdelivr.net/npm/sigpro@latest/ui/+esm)";
// Initialize UI components globally import { $, h, mount } from "https://cdn.jsdelivr.net/npm/sigpro@latest/dist/sigpro.esm.min.js";
UI($);
const count = $(0);
mount(() => h("h1", {}, () => `Count: ${count()}`), "#app");
</script> </script>
``` ```
</div> </div>
</div> </div>
<input type="radio" name="install_method" class="tab border-base-300 whitespace-nowrap" aria-label="CDN (ESM)" />
<div class="tab-content bg-base-100 border-base-300 rounded-box p-6">
<pre class="bg-base-200 p-4 rounded-lg"><code class="language-html">&lt;script type="module"&gt;
// Import the core and UI components
import SigPro from 'https://cdn.jsdelivr.net/npm/sigpro@latest/+esm';
import { UI } from 'https://cdn.jsdelivr.net/npm/sigpro@latest/ui/+esm';
// Initialize UI components globally
UI($);
&lt;/script&gt;</code></pre>
</div>
</div>
--- ---
## 2. Quick Start Examples ## 2. Quick Start Examples
SigPro uses **PascalCase** for Tag Helpers (e.g., `Div`, `Button`) to provide a clean, component-like syntax without needing JSX. SigPro uses **lowercase** Tag Helpers (e.g., `div`, `button`) to keep the syntax close to raw HTML, while still being pure JavaScript functions.
<div class="tabs tabs-box w-full mt-8 mb-12 bg-base-200/50 p-2 rounded-xl border border-base-300"> <div class="tabs tabs-box w-full mt-8 mb-12 bg-base-200/50 p-2 rounded-xl border border-base-300">
<input type="radio" name="quick_start_tabs" class="tab !rounded-lg" aria-label="Mainstream (Bundlers)" checked /> <input type="radio" name="quick_start_tabs" class="tab !rounded-lg" aria-label="ESM" checked />
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2"> <div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
```javascript ```javascript
// File: App.js // App.js Use named imports for the core, activate helpers if needed
import "sigpro"; import { $, mount } from "sigpro";
export const App = () => { const App = () => {
const $count = $(0); const count = $(0);
return div({ class: "card p-4" }, [
// Tag Helpers like Div, H1, Button are available globally h1(() => `Count is: ${count()}`),
return Div({ class: "card p-4" }, [ button(
H1(["Count is: ", $count]), { class: "btn btn-primary", onclick: () => count(count() + 1) },
Button(
{
class: "btn btn-primary",
onclick: () => $count((c) => c + 1),
},
"Increment", "Increment",
), ),
]); ]);
}; };
// File: main.js mount(App, "#app");
import "sigpro";
import { App } from "./App.js";
Mount(App, "#app");
``` ```
</div> </div>
<input type="radio" name="quick_start_tabs" class="tab !rounded-lg" aria-label="Classic (Direct CDN)" /> <input type="radio" name="quick_start_tabs" class="tab !rounded-lg" aria-label="CDN" />
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2"> <div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
```html ```html
@@ -121,23 +77,20 @@ Mount(App, "#app");
<div id="app"></div> <div id="app"></div>
<script type="module"> <script type="module">
import SigPro from "https://cdn.jsdelivr.net/npm/sigpro@latest/+esm"; // Import the core
import { $, h, mount } from "https://cdn.jsdelivr.net/npm/sigpro@latest/dist/sigpro.esm.min.js";
const $name = $("Developer"); const name = $("Developer");
// No need to import Div, Section, H2, Input... they are global!
const App = () => const App = () =>
Section({ class: "container" }, [ section({ class: "container" }, [
H2(["Welcome, ", $name]), h2(() => `Welcome, ${name()}`),
Input({ input({
type: "text", type: "text",
class: "input input-bordered", class: "input input-bordered",
$value: $name, // Automatic two-way binding value: name,
placeholder: "Type your name...", placeholder: "Type your name...",
}), }),
]); ]);
mount(App, "#app");
Mount(App, "#app");
</script> </script>
</body> </body>
</html> </html>
@@ -148,56 +101,25 @@ Mount(App, "#app");
--- ---
## 3. Global by Design ## 3. Why no build step?
One of SigPro's core strengths is its **Global API**, which eliminates "Import Hell" while remaining ESM-compatible.
- **The "Zero-Config" Import:** By simply adding `import SigPro from "sigpro"`, the framework automatically "hydrates" the global `window` object.
- **Core Functions:** You get immediate access to `$`, `Watch`, `Tag`, `If`, `For`, and `Router` anywhere in your scripts without using the `SigPro.` prefix.
- **Auto-Installation:** This happens instantly upon import thanks to its built-in `install()` routine, making it "Plug & Play" for both local projects and CDN usage.
- **PascalCase Tag Helpers:** Standard HTML tags are pre-registered as global functions (`Div`, `Span`, `Button`, `Section`, etc.).
- **Clean UI Syntax:** This allows you to write UI structures that look like HTML but are pure, reactive JavaScript: `Div({ class: "card" }, [ H1("Title") ])`.
- **Hybrid Tree Shaking:** \* For **Maximum Speed**, use `import SigPro from "sigpro"`.
- For **Maximum Optimization**, you can still use Named Imports: `import { $, Tag } from "sigpro"`. This allows modern bundlers like Vite to prune unused code while keeping your core reactive.
- **Custom Components:** We recommend using **PascalCase** for your own components (e.g., `UserCard()`) to maintain visual consistency with the built-in Tag Helpers and distinguish them from standard logic.
---
## 4. Why no build step?
Because SigPro uses **native ES Modules** and standard JavaScript functions to generate the DOM, you don't actually _need_ a compiler like Babel or a transformer for JSX. Because SigPro uses **native ES Modules** and standard JavaScript functions to generate the DOM, you don't actually _need_ a compiler like Babel or a transformer for JSX.
- **Development:** Just save and refresh. Pure JS, no "transpilation" required. - **Development:** Just save and refresh. Pure JS, no "transpilation" required.
- **Performance:** Extremely lightweight. Use any modern bundler (Vite, esbuild) only when you are ready to minify and tree-shake for production. - **Performance:** Extremely lightweight. Use any modern bundler (Vite, esbuild) only when you are ready to minify and tree-shake for production.
## 5. Why SigPro? (The Competitive Edge) ## 4. Key Advantages
SigPro stands out by removing the "Build Step" tax and the "Virtual DOM" overhead. It is the closest you can get to writing raw HTML/JS while maintaining modern reactivity. - **Extreme Performance**: No Virtual DOM reconciliation. SigPro updates the specific node or attribute instantly when a signal changes.
| Feature | **SigPro** | **SolidJS** | **Svelte** | **React** | **Vue** |
| :----------------- | :--------------- | :----------- | :----------- | :---------- | :---------- |
| **Bundle Size** | **~2KB** | ~7KB | ~4KB | ~40KB+ | ~30KB |
| **DOM Strategy** | **Direct DOM** | Direct DOM | Compiled DOM | Virtual DOM | Virtual DOM |
| **Reactivity** | **Fine-grained** | Fine-grained | Compiled | Re-renders | Proxies |
| **Build Step** | **Optional** | Required | Required | Required | Optional |
| **Learning Curve** | **Minimal** | Medium | Low | High | Medium |
| **Initialization** | **Ultra-Fast** | Very Fast | Fast | Slow | Medium |
---
## 6. Key Advantages
- **Extreme Performance**: No Virtual DOM reconciliation. SigPro updates the specific node or attribute instantly when a Signal changes.
- **Fine-Grained Reactivity**: State changes only trigger updates where the data is actually used, not on the entire component. - **Fine-Grained Reactivity**: State changes only trigger updates where the data is actually used, not on the entire component.
- **Native Web Standards**: Everything is a standard JS function. No custom template syntax to learn. - **Native Web Standards**: Everything is a standard JS function. No custom template syntax to learn.
- **Zero Magic**: No hidden compilers. What you write is what runs in the browser. - **Zero Magic**: No hidden compilers. What you write is what runs in the browser.
- **Global by Design**: Tag Helpers and the `$` function are available globally to eliminate "Import Hell" and keep your code clean. - **Global by Design** (with control): Tag helpers and core functions can be globally available (IIFE) or imported on demand (ESM) you choose.
## 7. Summary ---
SigPro isn't just another framework; it's a bridge to the native web. By using standard ES Modules and functional DOM generation, you gain the benefits of a modern library with the weight of a utility script. ## 5. Summary
SigPro isn't just another framework; it's a bridge to the native web. By using standard ES Modules and functional DOM generation, you get the benefits of a modern reactive library with the weight of a utility script.
**Because, in the end... why fight the web when we can embrace it?** **Because, in the end... why fight the web when we can embrace it?**

345
docs/router.md Normal file
View File

@@ -0,0 +1,345 @@
# Routing: `router( )` & Utilities
SigPro includes a builtin, lightweight **hash router** to create singlepage applications (SPA). It manages the URL hash, matches components to routes with dynamic segments (`:id`), and automatically cleans up each page when you navigate away.
## Function Signature
```typescript
router(routes: Route[]): HTMLElement
```
### Route Object
| Property | Type | Description |
| :--- | :--- | :--- |
| **`path`** | `string` | The URL fragment pattern (e.g. `"/"`, `"/user/:id"`, or `"*"` for catchall). |
| **`component`** | `Function` | A function that returns a Node, a string, or a reactive view. Receives `params` object as argument. |
**Returns:** A `div` element (with class `"router-hook"`) that acts as the router outlet. The router automatically destroys the previous view and mounts the matched component when the hash changes.
> You must import them (`import { router } from 'sigpro/router'`). The examples below assume the functions are already in scope.
---
## Usage Patterns
### 1. Defining Routes
Place the `router` element where you want the page content to appear. Inside the routes array, define your routes.
```javascript
// remember import router
import { router } from 'sigpro/router'
const Home = () => h1("Home Page");
const UserProfile = (params) => h1(`User ID: ${params.id}`);
const NotFound = () => h1("404 Page not found");
const App = () =>
div({ class: "app-layout" }, [
nav([
a({ href: "#/" }, "Home"),
a({ href: "#/user/42" }, "User 42")
]),
router([
{ path: "/", component: Home },
{ path: "/user/:id", component: UserProfile },
{ path: "*", component: NotFound }
])
]);
mount(App, "#app");
```
### 2. Dynamic Segments (`:id`)
When a route contains a colonprefixed segment (like `:id`), the router extracts the corresponding value from the current hash and passes it as a property inside the `params` object to the component function.
```javascript
// If the hash is #/user/42
const UserProfile = (params) => {
console.log(params.id); // "42"
return div(`User ${params.id}`);
};
```
### 3. Accessing Route Parameters Anywhere
The router maintains a reactive signal `router.params` that always holds the parameters of the currently matched route. You can read it anywhere in your app.
```javascript
watch(() => {
const params = router.params();
console.log("Current route params:", params);
});
```
---
## Navigation Utilities
SigPro provides several helper functions to control navigation and read the router state.
### `router.to(path)`
Navigates to the given path. It automatically formats the hash (e.g. `"/dashboard"` becomes `"#/dashboard"`). You can pass either a full hash string or a path without the `#`.
```javascript
button({ onclick: () => router.to("/dashboard") }, "Go to Dashboard")
```
### `router.back()`
Goes back one step in the browsers history, just like calling `history.back()`.
```javascript
button({ onclick: () => router.back() }, "← Back")
```
### `router.path()`
Returns the current route path **without the leading `#`**. This is a plain string, not a signal.
```javascript
console.log(router.path()); // e.g. "/user/42"
```
---
## Automatic Cleanup
Every time you navigate to a new route, the router calls `.destroy()` on the previous view. This recursively disposes of:
- All `watch` effects created inside that page
- All event listeners attached via SigPros event binding
- Any nested `when`, `each`, or `router` instances
**No manual cleanup is required** memory leaks are prevented automatically.
---
## Reactive Route Parameters
`router.params` is a **reactive signal** (created with `$({})`). You can watch it to react to parameter changes without remounting the whole router outlet.
```javascript
watch(() => router.params(), (params) => {
console.log("Params changed:", params);
// e.g. fetch new data when the :id changes
});
```
---
## Styling the Router Outlet
The router returns a `div` with the class `"router-hook"`. You can style it just like any other element:
```css
.router-hook {
display: block;
min-height: 60vh;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
```
If you want the router outlet to have no layout impact, you can set `display: contents` on it.
---
## Complete Example
```javascript
import { mount } from 'sigpro';
const Home = () => div("Welcome home");
const About = () => div("About us");
const User = (params) => div(`User profile: ${params.id}`);
const App = () =>
div([
nav([
a({ href: "#/" }, "Home"),
a({ href: "#/about" }, "About"),
a({ href: "#/user/5" }, "User 5")
]),
router([
{ path: "/", component: Home },
{ path: "/about", component: About },
{ path: "/user/:id", component: User },
{ path: "*", component: () => div("404 Not found") }
])
]);
mount(App, "#app");
```
---
## Summary
| Function | Description |
| :--- | :--- |
| `router(routes)` | Creates a router outlet. |
| `router.to(path)` | Navigates to a new hash route. |
| `router.back()` | Goes back in history. |
| `router.path()` | Returns the current path without `#`. |
| `router.params()` | Reactive signal of the current route parameters. |
---
# Vite Plugin: File-based Routing
The `sigproRouter` plugin for Vite automates route generation by scanning your `pages` directory. It creates a **virtual module** that you can import directly into your code, eliminating the need to maintain a manual routes array.
## 1. Project Structure
To use the plugin, organize your files within the `src/pages` directory. The folder hierarchy directly determines your application's URL structure. SigPro uses brackets `[param]` for dynamic segments.
<div class="mockup-code bg-base-300 text-base-content shadow-xl my-8">
<pre><code>my-sigpro-app/
├── src/
│ ├── pages/
│ │ ├── index.js → #/
│ │ ├── about.js → #/about
│ │ ├── users/
│ │ │ └── [id].js → #/users/:id
│ │ └── blog/
│ │ ├── index.js → #/blog
│ │ └── [slug].js → #/blog/:slug
│ ├── App.js (Main Layout)
│ └── main.js (Entry Point)
├── vite.config.js
└── package.json</code></pre>
</div>
---
## 2. Setup & Configuration
Add the plugin to your `vite.config.js`. It works out of the box with zero configuration.
```javascript
// vite.config.js
import { defineConfig } from 'vite';
import { sigproRouter } from 'sigpro/vite';
export default defineConfig({
plugins: [sigproRouter()]
});
```
---
## 3. Implementation
Thanks to **SigPro's synchronous initialization**, you no longer need to wrap your mounting logic in `.then()` blocks.
<div class="tabs tabs-box w-full mt-8 mb-12 bg-base-200/50 p-2 border border-base-300">
<input type="radio" name="route_impl" class="tab" aria-label="Option A: Direct in main.js" checked />
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
```javascript
// src/main.js
import { mount } from 'sigpro';
import { router } from 'sigpro/utils';
import { routes } from 'virtual:sigpro-routes';
// The Core already has Router ready
mount(router(routes), '#app');
```
</div>
<input type="radio" name="route_impl" class="tab" aria-label="Option B: Inside App.js (Persistent Layout)" />
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
```javascript
// src/App.js
import { routes } from 'virtual:sigpro-routes';
export default () => div({ class: 'layout' }, [
header([
h1("SigPro App"),
nav([
button({ onclick: () => Router.go('/') }, "Home"),
button({ onclick: () => Router.go('/blog') }, "Blog")
])
]),
// Only the content inside <main> will be swapped reactively
main(Router(routes))
]);
```
</div>
</div>
---
## 4. Route Mapping Reference
The plugin follows a simple convention to transform your file system into a routing map.
<div class="overflow-x-auto my-8">
<table class="table table-zebra w-full">
<thead class="bg-base-200">
<tr>
<th>File Path</th>
<th>Generated Path</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>index.js</code></td>
<td class="font-mono text-primary font-bold">/</td>
<td>The application root.</td>
</tr>
<tr>
<td><code>about.js</code></td>
<td class="font-mono text-primary font-bold">/about</td>
<td>A static page.</td>
</tr>
<tr>
<td><code>[id].js</code></td>
<td class="font-mono text-primary font-bold">/:id</td>
<td>Dynamic parameter (passed to the component).</td>
</tr>
<tr>
<td><code>blog/index.js</code></td>
<td class="font-mono text-primary font-bold">/blog</td>
<td>Folder index page.</td>
</tr>
<tr>
<td><code>_utils.js</code></td>
<td class="italic opacity-50 text-error">Ignored</td>
<td>Files starting with <code>_</code> are excluded from routing.</td>
</tr>
</tbody>
</table>
</div>
---
## 5. How it Works (Vite Virtual Module)
The plugin generates a virtual module named `virtual:sigpro-routes`. This module exports an array of objects compatible with `Router()`:
```javascript
// Internal representation generated by the plugin
export const routes = [
{ path: '/', component: () => import('/src/pages/index.js') },
{ path: '/users/:id', component: () => import('/src/pages/users/[id].js') },
// ...
];
```
Because it uses dynamic `import()`, Vite automatically performs **Code Splitting**, meaning each page is its own small JS file that only loads when the user navigates to it.

34
docs/sigpro.convert.js Normal file
View File

@@ -0,0 +1,34 @@
var{$:x}=window.SigPro;function f(s,l="tags"){let a=new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected","truespeed"]),p=(o)=>o.replace(/"/g,"\\\""),c=(o)=>{let t=[...o.attributes].map(({name:e,value:n})=>/^on/i.test(e)?`${e}: (e) => { ${n.replace(/\s+/g," ").trim()} }`:a.has(e.toLowerCase())&&(!n||n==e)?`${e}: true`:`${e}: "${p(n)}"`);return t.length?`{ ${t.join(", ")} }`:""},m=(o,t=0)=>{let e=" ".repeat(t);if(o.nodeType==3){let n=o.textContent;return n.trim()?`${e}"${p(n)}"`:""}if(o.nodeType==1){let n=o.tagName.toLowerCase(),d=c(o),i=l==="core"?`h('${n}'`:n,r=[...o.childNodes].map((h)=>m(h,t+1)).filter(Boolean),g=!!d;if(l==="core"){if(!r.length)return g?`${e}${i}, ${d})`:`${e}${i})`;if(r.length===1&&!r[0].includes(`
`))return g?`${e}${i}, ${d}, ${r[0].trim()})`:`${e}${i}, ${r[0].trim()})`;return g?`${e}${i}, ${d}, [
${r.join(`,
`)}
${e}])`:`${e}${i}, [
${r.join(`,
`)}
${e}])`}else{if(!r.length)return g?`${e}${i}(${d})`:`${e}${i}`;if(r.length===1&&!r[0].includes(`
`))return g?`${e}${i}(${d}, ${r[0].trim()})`:`${e}${i}(${r[0].trim()})`;return g?`${e}${i}(${d}, [
${r.join(`,
`)}
${e}])`:`${e}${i}([
${r.join(`,
`)}
${e}])`}}return""},u=[...new DOMParser().parseFromString(s,"text/html").body.childNodes].map((o)=>m(o)).filter(Boolean);return u.length==1?u[0].trim():`[
${u.join(`,
`)}
]`}var b=()=>{let s=x(""),l=x(""),a=x("tags"),p=x(""),c=()=>{try{l(f(s(),a()))}catch(t){l("Error: "+t.message)}p(s())},m=()=>{s(""),l(""),a("tags"),p("")},u="width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical",o="padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px";return div({style:"margin:20px auto;font-family:sans-serif"},[h1("HTML → SigPro"),div({style:"margin-bottom:10px"},[div({style:"display:flex;gap:20px;flex-wrap:wrap;margin-top:5px"},[label({style:"display:flex;align-items:center;gap:6px"},["Core",input({type:"radio",name:"mode",value:"core",checked:a()==="core",onchange:(t)=>{if(t.target.checked)a("core"),c()}}),span("core — h('tag', props, ...)")]),label({style:"display:flex;align-items:center;gap:6px"},["Tags",input({type:"radio",name:"mode",value:"tags",checked:a()==="tags",onchange:(t)=>{if(t.target.checked)a("tags"),c()}}),span("tags — tag({ props }, ...)")])])]),div({style:"margin-top:15px;display:flex;gap:10px"},[button({style:"padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px;background:#3b82f6;color:#fff",onclick:c},"Convert"),button({style:"padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px;background:#d1d5db",onclick:m},"Clear")]),div({style:"display:grid;grid-template-columns:1fr;gap:15px;margin-top:15px;width:100%"},[div({style:"border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column"},[label({style:"font-weight:bold;margin-bottom:8px"},"HTML Input"),textarea({style:"width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical",placeholder:"Paste your HTML here...",value:s,oninput:(t)=>{s(t.target.value),c()}})]),div({style:"border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column"},[div({style:"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px"},[span({style:"font-weight:bold"},"SigPro Output"),button({style:"padding:4px 8px;background:#10b981;color:white;border:none;border-radius:4px;cursor:pointer;font-size:12px",onclick:()=>{navigator.clipboard.writeText(l()),alert("Copied!")}},"Copy")]),textarea({style:"width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical;background:#f9fafb",readonly:!0,value:l,placeholder:"Converted code will appear here..."})]),div({style:"border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column"},[label({style:"font-weight:bold;margin-bottom:8px"},"Live Preview"),iframe({style:"width:100%;height:200px;border:1px solid #e2e8f0;border-radius:4px;background:white;",srcdoc:()=>{return`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style>
body { padding: 10px; margin: 0; font-family: sans-serif; }
</style>
</head>
<body>
${p()||""}
</body>
</html>
`},sandbox:"allow-same-origin allow-scripts allow-popups allow-forms allow-modals"})])])])};window.html2sigpro=f;window.converter=b;

1
docs/sigpro.db.js Normal file
View File

@@ -0,0 +1 @@
var o=async(e,n={},t=null)=>{if(t)t(!0);try{let r=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n),credentials:"include"});if(!r.ok){let s=await r.text();throw Error(`Error ${r.status}: ${s}`)}return await r.json()}finally{if(t)t(!1)}};export{o as db};

1
docs/sigpro.editor.js Normal file

File diff suppressed because one or more lines are too long

78
docs/sigpro.grid.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
docs/sigpro.locale.js Normal file
View File

@@ -0,0 +1 @@
var{$:c}=window.SigPro,s=c("en"),n={},e=(t)=>{for(let o of Object.keys(t)){if(!n[o])n[o]={};Object.assign(n[o],t[o])}},r=(t)=>{if(t&&n[t])s(t)},a=(t)=>{return()=>n[s()]?.[t]??t};export{a as t,r as setLocale,e as addLang};

1
docs/sigpro.router.js Normal file
View File

@@ -0,0 +1 @@
var{$:p,h:u,watch:m,render:g,isF:y}=window.SigPro,d=()=>window.location.hash.slice(1)||"/",s=p(d());window.addEventListener("hashchange",()=>s(d()));var w=p({}),c=(n)=>{let i=u("div",{class:"router-hook"}),r=null;return m([s],()=>{let l=s(),t=n.find((o)=>{let e=o.path.split("/").filter(Boolean),a=l.split("/").filter(Boolean);return e.length===a.length&&e.every((h,f)=>h[0]===":"||h===a[f])})||n.find((o)=>o.path==="*");if(t){r?.destroy();let o={};t.path.split("/").filter(Boolean).forEach((e,a)=>{if(e[0]===":")o[e.slice(1)]=l.split("/").filter(Boolean)[a]}),w(o),r=g(()=>y(t.component)?t.component(o):t.component),i.replaceChildren(r.container)}}),i.destroy=()=>{r?.destroy()},i};c.params=w;c.to=(n)=>window.location.hash=n.replace(/^#?\/?/,"#/");c.back=()=>window.history.back();c.path=()=>s();export{w as routerParams,c as router};

1
docs/sigpro.tags.js Normal file
View File

@@ -0,0 +1 @@
var{h:a}=window.SigPro;if(typeof window<"u")"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((e)=>{window[e]=(t,s)=>a(e,t,s)});

2
docs/sigpro.ui.css Normal file

File diff suppressed because one or more lines are too long

1
docs/sigpro.ui.js Normal file

File diff suppressed because one or more lines are too long

1
docs/sigpro.utils.js Normal file
View File

@@ -0,0 +1 @@
var{$:d,h:m,watch:g,render:x,isF:b}=window.SigPro,l=(t)=>{let e=()=>window.location.hash.slice(1)||"/",o=d(e()),n=()=>o(e());window.addEventListener("hashchange",n);let s=m("div",{class:"router-hook"}),h=null;return g([o],()=>{let f=o(),a=t.find((r)=>{let c=r.path.split("/").filter(Boolean),p=f.split("/").filter(Boolean);return c.length===p.length&&c.every((w,y)=>w[0]===":"||w===p[y])})||t.find((r)=>r.path==="*");if(a){h?.destroy();let r={};a.path.split("/").filter(Boolean).forEach((c,p)=>{if(c[0]===":")r[c.slice(1)]=f.split("/").filter(Boolean)[p]}),l.params(r),h=x(()=>b(a.component)?a.component(r):a.component),s.replaceChildren(h.container)}}),s.destroy=()=>{window.removeEventListener("hashchange",n),h?.destroy()},s};l.params=d({});l.to=(t)=>window.location.hash=t.replace(/^#?\/?/,"#/");l.back=()=>window.history.back();l.path=()=>window.location.hash.replace(/^#/,"")||"/";var k=async(t,e={},o=null)=>{if(o)o(!0);try{let n=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e),credentials:"include"});if(!n.ok){let s=await n.text();throw Error(`Error ${n.status}: ${s}`)}return await n.json()}finally{if(o)o(!1)}},u=d("en"),i={},v=(t)=>{for(let e of Object.keys(t)){if(!i[e])i[e]={};Object.assign(i[e],t[e])}},E=(t)=>{if(t&&i[t])u(t)},L=(t)=>{return()=>i[u()]?.[t]??t};export{L as t,E as setLocale,l as router,k as db,v as addLang};

4
docs/sigpro.vite.js Normal file
View File

@@ -0,0 +1,4 @@
function g(){let u="\x00virtual:sigpro-routes",i=(e)=>{if(!fs.existsSync(e))return[];return fs.readdirSync(e,{recursive:!0}).filter((r)=>/\.(js|jsx)$/.test(r)&&!path.basename(r).startsWith("_")).map((r)=>path.resolve(e,r))},l=(e,r)=>{return("/"+path.relative(e,r).replace(/\\/g,"/").replace(/\.(js|jsx)$/,"").replace(/\/index$/,"").replace(/^index$/,"")).replace(/\/+/g,"/").replace(/\[\.\.\.([^\]]+)\]/g,"*").replace(/\[([^\]]+)\]/g,":$1").replace(/\/$/,"")||"/"};return{name:"sigpro-router",resolveId(e){if(e==="virtual:sigpro-routes")return u},load(e){if(e!==u)return;let r=process.cwd(),t=path.resolve(r,"src/pages"),p=i(t).sort((n,a)=>{let o=l(t,n),c=l(t,a);if(o.includes(":")&&!c.includes(":"))return 1;if(!o.includes(":")&&c.includes(":"))return-1;return c.length-o.length}),s="";if(p.forEach((n)=>{let a=l(t,n),o="./"+path.relative(r,n).replace(/\\/g,"/");s+=` { path: '${a}', component: () => import('/${o}') },
`}),!s.includes("path: '*'"))s+=` { path: '*', component: () => ({ default: () => document.createTextNode('404 - Not Found') }) },
`;return`export const routes = [
${s}];`}}}export{g as sigproRouter};

388
docs/ui.md Normal file
View File

@@ -0,0 +1,388 @@
<div id="ui"></div>
<div id="tab"></div>
<div id="file"></div>
<div id="demo-toast"></div>
```js
const fruta = $("");
const color = $("#3b82f6");
const fecha = $("");
const rango = $({ start: null, end: null });
const pais = $("");
const frutas = [
"Manzana",
"Pera",
"Plátano",
"Fresa",
"Mango",
"Sandía",
"Melón",
"Uva",
];
const paises = [
{ label: "🇪🇸 España", value: "ES" },
{ label: "🇲🇽 México", value: "MX" },
{ label: "🇦🇷 Argentina", value: "AR" },
{ label: "🇨🇴 Colombia", value: "CO" },
{ label: "🇨🇱 Chile", value: "CL" },
];
mount(
() =>
div({ class: "p-8 max-w-md mx-auto flex flex-col gap-4" }, [
h1({ class: "text-2xl font-bold" }, "Field Components"),
ui.autocomplete({
label: "Fruta favorita",
items: frutas,
value: fruta,
placeholder: "Buscar fruta...",
}),
ui.autocomplete({
label: "País",
items: paises,
value: pais,
placeholder: "Elige un país...",
}),
ui.datepicker({
label: "Fecha de nacimiento",
value: fecha,
placeholder: "Selecciona fecha...",
}),
ui.datepicker({
label: "Estancia",
range: true,
value: rango,
placeholder: "Check-in → Check-out",
}),
ui.colorpicker({
label: "Color favorito",
value: color,
placeholder: "Elige un color...",
}),
ui.password({}),
ui.theme(),
div(
{ class: "bg-base-200 rounded-box p-4 flex flex-col gap-2 text-sm" },
[
div({}, () => `🍎 Fruta: ${val(fruta) || "—"}`),
div({}, () => `🌍 País: ${val(pais) || "—"}`),
div({}, () => `📅 Fecha: ${val(fecha) || "—"}`),
div({}, () => {
const r = val(rango);
return r.start && r.end
? `🏨 Estancia: ${r.start}${r.end}`
: "🏨 Estancia: —";
}),
div({ class: "flex items-center gap-2" }, [
span({}, "🎨 Color:"),
div({
class: "w-6 h-6 rounded border border-base-300",
style: () => `background:${val(color)}`,
}),
]),
],
),
]),
"#ui",
);
const archivos = $([]);
const drag = $(false);
const error = $("");
const subiendo = $(false);
const progreso = $(0);
const MAX_SIZE = 5 * 1024 * 1024;
const MAX_FILES = 3;
const handleFiles = (files) => {
const arr = Array.from(files);
if (arr.length > MAX_FILES) {
error(`Máximo ${MAX_FILES} archivos`);
return;
}
const big = arr.find(f => f.size > MAX_SIZE);
if (big) {
error(`"${big.name}" supera los 5MB`);
return;
}
error("");
archivos(arr);
};
const subirArchivos = async () => {
const files = archivos();
if (!files.length) return toast("Selecciona archivos primero", "alert-warning");
subiendo(true);
progreso(0);
const formData = new FormData();
files.forEach(f => formData.append('files', f));
formData.append('carpeta', 'demo');
try {
const xhr = new XMLHttpRequest();
const promise = new Promise((resolve, reject) => {
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
progreso(Math.round((e.loaded / e.total) * 100));
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`Error ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('Error de conexión'));
});
xhr.open('POST', '/api/upload');
xhr.send(formData);
await promise;
toast(`${files.length} archivo(s) subidos`, "alert-success");
archivos([]);
progreso(0);
} catch (err) {
toast(`${err.message}`, "alert-error");
} finally {
subiendo(false);
}
};
const fileInputProps = {
type: "file",
class: "hidden",
multiple: true,
accept: "image/*,.pdf,.doc,.docx",
onchange: (e) => { handleFiles(e.target.files); e.target.value = ''; }
};
mount(
() => div({ class: "p-8 max-w-md mx-auto flex flex-col gap-4" }, [
h1({ class: "text-2xl font-bold" }, "📁 Upload Files"),
// Zona drag & drop
h("label", {
class: () => `relative flex items-center justify-between h-14 px-4 border-2 border-dashed rounded-lg cursor-pointer transition-all ${
drag() ? 'border-primary bg-primary/10' : 'border-base-content/20 bg-base-100'
} ${subiendo() ? 'pointer-events-none opacity-50' : ''}`,
ondragover: (e) => { e.preventDefault(); if (!subiendo()) drag(true); },
ondragleave: () => drag(false),
ondrop: (e) => {
e.preventDefault();
drag(false);
if (subiendo()) return;
handleFiles(e.dataTransfer.files);
}
}, [
h("div", { class: "flex items-center gap-3" }, [
h("span", { class: "icon-[lucide--upload] w-5 h-5 text-base-content/60" }),
h("div", {}, [
h("div", { class: "text-sm font-medium" }, "Arrastra archivos aquí"),
h("div", { class: "text-xs text-base-content/50" }, `Máx ${MAX_FILES} archivos · ${MAX_SIZE / 1024 / 1024}MB c/u`),
]),
]),
h("span", { class: "text-xs text-base-content/40" }, "o haz clic"),
h("input", fileInputProps),
]),
() => error() ? ui.fileError({ message: error() }) : null,
() => archivos().length > 0 ? h("div", { class: "space-y-3" }, [
h("div", { class: "flex flex-wrap gap-2" },
archivos().map((f, i) => {
const isImage = f.type?.startsWith('image/');
const url = isImage ? URL.createObjectURL(f) : null;
return h("div", {
class: "relative group rounded-lg overflow-hidden border border-base-300 bg-base-200 w-20"
}, [
isImage ? h("img", {
src: url,
class: "w-20 h-20 object-cover",
onload: () => url && URL.revokeObjectURL(url)
}) : h("div", {
class: "w-20 h-20 flex flex-col items-center justify-center gap-1"
}, [
h("span", { class: "text-2xl" }, f.type?.includes('pdf') ? "📕" : "📄"),
h("span", { class: "text-[8px] uppercase opacity-50" }, f.name?.split('.').pop()),
]),
h("div", { class: "p-1" }, [
h("div", { class: "text-[9px] truncate font-medium leading-tight" }, f.name),
h("div", { class: "text-[8px] opacity-50" }, `${~~(f.size / 1024)} KB`),
]),
!subiendo() ? h("button", {
class: "absolute top-0.5 right-0.5 btn btn-circle btn-ghost btn-xs opacity-0 group-hover:opacity-100 bg-base-100/80",
onclick: () => archivos(archivos().filter((_, idx) => idx !== i))
}, h("span", { class: "icon-[lucide--x] w-3 h-3" })) : null,
])
})
),
() => subiendo() ? h("div", { class: "space-y-1" }, [
h("div", { class: "flex justify-between text-xs" }, [
h("span", {}, "Subiendo..."),
h("span", {}, () => `${progreso()}%`),
]),
h("progress", { class: "progress progress-primary w-full", value: progreso, max: "100" }),
]) : null,
h("div", { class: "flex gap-2" }, [
h("button", {
class: "btn btn-ghost btn-sm",
onclick: () => { archivos([]); error(""); }
}, "🗑 Limpiar"),
h("button", {
class: "btn btn-primary btn-sm",
disabled: subiendo,
onclick: subirArchivos
}, () => subiendo() ? "⏳ Subiendo..." : "☁️ Subir al servidor"),
]),
]) : null,
]),
"#file",
);
const tabsSignal = $([
{ id: "a", label: "Tab A", content: "Content of tab A", open: true },
{ id: "b", label: "Tab B", content: "Content of tab B", closable: true },
{ id: "c", label: "Tab C", content: "Content of tab C" },
]);
mount(
() => ui.tabs(
{ class: "tabs-box" },
() => tabsSignal().flatMap((tab, i) =>
ui.tab({
name: "demo-tabs",
classContent: "bg-base-100 border-base-300 p-6",
label: tab.label,
content: tab.content,
checked: tab.open || false,
tabs: tabsSignal,
index: i,
closable: tab.closable || false,
})
)
),
"#tab",
);
mount(
div({ class: "flex flex-wrap gap-2" }, [
button({ class: "btn", onclick: () => toast("File saved!") }, "Simple"),
button(
{ class: "btn", onclick: () => toast("Error!", "alert-error", 5000) },
"Error (5s)",
),
button(
{
class: "btn",
onclick: () =>
toast(
div({ class: "flex items-center gap-2" }, [
span({ class: "icon-[lucide--check] text-lg" }),
span({}, "Report generated"),
]),
"alert-success",
),
},
"With icon",
),
button(
{
class: "btn",
onclick: () =>
toast(
div({ class: "flex flex-col" }, [
strong({}, "ATTENTION!"),
span({}, "Error saving!"),
button(
{
class: "btn btn-xs mt-1",
onclick: () => console.log("Retry"),
},
"Retry",
),
]),
"alert-warning",
7000,
),
},
"Complex",
),
]),
"#toast",
);
mount(
div({ class: "flex flex-wrap gap-2" }, [
button({ class: "btn", onclick: () => toast("File saved!") }, "Simple"),
button(
{ class: "btn", onclick: () => toast("Error!", "alert-error", 5000) },
"Error (5s)",
),
button(
{
class: "btn",
onclick: () =>
toast(
div({ class: "flex items-center gap-2" }, [
ui.icon("icon-[lucide--check]"),
span({}, "Report generated"),
]),
"alert-success",
),
},
"With icon",
),
button(
{
class: "btn",
onclick: () =>
toast(
div({ class: "flex flex-col" }, [
strong({}, "ATTENTION!"),
span({}, "Error saving!"),
button(
{
class: "btn btn-xs mt-1",
onclick: () => console.log("Retry"),
},
"Retry",
),
]),
"alert-warning",
7000,
),
},
"Complex",
),
]),
"#demo-toast",
);
```

View File

@@ -1,148 +0,0 @@
# UI Components `(WIP)`
> **Status: Work In Progress.**
> SigPro UI is a complete component environment built with SigPro, Tailwind CSS and DaisyUI. It provides smart components that handle their own internal logic, reactivity, and professional styling.
<div class="alert alert-info shadow-md my-10 border-l-8 border-info bg-info/10 text-info-content">
<div class="flex flex-col md:flex-row items-start gap-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6 mt-1"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<div>
<h2 class="font-black text-xl tracking-tight mb-2">📢 Official Documentation</h2>
<p class="text-sm opacity-90 leading-relaxed mb-2">
All official documentation, interactive examples, and usage guides are available at:
</p>
<div class="bg-base-100 p-3 rounded-lg inline-block">
<a href="https://natxocc.github.io/sigpro-ui/#/" target="_blank" class="text-info font-mono text-base font-bold hover:underline">
https://natxocc.github.io/sigpro-ui/#/
</a>
</div>
<p class="text-xs mt-4 opacity-70 italic">
* Documentation is actively being updated as components are released.
</p>
</div>
</div>
</div>
---
## 1. What are UI Components?
Unlike **Tag Helpers** (which are just functional mirrors of HTML tags), SigPro UI Components are smart abstractions:
* **Stateful**: They manage complex internal states (like date ranges, search filtering, or API lifecycles).
* **Reactive**: Attributes prefixed with `$` are automatically tracked via `Watch`.
* **Self-Sane**: They automatically use `._cleanups` to destroy observers or event listeners when removed from the DOM.
* **Themed**: Fully compatible with DaisyUI v5 theme system and Tailwind v4 utility classes.
---
## 2. Prerequisites
<div class="alert alert-warning shadow-md my-6 border-l-8 border-warning bg-warning/10">
<div class="flex items-start gap-3">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-5 h-5 mt-0.5"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
<div>
<p class="text-sm font-semibold">To ensure all components render correctly, your project must have:</p>
<ul class="flex flex-wrap gap-3 mt-2">
<li class="badge badge-warning badge-md">Tailwind CSS v4+</li>
<li class="badge badge-warning badge-md">DaisyUI v5+</li>
</ul>
</div>
</div>
</div>
---
## 3. The UI Registry (Available Now)
| Category | Components |
| :--- | :--- |
| **Forms & Inputs** | `Button`, `Input`, `Select`, `Autocomplete`, `Datepicker`, `Colorpicker`, `CheckBox`, `Radio`, `Range`, `Rating`, `Swap` |
| **Feedback** | `Alert`, `Toast`, `Modal`, `Loading`, `Badge`, `Tooltip`, `Indicator` |
| **Navigation** | `Navbar`, `Menu`, `Drawer`, `Tabs`, `Accordion`, `Dropdown` |
| **Data & Layout** | `Request`, `Response`, `List`, `Stack`, `Timeline`, `Stat`, `Fieldset`, `Fab` |
---
## 4. Examples with "Superpowers"
### A. The Declarative API Flow (`Request` & `Response`)
Instead of manually managing `loading` and `error` flags, use these together to handle data fetching elegantly.
```javascript
// 1. Define the request (it tracks dependencies automatically)
const userProfile = Request(
() => `https://api.example.com/user/${userId()}`
);
// 2. Render the UI based on the request state
Div({ class: "p-4" }, [
Response(userProfile, (data) =>
Div([
H1(data.name),
P(data.email)
])
)
]);
```
### B. Smart Inputs & Autocomplete
SigPro UI inputs handle labels, icons, password toggles, and validation states out of the box using DaisyUI v5 classes.
```javascript
const searchQuery = $("");
Autocomplete({
label: "Find a Country",
placeholder: "Start typing...",
options: ["Spain", "France", "Germany", "Italy", "Portugal"],
$value: searchQuery,
onSelect: (val) => console.log("Selected:", val)
});
```
### C. The Reactive Datepicker
Handles single dates or ranges with a clean, reactive interface that automatically syncs with your signals.
```javascript
const myDate = $(""); // or { start: "", end: "" } for range
Datepicker({
label: "Select Expiry Date",
$value: myDate,
range: false
});
```
### D. Imperative Toasts & Modals
Trigger complex UI elements from your logic. These components use `Mount` internally to ensure they are properly cleaned up from memory after they close.
```javascript
// Show a notification (Self-destroying after 3s)
Toast("Settings saved successfully!", "alert-success", 3000);
// Control a modal with a simple signal
const isModalOpen = $(false);
Modal({
$open: isModalOpen,
title: "Delete Account",
buttons: [
Button({ class: "btn-error", onclick: doDelete }, "Confirm")
]
}, "This action cannot be undone.");
```
---
## 5. Internationalization (i18n)
The UI library comes with a built-in locale system.
```javascript
// Set the global UI language
Locale("en");
// Access translated strings (Returns a signal that tracks the current locale)
const t = tt("confirm");
```

View File

@@ -1,146 +0,0 @@
# Vite Plugin: File-based Routing
The `sigproRouter` plugin for Vite automates route generation by scanning your `pages` directory. It creates a **virtual module** that you can import directly into your code, eliminating the need to maintain a manual routes array.
## 1. Project Structure
To use the plugin, organize your files within the `src/pages` directory. The folder hierarchy directly determines your application's URL structure. SigPro uses brackets `[param]` for dynamic segments.
<div class="mockup-code bg-base-300 text-base-content shadow-xl my-8">
<pre><code>my-sigpro-app/
├── src/
│ ├── pages/
│ │ ├── index.js → #/
│ │ ├── about.js → #/about
│ │ ├── users/
│ │ │ └── [id].js → #/users/:id
│ │ └── blog/
│ │ ├── index.js → #/blog
│ │ └── [slug].js → #/blog/:slug
│ ├── App.js (Main Layout)
│ └── main.js (Entry Point)
├── vite.config.js
└── package.json</code></pre>
</div>
---
## 2. Setup & Configuration
Add the plugin to your `vite.config.js`. It works out of the box with zero configuration.
```javascript
// vite.config.js
import { defineConfig } from 'vite';
import { sigproRouter } from 'sigpro/vite';
export default defineConfig({
plugins: [sigproRouter()]
});
```
---
## 3. Implementation
Thanks to **SigPro's synchronous initialization**, you no longer need to wrap your mounting logic in `.then()` blocks.
<div class="tabs tabs-box w-full mt-8 mb-12 bg-base-200/50 p-2 border border-base-300">
<input type="radio" name="route_impl" class="tab" aria-label="Option A: Direct in main.js" checked />
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
```javascript
// src/main.js
import SigPro from 'sigpro';
import { routes } from 'virtual:sigpro-routes';
// The Core already has Router ready
Mount(Router(routes), '#app');
```
</div>
<input type="radio" name="route_impl" class="tab" aria-label="Option B: Inside App.js (Persistent Layout)" />
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
```javascript
// src/App.js
import { routes } from 'virtual:sigpro-routes';
export default () => div({ class: 'layout' }, [
header([
h1("SigPro App"),
nav([
button({ onclick: () => Router.go('/') }, "Home"),
button({ onclick: () => Router.go('/blog') }, "Blog")
])
]),
// Only the content inside <main> will be swapped reactively
main(Router(routes))
]);
```
</div>
</div>
---
## 4. Route Mapping Reference
The plugin follows a simple convention to transform your file system into a routing map.
<div class="overflow-x-auto my-8">
<table class="table table-zebra w-full">
<thead class="bg-base-200">
<tr>
<th>File Path</th>
<th>Generated Path</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>index.js</code></td>
<td class="font-mono text-primary font-bold">/</td>
<td>The application root.</td>
</tr>
<tr>
<td><code>about.js</code></td>
<td class="font-mono text-primary font-bold">/about</td>
<td>A static page.</td>
</tr>
<tr>
<td><code>[id].js</code></td>
<td class="font-mono text-primary font-bold">/:id</td>
<td>Dynamic parameter (passed to the component).</td>
</tr>
<tr>
<td><code>blog/index.js</code></td>
<td class="font-mono text-primary font-bold">/blog</td>
<td>Folder index page.</td>
</tr>
<tr>
<td><code>_utils.js</code></td>
<td class="italic opacity-50 text-error">Ignored</td>
<td>Files starting with <code>_</code> are excluded from routing.</td>
</tr>
</tbody>
</table>
</div>
---
## 5. How it Works (Vite Virtual Module)
The plugin generates a virtual module named `virtual:sigpro-routes`. This module exports an array of objects compatible with `Router()`:
```javascript
// Internal representation generated by the plugin
export const routes = [
{ path: '/', component: () => import('/src/pages/index.js') },
{ path: '/users/:id', component: () => import('/src/pages/users/[id].js') },
// ...
];
```
Because it uses dynamic `import()`, Vite automatically performs **Code Splitting**, meaning each page is its own small JS file that only loads when the user navigates to it.

View File

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

View File

@@ -1,57 +1,112 @@
{ {
"name": "sigpro", "name": "sigpro",
"version": "1.1.22", "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/",
"dist",
"sigpro",
"vite",
"README.md", "README.md",
"LICENSE" "LICENSE",
"sigpro.d.ts",
"sigpro.ui.d.ts"
], ],
"homepage": "https://natxocc.github.io/sigpro/", "homepage": "https://sigpro.natxocc.com/#/",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/natxocc/sigpro.git" "url": "https://github.com/natxocc/sigpro"
}, },
"bugs": { "bugs": {
"url": "https://github.com/natxocc/sigpro/issues" "url": "https://github.com/natxocc/sigpro/issues",
"email": "sigpro@natxocc.com"
}, },
"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"
] ]
} }

21
sigpro.code-workspace Normal file
View File

@@ -0,0 +1,21 @@
{
"folders": [
{
"path": ".",
},
{
"path": "../sigpro-ui",
},
{
"path": "../sigpro",
},
],
"settings": {
"files.associations": {
"*.js": "javascript",
},
"emmet.includeLanguages": {
"javascript": "html",
},
},
}

537
sigpro.d.ts vendored
View File

@@ -1,188 +1,395 @@
// sigpro.d.ts /**
* SigPro
* A minimalistic reactive library with fine-grained reactivity,
* direct DOM updates, and built-in component helpers.
*/
declare const SIG_BRAND: unique symbol; // ============================================================================
// Core Reactivity
// ============================================================================
export interface Signal<T = any> { /**
readonly [SIG_BRAND]: true; * Creates a reactive signal. When a function is passed, it becomes a computed signal.
* If a `localStorageKey` is provided, the value persists.
*
* @param value - Initial value or computation function
* @param localStorageKey - Optional key for persistence
* @returns A getter/setter function
*/
export function $<T>(value: T, localStorageKey?: string): Signal<T>;
export function $<T>(computation: () => T): Signal<T>;
export interface Signal<T> {
(): T; (): T;
(value: T): T; (value: T | ((prev: T) => T)): void;
(updater: (prev: T) => T): T;
} }
export interface Computed<T = any> { /**
readonly [SIG_BRAND]: true; * Creates a deep reactive proxy for objects and arrays.
(): T; * Tracks property access and mutations automatically.
*
* @param target - Object or array to make reactive
* @returns A reactive proxy
*/
export function $$<T extends object>(target: T): DeepReactive<T>;
export type DeepReactive<T> = T extends object
? {
[K in keyof T]: T[K] extends object ? DeepReactive<T[K]> : T[K];
} }
: T;
/**
* Watches reactive sources and runs a callback.
*
* @example
* // Auto-track mode
* watch(() => {
* console.log(count());
* });
*
* @example
* // Explicit sources
* watch([count, name], ([c, n]) => {
* console.log(c, n);
* });
*
* @returns A function to stop the watcher.
*/
export function watch(fn: () => void): () => void;
export function watch<T>(
sources: Array<Signal<any>> | (() => T),
callback: (values: T | any[]) => void
): () => void;
/**
* Batches multiple reactive updates into a single flush.
*/
export function batch<T>(fn: () => T): T;
// ============================================================================
// DOM Creation
// ============================================================================
/**
* Hyperscript function to create DOM elements or components.
*
* @param tag - HTML/SVG tag name or component function
* @param props - Optional properties/attributes
* @param children - Child nodes or reactive functions
* @returns DOM node or array of nodes
*/
export function h(
tag: string | ((props: any, ctx: ComponentContext) => any),
props?: any,
children?: any
): Node;
export interface ComponentContext {
children: any;
emit: (event: string, ...args: any[]) => void;
}
/**
* Conditionally renders content.
*
* @param condition - Boolean, signal, or function returning boolean
* @param thenBranch - Content when truthy (Node or function)
* @param elseBranch - Optional content when falsy
* @returns A placeholder element that updates reactively
*/
export function when(
condition: boolean | (() => boolean) | Signal<boolean>,
thenBranch: any | (() => any),
elseBranch?: any | (() => any)
): HTMLElement;
/**
* Keyed list renderer. Uses `item?.id` by default or a custom key field.
*
* @param src - Array, signal, or function returning array
* @param itemFn - Render function (item, index) => Node
* @param keyField - Optional property name for unique key (e.g., "id")
* @returns A container element with reactive list
*/
export function each<T>(
src: T[] | (() => T[]) | Signal<T[]>,
itemFn: (item: T, index: number) => any,
keyField?: keyof T
): HTMLElement;
// ============================================================================
// Router
// ============================================================================
/**
* Hash-based router.
*
* @param routes - Array of route definitions
* @returns A container that renders the current route
*/
export function router(routes: RouteDefinition[]): HTMLElement;
export interface RouteDefinition {
path: string; // e.g., "/", "/user/:id", "*"
component: any | ((params: Record<string, string>) => any);
}
export namespace router {
/** Reactive params signal */
export const params: Signal<Record<string, string>>;
/** Navigate to path */
export function to(path: string): void;
/** Go back in history */
export function back(): void;
/** Current path without hash */
export function path(): string;
}
// ============================================================================
// Mount API
// ============================================================================
export interface RuntimeInstance { export interface RuntimeInstance {
readonly _isRuntime: true; _isRuntime: true;
readonly container: HTMLElement; container: HTMLElement;
destroy(): void; destroy: () => void;
} }
export interface TransitionOptions { /**
on?: (el: HTMLElement) => void; * Mounts a component to a DOM target.
off?: (el: HTMLElement, destroy: () => void) => void; *
} * @param component - Component function or node
* @param target - CSS selector or DOM element
export interface Route { * @returns Runtime instance
path: string; */
component: (params: Record<string, string>) => any; export function mount(
} component: (() => any) | Node,
export interface RenderContext {
onCleanup: (fn: () => void) => void;
}
export interface TagProps extends Record<string, any> {
ref?: ((el: HTMLElement) => void) | { current: HTMLElement | null };
class?: string | (() => string);
style?: string | (() => string);
}
export type ReactiveObject<T extends object> = {
[K in keyof T]: T[K] extends object ? ReactiveObject<T[K]> : T[K];
};
export function $<T>(val: T, key?: string | null): Signal<T>;
export function $<T>(val: () => T): Computed<T>;
export function $$<T>(fn: () => T): Computed<T>;
export function $_<T extends object>(obj: T): ReactiveObject<T>;
export function untrack<T>(fn: () => T): T;
export function Watch(cb: () => void): () => void;
export function Watch(cb: () => () => void): () => void;
export function Render<T>(fn: (ctx: RenderContext) => T): RuntimeInstance;
export function Tag(tag: string, props?: TagProps | null, children?: any[]): HTMLElement;
export function Tag(tag: string, children?: any[]): HTMLElement;
export function If(
cond: boolean | (() => boolean),
a: any | (() => any),
b?: any | (() => any) | null,
options?: TransitionOptions
): HTMLElement;
export function For<T>(
source: T[] | (() => T[]),
renderFn: (item: T, index: number) => any,
keyFn?: (item: T, index: number) => any,
tag?: string,
props?: TagProps
): HTMLElement;
export function Router(routes: Route[]): HTMLElement;
export namespace Router {
const params: Signal<Record<string, string>>;
function to(path: string): void;
function back(): void;
function path(): string;
}
export function Mount(
component: (() => any) | any,
target: string | HTMLElement target: string | HTMLElement
): RuntimeInstance; ): RuntimeInstance | undefined;
export function Share<T>(key: string, value: T): void; // ============================================================================
// Tag Helpers (globally available, lowercase)
// ============================================================================
export function Use<T>(key: string, defaultValue?: T): T | undefined; // All standard HTML tags are available as global functions.
// They follow the same signature as `h` but with predefined tag names.
// Examples:
export const a: TagHelper;
export const abbr: TagHelper;
export const article: TagHelper;
export const aside: TagHelper;
export const audio: TagHelper;
export const b: TagHelper;
export const blockquote: TagHelper;
export const br: TagHelper;
export const button: TagHelper;
export const canvas: TagHelper;
export const caption: TagHelper;
export const cite: TagHelper;
export const code: TagHelper;
export const col: TagHelper;
export const colgroup: TagHelper;
export const datalist: TagHelper;
export const dd: TagHelper;
export const del: TagHelper;
export const details: TagHelper;
export const dfn: TagHelper;
export const dialog: TagHelper;
export const div: TagHelper;
export const dl: TagHelper;
export const dt: TagHelper;
export const em: TagHelper;
export const embed: TagHelper;
export const fieldset: TagHelper;
export const figcaption: TagHelper;
export const figure: TagHelper;
export const footer: TagHelper;
export const form: TagHelper;
export const h1: TagHelper;
export const h2: TagHelper;
export const h3: TagHelper;
export const h4: TagHelper;
export const h5: TagHelper;
export const h6: TagHelper;
export const header: TagHelper;
export const hr: TagHelper;
export const i: TagHelper;
export const iframe: TagHelper;
export const img: TagHelper;
export const input: TagHelper;
export const ins: TagHelper;
export const kbd: TagHelper;
export const label: TagHelper;
export const legend: TagHelper;
export const li: TagHelper;
export const main: TagHelper;
export const mark: TagHelper;
export const meter: TagHelper;
export const nav: TagHelper;
export const object: TagHelper;
export const ol: TagHelper;
export const optgroup: TagHelper;
export const option: TagHelper;
export const output: TagHelper;
export const p: TagHelper;
export const picture: TagHelper;
export const pre: TagHelper;
export const progress: TagHelper;
export const section: TagHelper;
export const select: TagHelper;
export const slot: TagHelper;
export const small: TagHelper;
export const source: TagHelper;
export const span: TagHelper;
export const strong: TagHelper;
export const sub: TagHelper;
export const summary: TagHelper;
export const sup: TagHelper;
export const svg: TagHelper;
export const table: TagHelper;
export const tbody: TagHelper;
export const td: TagHelper;
export const template: TagHelper;
export const textarea: TagHelper;
export const tfoot: TagHelper;
export const th: TagHelper;
export const thead: TagHelper;
export const time: TagHelper;
export const tr: TagHelper;
export const u: TagHelper;
export const ul: TagHelper;
export const video: TagHelper;
// Funciones JSX (etiquetas globales) export type TagHelper = (
export const Div: (props?: TagProps, children?: any[]) => HTMLElement; props?: any,
export const Span: (props?: TagProps, children?: any[]) => HTMLElement; children?: any
export const P: (props?: TagProps, children?: any[]) => HTMLElement; ) => HTMLElement | SVGElement | Text;
export const H1: (props?: TagProps, children?: any[]) => HTMLElement;
export const H2: (props?: TagProps, children?: any[]) => HTMLElement;
export const H3: (props?: TagProps, children?: any[]) => HTMLElement;
export const H4: (props?: TagProps, children?: any[]) => HTMLElement;
export const H5: (props?: TagProps, children?: any[]) => HTMLElement;
export const H6: (props?: TagProps, children?: any[]) => HTMLElement;
export const Button: (props?: TagProps, children?: any[]) => HTMLElement;
export const A: (props?: TagProps, children?: any[]) => HTMLElement;
export const Img: (props?: TagProps, children?: any[]) => HTMLElement;
export const Input: (props?: TagProps, children?: any[]) => HTMLElement;
export const Textarea: (props?: TagProps, children?: any[]) => HTMLElement;
export const Select: (props?: TagProps, children?: any[]) => HTMLElement;
export const Option: (props?: TagProps, children?: any[]) => HTMLElement;
export const Form: (props?: TagProps, children?: any[]) => HTMLElement;
export const Label: (props?: TagProps, children?: any[]) => HTMLElement;
export const Ul: (props?: TagProps, children?: any[]) => HTMLElement;
export const Ol: (props?: TagProps, children?: any[]) => HTMLElement;
export const Li: (props?: TagProps, children?: any[]) => HTMLElement;
export const Table: (props?: TagProps, children?: any[]) => HTMLElement;
export const Tr: (props?: TagProps, children?: any[]) => HTMLElement;
export const Td: (props?: TagProps, children?: any[]) => HTMLElement;
export const Th: (props?: TagProps, children?: any[]) => HTMLElement;
export const Section: (props?: TagProps, children?: any[]) => HTMLElement;
export const Article: (props?: TagProps, children?: any[]) => HTMLElement;
export const Aside: (props?: TagProps, children?: any[]) => HTMLElement;
export const Nav: (props?: TagProps, children?: any[]) => HTMLElement;
export const Header: (props?: TagProps, children?: any[]) => HTMLElement;
export const Footer: (props?: TagProps, children?: any[]) => HTMLElement;
export const Main: (props?: TagProps, children?: any[]) => HTMLElement;
export interface SigProAPI { // ============================================================================
// Default Export
// ============================================================================
declare const SigPro: {
$: typeof $; $: typeof $;
$$: typeof $$; $$: typeof $$;
$_: typeof $_; watch: typeof watch;
untrack: typeof untrack; h: typeof h;
Render: typeof Render; when: typeof when;
Watch: typeof Watch; each: typeof each;
Tag: typeof Tag; router: typeof router;
If: typeof If; mount: typeof mount;
For: typeof For; batch: typeof batch;
Router: typeof Router; };
Mount: typeof Mount;
Share: typeof Share;
Use: typeof Use;
}
declare const SigPro: SigProAPI;
export default SigPro; export default SigPro;
// ============================================================================
// Global augmentation for browser environments
// ============================================================================
declare global { declare global {
namespace JSX { interface Window {
interface IntrinsicElements { $: typeof $;
div: TagProps; $$: typeof $$;
span: TagProps; watch: typeof watch;
p: TagProps; h: typeof h;
h1: TagProps; when: typeof when;
h2: TagProps; each: typeof each;
h3: TagProps; router: typeof router;
h4: TagProps; mount: typeof mount;
h5: TagProps; batch: typeof batch;
h6: TagProps; SigPro: typeof SigPro;
button: TagProps;
a: TagProps; // Tag helpers (lowercase)
img: TagProps; a: TagHelper;
input: TagProps; abbr: TagHelper;
textarea: TagProps; article: TagHelper;
select: TagProps; aside: TagHelper;
option: TagProps; audio: TagHelper;
form: TagProps; b: TagHelper;
label: TagProps; blockquote: TagHelper;
ul: TagProps; br: TagHelper;
ol: TagProps; button: TagHelper;
li: TagProps; canvas: TagHelper;
table: TagProps; caption: TagHelper;
tr: TagProps; cite: TagHelper;
td: TagProps; code: TagHelper;
th: TagProps; col: TagHelper;
section: TagProps; colgroup: TagHelper;
article: TagProps; datalist: TagHelper;
aside: TagProps; dd: TagHelper;
nav: TagProps; del: TagHelper;
header: TagProps; details: TagHelper;
footer: TagProps; dfn: TagHelper;
main: TagProps; dialog: TagHelper;
} div: TagHelper;
interface Element extends HTMLElement {} dl: TagHelper;
dt: TagHelper;
em: TagHelper;
embed: TagHelper;
fieldset: TagHelper;
figcaption: TagHelper;
figure: TagHelper;
footer: TagHelper;
form: TagHelper;
h1: TagHelper;
h2: TagHelper;
h3: TagHelper;
h4: TagHelper;
h5: TagHelper;
h6: TagHelper;
header: TagHelper;
hr: TagHelper;
i: TagHelper;
iframe: TagHelper;
img: TagHelper;
input: TagHelper;
ins: TagHelper;
kbd: TagHelper;
label: TagHelper;
legend: TagHelper;
li: TagHelper;
main: TagHelper;
mark: TagHelper;
meter: TagHelper;
nav: TagHelper;
object: TagHelper;
ol: TagHelper;
optgroup: TagHelper;
option: TagHelper;
output: TagHelper;
p: TagHelper;
picture: TagHelper;
pre: TagHelper;
progress: TagHelper;
section: TagHelper;
select: TagHelper;
slot: TagHelper;
small: TagHelper;
source: TagHelper;
span: TagHelper;
strong: TagHelper;
sub: TagHelper;
summary: TagHelper;
sup: TagHelper;
svg: TagHelper;
table: TagHelper;
tbody: TagHelper;
td: TagHelper;
template: TagHelper;
textarea: TagHelper;
tfoot: TagHelper;
th: TagHelper;
thead: TagHelper;
time: TagHelper;
tr: TagHelper;
u: TagHelper;
ul: TagHelper;
video: TagHelper;
} }
} }

440
sigpro.js
View File

@@ -1,440 +0,0 @@
let activeEffect = null;
let currentOwner = null;
const effectQueue = new Set();
let isFlushing = false;
const MOUNTED_NODES = new WeakMap();
const doc = document;
const isArr = Array.isArray;
const assign = Object.assign;
const createEl = (t) => doc.createElement(t);
const createText = (t) => doc.createTextNode(String(t ?? ""));
const isFunc = (f) => typeof f === "function";
const isObj = (o) => typeof o === "object" && o !== null;
const runWithContext = (effect, callback) => {
const previousEffect = activeEffect;
activeEffect = effect;
try { return callback(); }
finally { activeEffect = previousEffect; }
};
const cleanupNode = (node) => {
if (node._cleanups) {
node._cleanups.forEach((dispose) => dispose());
node._cleanups.clear();
}
node.childNodes?.forEach(cleanupNode);
};
const flushEffects = () => {
if (isFlushing) return;
isFlushing = true;
while (effectQueue.size > 0) {
const sortedEffects = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
effectQueue.clear();
for (const effect of sortedEffects) {
if (!effect._deleted) effect();
}
}
isFlushing = false;
};
const trackSubscription = (subscribers) => {
if (activeEffect && !activeEffect._deleted) {
subscribers.add(activeEffect);
activeEffect._deps.add(subscribers);
}
};
const triggerUpdate = (subscribers) => {
subscribers.forEach((effect) => {
if (effect === activeEffect || effect._deleted) return;
if (effect._isComputed) {
effect.markDirty();
if (effect._subs) triggerUpdate(effect._subs);
} else {
effectQueue.add(effect);
}
});
if (!isFlushing) queueMicrotask(flushEffects);
};
const Render = (renderFn) => {
const cleanups = new Set();
const previousOwner = currentOwner;
const container = createEl("div");
container.style.display = "contents";
currentOwner = { cleanups };
const processResult = (result) => {
if (!result) return;
if (result._isRuntime) {
cleanups.add(result.destroy);
container.appendChild(result.container);
} else if (isArr(result)) {
result.forEach(processResult);
} else {
container.appendChild(result instanceof Node ? result : createText(result));
}
};
try {
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
} finally { currentOwner = previousOwner; }
return {
_isRuntime: true,
container,
destroy: () => {
cleanups.forEach((fn) => fn());
cleanupNode(container);
container.remove();
},
};
};
const $ = (initialValue, storageKey = null) => {
const subscribers = new Set();
if (isFunc(initialValue)) {
let cachedValue, isDirty = true;
const effect = () => {
if (effect._deleted) return;
effect._deps.forEach((dep) => dep.delete(effect));
effect._deps.clear();
runWithContext(effect, () => {
const newValue = initialValue();
if (!Object.is(cachedValue, newValue) || isDirty) {
cachedValue = newValue;
isDirty = false;
triggerUpdate(subscribers);
}
});
};
assign(effect, {
_deps: new Set(),
_isComputed: true,
_subs: subscribers,
_deleted: false,
markDirty: () => (isDirty = true),
stop: () => {
effect._deleted = true;
effect._deps.forEach((dep) => dep.delete(effect));
subscribers.clear();
}
});
if (currentOwner) currentOwner.cleanups.add(effect.stop);
return () => { if (isDirty) effect(); trackSubscription(subscribers); return cachedValue; };
}
let value = initialValue;
if (storageKey) {
try {
const saved = localStorage.getItem(storageKey);
if (saved !== null) value = JSON.parse(saved);
} catch (e) { console.warn("SigPro Storage Lock", e); }
}
return (...args) => {
if (args.length) {
const nextValue = isFunc(args[0]) ? args[0](value) : args[0];
if (!Object.is(value, nextValue)) {
value = nextValue;
if (storageKey) localStorage.setItem(storageKey, JSON.stringify(value));
triggerUpdate(subscribers);
}
}
trackSubscription(subscribers);
return value;
};
};
const $$ = (object, cache = new WeakMap()) => {
if (!isObj(object)) return object;
if (cache.has(object)) return cache.get(object);
const keySubscribers = {};
const proxy = new Proxy(object, {
get(target, key) {
if (activeEffect) trackSubscription(keySubscribers[key] ??= new Set());
const value = Reflect.get(target, key);
return isObj(value) ? $$(value, cache) : value;
},
set(target, key, value) {
if (Object.is(target[key], value)) return true;
const success = Reflect.set(target, key, value);
if (keySubscribers[key]) triggerUpdate(keySubscribers[key]);
return success;
}
});
cache.set(object, proxy);
return proxy;
};
const Watch = (target, callbackFn) => {
const isExplicit = isArr(target);
const callback = isExplicit ? callbackFn : target;
if (!isFunc(callback)) return () => { };
const owner = currentOwner;
const runner = () => {
if (runner._deleted) return;
runner._deps.forEach((dep) => dep.delete(runner));
runner._deps.clear();
runner._cleanups.forEach((cleanup) => cleanup());
runner._cleanups.clear();
const previousOwner = currentOwner;
runner.depth = activeEffect ? activeEffect.depth + 1 : 0;
runWithContext(runner, () => {
currentOwner = { cleanups: runner._cleanups };
if (isExplicit) {
runWithContext(null, callback);
target.forEach((dep) => isFunc(dep) && dep());
} else {
callback();
}
currentOwner = previousOwner;
});
};
assign(runner, {
_deps: new Set(),
_cleanups: new Set(),
_deleted: false,
stop: () => {
if (runner._deleted) return;
runner._deleted = true;
effectQueue.delete(runner);
runner._deps.forEach((dep) => dep.delete(runner));
runner._cleanups.forEach((cleanup) => cleanup());
if (owner) owner.cleanups.delete(runner.stop);
}
});
if (owner) owner.cleanups.add(runner.stop);
runner();
return runner.stop;
};
const Tag = (tag, props = {}, children = []) => {
if (props instanceof Node || isArr(props) || !isObj(props)) {
children = props; props = {};
}
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
const element = isSVG
? doc.createElementNS("http://www.w3.org/2000/svg", tag)
: createEl(tag);
element._cleanups = new Set();
element.onUnmount = (fn) => element._cleanups.add(fn);
const booleanAttributes = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
const updateAttribute = (name, value) => {
const sanitized = (name === 'src' || name === 'href') && String(value).toLowerCase().includes('javascript:') ? '#' : value;
if (booleanAttributes.includes(name)) {
element[name] = !!sanitized;
sanitized ? element.setAttribute(name, "") : element.removeAttribute(name);
} else {
sanitized == null ? element.removeAttribute(name) : element.setAttribute(name, sanitized);
}
};
for (let [key, value] of Object.entries(props)) {
if (key === "ref") { (isFunc(value) ? value(element) : (value.current = element)); continue; }
const isSignal = isFunc(value);
if (key.startsWith("on")) {
const eventName = key.slice(2).toLowerCase().split(".")[0];
element.addEventListener(eventName, value);
element._cleanups.add(() => element.removeEventListener(eventName, value));
} else if (isSignal) {
element._cleanups.add(Watch(() => {
const currentVal = value();
key === "class" ? (element.className = currentVal || "") : updateAttribute(key, currentVal);
}));
if (["INPUT", "TEXTAREA", "SELECT"].includes(element.tagName) && (key === "value" || key === "checked")) {
const event = key === "checked" ? "change" : "input";
const handler = (e) => value(e.target[key]);
element.addEventListener(event, handler);
element._cleanups.add(() => element.removeEventListener(event, handler));
}
} else {
updateAttribute(key, value);
}
}
const appendChildNode = (child) => {
if (isArr(child)) return child.forEach(appendChildNode);
if (isFunc(child)) {
const marker = createText("");
element.appendChild(marker);
let currentNodes = [];
element._cleanups.add(Watch(() => {
const result = child();
const nextNodes = (isArr(result) ? result : [result]).map((node) =>
node?._isRuntime ? node.container : node instanceof Node ? node : createText(node)
);
currentNodes.forEach((node) => { cleanupNode(node); node.remove(); });
nextNodes.forEach((node) => marker.parentNode?.insertBefore(node, marker));
currentNodes = nextNodes;
}));
} else {
element.appendChild(child instanceof Node ? child : createText(child));
}
};
appendChildNode(children);
return element;
};
const If = (condition, thenVal, otherwiseVal = null, transition = null) => {
const marker = createText("");
const container = Tag("div", { style: "display:contents" }, [marker]);
let currentView = null, lastState = null;
Watch(() => {
const state = !!(isFunc(condition) ? condition() : condition);
if (state === lastState) return;
lastState = state;
const dispose = () => { if (currentView) currentView.destroy(); currentView = null; };
if (currentView && !state && transition?.out) {
transition.out(currentView.container, dispose);
} else {
dispose();
}
const branch = state ? thenVal : otherwiseVal;
if (branch) {
currentView = Render(() => isFunc(branch) ? branch() : branch);
container.insertBefore(currentView.container, marker);
if (state && transition?.in) transition.in(currentView.container);
}
});
return container;
};
const For = (source, renderFn, keyFn, tag = "div", props = { style: "display:contents" }) => {
const marker = createText("");
const container = Tag(tag, props, [marker]);
let viewCache = new Map();
Watch(() => {
const items = (isFunc(source) ? source() : source) || [];
const nextCache = new Map();
const order = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const key = keyFn ? keyFn(item, i) : i;
let view = viewCache.get(key);
if (!view) {
const result = renderFn(item, i);
view = result instanceof Node
? { container: result, destroy: () => { cleanupNode(result); result.remove(); } }
: Render(() => result);
}
viewCache.delete(key);
nextCache.set(key, view);
order.push(key);
}
viewCache.forEach(v => v.destroy());
let anchor = marker;
for (let i = order.length - 1; i >= 0; i--) {
const view = nextCache.get(order[i]);
if (view.container.nextSibling !== anchor) {
container.insertBefore(view.container, anchor);
}
anchor = view.container;
}
viewCache = nextCache;
});
return container;
};
const Router = (routes) => {
const currentPath = $(window.location.hash.replace(/^#/, "") || "/");
window.addEventListener("hashchange", () => currentPath(window.location.hash.replace(/^#/, "") || "/"));
const outlet = Tag("div", { class: "router-transition" });
let currentView = null;
Watch([currentPath], async () => {
const path = currentPath();
const route = routes.find(r => {
const routeParts = r.path.split("/").filter(Boolean);
const pathParts = path.split("/").filter(Boolean);
return routeParts.length === pathParts.length && routeParts.every((part, i) => part.startsWith(":") || part === pathParts[i]);
}) || routes.find(r => r.path === "*");
if (route) {
let component = route.component;
if (isFunc(component) && component.toString().includes('import')) {
component = (await component()).default || (await component());
}
const params = {};
route.path.split("/").filter(Boolean).forEach((part, i) => {
if (part.startsWith(":")) params[part.slice(1)] = path.split("/").filter(Boolean)[i];
});
if (currentView) currentView.destroy();
if (Router.params) Router.params(params);
currentView = Render(() => {
try {
return isFunc(component) ? component(params) : component;
} catch (e) {
return Tag("div", { class: "p-4 text-error" }, "Error loading view");
}
});
outlet.appendChild(currentView.container);
}
});
return outlet;
};
Router.params = $({});
Router.to = (path) => (window.location.hash = path.replace(/^#?\/?/, "#/"));
Router.back = () => window.history.back();
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
const Mount = (component, target) => {
const targetEl = typeof target === "string" ? doc.querySelector(target) : target;
if (!targetEl) return;
if (MOUNTED_NODES.has(targetEl)) MOUNTED_NODES.get(targetEl).destroy();
const instance = Render(isFunc(component) ? component : () => component);
targetEl.replaceChildren(instance.container);
MOUNTED_NODES.set(targetEl, instance);
return instance;
};
const SigPro = { $, $$, Render, Watch, Tag, If, For, Router, Mount };
if (typeof window !== "undefined") {
assign(window, SigPro);
const tags = `div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption img video audio canvas svg iframe picture source progress meter`.split(" ");
tags.forEach((tag) => {
const helper = tag[0].toUpperCase() + tag.slice(1);
if (!(helper in window)) window[helper] = (p, c) => Tag(tag, p, c);
});
window.SigPro = Object.freeze(SigPro);
}
export { $, $$, Render, Watch, Tag, If, For, Router, Mount };
export default SigPro;

855
sigpro.ts
View File

@@ -1,855 +0,0 @@
type EffectFn = {
(): void;
e: Set<Set<EffectFn>>;
c?: EffectFn[];
d?: boolean;
};
type Signal<T> = {
(): T;
(next: T | ((prev: T) => T)): T;
react(): T;
};
type CleanupFn = () => void;
const DANGEROUS_PROTOCOLS = /^(javascript|data|vbscript):/i;
const DANGEROUS_ATTRIBUTES = /^on/i;
const sanitizeUrl = (url: unknown): string => {
const str = String(url ?? '').trim().toLowerCase();
if (DANGEROUS_PROTOCOLS.test(str)) return '#';
return str;
};
const sanitizeAttribute = (name: string, value: unknown): string | null => {
if (value == null) return null;
const strValue = String(value);
if (DANGEROUS_ATTRIBUTES.test(name)) {
console.warn(`[SigPro] XSS prevention: blocked attribute "${name}"`);
return null;
}
if (name === 'src' || name === 'href') {
return sanitizeUrl(strValue);
}
return strValue;
};
let activeEffect: EffectFn | null = null;
let isScheduled = false;
const queue = new Set<EffectFn>();
const tick = (): void => {
while (queue.size) {
const runs = [...queue];
queue.clear();
runs.forEach(fn => fn());
}
isScheduled = false;
};
const schedule = (fn: EffectFn): void => {
if (fn.d) return;
queue.add(fn);
if (!isScheduled) {
queueMicrotask(tick);
isScheduled = true;
}
};
const depend = (subs: Set<EffectFn>): void => {
if (activeEffect && !activeEffect.c) {
subs.add(activeEffect);
activeEffect.e.add(subs);
}
};
export const effect = (fn: () => any, isScope: boolean = false): CleanupFn => {
let cleanup: CleanupFn | null = null;
const run = () => {
if (run.d) return;
const prev = activeEffect;
activeEffect = run;
const result = fn();
if (typeof result === 'function') cleanup = result;
activeEffect = prev;
};
const stop = () => {
if (run.d) return;
run.d = true;
run.e.forEach(subs => subs.delete(run));
run.e.clear();
cleanup?.();
run.c?.forEach(f => f());
};
run.e = new Set();
run.d = false;
if (isScope) run.c = [];
run();
activeEffect?.c?.push(stop);
return stop;
};
effect.react = <T>(fn: () => T): T => {
const prev = activeEffect;
activeEffect = null;
const result = fn();
activeEffect = prev;
return result;
};
export function $<T>(initial: T, storageKey?: string): Signal<T>;
export function $<T>(fn: () => T, storageKey?: string): Signal<T>;
export function $<T>(initial: T | (() => T), storageKey?: string): Signal<T> {
const isComputed = typeof initial === 'function';
if (!isComputed) {
let value = initial as T;
const subs = new Set<EffectFn>();
if (storageKey) {
try {
const saved = localStorage.getItem(storageKey);
if (saved !== null) value = JSON.parse(saved);
} catch { }
}
const signalFn = ((...args: [] | [T | ((prev: T) => T)]) => {
if (args.length === 0) {
return value;
}
const next = typeof args[0] === 'function'
? (args[0] as (prev: T) => T)(value)
: args[0];
if (Object.is(value, next)) return value;
value = next;
if (storageKey) {
try {
localStorage.setItem(storageKey, JSON.stringify(value));
} catch { }
}
subs.forEach(fn => schedule(fn));
return value;
}) as Signal<T>;
signalFn.react = () => {
depend(subs);
return value;
};
return signalFn;
}
let cached: T;
let dirty = true;
const subs = new Set<EffectFn>();
const fn = initial as () => T;
effect(() => {
const newValue = fn();
if (!Object.is(cached, newValue) || dirty) {
cached = newValue;
dirty = false;
subs.forEach(fn => schedule(fn));
}
});
const computedFn = (() => {
return cached;
}) as Signal<T>;
computedFn.react = () => {
depend(subs);
return cached;
};
return computedFn;
}
export const scope = (fn: () => any): CleanupFn => effect(fn, true);
type WatchSource<T> = () => T;
export const watch = <T>(
source: WatchSource<T>,
callback: (newValue: T, oldValue: T) => any
): CleanupFn => {
let first = true;
let oldValue: T;
return effect(() => {
const newValue = source();
if (!first) {
effect.react(() => callback(newValue, oldValue));
} else {
first = false;
}
oldValue = newValue;
});
};
const reactiveCache = new WeakMap<object, object>();
export function $$<T extends object>(obj: T): T {
if (reactiveCache.has(obj)) {
return reactiveCache.get(obj) as T;
}
const subs: Record<string | symbol, Set<EffectFn>> = {};
const proxy = new Proxy(obj, {
get(target, key: string | symbol, receiver) {
const subsForKey = subs[key] ??= new Set();
depend(subsForKey);
const val = Reflect.get(target, key, receiver);
return (val && typeof val === 'object') ? $$(val) : val;
},
set(target, key: string | symbol, val, receiver) {
if (Object.is(target[key as keyof T], val)) return true;
const success = Reflect.set(target, key, val, receiver);
if (subs[key]) {
subs[key].forEach(fn => schedule(fn));
if (!isScheduled) {
queueMicrotask(tick);
isScheduled = true;
}
}
return success;
}
});
reactiveCache.set(obj, proxy);
return proxy;
}
type Context = {
m: CleanupFn[];
u: CleanupFn[];
p: Record<string | symbol, any>;
};
let context: Context | null = null;
export const onMount = (fn: CleanupFn): void => {
context?.m.push(fn);
};
export const onUnmount = (fn: CleanupFn): void => {
context?.u.push(fn);
};
export const share = (key: string | symbol, value: any): void => {
if (context) context.p[key] = value;
};
export const use = (key: string | symbol, defaultValue?: any): any => {
if (context && key in context.p) return context.p[key];
return defaultValue;
};
export function createContext<T>(defaultValue?: T): {
Provider: (props: { value: T; children?: any }) => any;
use: () => T;
} {
const key = Symbol('context');
const useContext = (): T => {
// Buscar en el stack de contextos
let current = context;
while (current) {
if (key in current.p) {
return current.p[key] as T;
}
// Subir al contexto padre (si existiera)
current = null; // Por ahora, solo contexto actual
}
if (defaultValue !== undefined) return defaultValue;
throw new Error(`Context not found: ${String(key)}`);
};
const Provider = ({ value, children }: { value: T; children?: any }) => {
// Guardar contexto anterior
const prevContext = context;
// Crear nuevo contexto o extender el existente
if (!context) {
context = { m: [], u: [], p: {} };
}
// Guardar valor
context.p[key] = value;
// Renderizar hijos
const result = h('div', { style: 'display: contents' }, children);
// Restaurar contexto anterior
context = prevContext;
return result;
};
return { Provider, use: useContext };
}
export function createSharedContext<T>(key: string | symbol, initialValue: T): {
set: (value: T) => void;
get: () => T;
} {
// Inicializar si estamos en un componente
if (context && !(key in context.p)) {
share(key, initialValue);
}
return {
set: (value: T) => share(key, value),
get: () => use(key) as T
};
}
type Component<P = Record<string, any>> = (
props: P,
ctx: { children?: any[]; emit: (event: string, ...args: any[]) => any }
) => any;
type ElementWithLifecycle = Node & {
$c?: Context;
$s?: CleanupFn;
$l?: (done: CleanupFn) => void;
};
const isFn = (v: unknown): v is Function => typeof v === 'function';
const isNode = (v: unknown): v is Node => v instanceof Node;
const append = (parent: Node, child: any): void => {
if (child === null) return;
if (isFn(child)) {
const anchor = document.createTextNode('');
parent.appendChild(anchor);
let nodes: Node[] = [];
effect(() => {
effect(() => {
const newNodes = [child()]
.flat(Infinity)
.map((node: any) => isFn(node) ? node() : node)
.flat(Infinity)
.filter((node: any) => node !== null)
.map((node: any) => isNode(node) ? node : document.createTextNode(String(node)));
const oldNodes = nodes.filter(node => {
const keep = newNodes.includes(node);
if (!keep) remove(node);
return keep;
});
const oldIdxs = new Map(oldNodes.map((node, i) => [node, i]));
for (let i = newNodes.length - 1, p = oldNodes.length - 1; i >= 0; i--) {
const node = newNodes[i];
const ref = newNodes[i + 1] || anchor;
if (!oldIdxs.has(node)) {
anchor.parentNode?.insertBefore(node, ref);
(node as ElementWithLifecycle).$c?.m.forEach(fn => fn());
} else if (oldNodes[p] !== node) {
if (newNodes[i - 1] !== oldNodes[p]) {
anchor.parentNode?.insertBefore(oldNodes[p], node);
oldNodes[oldIdxs.get(node)!] = oldNodes[p];
oldNodes[p] = node;
p--;
}
anchor.parentNode?.insertBefore(node, ref);
} else {
p--;
}
}
nodes = newNodes;
});
}, true);
} else if (isNode(child)) {
parent.appendChild(child);
} else {
parent.appendChild(document.createTextNode(String(child)));
}
};
const remove = (node: Node): void => {
const el = node as ElementWithLifecycle;
el.$s?.();
el.$l?.(() => node.remove());
if (!el.$l) node.remove();
};
const render = (fn: Function, ...data: any[]): Node => {
let node: any;
const stop = effect(() => {
node = fn(...data);
if (isFn(node)) node = node();
}, true);
if (node) node.$s = stop;
return node;
};
export const h = (tag: any, props?: any, ...children: any[]): any => {
props = props || {};
children = children.flat(Infinity);
if (isFn(tag)) {
const prev = context;
context = { m: [], u: [], p: { ...(prev?.p || {}) } };
let el: any;
const stop = effect(() => {
el = tag(props, {
children,
emit: (evt: string, ...args: any[]) =>
props[`on${evt[0].toUpperCase()}${evt.slice(1)}`]?.(...args),
});
return () => el.$c.u.forEach((fn: CleanupFn) => fn());
}, true);
if (isNode(el) || isFn(el)) {
el.$c = context;
el.$s = stop;
}
context = prev;
return el;
}
if (!tag) return () => children;
let el: ElementWithLifecycle;
let is_svg = false;
try {
el = document.createElement(tag);
if (el instanceof HTMLUnknownElement) {
is_svg = true;
el = document.createElementNS("http://www.w3.org/2000/svg", tag);
}
} catch {
is_svg = true;
el = document.createElementNS("http://www.w3.org/2000/svg", tag);
}
// Código MEJORADO
const booleanAttributes = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
for (const key in props) {
if (key.startsWith('on')) {
const eventName = key.slice(2).toLowerCase();
el.addEventListener(eventName, props[key]);
} else if (key === "ref") {
if (isFn(props[key])) {
props[key](el);
} else {
props[key].current = el;
}
} else if (isFn(props[key])) {
effect(() => {
const val = props[key]();
if (key === 'className') {
el.setAttribute('class', String(val ?? ''));
} else if (booleanAttributes.includes(key)) {
(el as any)[key] = !!val;
val ? el.setAttribute(key, '') : el.removeAttribute(key);
} else if (key in el && !is_svg) {
(el as any)[key] = val;
} else {
const safeVal = sanitizeAttribute(key, val);
if (safeVal !== null) el.setAttribute(key, safeVal);
}
});
} else {
const value = props[key];
if (key === 'className') {
el.setAttribute('class', String(value ?? ''));
} else if (booleanAttributes.includes(key)) {
(el as any)[key] = !!value;
value ? el.setAttribute(key, '') : el.removeAttribute(key);
} else if (key in el && !is_svg) {
(el as any)[key] = value;
} else {
const safeVal = sanitizeAttribute(key, value);
if (safeVal !== null) el.setAttribute(key, safeVal);
}
}
}
children.forEach((child: any) => append(el, child));
return el;
};
export const If = (
cond: (() => boolean) | boolean,
renderFn: any,
fallback: any = null
): (() => any) => {
let cached: any;
let current: boolean | null = null;
return () => {
const show = isFn(cond) ? cond() : cond;
if (show !== current) {
cached = show ? render(renderFn) : (isFn(fallback) ? render(fallback) : fallback);
}
current = show;
return cached;
};
};
export const For = <T>(
list: (() => T[]) | T[] | { value: T[] },
key: string | ((item: T, index: number) => string | number),
renderFn: (item: T, index: number) => any
): (() => any[]) => {
let cache = new Map<string | number, any>();
return () => {
const next = new Map();
const items = (isFn(list) ? list() : (list as any).value || list) as T[];
const nodes = items.map((item, index) => {
const idx = isFn(key) ? key(item, index) : key ? (item as any)[key] : index;
let node = cache.get(idx);
if (!node) {
node = render(renderFn, item, index);
}
next.set(idx, node);
return node;
});
cache = next;
return nodes;
};
};
type TransitionClasses = [string, string, string];
type TransitionConfig = {
enter?: TransitionClasses;
idle?: string;
leave?: TransitionClasses;
};
export const Transition = (
{ enter: e, idle, leave: l }: TransitionConfig,
{ children: [c] }: { children: any[] }
): any => {
const decorate = (el: any): any => {
if (!isNode(el)) return el;
const addClass = (c?: string) => c && (el as HTMLElement).classList.add(...c.split(' '));
const removeClass = (c?: string) => c && (el as HTMLElement).classList.remove(...c.split(' '));
if (e) {
requestAnimationFrame(() => {
addClass(e[1]);
requestAnimationFrame(() => {
addClass(e[0]);
removeClass(e[1]);
addClass(e[2]);
el.addEventListener('transitionend', () => {
removeClass(e[2]);
removeClass(e[0]);
addClass(idle);
}, { once: true });
});
});
}
if (l) {
(el as ElementWithLifecycle).$l = (done: CleanupFn) => {
removeClass(idle);
addClass(l[1]);
requestAnimationFrame(() => {
addClass(l[0]);
removeClass(l[1]);
addClass(l[2]);
el.addEventListener('transitionend', () => {
removeClass(l[2]);
removeClass(l[0]);
done();
}, { once: true });
});
};
}
return el;
};
if (!c) return null;
if (isFn(c)) {
return () => decorate(c());
}
return decorate(c);
};
type Route = {
path: string;
component: Component;
};
type RouterInstance = {
view: Node;
to: (path: string) => void;
back: () => void;
params: Signal<Record<string, string>>;
};
export const Router = (routes: Route[]): RouterInstance => {
const getPath = () => window.location.hash.slice(1) || "/";
const path = $(getPath());
const params = $<Record<string, string>>({});
const matchRoute = (path: string): { component: Component; params: Record<string, string> } | null => {
for (const route of routes) {
const routeParts = route.path.split("/").filter(Boolean);
const pathParts = path.split("/").filter(Boolean);
if (routeParts.length !== pathParts.length) continue;
const matchedParams: Record<string, string> = {};
let ok = true;
for (let i = 0; i < routeParts.length; i++) {
if (routeParts[i].startsWith(":")) {
matchedParams[routeParts[i].slice(1)] = pathParts[i];
} else if (routeParts[i] !== pathParts[i]) {
ok = false;
break;
}
}
if (ok) return { component: route.component, params: matchedParams };
}
const wildcard = routes.find(r => r.path === "*");
if (wildcard) return { component: wildcard.component, params: {} };
return null;
};
window.addEventListener("hashchange", () => path(getPath()));
const outlet = h("div");
effect(() => {
const matched = matchRoute(path());
if (!matched) return;
params(matched.params);
while (outlet.firstChild) outlet.removeChild(outlet.firstChild);
outlet.appendChild(h(matched.component));
});
return {
view: outlet,
to: (p: string) => { window.location.hash = p; },
back: () => window.history.back(),
params: () => params,
};
};
export const mount = (
component: Component,
target: string | HTMLElement,
props?: Record<string, any>
): CleanupFn => {
const targetEl = typeof target === "string" ? document.querySelector(target) : target;
if (!targetEl) throw new Error("Target element not found");
const el = h(component, props);
targetEl.appendChild(el);
(el as ElementWithLifecycle).$c?.m.forEach(fn => fn());
return () => remove(el);
};
export default (
target: HTMLElement,
root: Component,
props?: Record<string, any>
): CleanupFn => {
const el = h(root, props);
target.appendChild(el);
(el as ElementWithLifecycle).$c?.m.forEach(fn => fn());
return () => remove(el);
};
declare global {
namespace JSX {
type Element = HTMLElement | Text | DocumentFragment | string | number | boolean | null | undefined;
interface IntrinsicElements {
// Elementos HTML
div: HTMLAttributes;
span: HTMLAttributes;
p: HTMLAttributes;
a: AnchorHTMLAttributes;
button: ButtonHTMLAttributes;
input: InputHTMLAttributes;
form: FormHTMLAttributes;
img: ImgHTMLAttributes;
ul: HTMLAttributes;
ol: HTMLAttributes;
li: HTMLAttributes;
h1: HTMLAttributes;
h2: HTMLAttributes;
h3: HTMLAttributes;
h4: HTMLAttributes;
h5: HTMLAttributes;
h6: HTMLAttributes;
section: HTMLAttributes;
article: HTMLAttributes;
header: HTMLAttributes;
footer: HTMLAttributes;
nav: HTMLAttributes;
main: HTMLAttributes;
aside: HTMLAttributes;
label: HTMLAttributes;
select: SelectHTMLAttributes;
option: OptionHTMLAttributes;
textarea: TextareaHTMLAttributes;
table: TableHTMLAttributes;
tr: HTMLAttributes;
td: HTMLAttributes;
th: HTMLAttributes;
hr: HTMLAttributes;
br: HTMLAttributes;
// SVG
svg: SVGAttributes;
path: SVGAttributes;
circle: SVGAttributes;
rect: SVGAttributes;
line: SVGAttributes;
g: SVGAttributes;
}
interface HTMLAttributes {
id?: string;
className?: string;
class?: string;
style?: string | Partial<CSSStyleDeclaration>;
children?: any;
ref?: ((el: any) => void) | { current: any };
// Eventos
onClick?: (event: MouseEvent) => void;
onInput?: (event: Event) => void;
onChange?: (event: Event) => void;
onSubmit?: (event: Event) => void;
onKeyDown?: (event: KeyboardEvent) => void;
onKeyUp?: (event: KeyboardEvent) => void;
onFocus?: (event: FocusEvent) => void;
onBlur?: (event: FocusEvent) => void;
onMouseEnter?: (event: MouseEvent) => void;
onMouseLeave?: (event: MouseEvent) => void;
// Atributos ARIA
role?: string;
'aria-label'?: string;
'aria-hidden'?: boolean | 'true' | 'false';
'aria-expanded'?: boolean | 'true' | 'false';
// Atributos comunes
tabIndex?: number;
title?: string;
draggable?: boolean;
hidden?: boolean;
}
interface AnchorHTMLAttributes extends HTMLAttributes {
href?: string;
target?: '_blank' | '_self' | '_parent' | '_top';
rel?: string;
}
interface ButtonHTMLAttributes extends HTMLAttributes {
type?: 'button' | 'submit' | 'reset';
disabled?: boolean;
}
interface InputHTMLAttributes extends HTMLAttributes {
type?: 'text' | 'password' | 'email' | 'number' | 'checkbox' | 'radio' | 'file' | 'date';
value?: string | number;
checked?: boolean;
placeholder?: string;
disabled?: boolean;
required?: boolean;
name?: string;
min?: number;
max?: number;
step?: number;
}
interface FormHTMLAttributes extends HTMLAttributes {
action?: string;
method?: 'get' | 'post';
}
interface ImgHTMLAttributes extends HTMLAttributes {
src?: string;
alt?: string;
width?: number | string;
height?: number | string;
loading?: 'lazy' | 'eager';
}
interface SelectHTMLAttributes extends HTMLAttributes {
value?: string | string[];
disabled?: boolean;
required?: boolean;
multiple?: boolean;
}
interface OptionHTMLAttributes extends HTMLAttributes {
value?: string | number;
selected?: boolean;
disabled?: boolean;
}
interface TextareaHTMLAttributes extends HTMLAttributes {
value?: string;
placeholder?: string;
disabled?: boolean;
required?: boolean;
rows?: number;
cols?: number;
}
interface TableHTMLAttributes extends HTMLAttributes {
border?: number;
cellPadding?: number | string;
cellSpacing?: number | string;
}
interface SVGAttributes {
viewBox?: string;
width?: number | string;
height?: number | string;
fill?: string;
stroke?: string;
strokeWidth?: number | string;
xmlns?: string;
children?: any;
}
}
}
export { h as jsx, h as jsxs, h as Fragment };
export type { Signal, Component, CleanupFn };

308
sigpro.ui.d.ts vendored Normal file
View File

@@ -0,0 +1,308 @@
// sigpro.ui.d.ts
declare module "sigpro.ui" {
import type { Signal } from "./sigpro";
// Utilidades
function hide(): void;
// Toast
function toast(
message: string | (() => any) | any,
type?: "alert-success" | "alert-error" | "alert-warning" | "alert-info",
duration?: number
): () => void;
// Calendar
interface CalendarProps {
class?: string;
value?: Signal<string | { start?: string; end?: string; startHour?: number; endHour?: number }> | any;
range?: boolean | (() => boolean);
hour?: boolean;
onChange?: (value: any) => void;
}
function calendar(p: CalendarProps): HTMLElement;
// Pallete
interface PalleteProps {
class?: string;
value?: Signal<string> | ((v: string) => void);
onchange?: (color: string) => void;
}
function pallete(p: PalleteProps): HTMLElement;
// UI Components
namespace ui {
// Accordion
function accordion(p: { class?: string; name?: string; checked?: boolean }, ...c: any[]): HTMLElement;
function accordion_title(p: { class?: string }, ...c: any[]): HTMLElement;
function accordion_content(p: { class?: string }, ...c: any[]): HTMLElement;
// Alert
function alert(p: { class?: string }, ...c: any[]): HTMLElement;
// Autocomplete
interface AutocompleteProps {
label?: string;
items: string[] | { label: string; value: any }[] | Signal<any[]>;
value?: Signal<string> | ((v: any) => void);
placeholder?: string;
class?: string;
icon?: string;
onChange?: (v: any) => void;
}
function autocomplete(p: AutocompleteProps): HTMLElement;
// Avatar
function avatar(p: { class?: string; innerClass?: string }, ...c: any[]): HTMLElement;
function avatar_group(p: { class?: string }, ...c: any[]): HTMLElement;
// Badge
function badge(p: { class?: string }, ...c: any[]): HTMLElement;
// Breadcrumbs
function breadcrumbs(p: { class?: string }, ...c: any[]): HTMLElement;
// Button
function button(p: { class?: string; onclick?: (e: Event) => void; disabled?: boolean | Signal<boolean> }, ...c: any[]): HTMLElement;
// Card
function card(p: { class?: string }, ...c: any[]): HTMLElement;
function card_title(p: { class?: string }, ...c: any[]): HTMLElement;
function card_body(p: { class?: string }, ...c: any[]): HTMLElement;
function card_actions(p: { class?: string }, ...c: any[]): HTMLElement;
// Carousel
function carousel(p: { class?: string }, ...c: any[]): HTMLElement;
function carousel_item(p: { class?: string }, ...c: any[]): HTMLElement;
// Chat
function chat(p: { class?: string }, ...c: any[]): HTMLElement;
function chat_image(p: { class?: string }, ...c: any[]): HTMLElement;
function chat_header(p: { class?: string }, ...c: any[]): HTMLElement;
function chat_bubble(p: { class?: string }, ...c: any[]): HTMLElement;
function chat_footer(p: { class?: string }, ...c: any[]): HTMLElement;
// Checkbox
function checkbox(p: { class?: string; checked?: boolean | Signal<boolean>; [key: string]: any }): HTMLElement;
// Colorpicker
function colorpicker(p: AutocompleteProps): HTMLElement;
// Combo
interface ComboProps {
label?: string;
value?: Signal<string> | string | ((v: string) => void);
placeholder?: string;
class?: string;
icon?: string;
custom?: any;
disabled?: boolean | (() => boolean);
readonly?: string;
}
function combo(p: ComboProps, c?: Function): HTMLElement;
// Datepicker
interface DatepickerProps {
label?: string;
value?: Signal<string | { start?: string; end?: string }>;
range?: boolean | (() => boolean);
placeholder?: string;
class?: string;
fromPlaceholder?: string;
toPlaceholder?: string;
onChange?: (v: any) => void;
}
function datepicker(p: DatepickerProps): HTMLElement;
// Divider
function divider(p: { class?: string }): HTMLElement;
// Drawer
function drawer(p: { class?: string }, ...c: any[]): HTMLElement;
function drawer_toggle(p: { class?: string }): HTMLElement;
function drawer_content(p: { class?: string }, ...c: any[]): HTMLElement;
function drawer_side(p: { class?: string }, ...c: any[]): HTMLElement;
function drawer_overlay(p: { class?: string }): HTMLElement;
// Dropdown
function dropdown(p: { class?: string }, ...c: any[]): HTMLElement;
function dropdown_button(p: { class?: string; tabindex?: string; role?: string }, ...c: any[]): HTMLElement;
function dropdown_content(p: { class?: string; tabindex?: string }, ...c: any[]): HTMLElement;
// FAB
function fab(p: { class?: string }, ...c: any[]): HTMLElement;
function fab_button(p: { class?: string; tabindex?: string; role?: string }, ...c: any[]): HTMLElement;
// Fieldset
function fieldset(p: { class?: string; label?: string }, ...c: any[]): HTMLElement;
// File
function file(p: { class?: string; multiple?: boolean; accept?: string; onchange?: (e: Event) => void }): HTMLElement;
interface FileDragProps {
class?: string;
drag?: boolean | Signal<boolean>;
ondrag?: (v: boolean) => void;
ondrop?: (files: FileList) => void;
}
function file_drag(p: FileDragProps, ...c: any[]): HTMLElement;
interface FilePreviewProps {
class?: string;
files?: File[] | Signal<File[]>;
onremove?: (index: number) => void;
}
function file_preview(p: FilePreviewProps): HTMLElement;
function file_error(p: { class?: string; message?: string }): HTMLElement;
// Float
function float(p: { label?: string }, ...c: any[]): HTMLElement;
// Indicator
function indicator(p: { class?: string; value?: any; badgeClass?: string }, ...c: any[]): HTMLElement;
// Input
function input(p: {
label?: string;
class?: string;
icon?: string;
right?: any;
type?: string | (() => string);
placeholder?: string;
value?: Signal<string> | string;
[key: string]: any
}): HTMLElement;
// Kbd
function kbd(p: { class?: string }, ...c: any[]): HTMLElement;
// Label
function label(p: { class?: string }, ...c: any[]): HTMLElement;
// Loading
function loading(p: { class?: string }): HTMLElement;
// Menu
function menu(p: { class?: string }, ...c: any[]): HTMLElement;
interface MenuItem {
label?: string;
href?: string;
items?: MenuItem[];
open?: boolean;
submenuClass?: string;
}
function menu_items(p: { items?: MenuItem[] }): HTMLElement[];
// Modal
function modal(p: { class?: string }, ...c: any[]): HTMLElement;
function modal_box(p: { class?: string }, ...c: any[]): HTMLElement;
function modal_action(p: { class?: string }, ...c: any[]): HTMLElement;
// Navbar
function navbar(p: { class?: string }, ...c: any[]): HTMLElement;
// Option
function option(p: { [key: string]: any }, ...c: any[]): HTMLElement;
// Password
function password(p: { label?: string; class?: string; value?: Signal<string> }): HTMLElement;
// Progress
function progress(p: { class?: string; value?: number | Signal<number>; max?: number | string }): HTMLElement;
// Radial
function radial(p: { class?: string; value?: number | Signal<number>; role?: string }): HTMLElement;
// Radio
function radio(p: { class?: string; name?: string; checked?: boolean | Signal<boolean>; [key: string]: any }): HTMLElement;
// Range
function range(p: { class?: string; min?: number; max?: number; value?: number | Signal<number>; oninput?: (e: Event) => void }): HTMLElement;
// Rating
interface RatingProps {
class?: string;
count?: number;
mask?: string;
itemClass?: string;
name?: string;
value?: Signal<number> | number;
offset?: number;
onChange?: (i: number) => void;
}
function rating(p: RatingProps): HTMLElement;
// Search
function search(p: { label?: string; class?: string; icon?: string; value?: Signal<string>; placeholder?: string }): HTMLElement;
// Select
function select(p: { class?: string; [key: string]: any }, ...c: any[]): HTMLElement;
// Stack
function stack(p: { class?: string }, ...c: any[]): HTMLElement;
// Stat
function stat(p: { class?: string }, ...c: any[]): HTMLElement;
function stat_figure(p: { class?: string }, ...c: any[]): HTMLElement;
function stat_title(p: { class?: string }, ...c: any[]): HTMLElement;
function stat_value(p: { class?: string }, ...c: any[]): HTMLElement;
function stat_desc(p: { class?: string }, ...c: any[]): HTMLElement;
// Steps
function steps(p: { class?: string }, ...c: any[]): HTMLElement;
function step(p: { class?: string; dataContent?: string }, ...c: any[]): HTMLElement;
// Swap
function swap(p: { class?: string; value?: Signal<boolean> | boolean }, ...c: any[]): HTMLElement;
function swap_on(p: { class?: string }, ...c: any[]): HTMLElement;
function swap_off(p: { class?: string }, ...c: any[]): HTMLElement;
// Table
function table(p: { class?: string }, ...c: any[]): HTMLElement;
function table_head(p: { class?: string }, ...c: any[]): HTMLElement;
function table_body(p: { class?: string }, ...c: any[]): HTMLElement;
function table_foot(p: { class?: string }, ...c: any[]): HTMLElement;
function table_row(p: { class?: string }, ...c: any[]): HTMLElement;
function table_th(p: { class?: string }, ...c: any[]): HTMLElement;
function table_td(p: { class?: string }, ...c: any[]): HTMLElement;
// Tabs
function tabs(p: { class?: string }, ...c: any[]): HTMLElement;
interface TabProps {
class?: string;
classContent?: string;
name?: string;
label?: string;
content?: any;
checked?: boolean | (() => boolean);
tabs?: Signal<any[]> | ((v: any[]) => void);
index?: number;
closable?: boolean;
onclick?: () => void;
}
function tab(p: TabProps): any[];
// Textarea
function textarea(p: { class?: string; [key: string]: any }): HTMLElement;
// Textrotate
function textrotate(p: { class?: string }, ...c: any[]): HTMLElement;
// Theme
function theme(p?: { value?: string; class?: string }): HTMLElement;
// Timeline
function timeline(p: { class?: string }, ...c: any[]): HTMLElement;
function timeline_start(p: { class?: string }, ...c: any[]): HTMLElement;
function timeline_middle(p: { class?: string }, ...c: any[]): HTMLElement;
function timeline_end(p: { class?: string }, ...c: any[]): HTMLElement;
// Toggle
function toggle(p: { class?: string; value?: string | Signal<string>; checked?: boolean | Signal<boolean>; [key: string]: any }): HTMLElement;
// Tooltip
function tooltip(p: { class?: string; tip?: string }, ...c: any[]): HTMLElement;
// Validator
function validator(p: { class?: string }, ...c: any[]): HTMLElement;
}
}

View File

@@ -1,501 +0,0 @@
// =====================
// SECURITY
// =====================
const DANGEROUS_PROTOCOLS = /^(javascript|data|vbscript):/i;
const DANGEROUS_ATTR = /^on/i;
const sanitizeUrl = (url) => {
const str = String(url ?? "").trim().toLowerCase().replace(/\s+/g, "");
return DANGEROUS_PROTOCOLS.test(str) ? "#" : str;
};
const sanitizeAttr = (name, value) => {
if (value == null) return null;
if (DANGEROUS_ATTR.test(name)) return null;
if (name === "srcdoc") return null;
const str = String(value);
if (name === "href" || name === "src") {
return sanitizeUrl(str);
}
return str;
};
// =====================
// CORE
// =====================
let activeEffect = null;
let currentOwner = null;
const effectQueue = new Set();
let isFlushing = false;
const MOUNTED_NODES = new WeakMap();
const doc = document;
const isArr = Array.isArray;
const assign = Object.assign;
const createEl = (t) => doc.createElement(t);
const createText = (t) => doc.createTextNode(String(t ?? ""));
const isFunc = (f) => typeof f === "function";
const isObj = (o) => typeof o === "object" && o !== null;
const runWithContext = (effect, callback) => {
const prev = activeEffect;
activeEffect = effect;
try { return callback(); }
finally { activeEffect = prev; }
};
const cleanupNode = (node) => {
if (node._cleanups) {
node._cleanups.forEach((d) => d());
node._cleanups.clear();
}
node.childNodes?.forEach(cleanupNode);
};
const flushEffects = () => {
if (isFlushing) return;
isFlushing = true;
while (effectQueue.size) {
const list = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
effectQueue.clear();
for (const e of list) if (!e._deleted) e();
}
isFlushing = false;
};
const track = (subs) => {
if (activeEffect && !activeEffect._deleted) {
subs.add(activeEffect);
activeEffect._deps.add(subs);
}
};
const trigger = (subs) => {
subs.forEach((e) => {
if (e === activeEffect || e._deleted) return;
if (e._isComputed) {
e.markDirty();
if (e._subs) trigger(e._subs);
} else {
effectQueue.add(e);
}
});
if (!isFlushing) queueMicrotask(flushEffects);
};
// =====================
// RENDER
// =====================
const Render = (fn) => {
const cleanups = new Set();
const prevOwner = currentOwner;
const container = createEl("div");
container.style.display = "contents";
currentOwner = { cleanups };
const process = (res) => {
if (!res) return;
if (res._isRuntime) {
cleanups.add(res.destroy);
container.appendChild(res.container);
} else if (isArr(res)) {
res.forEach(process);
} else {
container.appendChild(res instanceof Node ? res : createText(res));
}
};
try {
process(fn({ onCleanup: (f) => cleanups.add(f) }));
} finally {
currentOwner = prevOwner;
}
return {
_isRuntime: true,
container,
destroy: () => {
cleanups.forEach((f) => f());
cleanupNode(container);
container.remove();
},
};
};
// =====================
// SIGNAL
// =====================
const $ = (init, key = null) => {
const subs = new Set();
if (isFunc(init)) {
let val, dirty = true;
const effect = () => {
if (effect._deleted) return;
effect._deps.forEach(d => d.delete(effect));
effect._deps.clear();
runWithContext(effect, () => {
const next = init();
if (!Object.is(val, next) || dirty) {
val = next;
dirty = false;
trigger(subs);
}
});
};
assign(effect, {
_deps: new Set(),
_isComputed: true,
_subs: subs,
_deleted: false,
markDirty: () => dirty = true,
stop: () => {
effect._deleted = true;
effect._deps.forEach(d => d.delete(effect));
subs.clear();
}
});
if (currentOwner) currentOwner.cleanups.add(effect.stop);
return () => {
if (dirty) effect();
track(subs);
return val;
};
}
let val = init;
if (key) {
try {
const saved = localStorage.getItem(key);
if (saved != null) val = JSON.parse(saved);
} catch {}
}
return (...args) => {
if (args.length) {
const next = isFunc(args[0]) ? args[0](val) : args[0];
if (!Object.is(val, next)) {
val = next;
if (key) localStorage.setItem(key, JSON.stringify(val));
trigger(subs);
}
}
track(subs);
return val;
};
};
// =====================
// REACTIVE OBJECT
// =====================
const $$ = (obj, cache = new WeakMap()) => {
if (!isObj(obj)) return obj;
if (cache.has(obj)) return cache.get(obj);
const subs = {};
const proxy = new Proxy(obj, {
get(t, k) {
track(subs[k] ??= new Set());
const v = Reflect.get(t, k);
return isObj(v) ? $$(v, cache) : v;
},
set(t, k, v) {
if (Object.is(t[k], v)) return true;
const ok = Reflect.set(t, k, v);
subs[k] && trigger(subs[k]);
return ok;
}
});
cache.set(obj, proxy);
return proxy;
};
// =====================
// WATCH
// =====================
const Watch = (target, cb) => {
const explicit = isArr(target);
const fn = explicit ? cb : target;
if (!isFunc(fn)) return () => {};
const owner = currentOwner;
const runner = () => {
if (runner._deleted) return;
runner._deps.forEach(d => d.delete(runner));
runner._deps.clear();
runner._cleanups.forEach(c => c());
runner._cleanups.clear();
const prevOwner = currentOwner;
runner.depth = activeEffect ? activeEffect.depth + 1 : 0;
runWithContext(runner, () => {
currentOwner = { cleanups: runner._cleanups };
if (explicit) {
runWithContext(null, fn);
target.forEach(d => isFunc(d) && d());
} else {
fn();
}
currentOwner = prevOwner;
});
};
assign(runner, {
_deps: new Set(),
_cleanups: new Set(),
_deleted: false,
stop: () => {
if (runner._deleted) return;
runner._deleted = true;
effectQueue.delete(runner);
runner._deps.forEach(d => d.delete(runner));
runner._cleanups.forEach(c => c());
if (owner) owner.cleanups.delete(runner.stop);
}
});
if (owner) owner.cleanups.add(runner.stop);
runner();
return runner.stop;
};
// =====================
// TAG (SECURE)
// =====================
const Tag = (tag, props = {}, children = []) => {
if (props instanceof Node || isArr(props) || !isObj(props)) {
children = props; props = {};
}
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
const el = isSVG
? doc.createElementNS("http://www.w3.org/2000/svg", tag)
: createEl(tag);
el._cleanups = new Set();
el.onUnmount = (fn) => el._cleanups.add(fn);
const booleanAttrs = ["disabled","checked","required","readonly","selected","multiple","autofocus"];
const setAttr = (k, v) => {
const safe = sanitizeAttr(k, v);
if (safe == null) return el.removeAttribute(k);
if (booleanAttrs.includes(k)) {
el[k] = !!safe;
safe ? el.setAttribute(k, "") : el.removeAttribute(k);
} else {
el.setAttribute(k, safe);
}
};
for (let [k, v] of Object.entries(props)) {
if (k === "ref") {
isFunc(v) ? v(el) : (v.current = el);
continue;
}
if (k.startsWith("on")) {
const evt = k.slice(2).toLowerCase().split(".")[0];
el.addEventListener(evt, v);
el._cleanups.add(() => el.removeEventListener(evt, v));
continue;
}
if (isFunc(v)) {
el._cleanups.add(Watch(() => {
const val = v();
k === "class" ? (el.className = val || "") : setAttr(k, val);
}));
} else {
setAttr(k, v);
}
}
const append = (c) => {
if (isArr(c)) return c.forEach(append);
if (isFunc(c)) {
const marker = createText("");
el.appendChild(marker);
let nodes = [];
el._cleanups.add(Watch(() => {
const res = c();
const next = (isArr(res) ? res : [res]).map(n =>
n?._isRuntime ? n.container : n instanceof Node ? n : createText(n)
);
nodes.forEach(n => { cleanupNode(n); n.remove(); });
next.forEach(n => marker.parentNode?.insertBefore(n, marker));
nodes = next;
}));
} else {
el.appendChild(c instanceof Node ? c : createText(c));
}
};
append(children);
return el;
};
// =====================
// IF
// =====================
const If = (cond, a, b = null) => {
const marker = createText("");
const container = Tag("div", { style: "display:contents" }, [marker]);
let view = null;
Watch(() => {
const state = !!(isFunc(cond) ? cond() : cond);
if (view) view.destroy();
const branch = state ? a : b;
if (branch) {
view = Render(() => isFunc(branch) ? branch() : branch);
container.insertBefore(view.container, marker);
}
});
return container;
};
// =====================
// FOR (OPTIMIZED)
// =====================
const For = (source, renderFn, keyFn, tag = "div", props = { style: "display:contents" }) => {
const marker = createText("");
const container = Tag(tag, props, [marker]);
let cache = new Map();
Watch(() => {
const items = (isFunc(source) ? source() : source) || [];
const next = new Map();
const order = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const key = keyFn ? keyFn(item, i) : i;
let view = cache.get(key);
if (!view) {
const res = renderFn(item, i);
view = res instanceof Node
? { container: res, destroy: () => { cleanupNode(res); res.remove(); } }
: Render(() => res);
}
cache.delete(key);
next.set(key, view);
order.push(key);
}
cache.forEach(v => v.destroy());
let anchor = marker;
for (let i = order.length - 1; i >= 0; i--) {
const v = next.get(order[i]);
if (v.container.nextSibling !== anchor) {
container.insertBefore(v.container, anchor);
}
anchor = v.container;
}
cache = next;
});
return container;
};
// =====================
// ROUTER
// =====================
const Router = (routes) => {
const path = $(window.location.hash.replace(/^#/, "") || "/");
window.addEventListener("hashchange", () =>
path(window.location.hash.replace(/^#/, "") || "/")
);
const outlet = Tag("div");
let view = null;
Watch([path], () => {
const p = path();
const route = routes.find(r => {
const rp = r.path.split("/").filter(Boolean);
const pp = p.split("/").filter(Boolean);
return rp.length === pp.length && rp.every((x, i) => x.startsWith(":") || x === pp[i]);
}) || routes.find(r => r.path === "*");
if (route) {
if (view) view.destroy();
view = Render(() => route.component());
outlet.appendChild(view.container);
}
});
return outlet;
};
// =====================
// MOUNT
// =====================
const Mount = (component, target) => {
const el = typeof target === "string" ? doc.querySelector(target) : target;
if (!el) return;
if (MOUNTED_NODES.has(el)) MOUNTED_NODES.get(el).destroy();
const instance = Render(isFunc(component) ? component : () => component);
el.replaceChildren(instance.container);
MOUNTED_NODES.set(el, instance);
return instance;
};
// =====================
// GLOBAL + TAG HELPERS
// =====================
const SigPro = { $, $$, Render, Watch, Tag, If, For, Router, Mount };
if (typeof window !== "undefined") {
Object.assign(window, SigPro);
const tags = `div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption img video audio canvas svg iframe picture source progress meter`.split(" ");
tags.forEach(tag => {
const name = tag[0].toUpperCase() + tag.slice(1);
if (!(name in window)) {
window[name] = (p, c) => Tag(tag, p, c);
}
});
window.SigPro = Object.freeze(SigPro);
}
export { $, $$, Render, Watch, Tag, If, For, Router, Mount };
export default SigPro;

View File

@@ -1,443 +0,0 @@
//sigpro
let activeEffect = null;
let currentOwner = null;
const effectQueue = new Set();
let isFlushing = false;
const MOUNTED_NODES = new WeakMap();
const reactiveCache = new WeakMap();
const DANGEROUS_PROTOCOLS = /^(javascript|data|vbscript):/i;
const sanitizeUrl = (url) => {
const str = String(url ?? '').trim().toLowerCase();
return DANGEROUS_PROTOCOLS.test(str) ? '#' : str;
};
const doc = document;
const createEl = (t) => doc.createElement(t);
const createText = (t) => doc.createTextNode(String(t ?? ""));
const flushEffects = () => {
if (isFlushing) return;
isFlushing = true;
const runs = [...effectQueue];
effectQueue.clear();
runs.forEach((e) => { if (!e._deleted) e(); });
isFlushing = false;
};
const cleanupNode = (node) => {
if (node._cleanups) {
node._cleanups.forEach((d) => d());
node._cleanups.clear();
}
node.childNodes?.forEach(cleanupNode);
};
const untrack = (fn) => {
const prev = activeEffect;
activeEffect = null;
try { return fn(); }
finally { activeEffect = prev; }
};
const $ = (val, key = null) => {
const subs = new Set();
if (key) {
try {
const saved = localStorage.getItem(key);
if (saved != null) val = JSON.parse(saved);
} catch {}
}
const sig = (...args) => {
if (args.length) {
const next = typeof args[0] === "function" ? untrack(() => args[0](val)) : args[0];
if (!Object.is(val, next)) {
val = next;
if (key) localStorage.setItem(key, JSON.stringify(val));
subs.forEach(e => effectQueue.add(e));
if (!isFlushing) queueMicrotask(flushEffects);
}
} else if (activeEffect && !activeEffect._deleted) {
subs.add(activeEffect);
activeEffect._deps.add(subs);
}
return val;
};
sig._isSig = true;
return sig;
};
const Computed = (fn) => {
const subs = new Set();
let cached, dirty = true;
const runner = Effect(() => {
if (!dirty) {
dirty = true;
subs.forEach(e => effectQueue.add(e));
if (!isFlushing) queueMicrotask(flushEffects);
}
});
const sig = () => {
if (dirty) {
cached = fn();
dirty = false;
}
if (activeEffect && !activeEffect._deleted) {
subs.add(activeEffect);
activeEffect._deps.add(subs);
}
return cached;
};
sig._isSig = true;
return sig;
};
const Store = (obj) => {
if (obj === null || typeof obj !== "object" || obj._isSig) return obj;
if (reactiveCache.has(obj)) return reactiveCache.get(obj);
const subs = new Map();
const proxy = new Proxy(obj, {
get(target, key) {
if (!subs.has(key)) subs.set(key, new Set());
const keySubs = subs.get(key);
if (activeEffect && !activeEffect._deleted) {
keySubs.add(activeEffect);
activeEffect._deps.add(keySubs);
}
const value = Reflect.get(target, key);
return (typeof value === "object" && value !== null) ? Store(value) : value;
},
set(target, key, value) {
const prev = Reflect.get(target, key);
if (Object.is(prev, value)) return true;
const result = Reflect.set(target, key, value);
const keySubs = subs.get(key);
if (keySubs) {
keySubs.forEach(e => effectQueue.add(e));
if (!isFlushing) queueMicrotask(flushEffects);
}
return result;
}
});
reactiveCache.set(obj, proxy);
return proxy;
};
const Effect = (cb) => {
if (typeof cb !== "function") return () => { };
const owner = currentOwner;
const runner = () => {
if (runner._deleted) return;
runner._deps.forEach(d => d.delete(runner));
runner._deps.clear();
runner._cleanups.forEach(c => c());
runner._cleanups.clear();
const prevOwner = currentOwner;
const prevEffect = activeEffect;
currentOwner = { cleanups: runner._cleanups, parent: owner };
activeEffect = runner;
try { cb(); }
finally {
currentOwner = prevOwner;
activeEffect = prevEffect;
}
};
runner._deps = new Set();
runner._cleanups = new Set();
runner._deleted = false;
runner.stop = () => {
if (runner._deleted) return;
runner._deleted = true;
effectQueue.delete(runner);
runner._deps.forEach(d => d.delete(runner));
runner._cleanups.forEach(c => c());
if (owner) owner.cleanups.delete(runner.stop);
};
if (owner) owner.cleanups.add(runner.stop);
runner();
return runner.stop;
};
const Watch = (source, cb) => {
let oldValue;
let first = true;
return Effect(() => {
const newValue = typeof source === "function" ? source() : source();
if (!first) {
untrack(() => cb(newValue, oldValue));
}
first = false;
oldValue = newValue;
});
};
const Tag = (tag, props = {}, children = []) => {
if (props instanceof Node || Array.isArray(props) || typeof props !== "object") {
children = props; props = {};
}
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
const el = isSVG
? doc.createElementNS("http://www.w3.org/2000/svg", tag)
: createEl(tag);
el._cleanups = new Set();
el.onUnmount = (fn) => el._cleanups.add(fn);
const booleanAttrs = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
for (let [k, v] of Object.entries(props)) {
if (k === "ref") {
typeof v === "function" ? v(el) : (v.current = el);
continue;
}
if (k.startsWith("on")) {
const evt = k.slice(2).toLowerCase().split(".")[0];
el.addEventListener(evt, v);
el._cleanups.add(() => el.removeEventListener(evt, v));
continue;
}
const setAttr = (val) => {
if (k === "class") el.className = val || "";
else if (booleanAttrs.includes(k)) {
el[k] = !!val;
val ? el.setAttribute(k, "") : el.removeAttribute(k);
} else {
const finalVal = (k === 'src' || k === 'href') ? sanitizeUrl(val) : val;
el.setAttribute(k, finalVal);
}
};
if (typeof v === "function") {
el._cleanups.add(Effect(() => setAttr(v())));
} else {
setAttr(v);
}
}
const append = (c) => {
if (Array.isArray(c)) return c.forEach(append);
if (typeof c === "function") {
const marker = createText("");
el.appendChild(marker);
let nodes = [];
el._cleanups.add(Effect(() => {
const res = c();
const next = (Array.isArray(res) ? res : [res]).map(n =>
n?._isRuntime ? n.container : n instanceof Node ? n : createText(n)
);
nodes.forEach(n => { cleanupNode(n); n.remove(); });
next.forEach(n => marker.parentNode?.insertBefore(n, marker));
nodes = next;
}));
} else {
el.appendChild(c instanceof Node ? c : createText(c));
}
};
append(children);
return el;
};
const Render = (fn) => {
const cleanups = new Set();
const prevOwner = currentOwner;
const container = createEl("div");
container.style.display = "contents";
currentOwner = {
cleanups,
parent: prevOwner,
context: null
};
const process = (res) => {
if (!res) return;
if (res._isRuntime) {
cleanups.add(res.destroy);
container.appendChild(res.container);
} else if (Array.isArray(res)) {
res.forEach(process);
} else {
container.appendChild(res instanceof Node ? res : createText(res));
}
};
try {
process(fn({ onCleanup: (f) => cleanups.add(f) }));
} finally {
currentOwner = prevOwner;
}
return {
_isRuntime: true,
container,
destroy: () => {
cleanups.forEach((f) => f());
cleanupNode(container);
container.remove();
},
};
};
const Share = (key, value) => {
if (!currentOwner) return;
if (!currentOwner.context) currentOwner.context = new Map();
currentOwner.context.set(key, value);
};
const Use = (key, defaultValue) => {
let owner = currentOwner;
while (owner) {
if (owner.context && owner.context.has(key)) {
return owner.context.get(key);
}
owner = owner.parent;
}
return defaultValue;
};
const If = (cond, a, b = null, options = {}) => {
const marker = createText("");
const container = Tag("div", { style: "display:contents" }, [marker]);
let currentView = null;
let lastState = null;
Effect(() => {
const state = !!(typeof cond === "function" ? cond() : cond);
if (state === lastState) return;
lastState = state;
const branch = state ? a : b;
const oldView = currentView;
if (oldView) {
if (options.off) {
options.off(oldView.container, () => oldView.destroy());
} else {
oldView.destroy();
}
}
if (branch) {
currentView = Render(() => typeof branch === "function" ? branch() : branch);
const el = currentView.container;
container.insertBefore(el, marker);
if (options.on) {
requestAnimationFrame(() => {
requestAnimationFrame(() => options.on(el));
});
}
} else {
currentView = null;
}
});
return container;
};
const For = (source, renderFn, keyFn, tag = "div", props = { style: "display:contents" }) => {
const marker = createText("");
const container = Tag(tag, props, [marker]);
let cache = new Map();
Effect(() => {
const items = (typeof source === "function" ? source() : source) || [];
const next = new Map();
const order = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const key = keyFn ? keyFn(item, i) : i;
let view = cache.get(key);
if (!view) {
const res = renderFn(item, i);
view = res instanceof Node
? { container: res, destroy: () => { cleanupNode(res); res.remove(); } }
: Render(() => res);
}
cache.delete(key);
next.set(key, view);
order.push(key);
}
cache.forEach(v => v.destroy());
let anchor = marker;
for (let i = order.length - 1; i >= 0; i--) {
const v = next.get(order[i]);
if (v.container.nextSibling !== anchor) {
container.insertBefore(v.container, anchor);
}
anchor = v.container;
}
cache = next;
});
return container;
};
const Router = (routes) => {
const path = $(window.location.hash.replace(/^#/, "") || "/");
window.addEventListener("hashchange", () =>
path(window.location.hash.replace(/^#/, "") || "/")
);
const outlet = Tag("div");
let view = null;
Effect(() => {
const p = path();
const route = routes.find(r => {
const rp = r.path.split("/").filter(Boolean);
const pp = p.split("/").filter(Boolean);
return rp.length === pp.length && rp.every((x, i) => x.startsWith(":") || x === pp[i]);
}) || routes.find(r => r.path === "*");
if (route) {
const params = {};
const rp = route.path.split("/").filter(Boolean);
const pp = p.split("/").filter(Boolean);
rp.forEach((part, i) => {
if (part.startsWith(":")) params[part.slice(1)] = pp[i];
});
Router.params(params);
if (view) view.destroy();
view = Render(() => route.component(params));
outlet.appendChild(view.container);
}
});
return outlet;
};
Router.params = $({});
Router.to = (p) => (window.location.hash = p.replace(/^#?\/?/, "#/"));
Router.back = () => window.history.back();
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
const Mount = (component, target) => {
const el = typeof target === "string" ? doc.querySelector(target) : target;
if (!el) return;
if (MOUNTED_NODES.has(el)) MOUNTED_NODES.get(el).destroy();
const instance = Render(typeof component === "function" ? component : () => component);
el.replaceChildren(instance.container);
MOUNTED_NODES.set(el, instance);
return instance;
};
const SigPro = { $, Computed, Store, untrack, Render, Effect, Watch, Tag, If, For, Router, Mount, Share, Use };
if (typeof window !== "undefined") {
Object.assign(window, SigPro);
const tags = `div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption img video audio canvas svg iframe picture source progress meter`.split(" ");
tags.forEach(tag => {
const name = tag[0].toUpperCase() + tag.slice(1);
if (!(name in window)) {
window[name] = (p, c) => Tag(tag, p, c);
}
});
window.SigPro = Object.freeze(SigPro);
}
export { $, Computed, Store, untrack, Render, Effect, Watch, Tag, If, For, Router, Mount, Share, Use };
export { Tag as jsx, Tag as jsxs, Tag as Fragment };
export default SigPro;

View File

@@ -1,272 +0,0 @@
/*
* SigPro
*/
// --- 1. CORE REACTIVO ---
let activeEffect = null, currentContext = null, isScheduled = false;
const queue = new Set(), nodeContexts = new WeakMap(), reactiveCache = new WeakMap();
const tick = () => {
while (queue.size) {
const runs = [...queue];
queue.clear();
runs.forEach(fn => fn());
}
isScheduled = false;
};
const track = (subs) => {
if (activeEffect) {
subs.add(activeEffect);
activeEffect.deps.add(subs);
}
};
export const untrack = (fn) => {
const prev = activeEffect;
activeEffect = null;
const res = fn();
activeEffect = prev;
return res;
};
// --- 2. ESTADOS Y REACTIVIDAD ---
export const Signal = (value) => {
const subs = new Set();
return {
_isSig: true,
get value() { track(subs); return value; },
set value(v) {
if (v === value) return;
value = v;
subs.forEach(f => queue.add(f));
if (!isScheduled) { isScheduled = true; queueMicrotask(tick); }
}
};
};
export const Reactive = (obj) => {
if (!obj || typeof obj !== 'object') return obj;
if (reactiveCache.has(obj)) return reactiveCache.get(obj);
const subs = {};
const proxy = new Proxy(obj, {
get(t, k) {
track(subs[k] ??= new Set());
const val = t[k];
return (val && typeof val === 'object') ? Reactive(val) : val;
},
set(t, k, v) {
if (t[k] === v) return true;
t[k] = v;
if (subs[k]) {
subs[k].forEach(f => queue.add(f));
if (!isScheduled) { isScheduled = true; queueMicrotask(tick); }
}
return true;
}
});
reactiveCache.set(obj, proxy);
return proxy;
};
export const Computed = (fn) => {
const c = Signal(fn());
Effect(() => c.value = fn());
return { get value() { return c.value; } };
};
export const Watch = (source, cb) => {
let old;
return Effect(() => {
const val = typeof source === 'function' ? source() : source.value;
const prev = old;
old = val;
untrack(() => cb(val, prev));
});
};
export const Storage = (key, val) => {
const saved = localStorage.getItem(key);
const s = Signal(saved !== null ? JSON.parse(saved) : val);
Effect(() => localStorage.setItem(key, JSON.stringify(s.value)));
return s;
};
// --- 3. EFECTOS Y CICLO DE VIDA ---
export const Effect = (fn) => {
let cleanup;
const runner = () => {
if (cleanup) cleanup();
const prevEff = activeEffect;
activeEffect = runner;
cleanup = fn();
activeEffect = prevEff;
};
runner.deps = new Set();
if (activeEffect?.scopes) activeEffect.scopes.push(runner);
else if (currentContext) currentContext.cleanups.push(runner);
runner();
return () => { if (cleanup) cleanup(); runner.deps.forEach(s => s.delete(runner)); };
};
export const Scope = (fn) => {
const scopes = [];
const prev = activeEffect;
activeEffect = { scopes };
fn();
activeEffect = prev;
return () => scopes.forEach(s => s());
};
export const onMount = f => currentContext?.mount.push(f);
export const onUnmount = f => currentContext?.unmount.push(f);
// --- 4. RENDERER & DIFFING ---
const isNode = v => v instanceof Node;
const DANGEROUS = /^(javascript|data|vbscript):/i;
const sanitize = v => DANGEROUS.test(String(v)) ? '#' : v;
export const destroy = async (node) => {
if (!node) return;
const ctx = nodeContexts.get(node);
if (node.off) await node.off(node); // Soporte para tu If/Transition manual
if (ctx) {
ctx.unmount.forEach(f => f());
ctx.cleanups.forEach(f => f());
nodeContexts.delete(node);
}
const children = Array.from(node.childNodes);
for (const child of children) await destroy(child);
node.remove();
};
const append = (parent, child) => {
if (child == null) return;
if (typeof child === 'function') {
const anchor = document.createTextNode('');
parent.appendChild(anchor);
let nodes = [];
Effect(async () => {
const next = [child()].flat(Infinity)
.map(n => typeof n === 'function' ? n() : n).flat(Infinity)
.filter(n => n != null)
.map(n => isNode(n) ? n : document.createTextNode(String(n)));
const nextSet = new Set(next);
for (const n of nodes) { if (!nextSet.has(n)) await destroy(n); }
const oldIdxs = new Map(nodes.filter(n => nextSet.has(n)).map((n, i) => [n, i]));
let p = oldIdxs.size - 1;
for (let i = next.length - 1; i >= 0; i--) {
const node = next[i];
const ref = next[i+1] || anchor;
if (!oldIdxs.has(node) || nodes[p] !== node) {
parent.insertBefore(node, ref);
if (node.on) queueMicrotask(() => node.on(node));
} else p--;
}
nodes = next;
});
} else {
const n = isNode(child) ? child : document.createTextNode(String(child));
parent.appendChild(n);
if (n.on) queueMicrotask(() => n.on(n));
}
};
export const h = (tag, props = {}, ...children) => {
props = props || {};
const flat = children.flat(Infinity);
if (!tag) return () => flat;
if (tag === 'component') return () => h(props.is, props, ...flat);
if (typeof tag === 'function') {
const ctx = { mount: [], unmount: [], cleanups: [], Share: {}, parent: currentContext };
const prev = currentContext;
currentContext = ctx;
const el = tag(props, { children: flat });
if (isNode(el)) nodeContexts.set(el, ctx);
currentContext = prev;
queueMicrotask(() => ctx.mount.forEach(f => f()));
return el;
}
const isSVG = /^(svg|path|circle|rect|g)$/i.test(tag);
const el = isSVG ? document.createElementNS("http://www.w3.org/2000/svg", tag) : document.createElement(tag);
for (const k in props) {
const v = props[k];
if (k.startsWith('on')) el.addEventListener(k.slice(2).toLowerCase(), v);
else if (k === 'ref') { if (typeof v === 'function') v(el); else if (v?._isSig) v.value = el; }
else if (typeof v === 'function' || v?._isSig) {
Effect(() => {
const val = typeof v === 'function' ? v() : v.value;
if (k === 'on' || k === 'off') { el[k] = val; return; }
const attr = (k === 'href' || k === 'src') ? sanitize(val) : val;
if (!isSVG && k in el) el[k] = attr; else el.setAttribute(k, attr);
});
} else {
if (k === 'on' || k === 'off') { el[k] = v; continue; }
const attr = (k === 'href' || k === 'src') ? sanitize(v) : v;
if (!isSVG && k in el) el[k] = attr; else el.setAttribute(k, attr);
}
}
flat.forEach(c => append(el, c));
return el;
};
// --- 5. COMPONENTES Y RUTAS ---
export const If = (c, t, e) => () => c() ? t() : (e ? e() : null);
export const For = (l, k, r) => {
let cache = new Map();
return () => {
const items = typeof l === 'function' ? l() : l.value;
const next = new Map();
const res = items.map((item, i) => {
const id = k ? k(item, i) : (item.id || i);
const n = cache.get(id) || r(item, i);
next.set(id, n);
return n;
});
cache = next; return res;
};
};
export const Router = (routes) => {
const path = Signal(window.location.hash.replace(/^#/, '') || '/');
window.onhashchange = () => path.value = window.location.hash.replace(/^#/, '') || '/';
const outlet = h('div', { class: 'router-outlet' });
let view = null;
Effect(async () => {
const r = routes.find(x => x.path === path.value) || routes.find(x => x.path === '*');
if (view) await destroy(view);
if (r) {
view = r.component();
outlet.appendChild(view);
}
});
return outlet;
};
export const Mount = (r, t) => {
const el = typeof r === 'function' ? r() : r;
const container = (typeof t === 'string' ? document.querySelector(t) : t);
container.replaceChildren(el);
return () => destroy(el);
};
// --- 6. CONTEXTO (DEPENDENCY INJECTION) ---
export const Share = (k, v) => { if (currentContext) currentContext.Share[k] = v; };
export const Use = (k, d) => {
let c = currentContext;
while (c) { if (c.Share[k] !== undefined) return c.Share[k]; c = c.parent; }
return d;
};
export default {
Signal, Reactive, Computed, Effect, Watch, Storage,
untrack, Scope, h, If, For, Router, Mount,
onMount, onUnmount, Share, Use
};

View File

@@ -1,299 +0,0 @@
const isFn = (v) => typeof v === 'function';
const isNode = (v) => v instanceof Node;
let isScheduled = false;
const queue = new Set();
const tick = () => {
queue.forEach(fn => fn());
queue.clear();
isScheduled = false;
}
let activeEffect = null;
export const effect = (fn, is_scope = false) => {
let cleanup = null;
const run = () => {
stop();
const prev = activeEffect;
activeEffect = run;
try { cleanup = fn(); } finally { activeEffect = prev; }
}
const stop = () => {
run.e.forEach(subs => subs.delete(run));
run.e.clear();
if (isFn(cleanup)) cleanup();
if (run.c) {
run.c.forEach(s => s());
run.c.length = 0;
}
}
run.e = new Set();
if (is_scope) run.c = [];
run();
if (activeEffect?.c) activeEffect.c.push(stop);
return stop;
}
export const scope = f => effect(f, true);
const track = (subs) => {
if (activeEffect && !activeEffect.c) {
subs.add(activeEffect);
activeEffect.e.add(subs);
}
}
export const signal = (value) => {
const subs = new Set();
return {
_isSig: true,
get value() { track(subs); return value; },
set value(newValue) {
if (newValue === value) return;
value = newValue;
subs.forEach(fn => queue.add(fn));
if (!isScheduled) { isScheduled = true; queueMicrotask(tick); }
}
}
}
export const untrack = (fn) => {
const prev = activeEffect;
activeEffect = null;
const result = fn();
activeEffect = prev;
return result;
}
export const computed = (fn) => {
const sig = signal();
effect(() => sig.value = fn());
return { get value() { return sig.value; } };
}
const reactiveCache = new WeakMap();
export const reactive = (obj) => {
if (reactiveCache.has(obj)) return reactiveCache.get(obj);
const subs = {};
const proxy = new Proxy(obj, {
get(t, key) {
track(subs[key] ??= new Set());
const val = t[key];
return (val && typeof val === 'object') ? reactive(val) : val;
},
set(t, key, val) {
if (t[key] === val) return true;
t[key] = val;
if (subs[key]) {
subs[key].forEach(fn => queue.add(fn));
if (!isScheduled) { isScheduled = true; queueMicrotask(tick); }
}
return true;
}
});
reactiveCache.set(obj, proxy);
return proxy;
}
export const persist = (key, target) => {
const saved = localStorage.getItem(key);
if (saved !== null) {
const data = JSON.parse(saved);
if (target._isSig) target.value = data;
else Object.assign(target, data);
}
effect(() => {
const val = target._isSig ? target.value : target;
localStorage.setItem(key, JSON.stringify(val));
});
return target;
};
export const storage = (key, val) => persist(key, signal(val));
export const watch = (source, cb) => {
let first = true, oldValue;
return effect(() => {
const newValue = isFn(source) ? source() : source.value;
if (!first) untrack(() => cb(newValue, oldValue));
else first = false;
oldValue = newValue;
});
}
let context = null;
export const onMount = (fn) => context?.m.push(fn);
export const onUnmount = (fn) => context?.u.push(fn);
export const provide = (key, value) => context && (context.p[key] = value);
export const inject = (key, dft) => context && (key in context.p ? context.p[key] : dft);
const remove = async (node) => {
if (Array.isArray(node)) return Promise.all(node.map(remove));
if (node.$off) await node.$off(node);
else if (node.$l) await new Promise(res => node.$l(res));
node.$s?.();
if (node.$c) node.$c.u.forEach(f => f());
node.remove();
}
const render = (fn, ...data) => {
let node;
const stop = effect(() => {
node = fn(...data);
if (isFn(node)) node = node();
}, true);
if (node) node.$s = stop;
return node;
}
export const h = (tag, props = {}, ...children) => {
children = children.flat(Infinity);
if (isFn(tag)) {
const prev = context;
context = { m: [], u: [], p: { ...(prev?.p || {}) } };
const ctx = context;
let el;
const stop = effect(() => {
el = tag(props, { children, emit: (evt, ...args) => props[`on${evt[0].toUpperCase()}${evt.slice(1)}`]?.(...args) });
return () => ctx.u.forEach(f => f());
}, true);
const out = isNode(el) ? el : document.createTextNode(String(el));
out.$c = ctx;
out.$s = stop;
if (props.on) out.$on = props.on;
if (props.off) out.$off = props.off;
context = prev;
return out;
}
if (!tag) return children;
const isSvg = tag === 'svg' || tag === 'path' || tag === 'circle';
const el = isSvg ? document.createElementNS("http://www.w3.org/2000/svg", tag) : document.createElement(tag);
for (const key in props) {
const val = props[key];
if (key.startsWith('on') && key !== 'on' && key !== 'off') el.addEventListener(key.slice(2).toLowerCase(), val);
else if (key === "ref") isFn(val) ? val(el) : val.value = el;
else if (key === "on") el.$on = val;
else if (key === "off") el.$off = val;
else if (isFn(val)) effect(() => el[key] = val());
else el[key] = val;
}
children.forEach(child => append(el, child));
return el;
}
const append = (parent, child) => {
if (child == null) return;
if (isFn(child)) {
const anchor = document.createTextNode('');
parent.appendChild(anchor);
let nodes = [];
effect(async () => {
const raw = [child()].flat(Infinity).filter(n => n != null);
const newNodes = raw.map(n => isNode(n) ? n : document.createTextNode(String(n)));
for (const n of nodes) { if (!newNodes.includes(n)) await remove(n); }
newNodes.forEach((n, i) => {
if (!nodes.includes(n)) {
parent.insertBefore(n, newNodes[i+1] || anchor);
if (n.$on) n.$on(n);
if (n.$c) n.$c.m.forEach(f => f());
}
});
nodes = newNodes;
}, true);
} else {
const n = isNode(child) ? child : document.createTextNode(String(child));
parent.appendChild(n);
if (n.$on) n.$on(n);
}
}
export const If = (cond, renderFn, fallback = null, transitions = {}) => {
let cached, current;
return () => {
const show = !!cond();
if (show !== current) {
const update = async () => {
if (cached) await remove(cached);
cached = show ? render(renderFn) : (isFn(fallback) ? render(fallback) : fallback);
if (isNode(cached)) {
if (transitions.on) cached.$on = transitions.on;
if (transitions.off) cached.$off = transitions.off;
}
current = show;
};
update();
}
return cached;
}
}
export const For = (list, key, renderFn) => {
let cache = new Map();
return () => {
const next = new Map();
const items = isFn(list) ? list() : (list.value || list);
const res = items.map((item, i) => {
const id = isFn(key) ? key(item, i) : (key ? item[id] : item);
let node = cache.get(id);
if (!node) node = render(renderFn, item, i);
next.set(id, node);
return node;
});
cache.forEach(async (node, id) => { if (!next.has(id)) await remove(node); });
cache = next;
return res;
}
}
export const Router = (routes) => {
const path = signal(window.location.hash.slice(1) || '/');
window.onhashchange = () => path.value = window.location.hash.slice(1) || '/';
return h('div', { class: 'router-view' }, () => {
const route = routes.find(r => r.path === path.value) || routes.find(r => r.path === '*');
return route ? h(route.component) : null;
});
};
export const Component = ({ is, ...props }, { children }) => () => h(isFn(is) ? is() : is, props, children);
export const Transition = ({ enter: e, idle, leave: l }, { children: [c] }) => {
const decorate = (el) => {
if (!isNode(el)) return el;
const addClass = c => c && el.classList.add(...c.split(' '));
const removeClass = c => c && el.classList.remove(...c.split(' '));
el.$on = () => {
if (!e) return;
requestAnimationFrame(() => {
addClass(e[1]);
requestAnimationFrame(() => {
addClass(e[0]); removeClass(e[1]); addClass(e[2]);
el.addEventListener('transitionend', () => {
removeClass(e[2]); removeClass(e[0]); addClass(idle);
}, { once: true });
});
});
};
el.$off = (node) => {
if (!l) return node.remove();
return new Promise(res => {
removeClass(idle); addClass(l[1]);
requestAnimationFrame(() => {
addClass(l[0]); removeClass(l[1]); addClass(l[2]);
el.addEventListener('transitionend', () => {
removeClass(l[2]); removeClass(l[0]); res();
}, { once: true });
});
});
};
return el;
}
return isFn(c) ? () => decorate(c()) : decorate(c);
}
export default (target, root, props) => {
const el = h(root, props);
target.appendChild(el);
if (el.$on) el.$on(el);
if (el.$c) el.$c.m.forEach(f => f());
return () => remove(el);
}

66843
src/grid-e/main.esm.mjs Normal file

File diff suppressed because one or more lines are too long

131
src/grid-e/package.json Normal file
View File

@@ -0,0 +1,131 @@
{
"name": "ag-grid-enterprise",
"version": "35.2.0",
"description": "Advanced Data Grid / Data Table supporting Javascript / Typescript / React / Angular / Vue",
"main": "./dist/package/main.cjs.js",
"types": "./dist/types/src/main.d.ts",
"module": "./main.esm.mjs",
"exports": {
".": {
"import": "./dist/package/main.esm.mjs",
"types": "./dist/types/src/main.d.ts",
"require": "./dist/package/main.cjs.js",
"default": "./dist/package/main.cjs.js"
},
"./styles/ag-grid-no-native-widgets.css": "./styles/ag-grid-no-native-widgets.css",
"./styles/ag-grid-no-native-widgets.min.css": "./styles/ag-grid-no-native-widgets.min.css",
"./styles/ag-grid.css": "./styles/ag-grid.css",
"./styles/ag-grid.min.css": "./styles/ag-grid.min.css",
"./styles/ag-theme-alpine-no-font.css": "./styles/ag-theme-alpine-no-font.css",
"./styles/ag-theme-alpine-no-font.min.css": "./styles/ag-theme-alpine-no-font.min.css",
"./styles/ag-theme-alpine.css": "./styles/ag-theme-alpine.css",
"./styles/ag-theme-alpine.min.css": "./styles/ag-theme-alpine.min.css",
"./styles/ag-theme-balham-no-font.css": "./styles/ag-theme-balham-no-font.css",
"./styles/ag-theme-balham-no-font.min.css": "./styles/ag-theme-balham-no-font.min.css",
"./styles/ag-theme-balham.css": "./styles/ag-theme-balham.css",
"./styles/ag-theme-balham.min.css": "./styles/ag-theme-balham.min.css",
"./styles/ag-theme-material-no-font.css": "./styles/ag-theme-material-no-font.css",
"./styles/ag-theme-material-no-font.min.css": "./styles/ag-theme-material-no-font.min.css",
"./styles/ag-theme-material.css": "./styles/ag-theme-material.css",
"./styles/ag-theme-material.min.css": "./styles/ag-theme-material.min.css",
"./styles/ag-theme-quartz-no-font.css": "./styles/ag-theme-quartz-no-font.css",
"./styles/ag-theme-quartz-no-font.min.css": "./styles/ag-theme-quartz-no-font.min.css",
"./styles/ag-theme-quartz.css": "./styles/ag-theme-quartz.css",
"./styles/ag-theme-quartz.min.css": "./styles/ag-theme-quartz.min.css",
"./styles/agGridAlpineFont.css": "./styles/agGridAlpineFont.css",
"./styles/agGridAlpineFont.min.css": "./styles/agGridAlpineFont.min.css",
"./styles/agGridBalhamFont.css": "./styles/agGridBalhamFont.css",
"./styles/agGridBalhamFont.min.css": "./styles/agGridBalhamFont.min.css",
"./styles/agGridClassicFont.css": "./styles/agGridClassicFont.css",
"./styles/agGridClassicFont.min.css": "./styles/agGridClassicFont.min.css",
"./styles/agGridMaterialFont.css": "./styles/agGridMaterialFont.css",
"./styles/agGridMaterialFont.min.css": "./styles/agGridMaterialFont.min.css",
"./styles/agGridQuartzFont.css": "./styles/agGridQuartzFont.css",
"./styles/agGridQuartzFont.min.css": "./styles/agGridQuartzFont.min.css",
"./styles": "./styles/_index.scss"
},
"sideEffects": [
"./styles/ag-grid-no-native-widgets.css",
"./styles/ag-grid-no-native-widgets.min.css",
"./styles/ag-grid.css",
"./styles/ag-grid.min.css",
"./styles/ag-theme-alpine-no-font.css",
"./styles/ag-theme-alpine-no-font.min.css",
"./styles/ag-theme-alpine.css",
"./styles/ag-theme-alpine.min.css",
"./styles/ag-theme-balham-no-font.css",
"./styles/ag-theme-balham-no-font.min.css",
"./styles/ag-theme-balham.css",
"./styles/ag-theme-balham.min.css",
"./styles/ag-theme-material-no-font.css",
"./styles/ag-theme-material-no-font.min.css",
"./styles/ag-theme-material.css",
"./styles/ag-theme-material.min.css",
"./styles/ag-theme-quartz-no-font.css",
"./styles/ag-theme-quartz-no-font.min.css",
"./styles/ag-theme-quartz.css",
"./styles/ag-theme-quartz.min.css",
"./styles/agGridAlpineFont.css",
"./styles/agGridAlpineFont.min.css",
"./styles/agGridBalhamFont.css",
"./styles/agGridBalhamFont.min.css",
"./styles/agGridClassicFont.css",
"./styles/agGridClassicFont.min.css",
"./styles/agGridMaterialFont.css",
"./styles/agGridMaterialFont.min.css",
"./styles/agGridQuartzFont.css",
"./styles/agGridQuartzFont.min.css"
],
"repository": {
"type": "git",
"url": "https://github.com/ag-grid/ag-grid.git"
},
"keywords": [
"ag",
"ag-grid",
"datagrid",
"data-grid",
"datatable",
"data-table",
"grid",
"table",
"react",
"table",
"angular",
"angular-component",
"react",
"react-component",
"reactjs",
"vue",
"vuejs"
],
"author": "Sean Landsman <sean@thelandsmans.com>",
"license": "Commercial",
"bugs": {
"url": "https://github.com/ag-grid/ag-grid/issues"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie >= 0",
"not ie_mob >= 0",
"not blackberry > 0"
],
"homepage": "https://www.ag-grid.com/",
"dependencies": {
"ag-grid-community": "35.2.0"
},
"optionalDependencies": {
"ag-charts-community": "13.2.0",
"ag-charts-enterprise": "13.2.0"
},
"devDependencies": {
"ag-charts-community": "13.2.0",
"ag-charts-enterprise": "13.2.0",
"@types/jest": "^29.5.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.7.0",
"jest-canvas-mock": "2.5.2",
"canvas": "^3.2.1"
}
}

147
src/sigpro.convert.js Normal file
View File

@@ -0,0 +1,147 @@
/// <reference path="../sigpro.d.ts" />
var { $ } = window.SigPro;
function html2sigpro(h, mode = "tags") {
const B = new Set(["allowfullscreen", "async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "formnovalidate", "hidden", "ismap", "itemscope", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected", "truespeed"]);
const esc = v => v.replace(/"/g, '\\"');
const bP = el => {
let a = [...el.attributes].map(({ name: n, value: v }) =>
/^on/i.test(n) ? `${n}: (e) => { ${v.replace(/\s+/g, " ").trim()} }` :
(B.has(n.toLowerCase()) && (!v || v == n)) ? `${n}: true` : `${n}: "${esc(v)}"`
);
return a.length ? `{ ${a.join(", ")} }` : "";
};
const cN = (n, d = 0) => {
let s = " ".repeat(d);
if (n.nodeType == 3) {
let t = n.textContent;
return t.trim() ? `${s}"${esc(t)}"` : "";
}
if (n.nodeType == 1) {
let tag = n.tagName.toLowerCase();
let props = bP(n);
let prefix = mode === "core" ? `h('${tag}'` : tag;
let children = [...n.childNodes].map(i => cN(i, d + 1)).filter(Boolean);
const hasProps = !!props;
if (mode === "core") {
if (!children.length) return hasProps ? `${s}${prefix}, ${props})` : `${s}${prefix})`;
if (children.length === 1 && !children[0].includes("\n"))
return hasProps ? `${s}${prefix}, ${props}, ${children[0].trim()})` : `${s}${prefix}, ${children[0].trim()})`;
return hasProps ? `${s}${prefix}, ${props}, [\n${children.join(",\n")}\n${s}])` : `${s}${prefix}, [\n${children.join(",\n")}\n${s}])`;
} else {
if (!children.length) return hasProps ? `${s}${prefix}(${props})` : `${s}${prefix}`;
if (children.length === 1 && !children[0].includes("\n"))
return hasProps ? `${s}${prefix}(${props}, ${children[0].trim()})` : `${s}${prefix}(${children[0].trim()})`;
return hasProps ? `${s}${prefix}(${props}, [\n${children.join(",\n")}\n${s}])` : `${s}${prefix}([\n${children.join(",\n")}\n${s}])`;
}
}
return "";
};
const r = [...new DOMParser().parseFromString(h, "text/html").body.childNodes].map(n => cN(n)).filter(Boolean);
return r.length == 1 ? r[0].trim() : `[\n${r.join(",\n")}\n]`;
}
const converter = () => {
const inH = $("");
const outS = $("");
const mode = $("tags");
const previewHtml = $("");
const cnv = () => {
try {
outS(html2sigpro(inH(), mode()));
} catch (e) {
outS("Error: " + e.message);
}
previewHtml(inH());
};
const clearAll = () => {
inH("");
outS("");
mode("tags");
previewHtml("");
};
const txS = "width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical";
const btS = "padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px";
return div({ style: "margin:20px auto;font-family:sans-serif" }, [
h1("HTML → SigPro"),
div({ style: "margin-bottom:10px" }, [
div({ style: "display:flex;gap:20px;flex-wrap:wrap;margin-top:5px" }, [
label({ style: "display:flex;align-items:center;gap:6px" }, [
"Core",
input({ type: "radio", name: "mode", value: "core", checked: mode() === "core", onchange: e => { if (e.target.checked) { mode("core"); cnv(); } } }),
span("core — h('tag', props, ...)")
]),
label({ style: "display:flex;align-items:center;gap:6px" }, [
"Tags",
input({ type: "radio", name: "mode", value: "tags", checked: mode() === "tags", onchange: e => { if (e.target.checked) { mode("tags"); cnv(); } } }),
span("tags — tag({ props }, ...)")
])
])
]),
div({ style: "margin-top:15px;display:flex;gap:10px" }, [
button({ style: btS + ";background:#3b82f6;color:#fff", onclick: cnv }, "Convert"),
button({ style: btS + ";background:#d1d5db", onclick: clearAll }, "Clear")
]),
div({ style: "display:grid;grid-template-columns:1fr;gap:15px;margin-top:15px;width:100%" }, [
div({ style: "border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column" }, [
label({ style: "font-weight:bold;margin-bottom:8px" }, "HTML Input"),
textarea({
style: txS,
placeholder: "Paste your HTML here...",
value: inH,
oninput: e => { inH(e.target.value); cnv(); }
})
]),
div({ style: "border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column" }, [
div({ style: "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px" }, [
span({ style: "font-weight:bold" }, "SigPro Output"),
button({
style: "padding:4px 8px;background:#10b981;color:white;border:none;border-radius:4px;cursor:pointer;font-size:12px",
onclick: () => { navigator.clipboard.writeText(outS()); alert("Copied!"); }
}, "Copy")
]),
textarea({ style: txS + ";background:#f9fafb", readonly: true, value: outS, placeholder: "Converted code will appear here..." })
]),
div({ style: "border:1px solid #ccc;border-radius:8px;padding:10px;display:flex;flex-direction:column" }, [
label({ style: "font-weight:bold;margin-bottom:8px" }, "Live Preview"),
iframe({
style: "width:100%;height:200px;border:1px solid #e2e8f0;border-radius:4px;background:white;",
srcdoc: () => {
const html = previewHtml() || "";
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style>
body { padding: 10px; margin: 0; font-family: sans-serif; }
</style>
</head>
<body>
${html}
</body>
</html>
`;
},
sandbox: "allow-same-origin allow-scripts allow-popups allow-forms allow-modals"
})
])
]),
]);
};
window.html2sigpro = html2sigpro;
window.converter = converter;

18
src/sigpro.db.js Normal file
View File

@@ -0,0 +1,18 @@
export const db = async (url, data = {}, loading = null) => {
if (loading) loading(true);
try {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
credentials: 'include'
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Error ${res.status}: ${errorText}`);
}
return await res.json();
} finally {
if (loading) loading(false);
}
};

21
src/sigpro.editor.js Normal file
View File

@@ -0,0 +1,21 @@
/// <reference path="../sigpro.d.ts" />
const { $, isF } = window.SigPro
export const Editor = p => {
let { value: v, class: x } = p, i = $(0), s = $(""), c = $(0), t = $(0), o = $(0), S = () => window.getSelection().rangeCount ? window.getSelection().getRangeAt(0) : 0, R, r, E = "😀 😊 😉 🧐 😮 🤔 😅 😂 😍 😘 🥰 👍 👎 👌 🤝 🤞 👋 👏 🙌 🙏 💪 ☝️ 👇 👈 👉 🖕 ✅ ⚠️ 🚀 📢 ✉️ ❤️".split(" "),
U = () => { t(t() + 1); R && c(R.innerText.length) }, N = () => { if (R) { let H = R.innerHTML; isF(v) ? v(H) : p.onchange?.(H); U() } },
X = (m, k = null) => { R?.focus(); if (r) { let w = window.getSelection(); w.removeAllRanges(); w.addRange(r) } document.execCommand(m, 0, k); r = 0; N() },
q = (k, Z = null) => { t(); if (!R || i()) return 0; try { if (k == 'formatBlock') { let n = window.getSelection().getRangeAt(0).commonAncestorContainer; while (n && n !== R) { if (n.tagName === Z) return 1; n = n.parentNode } return 0 } return document.queryCommandState(k) } catch (e) { return 0 } },
hU = f => { if (f) { let d = new FileReader; d.onload = e => { let I = f.type.startsWith('image/'), z = e.target.result; X("insertHTML", I ? div({ style: "display:inline-block;resize:both;overflow:hidden;vertical-align:bottom;width:200px;border:1px dashed #ccc;padding:2px;cursor:pointer", class: "resizable-img-container" }, [img({ src: z, style: "width:100%;height:100%;object-fit:contain;pointer-events:none" })]) : a({ href: z, download: f.name, contenteditable: "false", style: "display:inline-flex;align-items:center;gap:5px;padding:4px 8px;border:1px solid #ccc;border-radius:4px;background:#f9f9f9;text-decoration:none;color:#333;font-size:12px;margin:2px;cursor:pointer" }, [span({ class: "icon-[lucide--paperclip] w-3 h-3" }), f.name])) }; d.readAsDataURL(f) } },
B = (I, m, k) => button({ type: "button", class: () => `btn btn-ghost btn-xs ${q(m, k) ? 'btn-active bg-primary/20' : ''}`, onclick: () => typeof m == 'function' ? m() : X(m, k) }, [span({ class: `icon-[lucide--${I}]` })]);
return div({ class: `border border-base-300 rounded-box bg-base-100 overflow-hidden flex flex-col ${x||""}` }, [
div({ class: "flex flex-wrap items-center gap-1 p-2 border-b border-base-300 bg-base-200 sticky top-0 z-20" }, [
div({ class: "flex flex-wrap gap-1 flex-1" }, [B("bold", "bold"), B("italic", "italic"), B("underline", "underline"), input({ type: "color", class: "w-5 h-5 p-0 bg-transparent cursor-pointer", oninput: e => X("foreColor", e.target.value) }), span({ class: "w-px h-5 bg-base-300 mx-1" }), B("align-left", "justifyLeft"), B("align-center", "justifyCenter"), B("align-right", "justifyRight"), span({ class: "w-px h-5 bg-base-300 mx-1" }), B("list", "insertUnorderedList"), B("list-ordered", "insertOrderedList"), B("indent-decrease", "outdent"), B("indent-increase", "indent"), B("quote", () => X("formatBlock", q('formatBlock', 'BLOCKQUOTE') ? 'P' : 'BLOCKQUOTE'), 'BLOCKQUOTE'), span({ class: "w-px h-5 bg-base-300 mx-1" }), B("link", () => { let u = prompt('URL:'); u && X("createLink", u) }), B("paperclip", () => { let I = document.createElement('input'); I.type = 'file'; I.onchange = e => hU(e.target.files[0]); I.click() }), div({ class: "relative" }, [B("smile", () => { r = S(); o(!o()) }), div({ class: "absolute top-full left-0 mt-1 p-2 bg-base-100 border shadow-xl rounded-box w-52 z-50 flex flex-wrap gap-1", style: () => o() ? "" : "display:none" }, E.map(e => span({ class: "cursor-pointer p-1 text-lg", onclick: () => { X("insertText", e); o(0) } }, e)))]), B("undo-2", "undo"), B("redo-2", "redo")]), B("code-2", () => { if (!i()) s(R?.innerHTML || ""); else if (R) { R.innerHTML = s(); N() } i(!i()) })]),
div({ class: "relative flex-1 flex flex-col", onclick: () => o(0) }, [
div({ ref: l => { if (l && !R) { R = l; l.innerHTML = (isF(v) ? v() : v) || ""; document.execCommand("defaultParagraphSeparator", 0, "br"); l.onclick = e => { let c = e.target.closest('.resizable-img-container'); if (c) { let I = c.querySelector('img'); I && (k => { let O = document.createElement('div'); O.style = "position:fixed;top:0;left:0;width:100%;height:100%;background:#000e;z-index:9999;display:flex;align-items:center;justify-content:center;cursor:zoom-out"; O.onclick = () => O.remove(); let M = document.createElement('img'); M.src = k; M.style = "max-width:95%;max-height:95%"; O.appendChild(M); document.body.appendChild(O) })(I.src) } } } }, style: () => `min-height:22rem;${i() ? 'display:none' : ''}`, class: "p-4 outline-none text-base-content leading-relaxed [&>div]:m-0 [&>p]:m-0 [&>div]:min-h-[1em] [&_.resizable-img-container]:hover:border-primary [&_blockquote]:border-l-4 [&_blockquote]:border-base-300 [&_blockquote]:pl-4 [&_blockquote]:italic [&_ul]:list-disc [&_ul]:pl-8 [&_ol]:list-decimal [&_ol]:pl-8", contenteditable: "true", oninput: N, onkeydown: e => e.key === 'Tab' && (e.preventDefault(), X("indent")), onkeyup: () => { U(); r = S() }, onmouseup: N, onpaste: e => { e.preventDefault(); X('insertText', e.clipboardData.getData('text/plain')) }, ondrop: e => { e.preventDefault(); hU(e.dataTransfer.files[0]) }, ondragover: e => e.preventDefault() }),
textarea({ class: "w-full flex-1 min-h-[22rem] p-4 font-mono text-sm bg-base-200 border-0", style: () => i() ? '' : 'display:none', value: s, oninput: e => { s(e.target.value); if (R) R.innerHTML = e.target.value; p.onchange?.(e.target.value) } })]),
div({ class: "px-3 py-1 border-t text-[10px] text-right opacity-60" }, [span(() => c())])
])
}

207
src/sigpro.grid.js Normal file
View File

@@ -0,0 +1,207 @@
/// <reference path="../sigpro.d.ts" />
const { h, watch, onUnmount } = window.SigPro
import {
ModuleRegistry,
ValidationModule,
ColumnAutoSizeModule,
CellStyleModule,
QuickFilterModule,
RowSelectionModule,
TextEditorModule,
ClientSideRowModelModule,
themeQuartz,
createGrid,
NumberFilterModule,
TextFilterModule,
DateFilterModule
} from "ag-grid-community";
import {
MultiFilterModule,
SetFilterModule,
CellSelectionModule,
PivotModule,
MasterDetailModule,
SideBarModule,
ColumnsToolPanelModule,
ColumnMenuModule,
StatusBarModule,
ExcelExportModule,
ClipboardModule,
ContextMenuModule
} from "./grid-e";
ModuleRegistry.registerModules([
ValidationModule,
ColumnAutoSizeModule,
CellStyleModule,
QuickFilterModule,
RowSelectionModule,
TextEditorModule,
ClientSideRowModelModule,
MultiFilterModule,
CellSelectionModule,
PivotModule,
MasterDetailModule,
SideBarModule,
ColumnsToolPanelModule,
ColumnMenuModule,
StatusBarModule,
ExcelExportModule,
ClipboardModule,
NumberFilterModule,
TextFilterModule,
SetFilterModule,
DateFilterModule,
ContextMenuModule
]);
const Grid = (props) => {
const { data, options, api, on, class: className, style = "height: 100%; width: 100%", dark } = props;
let gridApi = null;
let cleanupFn = null;
const getDark = () =>
dark !== undefined
? (typeof dark === 'function' ? dark() : dark)
: document.documentElement.getAttribute('data-theme') === 'dark' ||
window.matchMedia('(prefers-color-scheme: dark)').matches;
const getTheme = () => {
const isDark = getDark();
if (isDark) {
return themeQuartz.withParams({
headerFontSize: 14,
headerVerticalPaddingScale: 0.4,
rowVerticalPaddingScale: 0.4,
backgroundColor: "#1d1d1d",
foregroundColor: "#ffffff",
headerBackgroundColor: "#2a2a2a",
headerForegroundColor: "#ffffff",
oddRowBackgroundColor: "#262626",
borderColor: "#404040",
browserColorScheme: "dark"
});
}
return themeQuartz.withParams({
browserColorScheme: "light",
headerFontSize: 14,
headerVerticalPaddingScale: 0.4,
rowVerticalPaddingScale: 0.4
});
};
const initGrid = (container) => {
if (cleanupFn) {
cleanupFn();
cleanupFn = null;
}
if (gridApi && !gridApi.isDestroyed()) {
gridApi.destroy();
if (api) api.current = null;
gridApi = null;
}
if (!container) return;
const initialData = typeof data === "function" ? data() : data;
const initialOptions = typeof options === "function" ? options() : options;
const commonEvents = [
'onFilterChanged', 'onModelUpdated', 'onGridSizeChanged',
'onFirstDataRendered', 'onRowValueChanged', 'onSelectionChanged',
'onCellClicked', 'onCellDoubleClicked', 'onCellValueChanged',
'onRowClicked', 'onSortChanged', 'onContextMenu',
'onColumnResized', 'onColumnMoved', 'onRowDataUpdated',
'onCellEditingStarted', 'onCellEditingStopped',
'onPaginationChanged', 'onBodyScroll'
];
const eventHandlers = {};
commonEvents.forEach(eventName => {
if (on?.[eventName]) {
eventHandlers[eventName] = (params) => on[eventName](params);
}
});
const gridOptions = {
...initialOptions,
theme: getTheme(),
rowData: initialData || [],
onGridReady: (params) => {
gridApi = params.api;
if (api) api.current = gridApi;
if (on?.onGridReady) on.onGridReady(params);
if (initialOptions?.autoSizeColumns) {
params.api.autoSizeAllColumns();
}
},
...eventHandlers
};
gridApi = createGrid(container, gridOptions);
const stopData = watch(() => {
if (!gridApi || gridApi.isDestroyed()) return;
const newData = typeof data === "function" ? data() : data;
if (Array.isArray(newData)) {
const currentData = gridApi.getGridOption("rowData");
if (newData !== currentData) {
gridApi.setGridOption("rowData", newData);
}
}
});
const stopTheme = watch(() => {
if (!gridApi || gridApi.isDestroyed()) return;
getDark();
const newTheme = getTheme();
const currentTheme = gridApi.getGridOption("theme");
if (JSON.stringify(newTheme) !== JSON.stringify(currentTheme)) {
gridApi.setGridOption("theme", newTheme);
}
});
const stopOptions = watch(() => {
if (!gridApi || gridApi.isDestroyed() || !options) return;
const newOptions = typeof options === "function" ? options() : options;
if (newOptions) {
Object.entries(newOptions).forEach(([key, val]) => {
try {
gridApi.setGridOption(key, val);
} catch (e) { }
});
}
});
cleanupFn = () => {
stopData();
stopTheme();
stopOptions();
if (gridApi && !gridApi.isDestroyed()) {
gridApi.destroy();
if (api) api.current = null;
gridApi = null;
}
};
onUnmount(() => {
if (cleanupFn) {
cleanupFn();
cleanupFn = null;
}
});
};
return h("div", {
class: className,
style: style,
ref: initGrid
});
};
export { Grid };

254
src/sigpro.js Normal file
View File

@@ -0,0 +1,254 @@
export const isF = f => typeof f == "function";
export const isO = o => o && typeof o == "object";
export const isA = Array.isArray;
const doc = typeof document < "u" ? document : null;
const txt = s => doc.createTextNode(s == null ? "" : String(s));
const toNd = n => n?._rt ? n._cnt : (n instanceof Node ? n : txt(n));
export const fragment = p => p.children;
export const val = v => isF(v) ? v() : v;
let aEff = null, aOwn = null, isFlushing = 0, bDepth = 0;
const eQ = new Set(), MOUNTED = new WeakMap();
const SVG_NS = "http://www.w3.org/2000/svg", XLINK = "http://www.w3.org/1999/xlink";
const SVG_TAGS = new Set("svg,path,circle,rect,line,polyline,polygon,g,defs,text,textPath,tspan,use,symbol,image,marker,ellipse".split(","));
const DANG_ATTR = new Set(["src", "href", "formaction", "action", "background", "code", "archive"]);
const clr = s => { if (s) { s.forEach(f => f()); s.clear(); } };
const dispose = e => {
if (!e || e._x) return;
e._x = 1;
let st = [e], c;
while ((c = st.pop())) {
clr(c._c);
if (c._ch) { c._ch.forEach(x => st.push(x)); c._ch.clear(); }
if (c._d) { c._d.forEach(d => d.delete(c)); c._d.clear(); }
}
};
export const onUnmount = f => aOwn && (aOwn._c ||= new Set()).add(f);
const untrack = f => { let p = aEff; aEff = null; try { return f() } finally { aEff = p } };
const createEffect = (f, isC = 0) => {
const e = () => {
if (e._x) return;
if (e._d) e._d.forEach(s => s.delete(e));
clr(e._c);
let pE = aEff, pO = aOwn;
aEff = aOwn = e;
try { return e._res = f(); }
catch (err) { console.error("[SigPro]", err); }
finally { aEff = pE; aOwn = pO; }
};
e._d = e._c = e._ch = null;
e._x = 0; e._iC = isC;
e._dp = aEff ? aEff._dp + 1 : 0;
e._m = []; e._p = aOwn;
if (aOwn) (aOwn._ch ||= new Set()).add(e);
return e;
};
const flush = () => {
if (isFlushing) return;
isFlushing = 1;
let q = [...eQ].sort((a, b) => a._dp - b._dp);
eQ.clear();
for (let e of q) if (!e._x) e();
isFlushing = 0;
};
export const batch = f => {
bDepth++;
try { return f() } finally { if (!--bDepth && eQ.size && !isFlushing) flush() }
};
const trkUpd = (s, trg = 0) => {
if (!trg && aEff && !aEff._x) {
s.add(aEff);
(aEff._d ||= new Set()).add(s);
} else if (trg && s.size) {
let q = 0;
for (let e of s) {
if (e === aEff || e._x) continue;
if (e._iC) { e._dt = 1; if (e._sb) trkUpd(e._sb, 1); }
else { eQ.add(e); q = 1; }
}
if (q && !isFlushing && !bDepth) queueMicrotask(flush);
}
};
export const $ = (v, k = null) => {
let s = new Set();
if (isF(v)) {
let c, cp = () => {
if (cp._dt) {
let p = aEff; aEff = cp;
try { let n = v(); if (!Object.is(c, n)) { c = n; trkUpd(s, 1); } }
finally { aEff = p; }
cp._dt = 0;
}
trkUpd(s); return c;
};
cp._iC = cp._dt = 1; cp._sb = s; cp._d = null; cp._x = 0;
return cp;
}
if (k) try { v = JSON.parse(localStorage.getItem(k)) ?? v } catch (e) { }
return (...a) => {
if (a.length) {
let n = isF(a[0]) ? a[0](v) : a[0];
if (!Object.is(v, n)) { v = n; if (k) localStorage.setItem(k, JSON.stringify(v)); trkUpd(s, 1); }
}
trkUpd(s); return v;
};
};
export const watch = (src, cb) => {
let e = createEffect(cb ? () => { let v = isA(src) ? src.map(s => s()) : src(); untrack(() => cb(v)) } : src);
e(); return () => dispose(e);
};
const clnNd = n => {
if (!n) return;
clr(n._c);
if (n._oE) dispose(n._oE);
if (n.childNodes) n.childNodes.forEach(clnNd);
};
const valAtt = (k, v) => (v == null || v === false) ? null :
(DANG_ATTR.has(k) || k.startsWith("on")) && /^\s*(javascript|data|vbscript):/i.test(String(v)) ? '#' : v;
export const h = (tag, prp = {}, ch = []) => {
if (prp instanceof Node || isA(prp) || !isO(prp)) { ch = prp; prp = {}; }
if (isF(tag)) {
let e = createEffect(() => e._res = tag(prp, { children: ch, emit: (ev, ...a) => prp[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...a) }));
e();
if (e._res == null) return null;
let nd = e._res instanceof Node || (isA(e._res) && e._res.every(n => n instanceof Node)) ? e._res : txt(e._res);
let att = n => { if (isO(n) && !n._rt) { n._m = e._m || []; n._c = e._c || new Set(); n._oE = e; } };
isA(nd) ? nd.forEach(att) : att(nd);
return nd;
}
let isS = SVG_TAGS.has(tag), el = isS ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag);
el._c = new Set();
for (let k in prp) {
let v = prp[k];
if (k === "ref") { isF(v) ? v(el) : (v.current = el); continue; }
if (isS && k.startsWith("xlink:")) {
let cv = valAtt(k.slice(6), v);
cv == null ? el.removeAttributeNS(XLINK, k.slice(6)) : el.setAttributeNS(XLINK, k.slice(6), cv);
continue;
}
if (k.startsWith("on")) {
let ev = k.slice(2).toLowerCase(); el.addEventListener(ev, v);
let off = () => el.removeEventListener(ev, v); el._c.add(off); onUnmount(off);
} else if (isF(v)) {
let e = createEffect(() => {
let r = valAtt(k, v());
if (k === "class") el.className = r || "";
else if (r == null) el.removeAttribute(k);
else if (k === "style" && typeof r == "string") el.setAttribute("style", r);
else if (k in el && !isS) el[k] = r;
else el.setAttribute(k, r === true ? "" : r);
});
e(); el._c.add(() => dispose(e)); onUnmount(() => dispose(e));
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
el.addEventListener(k === "checked" ? "change" : "input", ev => v(ev.target[k]));
}
} else {
let r = valAtt(k, v);
if (r != null) {
if (k === "style" && typeof r == "string") el.setAttribute("style", r);
else if (k in el && !isS) el[k] = r;
else el.setAttribute(k, r === true ? "" : r);
}
}
}
const app = c => {
if (isA(c)) return c.forEach(app);
if (isF(c)) {
let anc = txt(""), cur = []; el.appendChild(anc);
let e = createEffect(() => {
let r = c(), nxt = (isA(r) ? r : [r]).map(toNd), ref = anc;
cur.forEach(n => { n._rt ? n.destroy() : clnNd(n); if (n.parentNode) n.remove(); });
for (let i = nxt.length - 1; i >= 0; i--) {
let nd = nxt[i];
if (nd.parentNode !== ref.parentNode) ref.parentNode?.insertBefore(nd, ref);
if (nd._m) nd._m.forEach(f => f()); ref = nd;
}
cur = nxt;
});
e(); el._c.add(() => dispose(e)); onUnmount(() => dispose(e));
} else {
let nd = toNd(c); el.appendChild(nd);
if (nd._m) nd._m.forEach(f => f());
}
};
app(ch); return el;
};
export const render = rFn => {
let c = new Set(), pO = aOwn, pE = aEff, cnt = doc.createElement("div");
cnt.style.display = "contents"; cnt.setAttribute("role", "presentation");
aOwn = { _c: c }; aEff = null;
const pRes = r => {
if (!r) return;
if (r._rt) { c.add(r.destroy); cnt.appendChild(r._cnt); }
else if (isA(r)) r.forEach(pRes);
else cnt.appendChild(r instanceof Node ? r : txt(r));
};
try { pRes(rFn({ onCleanup: f => c.add(f) })); } finally { aOwn = pO; aEff = pE; }
return { _rt: 1, _cnt: cnt, destroy: () => { clr(c); clnNd(cnt); cnt.remove(); } };
};
export const when = (c, Y, N = null) => {
let anc = txt(""), rt = h("div", { style: "display:contents" }, [anc]), v;
watch(() => !!val(c), s => {
if (v) { v.destroy(); v = null; }
let ct = s ? Y : N;
if (ct) { v = render(() => val(ct)); rt.insertBefore(v._cnt, anc); }
});
onUnmount(() => v?.destroy()); return rt;
};
export const each = (s, fn, kF) => {
let anc = txt(""), rt = h("div", { style: "display:contents" }, [anc]), cch = new Map();
watch(() => val(s) || [], it => {
let nCc = new Map(), nOd = [];
for (let i = 0, l = (it || []).length; i < l; i++) {
let t = it[i], k = kF ? (t?.[kF] ?? i) : (t?.id ?? i), v = cch.get(k);
if (!v) v = render(() => fn(t, i)); else cch.delete(k);
nCc.set(k, v); nOd.push(v);
}
cch.forEach(v => v.destroy());
let ref = anc;
for (let i = nOd.length - 1; i >= 0; i--) {
let nd = nOd[i]._cnt;
if (nd.nextSibling !== ref) rt.insertBefore(nd, ref); ref = nd;
}
cch = nCc;
});
return rt;
};
export const mount = (c, tgt) => {
let t = typeof tgt == "string" ? doc.querySelector(tgt) : tgt;
if (!t) return;
if (MOUNTED.has(t)) MOUNTED.get(t).destroy();
let i = render(isF(c) ? c : () => c);
t.replaceChildren(i._cnt); MOUNTED.set(t, i); return i;
};
// const htmlTags = "a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video";
export const SigPro = { $, watch, batch, h, fragment, render, mount, when, each, onUnmount, val, isA, isF, isO };
if (typeof window !== "undefined") {
window.SigPro = SigPro;
// htmlTags.split(" ").forEach(tag => {
// window[tag] = (props, children) => h(tag, props, children);
// });
}

21
src/sigpro.locale.js Normal file
View File

@@ -0,0 +1,21 @@
const { $ } = window.SigPro;
const currentLocale = $("en");
const translations = {};
export const addLang = obj => {
for (const locale of Object.keys(obj)) {
if (!translations[locale]) translations[locale] = {};
Object.assign(translations[locale], obj[locale]);
}
};
export const setLocale = locale => {
if (locale && translations[locale]) {
currentLocale(locale);
}
};
export const t = key => {
return () => translations[currentLocale()]?.[key] ?? key;
};

49
src/sigpro.router.js Normal file
View File

@@ -0,0 +1,49 @@
const { $, h, watch, render, isF } = window.SigPro;
const getHash = () => window.location.hash.slice(1) || "/";
const currentPath = $(getHash());
window.addEventListener("hashchange", () => currentPath(getHash()));
export const routerParams = $({});
export const router = routes => {
const hook = h("div", { class: "router-hook" });
let currentView = null;
watch([currentPath], () => {
const cur = currentPath();
const route = routes.find(r => {
const p1 = r.path.split("/").filter(Boolean);
const p2 = cur.split("/").filter(Boolean);
return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i]);
}) || routes.find(r => r.path === "*");
if (route) {
currentView?.destroy();
const params = {};
route.path.split("/").filter(Boolean).forEach((p, i) => {
if (p[0] === ":") params[p.slice(1)] = cur.split("/").filter(Boolean)[i];
});
routerParams(params);
currentView = render(() => isF(route.component) ? route.component(params) : route.component);
hook.replaceChildren(currentView.container);
}
});
hook.destroy = () => {
currentView?.destroy();
};
return hook;
};
router.params = routerParams;
router.to = p => window.location.hash = p.replace(/^#?\/?/, "#/");
router.back = () => window.history.back();
router.path = () => currentPath();

108
src/sigpro.ui.css Normal file
View File

@@ -0,0 +1,108 @@
@import "tailwindcss";
@plugin "daisyui";
@plugin "@iconify/tailwind4";
@plugin "daisyui/theme" {
name: "splight";
default: true;
prefersdark: false;
color-scheme: "light";
--color-base-100: oklch(100% 0 0);
--color-base-200: oklch(98% 0 0);
--color-base-300: oklch(92% 0 0);
--color-base-content: oklch(25% 0.006 285);
--color-primary: oklch(25% 0.006 285);
--color-primary-content: oklch(98% 0 0);
--color-secondary: oklch(55% 0.046 257.417);
--color-secondary-content: oklch(98% 0 0);
--color-accent: oklch(96% 0 0);
--color-accent-content: oklch(25% 0.006 285);
--color-neutral: oklch(14% 0.005 285.823);
--color-neutral-content: oklch(92% 0.004 286.32);
--color-info: oklch(74% 0.16 232);
--color-success: oklch(62% 0.17 163);
--color-warning: oklch(82% 0.18 84);
--color-error: oklch(60% 0.25 27);
--radius-selector: 0.5rem;
--radius-field: 0.5rem;
--radius-box: 0.5rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 1;
--noise: 0;
}
@plugin "daisyui/theme" {
name: "spdark";
default: false;
prefersdark: true;
color-scheme: "dark";
--color-base-100: oklch(15% 0.005 285.823);
--color-base-200: oklch(20% 0.005 285.823);
--color-base-300: oklch(30% 0.005 285.823);
--color-base-content: oklch(92% 0.004 286.32);
--color-primary: oklch(98% 0 0);
--color-primary-content: oklch(15% 0 0);
--color-secondary: oklch(65% 0.046 257.417);
--color-secondary-content: oklch(15% 0.005 285.823);
--color-accent: oklch(25% 0 0);
--color-accent-content: oklch(98% 0 0);
--color-neutral: oklch(92% 0.004 286.32);
--color-neutral-content: oklch(14% 0.005 285.823);
--color-info: oklch(70% 0.1 230);
--color-success: oklch(65% 0.15 160);
--color-warning: oklch(85% 0.15 90);
--color-error: oklch(55% 0.2 27);
--radius-selector: 0.5rem;
--radius-field: 0.5rem;
--radius-box: 0.5rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 1;
--noise: 0;
}
:root {
font-size: 14px;
}
.input,
.label,
.select,
.textarea {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:focus,
&:focus-within {
outline: none !important;
box-shadow: 0 0 4px 0px;
}
&:hover:not(:focus) {
background-color: oklch(from var(--color-base-100) calc(l - 0.03) c h);
}
}
.floating-label span {
color: oklch(30% 0.01 260);
font-size: 1.1rem;
transition: all 0.2s ease;
}
.floating-label:focus-within span {
color: oklch(25% 0.02 260);
font-size: 1.1rem;
}
.floating-label:has(input:not(:placeholder-shown)) span {
color: oklch(28% 0.01 260);
font-size: 1.1rem;
}
/* Solo para la Demo de docsify */
.markdown-section progress.progress {
all: revert-layer;
}

367
src/sigpro.ui.js Normal file
View File

@@ -0,0 +1,367 @@
/// <reference path="../sigpro.d.ts" />
const { $, h, mount, watch, val, isF, isA, isO } = window.SigPro;
export const hide = () => document.activeElement?.blur();
export const ui = {
accordion: (p, c) => h("div", { ...p, class: `collapse ${p.class || ''}` }, [h("input", { type: "radio", name: p.name, checked: p.checked }), c]),
accordion_title: (p, c) => h("div", { ...p, class: `collapse-title ${p.class || ''}` }, c),
accordion_content: (p, c) => h("div", { ...p, class: `collapse-content ${p.class || ''}` }, c),
alert: (p, c) => h("div", { ...p, class: `alert ${p.class || ''}` }, c),
autocomplete: (p) => ui.combo(p, ({ query, close, setValue }) =>
h("ul", { class: "menu bg-base-100 w-full" }, () => {
const q = String(val(query)).toLowerCase();
const list = (val(p.items) || []).filter(i =>
(isO(i) ? (i.label ?? i.value) : String(i)).toLowerCase().includes(q)
);
return list.length
? list.map((item, idx) =>
h("li", { key: item.value ?? idx },
h("a", {
onclick: e => {
e.preventDefault();
const v = item?.value ?? item;
setValue(isO(item) ? (item.label ?? item.value) : String(item));
if (isF(p.value)) p.value(v); else p.onChange?.(v);
close();
}
}, isO(item) ? (item.label ?? item.value) : item)
)
)
: [h("li", { class: "disabled" }, h("a", {}, "Sin resultados"))];
})
),
avatar: (p, c) => h("div", { ...p, class: `avatar ${p.class || ''}` }, h("div", { class: p.innerClass || '' }, c)),
avatar_group: (p, c) => h("div", { ...p, class: `avatar-group -space-x-6 ${p.class || ''}` }, c),
badge: (p, c) => h("span", { ...p, class: `badge ${p.class || ''}` }, c),
breadcrumbs: (p, c) => h("div", { ...p, class: `breadcrumbs ${p.class || ''}` }, c),
button: (p, c) => h("button", { ...p, class: `btn ${p.class || ''}` }, c),
card: (p, c) => h("div", { ...p, class: `card ${p.class || ''}` }, c),
card_title: (p, c) => h("div", { ...p, class: `card-title ${p.class || ''}` }, c),
card_body: (p, c) => h("div", { ...p, class: `card-body ${p.class || ''}` }, c),
card_actions: (p, c) => h("div", { ...p, class: `card-actions ${p.class || ''}` }, c),
carousel: (p, c) => h("div", { ...p, class: `carousel ${p.class || ''}` }, c),
carousel_item: (p, c) => h("div", { ...p, class: `carousel-item ${p.class || ''}` }, c),
chat: (p, c) => h("div", { ...p, class: `chat ${p.class || ''}` }, c),
chat_image: (p, c) => h("div", { ...p, class: `chat-image avatar ${p.class || ''}` }, c),
chat_header: (p, c) => h("div", { ...p, class: `chat-header ${p.class || ''}` }, c),
chat_bubble: (p, c) => h("div", { ...p, class: `chat-bubble ${p.class || ''}` }, c),
chat_footer: (p, c) => h("div", { ...p, class: `chat-footer ${p.class || ''}` }, c),
checkbox: (p) => h("input", { ...p, type: "checkbox", class: `checkbox ${p.class || ''}` }),
colorpicker: (p) => ui.combo({
...p, custom: () => h("span", {
class: "w-4 h-4 rounded border border-base-300",
style: `background:${val(p.value) || '#000'}`
})
}, ({ close, setValue }) =>
pallete({ ...p, onchange: (c) => { setValue(c); close(); } })
),
combo: (p, c) => {
const { placeholder = "", class: cls = "" } = p;
const query = isF(p.value) ? p.value : $(p.value ?? "");
let inputEl, open = $(false);
return ui.float({ label: p.label }, [
h("div", { class: `dropdown w-full ${cls} ${val(open) ? "dropdown-open" : ""}` }, [
h("label", { class: "input w-full" }, [
h("span", { class: p.icon ?? "icon-[lucide--search]" }),
p.custom ?? null,
h("input", {
type: "search", placeholder, tabindex: "0",
value: query,
onfocus: () => open(true),
ref: el => inputEl = el
})
]),
h("div", {
class: "dropdown-content bg-base-100 rounded-box z-50 max-w-80 shadow-sm",
onmousedown: e => e.preventDefault()
}, () => val(open) && typeof c === "function"
? c({ query, open, close: () => { open(false); inputEl?.blur(); }, setValue: v => query(v) })
: null
)
])
]);
},
datepicker: (p) => {
const range = isF(p.range) ? p.range() : p.range;
if (!range) return ui.combo({ value: (isF(p.value) ? p.value() : p.value) || '', ...p },
({ close, setValue }) => h("div", { class: "w-80" },
calendar({ ...p, class: "w-full", onChange: v => { setValue(v); close(); if (isF(p.value)) p.value(v) } })
)
);
const v = $(isF(p.value) ? p.value() : p.value || { start: null, end: null });
const start = $((v() || {}).start || ''), end = $((v() || {}).end || '');
const sync = () => { v({ start: start(), end: end() }); if (isF(p.value)) p.value(v()); };
const cal = (key, sig, ph, dis) => ui.combo({ value: sig, placeholder: ph, class: "flex-1", disabled: dis },
({ close, setValue }) => h("div", { class: "w-72" },
calendar({
...p, class: "w-full", value: v, range: true, onChange: r => {
v(r); start(r?.start || ''); end(r?.end || ''); setValue(r?.[key] || ''); if (r?.end) close(); if (isF(p.value)) p.value(r)
}
})
)
);
return h("div", { class: `flex gap-1 ${p.class || ''}`, onchange: sync }, [
cal('start', start, p.fromPlaceholder || "Inicio"),
cal('end', end, p.toPlaceholder || "Fin", () => !v()?.start)
]);
},
dialog: (p, c) => {
const pos = $(p.pos || { x: 100, y: 100 });
const show = $(p.show || false);
const anim = $(false);
watch(show, s => s ? setTimeout(() => anim(true), 10) : anim(false));
return h("div", {
class: () => `fixed z-50 transition-opacity duration-300 ${show() && anim() ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'}`,
style: () => `left: ${pos().x}px; top: ${pos().y}px; transition: left 50ms, top 50ms;`
}, [
h("div", { class: `bg-base-100 rounded-box shadow-2xl border ${p.class || ''}` }, [
p.title && h("div", {
class: "flex justify-between items-center cursor-move p-2 border-b select-none bg-base-200 rounded-t-box",
onmousedown: e => {
const s = { x: e.clientX - pos().x, y: e.clientY - pos().y };
const m = ev => pos({ x: ev.clientX - s.x, y: ev.clientY - s.y });
const u = () => { document.removeEventListener('mousemove', m); document.removeEventListener('mouseup', u); };
document.addEventListener('mousemove', m); document.addEventListener('mouseup', u);
e.preventDefault();
}
}, [h("span", { class: "font-bold" }, p.title), h("button", { class: "btn btn-sm btn-circle btn-ghost", onclick: () => show(false) }, "✕")]),
h("div", { class: "p-4" }, c),
p.footer && h("div", { class: "p-2 border-t flex justify-end gap-2" }, p.footer)
])
]);
},
divider: (p) => h("div", { ...p, class: `divider ${p.class || ''}` }),
drawer: (p, c) => h("div", { ...p, class: `drawer ${p.class || ''}` }, c),
drawer_toggle: (p) => h("input", { ...p, type: "checkbox", class: `drawer-toggle ${p.class || ''}` }),
drawer_content: (p, c) => h("div", { ...p, class: `drawer-content ${p.class || ''}` }, c),
drawer_side: (p, c) => h("div", { ...p, class: `drawer-side ${p.class || ''}` }, c),
drawer_overlay: (p) => h("label", { ...p, class: `drawer-overlay ${p.class || ''}` }),
dropdown: (p, c) => h("div", { ...p, class: `dropdown ${p.class || ''}` }, c),
dropdown_button: (p, c) => h("div", { ...p, tabindex: "0", role: "button", class: `btn ${p.class || ''}` }, c),
dropdown_content: (p, c) => h("div", { ...p, tabindex: "0", class: `dropdown-content ${p.class || ''}` }, c),
fab: (p, c) => h("div", { ...p, class: `fab ${p.class || ''}` }, c),
fab_button: (p, c) => h("div", { ...p, tabindex: "0", role: "button", class: `btn ${p.class || ''}` }, c),
fieldset: (p, c) => h("fieldset", { class: `fieldset ${p.class || ''}` }, [h("legend", { class: "fieldset-legend" }, p.label), c]),
file: (p) => h("input", { ...p, type: "file", class: `file-input ${p.class || ''}` }),
file_drag: (p, c) => h("label", {
class: () => `relative flex items-center justify-between h-12 px-4 border-2 border-dashed rounded-lg cursor-pointer transition-all ${p.drag ? 'border-primary bg-primary/10' : 'border-base-content/20 bg-base-100'} ${p.class || ''}`,
ondragover: (e) => { e.preventDefault(); p.ondrag?.(true); },
ondragleave: () => p.ondrag?.(false),
ondrop: (e) => { e.preventDefault(); p.ondrag?.(false); p.ondrop?.(e.dataTransfer.files); }
}, c),
file_preview: (p) => h("ul", { class: `mt-2 space-y-1 ${p.class || ''}` },
(p.files || []).map((f, i) =>
h("li", { class: "flex items-center justify-between p-1.5 pl-3 text-xs bg-base-200/50 rounded-md border" }, [
h("div", { class: "flex items-center gap-2 truncate opacity-70" }, [
h("span", {}, "📄"),
h("span", { class: "truncate max-w-[180px]" }, f.name),
h("span", { class: "text-[9px] opacity-50" }, `(${~~(f.size / 1024)}KB)`)
]),
h("button", { class: "btn btn-ghost btn-xs btn-circle", onclick: () => p.onremove?.(i) }, h("span", { class: "icon-[lucide--x]" }))
])
)
),
file_error: (p) => h("div", { class: `text-[10px] text-error mt-1 px-1 ${p.class || ''}` }, p.message),
float: (p, c) => h("label", { class: "floating-label" }, [h("span", {}, p.label ?? null), c]),
indicator: (p, c) => h("div", { ...p, class: `indicator ${p.class || ''}` }, [p.value && h("span", { class: `indicator-item badge ${p.badgeClass || ''}` }, p.value), c]),
input: (p) => ui.float({ label: p.label }, [
h("label", { class: "input w-full" }, [
span({ class: `${p.icon ?? ''}` }),
h("input", { ...p, class: `w-full ${p.class || ''}` }),
p.right || null
])
]),
kbd: (p, c) => h("kbd", { ...p, class: `kbd ${p.class || ''}` }, c),
label: (p, c) => h("span", { ...p, class: `label ${p.class || ''}` }, c),
loading: (p) => h("span", { ...p, class: `loading loading-spinner ${p.class || ''}` }),
menu: (p, c) => h("ul", { ...p, class: `menu ${p.class || ''}` }, c),
menu_title: (p, c) => h('li', { ...p, class: "menu-title" }, c),
menu_item: (p) =>
p.items
? h('li', {}, [h('details', { open: p.open || false }, [h('summary', {}, p.label), h('ul', { class: p.submenuClass || '' }, p.items.map(i => ui.menu_item(i)))])])
: h('li', {}, p.href || p.onclick
? h('a', { ...(p.href ? { href: p.href } : {}), onclick: p.onclick }, p.label)
: p.label),
modal: (p, c) => h("dialog", { ...p, class: `modal ${p.class || ''}` }, [c, h("form", { method: "dialog", class: "modal-backdrop" }, h("button", {}, "close"))]),
modal_box: (p, c) => h("div", { ...p, class: `modal-box ${p.class || ''}` }, [h("form", { method: "dialog" }, h("button", { class: "btn btn-sm btn-circle btn-ghost absolute right-2 top-2" }, "✕")), c]),
modal_action: (p, c) => h("div", { ...p, class: `modal-action ${p.class || ''}` }, c),
navbar: (p, c) => h("div", { ...p, class: `navbar ${p.class || ''}` }, c),
option: (p, c) => h("option", { ...p }, c),
password: (p) => {
const show = $(false);
const { right, ...rest } = p;
return ui.input({
...rest,
type: () => val(show) ? "text" : "password",
icon: "icon-[lucide--lock]",
right: ui.swap({ value: show, class: "swap-rotate" }, [
ui.swap_on({}, span({ class: "icon-[lucide--eye]" })),
ui.swap_off({}, span({ class: "icon-[lucide--eye-off]" }))
])
});
},
progress: (p) => h("progress", { ...p, class: `progress ${p.class || ''}` }),
radial: (p) => h("div", { ...p, class: `radial-progress ${p.class || ''}`, style: `--value:${val(p.value) ?? 0}`, role: "progressbar" }, p.value ?? ""),
radio: (p) => h("input", { ...p, type: "radio", class: `radio ${p.class || ''}` }),
range: (p) => h("input", { ...p, type: "range", class: `range ${p.class || ''}` }),
rating: (p) => h("div", { class: `rating ${p.class || ''}` },
[...Array(p.count || 5)].map((_, i) =>
h("input", {
class: `mask ${p.mask || 'mask-star'} ${p.itemClass || ''}`,
name: p.name,
type: "radio",
checked: () => val(p.value) === (p.offset ? i + p.offset : i),
onclick: () => isF(p.value) ? p.value(i) : p.onChange?.(i)
})
)
),
search: (p) => ui.input({ ...p, type: "search", icon: p.icon ?? "icon-[lucide--search]" }),
select: (p, c) => h("select", { ...p, class: `select ${p.class || ''}` }, c),
stack: (p, c) => h("div", { ...p, class: `stack ${p.class || ''}` }, c),
stat: (p, c) => h("div", { ...p, class: `stat ${p.class || ''}` }, c),
stat_figure: (p, c) => h("div", { ...p, class: `stat-figure ${p.class || ''}` }, c),
stat_title: (p, c) => h("div", { ...p, class: `stat-title ${p.class || ''}` }, c),
stat_value: (p, c) => h("div", { ...p, class: `stat-value ${p.class || ''}` }, c),
stat_desc: (p, c) => h("div", { ...p, class: `stat-desc ${p.class || ''}` }, c),
steps: (p, c) => h("ul", { ...p, class: `steps ${p.class || ''}` }, c),
step: (p, c) => h("li", { ...p, class: `step ${p.class || ''}`, "data-content": p.dataContent }, c),
swap: (p, c) => h("label", { class: `swap ${p.class || ''}` }, [h("input", { type: "checkbox", checked: p.value }), ...(isA(c) ? c : [c])]),
swap_on: (p, c) => h("div", { ...p, class: `swap-on ${p.class || ''}` }, c),
swap_off: (p, c) => h("div", { ...p, class: `swap-off ${p.class || ''}` }, c),
table: (p, c) => h("table", { ...p, class: `table ${p.class || ''}` }, c),
thead: (p, c) => h("thead", { ...p, class: p.class || '' }, c),
tbody: (p, c) => h("tbody", { ...p, class: p.class || '' }, c),
tfoot: (p, c) => h("tfoot", { ...p, class: p.class || '' }, c),
tr: (p, c) => h("tr", { ...p, class: p.class || '' }, c),
th: (p, c) => h("th", { ...p, class: p.class || '' }, c),
td: (p, c) => h("td", { ...p, class: p.class || '' }, c),
tabs: (p, c) => div({ ...p, class: `tabs ${p.class || ''}` }, c),
tab: (p) => {
const close = () => p.tabs?.(p.tabs().filter((_, idx) => idx !== p.index))
return [
h('label', { class: `tab ${p.class || ''}` }, [
h('input', { type: 'radio', name: p.name, checked: p.checked || undefined }),
p.label,
p.closable ? h('span', {
class: 'ml-1 inline-flex items-center justify-center w-4 h-4 rounded-full hover:bg-base-300 text-base-content/60 hover:text-base-content cursor-pointer',
onclick: (e) => { e.stopPropagation(); close() }
}, h('span', { class: 'icon-[lucide--x] w-3 h-3' })) : null,
]),
div({ class: `tab-content bg-base-100 border-base-300 p-6 ${p?.classContent || ''}` }, p.content),
]
},
textarea: (p) => h("textarea", { ...p, class: `textarea ${p.class || ''}` }),
textrotate: (p, c) => h("span", { ...p, class: `text-rotate ${p.class || ''}` }, h("span", {}, c)),
theme: (p) => ui.swap({ class: `text-xl ${p.class || ''}`, value: p.value }, [
ui.swap_on({}, span({ class: "icon-[lucide--moon]" })),
ui.swap_off({}, span({ class: "icon-[lucide--sun]" }))
]),
timeline: (p, c) => h("ul", { ...p, class: `timeline ${p.class || ''}` }, c),
timeline_start: (p, c) => h("div", { ...p, class: `timeline-start ${p.class || ''}` }, c),
timeline_middle: (p, c) => h("div", { ...p, class: `timeline-middle ${p.class || ''}` }, c),
timeline_end: (p, c) => h("div", { ...p, class: `timeline-end ${p.class || ''}` }, c),
toggle: (p) => h("input", { ...p, type: "checkbox", class: `toggle ${p.class || ''}` }),
tooltip: (p, c) => h('div', { class: `tooltip ${p.class || ''}`, "data-tip": p.tip }, c),
validator: (p, c) => h("div", { ...p, class: `validator-hint ${p.class || ''}` }, c),
};
export const calendar = p => {
let [d, hv, sh, eh] = [$(new Date()), $(0), $(0), $(0)], now = new Date(),
F = v => v ? `${v.getFullYear()}-${String(v.getMonth() + 1).padStart(2, '0')}-${String(v.getDate()).padStart(2, '0')}` : '',
P = n => (n < 10 ? '0' : '') + n,
M = (m, y = 0) => d(new Date(d().getFullYear() + y, d().getMonth() + m, 1)),
V = () => typeof p.value == 'function' ? p.value() : p.value,
G = () => typeof p.range == 'function' ? p.range() : p.range,
L = dt => {
let s = F(dt), v = V(), r = G();
if (!r) return p.onChange?.(p.hour ? `${s}T${P(sh())}:00:00` : s);
if (!v?.start || v.end) return p.onChange?.({ start: s, end: null, ...(p.hour && { startHour: sh() }) });
let nv = s < v.start ? { start: s, end: v.start } : { start: v.start, end: s };
p.onChange?.({ ...nv, ...(p.hour && { startHour: v.startHour ?? sh(), endHour: eh() }) });
},
I = ({ v, on }) => h('div', { class: 'flex-1 flex gap-2 items-center' }, [
h('input', { type: 'range', min: 0, max: 23, value: v, class: 'range range-xs', oninput: e => on(+e.target.value) }),
h('span', { class: 'text-sm font-mono' }, () => P(v()) + ':00')
]);
return h('div', { class: `p-4 bg-base-100 rounded-box w-80 select-none ${p.class || ''}` }, [
h('div', { class: 'flex justify-between items-center mb-4' }, [
h('div', { class: 'flex gap-1' }, [
h('button', { class: 'btn btn-ghost btn-xs', onclick: () => M(0, -1) }, h('span', { class: 'icon-[lucide--chevrons-left]' })),
h('button', { class: 'btn btn-ghost btn-xs', onclick: () => M(-1, 0) }, h('span', { class: 'icon-[lucide--chevron-left]' }))
]),
h('span', { class: 'font-bold uppercase' }, () => d().toLocaleString('es', { month: 'short', year: 'numeric' })),
h('div', { class: 'flex gap-1' }, [
h('button', { class: 'btn btn-ghost btn-xs', onclick: () => M(1, 0) }, h('span', { class: 'icon-[lucide--chevron-right]' })),
h('button', { class: 'btn btn-ghost btn-xs', onclick: () => M(0, 1) }, h('span', { class: 'icon-[lucide--chevrons-right]' }))
])
]),
h('div', { class: 'grid grid-cols-7 gap-1', onmouseleave: () => hv(null) }, [
...'LMXJVSD'.split('').map(l => h('div', { class: 'text-[10px] opacity-40 font-bold text-center' }, l)),
() => {
let y = d().getFullYear(), m = d().getMonth(), first = (new Date(y, m, 1).getDay() + 6) % 7;
return [...Array(first).fill(h('div')), ...Array(new Date(y, m + 1, 0).getDate()).keys()].map(i => {
if (typeof i != 'number') return i;
let day = i + 1, ds = F(new Date(y, m, day)), today = F(now) == ds;
return h('button', {
type: 'button', onclick: () => L(new Date(y, m, day)), onmouseenter: () => G() && hv(ds),
class: () => {
let v = V(), hov = hv(), s = v?.start || (typeof v == 'string' ? v.slice(0, 10) : 0),
isE = v?.end == ds, isS = s == ds,
inR = G() && v?.start && (v.end ? (ds > v.start && ds < v.end) : (hov && ((ds > s && ds <= hov) || (ds < s && ds >= hov))));
return `btn btn-xs p-0 aspect-square min-h-0 h-auto font-normal relative ${isS || isE ? 'btn-primary z-10' : inR ? 'bg-primary/20 border-none rounded-none' : 'btn-ghost'} ${today ? 'ring-1 ring-primary font-black' : ''}`
}
}, day)
})
}
]),
p.hour && h('div', { class: 'mt-3 pt-2 border-t flex gap-4' }, G() ? [I({ v: sh, on: sh }), I({ v: eh, on: eh })] : [I({ v: sh, on: sh })])
])
}
export const pallete = p => {
let L = s => (s || '').toLowerCase(),
C = ['#000', '#1A1A1A', '#333', '#4D4D4D', '#666', '#808080', '#B3B3B3', '#FFF', '#450a0a', '#7f1d1d', '#991b1b', '#b91c1c', '#dc2626', '#ef4444', '#f87171', '#fca5a5', '#431407', '#7c2d12', '#9a3412', '#c2410c', '#ea580c', '#f97316', '#fb923c', '#ffedd5', '#713f12', '#a16207', '#ca8a04', '#eab308', '#facc15', '#fde047', '#fef08a', '#fff9c4', '#064e3b', '#065f46', '#059669', '#10b981', '#34d399', '#4ade80', '#84cc16', '#d9f99d', '#082f49', '#075985', '#0284c7', '#0ea5e9', '#38bdf8', '#7dd3fc', '#22d3ee', '#cffafe', '#1e1b4b', '#312e81', '#4338ca', '#4f46e5', '#6366f1', '#818cf8', '#a5b4fc', '#e0e7ff', '#2e1065', '#4c1d95', '#6d28d9', '#7c3aed', '#8b5cf6', '#a855f7', '#d946ef', '#fae8ff'];
return h('div', { class: `p-3 bg-base-100 rounded-box shadow w-64 ${p.class || ''}` },
h('div', { class: 'grid grid-cols-8 gap-1' },
C.map(c => h('button', {
type: 'button',
style: `background:${c}`,
onclick: () => (isF(p.value) ? p.value(c) : p.onchange?.(c), hide()),
class: () => `size-6 rounded-sm transition-all hover:scale-125 hover:z-10 active:scale-95 border border-black/5 p-0 min-h-0 ${L(val(p.value)) == L(c) ? 'ring-2 ring-offset-1 ring-primary z-10 scale-110' : ''}`
}))
)
);
};
export const toast = (m, t = "alert-success", d = 3500) => {
let C = document.getElementById("stc"), T, E, w = h("div", { style: "display:contents" });
if (!C) document.body.append(C = h("div", { id: "stc", class: "fixed top-0 right-0 z-[9999] p-4 flex flex-col items-end gap-2 pointer-events-none" }));
C.append(w);
const i = mount(() => {
let v = $(0), l = $(0);
E = () => l() || (l(1), clearTimeout(T), setTimeout(() => (i.destroy(), w.remove(), C.firstChild || C.remove()), 300));
setTimeout(() => v(1));
return h("div", {
class: () => `alert alert-soft ${t} shadow-lg transition-all duration-300 inline-flex w-auto pointer-events-auto ${l() ? 'translate-x-full opacity-0' : v() ? 'translate-x-0 opacity-100' : 'translate-x-10 opacity-0'}`
}, [
typeof m == 'function' ? m() : typeof m == 'string' ? h("span", m) : m,
h("button", { class: "btn btn-xs btn-circle btn-ghost", onclick: E }, h("span", { class: "icon-[lucide--x]" }))
])
}, w);
if (d > 0) T = setTimeout(E, d);
return E;
};
window.ui = ui;
window.toast = toast;
window.calendar = calendar;
window.pallete = pallete

View File

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

117
src/tailwind Normal file
View File

@@ -0,0 +1,117 @@
const layout = [
'join', 'join-vertical', 'lg:join-horizontal',
'divider', 'divider-horizontal',
'validator', 'validator-hint',
'glass'
]
const icons = [
'icon-[lucide--calendar]', 'icon-[lucide--chevrons-left]',
'icon-[lucide--chevron-left]', 'icon-[lucide--chevron-right]',
'icon-[lucide--chevrons-right]', 'icon-[lucide--info]',
'icon-[lucide--check-circle]', 'icon-[lucide--alert-triangle]',
'icon-[lucide--alert-circle]', 'icon-[lucide--heart]',
'icon-[lucide--upload]', 'icon-[lucide--x]', 'icon-[lucide--text]',
'icon-[lucide--lock]', 'icon-[lucide--hash]', 'icon-[lucide--mail]',
'icon-[lucide--search]', 'icon-[lucide--phone]', 'icon-[lucide--link]',
'icon-[lucide--eye-off]', 'icon-[lucide--eye]'
]
const inputs = [
'input', 'input-bordered', 'input-ghost',
'input-primary', 'input-secondary', 'input-accent',
'input-info', 'input-success', 'input-warning', 'input-error',
'input-xs', 'input-sm', 'input-md', 'input-lg',
'floating-label'
]
const alerts = [
'alert', 'alert-info', 'alert-success', 'alert-warning', 'alert-error',
'alert-soft', 'alert-outline', 'alert-dash'
]
const avatars = [
'avatar', 'avatar-group', 'avatar-online', 'avatar-offline', 'avatar-placeholder'
]
const badges = [
'badge', 'badge-primary', 'badge-secondary', 'badge-accent',
'badge-info', 'badge-success', 'badge-warning', 'badge-error',
'badge-outline', 'badge-soft', 'badge-dash',
'badge-xs', 'badge-sm', 'badge-md', 'badge-lg'
]
const buttons = [
'btn', 'btn-primary', 'btn-secondary', 'btn-accent',
'btn-ghost', 'btn-info', 'btn-success', 'btn-warning',
'btn-error', 'btn-neutral',
'btn-xs', 'btn-sm', 'btn-md', 'btn-lg', 'btn-xl',
'btn-outline', 'btn-soft', 'btn-dash', 'btn-link',
'btn-circle', 'btn-square', 'btn-wide', 'btn-block',
'btn-active', 'btn-disabled'
]
const checkboxes = [
'checkbox', 'checkbox-primary', 'checkbox-secondary', 'checkbox-accent',
'checkbox-info', 'checkbox-success', 'checkbox-warning', 'checkbox-error',
'checkbox-xs', 'checkbox-sm', 'checkbox-md', 'checkbox-lg'
]
const toggles = [
'toggle', 'toggle-primary', 'toggle-secondary', 'toggle-accent',
'toggle-xs', 'toggle-sm', 'toggle-md', 'toggle-lg'
]
const chats = [
'chat', 'chat-end', 'chat-start', 'chat-image',
'chat-header', 'chat-footer', 'chat-bubble'
]
const drawers = [
'drawer', 'drawer-end', 'drawer-toggle', 'drawer-content',
'drawer-side', 'drawer-overlay'
]
const dropdowns = [
'dropdown', 'dropdown-content', 'dropdown-end',
'dropdown-top', 'dropdown-bottom', 'dropdown-left', 'dropdown-right'
]
const misc = [
'breadcrumbs', 'fab', 'fieldset', 'fieldset-legend',
'indicator', 'indicator-item', 'menu', 'menu-dropdown', 'menu-dropdown-show',
'kbd', 'kbd-xs', 'kbd-sm', 'kbd-md', 'kbd-lg', 'kbd-xl',
'list', 'list-row', 'list-bullet', 'list-image', 'list-none',
'mask', 'mask-star', 'mask-star-2', 'mask-heart', 'mask-circle',
'modal', 'modal-box', 'modal-action', 'modal-backdrop',
'modal-open', 'modal-middle', 'modal-top', 'modal-bottom',
'navbar', 'navbar-start', 'navbar-center', 'navbar-end',
'progress', 'progress-neutral', 'progress-primary', 'progress-secondary',
'progress-accent', 'progress-info', 'progress-success',
'progress-warning', 'progress-error', 'radial-progress',
'radio', 'radio-primary', 'radio-secondary', 'radio-accent',
'radio-info', 'radio-success', 'radio-warning', 'radio-error',
'radio-xs', 'radio-sm', 'radio-md', 'radio-lg',
'range', 'range-primary', 'range-secondary', 'range-accent',
'range-info', 'range-success', 'range-warning', 'range-error',
'range-xs', 'range-sm', 'range-md', 'range-lg',
'rating', 'rating-half', 'rating-hidden',
'select', 'select-bordered', 'select-primary', 'select-secondary',
'select-accent', 'select-info', 'select-success',
'select-warning', 'select-error',
'select-xs', 'select-sm', 'select-md', 'select-lg',
'stack', 'stack-top', 'stack-bottom', 'stack-start', 'stack-end',
'stat', 'stat-figure', 'stat-title', 'stat-value', 'stat-desc',
'swap', 'swap-on', 'swap-off', 'swap-active',
'swap-rotate', 'swap-flip', 'swap-indeterminate',
'table', 'table-zebra', 'table-pin-rows', 'table-pin-cols',
'table-xs', 'table-sm', 'table-md', 'table-lg',
'tabs', 'tabs-box', 'tabs-lift', 'tabs-border', 'tab', 'tab-content',
'timeline', 'timeline-vertical', 'timeline-horizontal',
'timeline-compact', 'timeline-start', 'timeline-middle',
'timeline-end', 'timeline-box',
'tooltip', 'tooltip-top', 'tooltip-bottom', 'tooltip-left', 'tooltip-right',
'tooltip-primary', 'tooltip-secondary', 'tooltip-accent',
'tooltip-info', 'tooltip-success', 'tooltip-warning', 'tooltip-error',
'tooltip-open'
]