sibujs 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +105 -119
  2. package/dist/browser.cjs +53 -14
  3. package/dist/browser.d.cts +14 -9
  4. package/dist/browser.d.ts +14 -9
  5. package/dist/browser.js +4 -4
  6. package/dist/build.cjs +124 -42
  7. package/dist/build.d.cts +1 -1
  8. package/dist/build.d.ts +1 -1
  9. package/dist/build.js +10 -10
  10. package/dist/cdn.global.js +7 -7
  11. package/dist/chunk-5ZYQ6KDD.js +154 -0
  12. package/dist/chunk-6BMPXPUW.js +26 -0
  13. package/dist/chunk-7GRNSCFT.js +1097 -0
  14. package/dist/chunk-BGTHZHJ5.js +1016 -0
  15. package/dist/chunk-BMPL52BF.js +654 -0
  16. package/dist/chunk-GJPXRJ45.js +37 -0
  17. package/dist/chunk-JCDUJN2F.js +2779 -0
  18. package/dist/chunk-K4G4ZQNR.js +286 -0
  19. package/dist/chunk-MB6QFH3I.js +2776 -0
  20. package/dist/chunk-MYRV7VDM.js +742 -0
  21. package/dist/chunk-NZIIMDWI.js +84 -0
  22. package/dist/chunk-P3XWXJZU.js +282 -0
  23. package/dist/chunk-PDZQY43A.js +616 -0
  24. package/dist/chunk-RJ46C3CS.js +1293 -0
  25. package/dist/chunk-SFKNRVCU.js +292 -0
  26. package/dist/chunk-TDGZL5CU.js +365 -0
  27. package/dist/chunk-VAPYJN4X.js +368 -0
  28. package/dist/chunk-VQDZK23A.js +1023 -0
  29. package/dist/chunk-VQNQZCWJ.js +61 -0
  30. package/dist/chunk-XHK6BDAJ.js +76 -0
  31. package/dist/chunk-XUEEGU5O.js +409 -0
  32. package/dist/contracts-ey_Qh8ef.d.cts +239 -0
  33. package/dist/contracts-ey_Qh8ef.d.ts +239 -0
  34. package/dist/customElement-BL3Uo8dL.d.cts +318 -0
  35. package/dist/customElement-BL3Uo8dL.d.ts +318 -0
  36. package/dist/data.cjs +52 -11
  37. package/dist/data.js +6 -6
  38. package/dist/devtools.cjs +22 -24
  39. package/dist/devtools.js +26 -28
  40. package/dist/ecosystem.cjs +31 -6
  41. package/dist/ecosystem.d.cts +4 -4
  42. package/dist/ecosystem.d.ts +4 -4
  43. package/dist/ecosystem.js +7 -7
  44. package/dist/extras.cjs +304 -108
  45. package/dist/extras.d.cts +3 -3
  46. package/dist/extras.d.ts +3 -3
  47. package/dist/extras.js +19 -19
  48. package/dist/index.cjs +124 -42
  49. package/dist/index.d.cts +58 -48
  50. package/dist/index.d.ts +58 -48
  51. package/dist/index.js +10 -10
  52. package/dist/motion.cjs +13 -2
  53. package/dist/motion.d.cts +1 -1
  54. package/dist/motion.d.ts +1 -1
  55. package/dist/motion.js +3 -3
  56. package/dist/patterns.cjs +91 -24
  57. package/dist/patterns.d.cts +46 -12
  58. package/dist/patterns.d.ts +46 -12
  59. package/dist/patterns.js +5 -5
  60. package/dist/performance.cjs +97 -12
  61. package/dist/performance.d.cts +6 -1
  62. package/dist/performance.d.ts +6 -1
  63. package/dist/performance.js +5 -3
  64. package/dist/plugins.cjs +19 -13
  65. package/dist/plugins.d.cts +3 -3
  66. package/dist/plugins.d.ts +3 -3
  67. package/dist/plugins.js +16 -18
  68. package/dist/ssr.cjs +9 -0
  69. package/dist/ssr.d.cts +1 -1
  70. package/dist/ssr.d.ts +1 -1
  71. package/dist/ssr.js +7 -7
  72. package/dist/testing.js +2 -2
  73. package/dist/ui.cjs +130 -48
  74. package/dist/ui.d.cts +13 -16
  75. package/dist/ui.d.ts +13 -16
  76. package/dist/ui.js +6 -6
  77. package/dist/widgets.cjs +31 -6
  78. package/dist/widgets.js +5 -5
  79. package/package.json +1 -1
package/README.md CHANGED
@@ -1,119 +1,105 @@
1
- # SibuJS
2
-
3
- A lightweight, function-based frontend framework with fine-grained reactivity, direct DOM rendering, and zero compilation. **No Virtual DOM. No magic.**
4
-
5
- [[NPM Version]](https://www.npmjs.com/package/sibujs)
6
- [[License]](https://github.com/hexplus/sibujs/blob/main/LICENSE)
7
-
8
- ## Why SibuJS?
9
-
10
- - **Zero VDOM:** Updates only what changes, directly in the DOM.
11
- - **Function-Based:** Components are just plain functions. No classes, no complex life cycles.
12
- - **Fine-Grained Reactivity:** Powered by lightweight signals.
13
- - **No Build Step Required:** Works natively in the browser, but includes a Vite plugin for advanced optimizations.
14
- - **Modular & Lean:** Core is minimal; features like Router and i18n are optional plugins.
15
-
16
- ## Quick Start
17
-
18
- ```bash
19
- npm install sibujs
20
- ```
21
-
22
- ```javascript
23
- import { div, h1, button, signal, mount } from "sibujs";
24
-
25
- function Counter() {
26
- const [count, setCount] = signal(0);
27
-
28
- return div({ class: "counter" }, [
29
- h1(() => `Count: ${count()}`),
30
- button({ on: { click: () => setCount(c => c + 1) } }, "Increment"),
31
- ]);
32
- }
33
-
34
- mount(Counter, document.getElementById("app"));
35
- ```
36
-
37
- ### Three Ways to Author Components
38
-
39
- SibuJS gives you maximum flexibility with three interoperable styles:
40
-
41
- #### 1. Tag Factory
42
- The canonical form: a props object followed by children as a second
43
- positional argument. No `nodes:` key required at any level of the tree —
44
- children can be a string, a number, a single node, an array, or a
45
- reactive getter.
46
-
47
- ```javascript
48
- import { div, h1, label, input, button } from "sibujs";
49
-
50
- return div({ class: "counter" }, [
51
- h1({ class: "title" }, () => `Count: ${count()}`),
52
- label({ for: "amount" }, "Step"),
53
- input({ id: "amount", type: "number", value: 1 }),
54
- button(
55
- { class: "primary", on: { click: () => setCount(c => c + 1) } },
56
- "Increment",
57
- ),
58
- ]);
59
- ```
60
-
61
- All legacy forms — `tag({ class, nodes })`, `tag("className", children)`,
62
- `tag("text")`, `tag([children])`, `tag(node)`, `tag(() => reactive)` —
63
- continue to work unchanged. When both `props.nodes` and the positional
64
- second argument are present, the positional wins.
65
-
66
- #### 2. Positional Shorthand
67
- The tersest form. Class and children as positional arguments, for
68
- layouts with no event handlers or custom props.
69
-
70
- ```javascript
71
- import { div, h1, button } from "sibujs";
72
-
73
- return div("counter", [
74
- h1(() => `Count: ${count()}`),
75
- button({ on: { click: () => setCount(c => c + 1) } }, "Increment"),
76
- ]);
77
- ```
78
-
79
- #### 3. HTML Tagged Template
80
- Familiar HTML-like syntax using tagged template literals. No compiler needed!
81
-
82
- ```javascript
83
- import { html } from "sibujs";
84
-
85
- return html`
86
- <div class="counter">
87
- <h1>Count: ${() => count()}</h1>
88
- <button on:click=${() => setCount(c => c + 1)}>Increment</button>
89
- </div>
90
- `;
91
- ```
92
-
93
- ## Learn More
94
-
95
- For full documentation, guides, and advanced examples, visit our official website:
96
-
97
- ### 🌐 [sibujs.dev](https://sibujs.dev/)
98
-
99
- ---
100
-
101
- ## Features at a Glance
102
-
103
- - **Reactivity:** `signal`, `effect`, `derived`, `watch`, `batch`.
104
- - **Components:** Functional, reusable, and lifecycle-aware (`onMount`, `onUnmount`).
105
- - **Control Flow:** `when` (conditional swaps), `each` (efficient keyed lists), `match` $(pattern matching)$, `show` (toggle visibility).
106
- - **DOM Utilities:** `Portal` (render out-of-tree), `Fragment` (group children), `Suspense` & `lazy` (async components), `ErrorBoundary`.
107
- - **State Management:** `store` (simple state containers), `deepSignal` (object proxies), `ref`.
108
- - **Performance:** Zero VDOM overhead, LIS-based list diffing, and optional template compilation.
109
- - **Plugins:** Official Router (nested routes, guards), i18n (reactive translations), logic patterns (Finite State Machines).
110
-
111
- ---
112
-
113
- ## Ecosystem
114
-
115
- - [SibuJS UI](https://github.com/hexplus/sibujs-ui) - Component library.
116
-
117
- ## License
118
-
119
- MIT © [hexplus](https://github.com/hexplus)
1
+ # SibuJS
2
+
3
+ A lightweight, function-based frontend framework with fine-grained reactivity, direct DOM rendering, and zero compilation. **No Virtual DOM. No magic.**
4
+
5
+ [[NPM Version]](https://www.npmjs.com/package/sibujs)
6
+ [[License]](https://github.com/hexplus/sibujs/blob/main/LICENSE)
7
+
8
+ ## Why SibuJS?
9
+
10
+ - **Zero VDOM:** Updates only what changes, directly in the DOM.
11
+ - **Function-Based:** Components are just plain functions. No classes, no complex life cycles.
12
+ - **Fine-Grained Reactivity:** Powered by lightweight signals.
13
+ - **No Build Step Required:** Works natively in the browser, but includes a Vite plugin for advanced optimizations.
14
+ - **Modular & Lean:** Core is minimal; features like Router and i18n are optional plugins.
15
+
16
+ ## Quick Start
17
+
18
+ ```bash
19
+ npm install sibujs
20
+ ```
21
+
22
+ ```javascript
23
+ import { div, h1, button, signal, mount } from "sibujs";
24
+
25
+ function Counter() {
26
+ const [count, setCount] = signal(0);
27
+
28
+ return div("counter", [
29
+ h1(() => `Count: ${count()}`),
30
+ button(
31
+ { on: { click: () => setCount(count() + 1) } },
32
+ "Increment",
33
+ ),
34
+ ]);
35
+ }
36
+
37
+ mount(Counter, document.getElementById("app"));
38
+ ```
39
+
40
+ ### Authoring Style
41
+
42
+ Every tag factory accepts children as an optional second positional
43
+ argument. This is **the canonical authoring style** no `nodes:` key
44
+ at any level of the tree. The first argument can be a className string,
45
+ a props object, or the children themselves.
46
+
47
+ ```javascript
48
+ import { div, h1, label, input, button } from "sibujs";
49
+
50
+ // Positional className + children — the default form for styled wrappers
51
+ div("page", [
52
+ h1("title", "Welcome"),
53
+ div("row", [
54
+ label({ for: "email" }, "Email"),
55
+ input({ id: "email", type: "email" }),
56
+ button(
57
+ { class: "primary", type: "submit", on: { click: handleSubmit } },
58
+ "Submit",
59
+ ),
60
+ ]),
61
+ ]);
62
+
63
+ // Children-only bare containers
64
+ div([h1("Hello"), p("World")]);
65
+
66
+ // Text-only
67
+ h1("Hello, world!");
68
+
69
+ // Reactive children
70
+ div(() => `Count: ${count()}`);
71
+ ```
72
+
73
+ Legacy forms — the `{ class, nodes }` prop object and the `html` tagged
74
+ template remain supported by the runtime so existing code keeps
75
+ working, but they are no longer the recommended authoring style. When
76
+ both `props.nodes` and the positional second argument are present, the
77
+ positional wins.
78
+
79
+ ## Learn More
80
+
81
+ For full documentation, guides, and advanced examples, visit our official website:
82
+
83
+ ### 🌐 [sibujs.dev](https://sibujs.dev/)
84
+
85
+ ---
86
+
87
+ ## Features at a Glance
88
+
89
+ - **Reactivity:** `signal`, `effect`, `derived`, `watch`, `batch`.
90
+ - **Components:** Functional, reusable, and lifecycle-aware (`onMount`, `onUnmount`).
91
+ - **Control Flow:** `when` (conditional swaps), `each` (efficient keyed lists), `match` (pattern matching), `show` (toggle visibility).
92
+ - **DOM Utilities:** `Portal` (render out-of-tree), `Fragment` (group children), `Suspense` & `lazy` (async components), `ErrorBoundary`.
93
+ - **State Management:** `store` (simple state containers), `deepSignal` (object proxies), `ref`.
94
+ - **Performance:** Zero VDOM overhead, LIS-based list diffing, and optional template compilation.
95
+ - **Plugins:** Official Router (nested routes, guards), i18n (reactive translations), logic patterns (Finite State Machines).
96
+
97
+ ---
98
+
99
+ ## Ecosystem
100
+
101
+ - [SibuJS UI](https://github.com/hexplus/sibujs-ui) - Component library.
102
+
103
+ ## License
104
+
105
+ MIT © [hexplus](https://github.com/hexplus)
package/dist/browser.cjs CHANGED
@@ -153,12 +153,21 @@ function queueSignalNotification(signal2) {
153
153
  }
154
154
  }
155
155
  }
156
+ var MAX_DRAIN_ITERATIONS = 1e3;
156
157
  function drainNotificationQueue() {
157
158
  if (notifyDepth > 0) return;
158
159
  notifyDepth++;
159
160
  try {
160
161
  let i = 0;
161
162
  while (i < pendingQueue.length) {
163
+ if (i >= MAX_DRAIN_ITERATIONS) {
164
+ if (typeof console !== "undefined") {
165
+ console.error(
166
+ `[SibuJS] Notification queue exceeded ${MAX_DRAIN_ITERATIONS} iterations \u2014 likely an effect that writes to a signal it reads. Breaking to prevent infinite loop.`
167
+ );
168
+ }
169
+ break;
170
+ }
162
171
  safeInvoke(pendingQueue[i]);
163
172
  i++;
164
173
  }
@@ -474,6 +483,8 @@ function scroll(target) {
474
483
  const [y, setY] = signal(0);
475
484
  const [isScrolling, setIsScrolling] = signal(false);
476
485
  let scrollTimer = null;
486
+ let currentTarget = null;
487
+ let effectCleanup = null;
477
488
  if (typeof window === "undefined") {
478
489
  return { x, y, isScrolling, dispose: () => {
479
490
  } };
@@ -496,11 +507,26 @@ function scroll(target) {
496
507
  scrollTimer = null;
497
508
  }, 150);
498
509
  };
499
- const scrollTarget = target ? target() : null;
500
- const eventTarget = scrollTarget || window;
501
- eventTarget.addEventListener("scroll", handler, { passive: true });
510
+ function attachListener(eventTarget) {
511
+ if (currentTarget === eventTarget) return;
512
+ if (currentTarget) currentTarget.removeEventListener("scroll", handler);
513
+ currentTarget = eventTarget;
514
+ currentTarget.addEventListener("scroll", handler, { passive: true });
515
+ }
516
+ if (target) {
517
+ effectCleanup = effect(() => {
518
+ const el = target();
519
+ attachListener(el || window);
520
+ });
521
+ } else {
522
+ attachListener(window);
523
+ }
502
524
  function dispose() {
503
- eventTarget.removeEventListener("scroll", handler);
525
+ effectCleanup?.();
526
+ if (currentTarget) {
527
+ currentTarget.removeEventListener("scroll", handler);
528
+ currentTarget = null;
529
+ }
504
530
  if (scrollTimer !== null) {
505
531
  clearTimeout(scrollTimer);
506
532
  scrollTimer = null;
@@ -1019,31 +1045,44 @@ function urlState() {
1019
1045
  }
1020
1046
  };
1021
1047
  }
1022
- const [params, setParamsSignal] = signal(new URLSearchParams(window.location.search));
1023
- const [hash, setHashSignal] = signal(window.location.hash);
1024
- const syncFromLocation = () => {
1025
- setParamsSignal(new URLSearchParams(window.location.search));
1026
- setHashSignal(window.location.hash);
1027
- };
1028
- const onPopState = () => syncFromLocation();
1029
- window.addEventListener("popstate", onPopState);
1048
+ let lastSearch = window.location.search;
1049
+ let lastHash = window.location.hash;
1050
+ const [params, setParamsSignal] = signal(new URLSearchParams(lastSearch));
1051
+ const [hash, setHashSignal] = signal(lastHash);
1052
+ function syncFromLocation() {
1053
+ const currentSearch = window.location.search;
1054
+ const currentHash = window.location.hash;
1055
+ if (currentSearch !== lastSearch) {
1056
+ lastSearch = currentSearch;
1057
+ setParamsSignal(new URLSearchParams(currentSearch));
1058
+ }
1059
+ if (currentHash !== lastHash) {
1060
+ lastHash = currentHash;
1061
+ setHashSignal(currentHash);
1062
+ }
1063
+ }
1064
+ window.addEventListener("popstate", syncFromLocation);
1065
+ window.addEventListener("hashchange", syncFromLocation);
1030
1066
  function setParams(next, opts = {}) {
1031
1067
  const p = next instanceof URLSearchParams ? next : new URLSearchParams(next);
1032
1068
  const query = p.toString();
1033
1069
  const newUrl = `${window.location.pathname}${query ? `?${query}` : ""}${window.location.hash}`;
1034
1070
  if (opts.replace) window.history.replaceState(null, "", newUrl);
1035
1071
  else window.history.pushState(null, "", newUrl);
1072
+ lastSearch = window.location.search;
1036
1073
  setParamsSignal(new URLSearchParams(p));
1037
1074
  }
1038
1075
  function setHash(next, opts = {}) {
1039
- const normalized = next.startsWith("#") ? next : next ? `#${next}` : "";
1076
+ const normalized = next && next !== "#" ? next.startsWith("#") ? next : `#${next}` : "";
1040
1077
  const newUrl = `${window.location.pathname}${window.location.search}${normalized}`;
1041
1078
  if (opts.replace) window.history.replaceState(null, "", newUrl);
1042
1079
  else window.history.pushState(null, "", newUrl);
1080
+ lastHash = normalized;
1043
1081
  setHashSignal(normalized);
1044
1082
  }
1045
1083
  function dispose() {
1046
- window.removeEventListener("popstate", onPopState);
1084
+ window.removeEventListener("popstate", syncFromLocation);
1085
+ window.removeEventListener("hashchange", syncFromLocation);
1047
1086
  }
1048
1087
  return { params, hash, setParams, setHash, dispose };
1049
1088
  }
@@ -31,6 +31,9 @@ declare function resize(target: ElementTarget$1): {
31
31
  * Returns reactive x/y scroll positions and an isScrolling indicator
32
32
  * that resets after 150ms of inactivity.
33
33
  *
34
+ * If a reactive target getter is provided, the listener re-attaches
35
+ * whenever the target element changes (same pattern as resize/dragDrop).
36
+ *
34
37
  * @param target Optional reactive getter for the scroll target element.
35
38
  * If omitted or returns null, tracks window scroll.
36
39
  * @returns Object with reactive x, y, isScrolling getters and a dispose function
@@ -336,7 +339,9 @@ declare function windowSize(): {
336
339
  * to sync a handful of UI state bits with the URL (filters, tabs, modals)
337
340
  * without a full router setup.
338
341
  *
339
- * Listens to `popstate` so browser back/forward updates the signals.
342
+ * Listens to both `popstate` (back/forward) and `hashchange` (anchor clicks,
343
+ * direct `location.hash` assignments) so the signals stay in sync regardless
344
+ * of how the URL was changed.
340
345
  *
341
346
  * @example
342
347
  * ```ts
@@ -406,10 +411,10 @@ declare function broadcast<T = unknown>(channelName: string): {
406
411
  * @example
407
412
  * ```ts
408
413
  * const fs = fullscreen();
409
- * button({
410
- * nodes: () => (fs.isFullscreen() ? "Exit fullscreen" : "Enter fullscreen"),
411
- * on: { click: () => fs.toggle(videoEl) },
412
- * });
414
+ * button(
415
+ * { on: { click: () => fs.toggle(videoEl) } },
416
+ * () => (fs.isFullscreen() ? "Exit fullscreen" : "Enter fullscreen"),
417
+ * );
413
418
  * ```
414
419
  */
415
420
  declare function fullscreen(): {
@@ -594,10 +599,10 @@ interface SpeakOptions {
594
599
  * @example
595
600
  * ```ts
596
601
  * const tts = speech();
597
- * button({
598
- * nodes: "Read it to me",
599
- * on: { click: () => tts.speak("Hello, world!", { rate: 1.1 }) },
600
- * });
602
+ * button(
603
+ * { on: { click: () => tts.speak("Hello, world!", { rate: 1.1 }) } },
604
+ * "Read it to me",
605
+ * );
601
606
  * ```
602
607
  */
603
608
  declare function speech(): {
package/dist/browser.d.ts CHANGED
@@ -31,6 +31,9 @@ declare function resize(target: ElementTarget$1): {
31
31
  * Returns reactive x/y scroll positions and an isScrolling indicator
32
32
  * that resets after 150ms of inactivity.
33
33
  *
34
+ * If a reactive target getter is provided, the listener re-attaches
35
+ * whenever the target element changes (same pattern as resize/dragDrop).
36
+ *
34
37
  * @param target Optional reactive getter for the scroll target element.
35
38
  * If omitted or returns null, tracks window scroll.
36
39
  * @returns Object with reactive x, y, isScrolling getters and a dispose function
@@ -336,7 +339,9 @@ declare function windowSize(): {
336
339
  * to sync a handful of UI state bits with the URL (filters, tabs, modals)
337
340
  * without a full router setup.
338
341
  *
339
- * Listens to `popstate` so browser back/forward updates the signals.
342
+ * Listens to both `popstate` (back/forward) and `hashchange` (anchor clicks,
343
+ * direct `location.hash` assignments) so the signals stay in sync regardless
344
+ * of how the URL was changed.
340
345
  *
341
346
  * @example
342
347
  * ```ts
@@ -406,10 +411,10 @@ declare function broadcast<T = unknown>(channelName: string): {
406
411
  * @example
407
412
  * ```ts
408
413
  * const fs = fullscreen();
409
- * button({
410
- * nodes: () => (fs.isFullscreen() ? "Exit fullscreen" : "Enter fullscreen"),
411
- * on: { click: () => fs.toggle(videoEl) },
412
- * });
414
+ * button(
415
+ * { on: { click: () => fs.toggle(videoEl) } },
416
+ * () => (fs.isFullscreen() ? "Exit fullscreen" : "Enter fullscreen"),
417
+ * );
413
418
  * ```
414
419
  */
415
420
  declare function fullscreen(): {
@@ -594,10 +599,10 @@ interface SpeakOptions {
594
599
  * @example
595
600
  * ```ts
596
601
  * const tts = speech();
597
- * button({
598
- * nodes: "Read it to me",
599
- * on: { click: () => tts.speak("Hello, world!", { rate: 1.1 }) },
600
- * });
602
+ * button(
603
+ * { on: { click: () => tts.speak("Hello, world!", { rate: 1.1 }) } },
604
+ * "Read it to me",
605
+ * );
601
606
  * ```
602
607
  */
603
608
  declare function speech(): {
package/dist/browser.js CHANGED
@@ -35,11 +35,11 @@ import {
35
35
  visibility,
36
36
  wakeLock,
37
37
  windowSize
38
- } from "./chunk-3AIRKM3B.js";
39
- import "./chunk-CHF5OHIA.js";
38
+ } from "./chunk-RJ46C3CS.js";
39
+ import "./chunk-VQNQZCWJ.js";
40
40
  import "./chunk-EUZND3CB.js";
41
- import "./chunk-WZSPOOER.js";
42
- import "./chunk-ZD6OAMTH.js";
41
+ import "./chunk-NZIIMDWI.js";
42
+ import "./chunk-K4G4ZQNR.js";
43
43
  import "./chunk-5X6PP2UK.js";
44
44
  export {
45
45
  animationFrame,