sibujs 1.0.6 → 1.0.7

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.
package/README.md CHANGED
@@ -1,1654 +1,115 @@
1
- # SibuJS
2
-
3
- A function-based frontend framework with fine-grained reactivity, direct DOM rendering, and zero compilation. No virtual DOM. No JSX. No magic.
4
-
5
- Named after **Sibu** -- the creator deity of the Bribri and Cabecar indigenous peoples of Costa Rica -- representing clarity, harmony, and simplicity in creation.
6
-
7
- ```
8
- npm install sibujs
9
- ```
10
-
11
- ---
12
-
13
- ## Quick Start
14
-
15
- ```ts
16
- import { html, signal, mount } from "sibujs";
17
-
18
- function Counter() {
19
- const [count, setCount] = signal(0);
20
-
21
- return html`<div>
22
- <h1>${() => `Count: ${count()}`}</h1>
23
- <button on:click=${() => setCount(c => c + 1)}>Increment</button>
24
- </div>`;
25
- }
26
-
27
- const { unmount } = mount(Counter, document.getElementById("app"));
28
- ```
29
-
30
- Components are plain functions that return DOM elements. State is a getter/setter pair. Reactive nodes are wrapped in `() =>`. That is the entire mental model.
31
-
32
- ---
33
-
34
- ## Three Ways to Write Components
35
-
36
- Sibu offers three authoring styles. All produce identical DOM elements and are fully interoperable -- mix them freely.
37
-
38
- ### 1. `html` Tagged Template
39
-
40
- HTML-like syntax using tagged template literals. No compiler needed -- it's a runtime function.
41
-
42
- ```ts
43
- import { html, signal } from "sibujs";
44
-
45
- function Counter() {
46
- const [count, setCount] = signal(0);
47
-
48
- return html`<div class="counter">
49
- <h1>${() => `Count: ${count()}`}</h1>
50
- <button on:click=${() => setCount(c => c + 1)}>Increment</button>
51
- <button on:click=${() => setCount(0)}>Reset</button>
52
- </div>`;
53
- }
54
- ```
55
-
56
- **Best for:** Most components. Familiar HTML syntax, minimal nesting, easy to scan.
57
-
58
- ### 2. Positional Shorthand
59
-
60
- Concise function calls with optional class and children arguments.
61
-
62
- ```ts
63
- import { div, h1, button, signal } from "sibujs";
64
-
65
- function Counter() {
66
- const [count, setCount] = signal(0);
67
-
68
- return div("counter", [
69
- h1(() => `Count: ${count()}`),
70
- button({ nodes: "Increment", on: { click: () => setCount(c => c + 1) } }),
71
- button({ nodes: "Reset", on: { click: () => setCount(0) } }),
72
- ]);
73
- }
74
- ```
75
-
76
- **Signatures:**
77
-
78
- - `div("Hello")` -- text only
79
- - `div([child1, child2])` -- children only
80
- - `div("my-class", "Hello")` -- class + text
81
- - `div("my-class", [child1, child2])` -- class + children
82
-
83
- **Best for:** Quick layouts where you just need class + children, no events.
84
-
85
- ### 3. Full Props Object (original API)
86
-
87
- Maximum control with explicit props object containing all configuration.
88
-
89
- ```ts
90
- import { div, h1, button, signal } from "sibujs";
91
-
92
- function Counter() {
93
- const [count, setCount] = signal(0);
94
-
95
- return div({
96
- class: "counter",
97
- nodes: [
98
- h1({ nodes: () => `Count: ${count()}` }),
99
- button({
100
- nodes: "Increment",
101
- on: { click: () => setCount(c => c + 1) },
102
- }),
103
- button({
104
- nodes: "Reset",
105
- on: { click: () => setCount(0) },
106
- }),
107
- ],
108
- });
109
- }
110
- ```
111
-
112
- **Best for:** Complex elements with refs, styles, custom attributes, or dynamic props.
113
-
114
- ### Mixing Styles
115
-
116
- All three styles produce the same DOM elements, so you can combine them:
117
-
118
- ```ts
119
- import { html, div, button, signal } from "sibujs";
120
-
121
- function App() {
122
- const [count, setCount] = signal(0);
123
-
124
- return html`<div class="app">
125
- <h1>My App</h1>
126
- ${div("content", [
127
- html`<p>Count: ${() => count()}</p>`,
128
- button({ nodes: "Click", on: { click: () => setCount(c => c + 1) } }),
129
- ])}
130
- </div>`;
131
- }
132
- ```
133
-
134
- ### Performance by Authoring Style
135
-
136
- All three styles produce the same DOM output, but their runtime cost differs:
137
-
138
- | Style | First render | Subsequent renders | With Vite plugin |
139
- | ----------------------------------------- | ---------------------------------------------- | ---------------------- | -------------------------------------------- |
140
- | **Props Object / Positional** | Fastest — direct function calls, zero parsing | Fastest | No change needed |
141
- | **`html\`\`` with build step** | Same as above — compiled to direct calls | Same as above | `compileTemplates: true` (default in prod) |
142
- | **`html\`\`` without build step** | ~1.5x slower — runtime parser runs once | Same as above (cached) | N/A |
143
-
144
- The `html` tagged template parser caches its output per call site using a `WeakMap` keyed by the template's static strings identity. This means:
145
-
146
- - **First call** at a given source location pays the parsing cost (~1.5x overhead)
147
- - **Every subsequent call** at the same location skips parsing entirely and replays the cached structure with fresh expression values
148
-
149
- With the Vite plugin, even the first-call cost disappears.
150
-
151
- ### Compiling Templates (Build-Time Optimization)
152
-
153
- The Sibu Vite plugin includes a **template compiler** that transforms `html\`...\`` tagged templates into direct function calls at build time. The runtime parser is never loaded — the output is identical to writing Props Object code by hand.
154
-
155
- **Setup:**
156
-
157
- ```ts
158
- // vite.config.ts
159
- import { sibuVitePlugin } from "sibujs/build";
160
-
161
- export default {
162
- plugins: [
163
- sibuVitePlugin()
164
- // compileTemplates is enabled by default in production builds
165
- ]
166
- };
167
- ```
168
-
169
- **What it does:**
170
-
171
- ```ts
172
- // Your source code:
173
- const el = html`<div class=${cls}>
174
- <span>${() => count()}</span>
175
- <button on:click=${handler}>Click</button>
176
- </div>`;
177
-
178
- // After compilation (production build):
179
- const el = ((v) => div({
180
- class: v[0],
181
- nodes: [
182
- span({ nodes: v[1] }),
183
- button({ on: { click: v[2] }, nodes: "Click" })
184
- ]
185
- }))([cls, () => count(), handler]);
186
- ```
187
-
188
- The compiler handles all template features: static/dynamic attributes, event handlers (`on:click`), expression children, nested elements, self-closing/void elements, and SVG.
189
-
190
- **Plugin options:**
191
-
192
- ```ts
193
- sibuVitePlugin({
194
- compileTemplates: true, // Compile html`` to direct calls (default: true in prod, false in dev)
195
- staticOptimize: true, // Convert fully-static tag calls to template cloning (default: true in prod)
196
- pureAnnotations: true, // Add /*#__PURE__*/ for tree-shaking (default: true)
197
- hmr: true, // HMR support for Sibu components (default: true)
198
- devMode: false, // Dev helpers: __SIBU_DEV__ flag, debug logging (default: auto from NODE_ENV)
199
- })
200
- ```
201
-
202
- **Without a build step:** The runtime `html` parser still works. Templates are parsed once per call site and cached via `WeakMap` — subsequent renders at the same source location skip parsing entirely. The ~1.5x overhead only applies to the very first render of each component.
203
-
204
- **Recommendation:** Use whichever style reads best for your team. If you use Vite (or any build step), the `html` style has zero performance penalty. Without a build step, the overhead is only on first render per component and is negligible for most applications.
205
-
206
- ---
207
-
208
- ## Why Sibu Instead of React, Vue, Svelte, or Solid
209
-
210
- Every mainstream framework makes a set of tradeoffs. Sibu makes different ones.
211
-
212
- ### vs. React
213
-
214
- React uses a virtual DOM. Every state change re-executes the entire component function, diffs a virtual tree against the previous one, and patches the real DOM. This is conceptually simple but inherently wasteful -- most of the tree hasn't changed.
215
-
216
- Sibu has no virtual DOM. When `setCount` is called, only the text node displaying `count()` updates. The `div`, the `h1`, the `button` -- none of them are re-evaluated or diffed. They were created once and they persist. This is not an optimization layered on top; it is the fundamental architecture.
217
-
218
- React also requires JSX, which requires a compiler. Sibu runs as plain TypeScript or JavaScript. No Babel plugin. No compiler transform. The code you write is the code that runs.
219
-
220
- | | React | Sibu |
221
- | ---------------------- | ------------------------------- | ------------------------ |
222
- | Rendering model | Virtual DOM diffing | Direct DOM, signal-based |
223
- | Compilation | Required (JSX) | None |
224
- | Component re-execution | Entire function on every render | Never -- created once |
225
- | Granularity of updates | Component-level | Node-level |
226
- | Bundle overhead | ~45 KB min (react + react-dom) | Core only |
227
-
228
- ### vs. Vue
229
-
230
- Vue's template compiler generates optimized render functions behind the scenes. Its reactivity system (Proxy-based in Vue 3) is powerful but requires understanding refs, reactive objects, `computed`, `watch`, `toRefs`, `unref`, and the distinction between `.value` access and template auto-unwrapping.
231
-
232
- Sibu's reactivity is simpler. There is one primitive: `signal` returns `[getter, setter]`. You call the getter to read, the setter to write. Dependencies are tracked automatically. There is no `.value`, no `ref()` vs `reactive()`, no unwrapping rules.
233
-
234
- Vue templates are a separate language with their own directives (`v-if`, `v-for`, `v-bind`, `v-model`, `v-slot`). In Sibu, all of these are plain functions (`when`, `each`, reactive props, slots-as-functions).
235
-
236
- | | Vue | Sibu |
237
- | ---------------------- | --------------------------------------------- | ------------------------------- |
238
- | Template language | Custom (SFCs, directives) | HTML tagged templates (runtime) |
239
- | Reactivity API surface | ref, reactive, computed, watch, toRefs, unref | signal, derived, watch |
240
- | Build requirement | Vite/Vue CLI for SFCs | None |
241
- | Learning curve | Moderate (Options + Composition API) | Minimal (functions + signals) |
242
-
243
- ### vs. Svelte
244
-
245
- Svelte shifts work to compile time: it analyzes `.svelte` files and generates imperative DOM operations. The result is fast and small, but you must use the Svelte compiler, Svelte's file format, and Svelte's reactivity syntax (`$:`, `$state`, etc.).
246
-
247
- Sibu achieves similar fine-grained DOM updates at runtime, without a compiler. The tradeoff is that Svelte can optimize away more framework code at build time. The benefit is that Sibu is just TypeScript -- your existing tooling, type checking, and editor support work without any Svelte-specific plugins.
248
-
249
- | | Svelte | Sibu |
250
- | ---------------------- | ----------------------------- | ---------------------- |
251
- | Compilation | Required (.svelte files) | None |
252
- | File format | Custom (.svelte) | Standard .ts / .js |
253
- | Reactivity | Compiler-analyzed ($:, runes) | Runtime signals |
254
- | Editor support | Requires Svelte plugin | Standard TypeScript |
255
- | DOM update granularity | Fine-grained (compiled) | Fine-grained (runtime) |
256
-
257
- ### vs. Solid
258
-
259
- Solid is the closest comparison. Both use fine-grained reactivity with signals. Both skip the virtual DOM. Both update at the node level.
260
-
261
- The key differences:
262
-
263
- 1. **No compiler.** Solid strongly recommends JSX with its Babel plugin for optimal performance. Sibu's `html` tagged template gives you familiar HTML syntax without any build step.
264
- 2. **API style.** Solid uses `createSignal`, `createEffect`, `createMemo` and JSX. Sibu uses `signal`, `effect`, `derived` and `html` templates. If you prefer a runtime-only approach with zero tooling, Sibu is a more natural fit.
265
- 3. **Architecture.** Sibu provides a disposal system (`dispose`/`registerDisposer`), an explicit `mount`/`unmount` lifecycle, and a modular package split (core / extras / plugins / build). Solid bundles more into its core and relies on its compiler for tree shaking.
266
-
267
- | | Solid | Sibu |
268
- | --------------------- | --------------------------- | ---------------------------------- |
269
- | Recommended authoring | JSX (compiled) | `html` tagged template (runtime) |
270
- | Signal API | createSignal / createEffect | signal / effect |
271
- | Disposal model | Owner tree (automatic) | Explicit dispose + WeakMap |
272
- | Package structure | Monolithic core | Modular (core / extras / plugins) |
273
-
274
- ### The Core Principle
275
-
276
- Most frameworks ask you to describe the UI and then figure out how to update the DOM efficiently. Sibu asks you to build the DOM directly and tell it which parts are reactive. The result is code that is predictable, inspectable, and close to the metal.
277
-
278
- ---
279
-
280
- ## Architecture
281
-
282
- ### Reactivity
283
-
284
- The reactivity system is three functions: `track`, `recordDependency`, and `notifySubscribers`.
285
-
286
- When a signal's getter is called, `recordDependency` links the signal to the current subscriber. When a signal's setter is called, `notifySubscribers` calls every subscriber. `track` runs a function while setting up the subscriber context. That is the entire reactivity engine.
287
-
288
- ```
289
- signal(0)
290
- |
291
- +--> getter: calls recordDependency(signal)
292
- | |
293
- | +--> links signal <-> current subscriber (set by track)
294
- |
295
- +--> setter: calls notifySubscribers(signal)
296
- |
297
- +--> runs every subscriber linked to signal
298
- ```
299
-
300
- Dependencies are tracked via `WeakMap<Signal, Set<Subscriber>>`, so unused signals and subscribers are garbage collected automatically.
301
-
302
- ### The `html` Tagged Template
303
-
304
- The `html` function is a runtime tagged template literal that parses HTML-like syntax and creates DOM elements via Sibu's tag factories. No compiler, no build step -- just a function call.
305
-
306
- ```ts
307
- import { html, signal } from "sibujs";
308
-
309
- html`<div id="app" class="container">
310
- <h1>${() => `Hello, ${name()}`}</h1>
311
- <button on:click=${handler}>Click me</button>
312
- <input type="text" value=${() => text()} />
313
- </div>`;
314
- ```
315
-
316
- **Supported features:**
317
-
318
- - All HTML and SVG tags
319
- - Static attributes: `class="foo"`
320
- - Dynamic attributes: `class=${() => "active"}`, `id=${myId}`
321
- - Event handlers: `on:click=${handler}`, `on:input=${handler}`
322
- - Reactive text children: `${() => count()}`
323
- - Element children: `${MyComponent()}`, `${each(...)}`, `${when(...)}`
324
- - Self-closing tags: `<br />`, `<img src="..." />`
325
- - Void elements: `<br>`, `<input>`, `<hr>`
326
-
327
- ### Reactive Props (full props API)
328
-
329
- When using the tag factory API directly, every prop can be reactive:
330
-
331
- ```ts
332
- import { div } from "sibujs";
333
-
334
- div({
335
- // Static class
336
- class: "card",
337
-
338
- // Reactive class (string)
339
- class: () => isActive() ? "card active" : "card",
340
-
341
- // Conditional class (object)
342
- class: { card: true, active: isActive, bold: () => isBold() },
343
-
344
- // Static style
345
- style: { color: "red", fontSize: "14px" },
346
-
347
- // Reactive style (per-property)
348
- style: { color: () => theme().primary },
349
-
350
- // Reactive nodes
351
- nodes: () => `Count: ${count()}`,
352
-
353
- // Events
354
- on: { click: handleClick, mouseover: handleHover },
355
-
356
- // Ref (reactive — works with resize(), draggable(), etc.)
357
- ref: myRef,
358
-
359
- // onElement callback — called after element creation
360
- onElement: (el) => inputMask.bind(el),
361
-
362
- // Any other attribute (empty strings set boolean attributes)
363
- "data-testid": "my-card",
364
- disabled: () => isDisabled(),
365
- });
366
- ```
367
-
368
- ### Disposal
369
-
370
- Reactive bindings (class, style, attribute, node) register teardown functions on their DOM nodes via `registerDisposer`. When you call `dispose(node)`, it walks the subtree depth-first and tears down every binding, preventing memory leaks.
371
-
372
- `mount()` returns an `unmount` function that calls `dispose` and removes the node:
373
-
374
- ```ts
375
- const { node, unmount } = mount(App, document.getElementById("root"));
376
-
377
- // Later:
378
- unmount(); // disposes all reactive bindings + removes from DOM
379
- ```
380
-
381
- ---
382
-
383
- ## Core API
384
-
385
- ### State and Reactivity
386
-
387
- ```ts
388
- import { signal, effect, derived, watch, batch } from "sibujs";
389
-
390
- // Reactive state
391
- const [count, setCount] = signal(0);
392
- count(); // read (tracks dependency)
393
- setCount(5); // write (notifies subscribers)
394
- setCount(c => c + 1); // updater function
395
-
396
- // Derived state
397
- const doubled = derived(() => count() * 2);
398
- doubled(); // always 2x count, auto-updates
399
-
400
- // Side effects
401
- const cleanup = effect(() => {
402
- console.log("count changed:", count());
403
- });
404
- cleanup(); // stop watching
405
-
406
- // Watch with old/new values
407
- const stop = watch(count, (newVal, oldVal) => {
408
- console.log(`${oldVal} -> ${newVal}`);
409
- });
410
-
411
- // Batch multiple updates into one notification
412
- batch(() => {
413
- setCount(10);
414
- setName("Alice");
415
- }); // subscribers notified once
416
- ```
417
-
418
- ### Additional Signals & Utilities
419
-
420
- ```ts
421
- import { ref, memo, memoFn, array, deepSignal, store } from "sibujs";
422
-
423
- // Reactive ref — reading .current tracks, writing .current notifies
424
- // Works with browser APIs like resize(), draggable(), dropZone()
425
- const elRef = ref<HTMLElement>();
426
- elRef.current; // read (tracks dependency)
427
- elRef.current = myElement; // write (notifies subscribers)
428
-
429
- // Memoized value (alias for derived)
430
- const expensive = memo(() => heavyComputation(data()));
431
-
432
- // Memoized callback
433
- const handler = memoFn(() => (e: Event) => process(e, count()));
434
-
435
- // Reactive array with mutation methods
436
- const [items, actions] = array<string>(["a", "b"]);
437
- actions.push("c");
438
- actions.removeWhere(item => item === "a");
439
- actions.sort((a, b) => a.localeCompare(b));
440
-
441
- // Deep equality state (objects/arrays)
442
- const [config, setConfig] = deepSignal({ theme: "dark", lang: "en" });
443
-
444
- // Shared store with actions
445
- const [store, { setState, reset, subscribe }] = store({ count: 0, name: "" });
446
- ```
447
-
448
- ### Rendering
449
-
450
- ```ts
451
- import { html, mount, each, when, match, show, Fragment, Portal, signal } from "sibujs";
452
-
453
- // Mount with unmount support
454
- const { unmount } = mount(App, document.getElementById("root"));
455
-
456
- // Keyed list rendering with LIS-based diffing
457
- html`<ul>
458
- ${each(
459
- () => items(),
460
- (item, i) => html`<li>${item.name}</li>`,
461
- { key: item => item.id }
462
- )}
463
- </ul>`;
464
-
465
- // Conditional rendering (swaps DOM nodes)
466
- html`<div>
467
- ${when(
468
- () => isLoggedIn(),
469
- () => Dashboard(),
470
- () => LoginForm()
471
- )}
472
- </div>`;
473
-
474
- // Toggle visibility (keeps node, toggles display)
475
- show(() => isVisible(), myElement);
476
-
477
- // Pattern matching
478
- html`<div>
479
- ${match(
480
- () => status(),
481
- {
482
- loading: () => Spinner(),
483
- error: () => ErrorView(),
484
- success: () => Content(),
485
- },
486
- () => html`<div>Unknown</div>`
487
- )}
488
- </div>`;
489
-
490
- // Fragment (no wrapper element)
491
- Fragment([child1, child2, child3]);
492
-
493
- // Portal (render into a different container)
494
- Portal(() => Modal(), document.getElementById("modal-root"));
495
- ```
496
-
497
- ### Dynamic Components
498
-
499
- ```ts
500
- import { html, DynamicComponent, registerComponent, signal } from "sibujs";
501
-
502
- // Register components by name
503
- registerComponent("greeting", () => html`<div>Hello!</div>`);
504
- registerComponent("farewell", () => html`<div>Goodbye!</div>`);
505
-
506
- // Reactively switch between components
507
- const [view, setView] = signal("greeting");
508
- DynamicComponent(() => view()); // renders "greeting" component
509
- setView("farewell"); // swaps to "farewell" component
510
-
511
- // Or pass a component function directly
512
- DynamicComponent(() => view() === "admin" ? AdminPanel : UserPanel);
513
- ```
514
-
515
- ### Error Handling
516
-
517
- ```ts
518
- import { catchError, catchErrorAsync, setGlobalErrorHandler } from "sibujs";
519
-
520
- // Wrap sync functions
521
- const result = catchError(
522
- () => JSON.parse(input),
523
- (err, context) => console.error(`${context} error:`, err),
524
- );
525
-
526
- // Wrap async functions
527
- const data = await catchErrorAsync(
528
- () => fetch("/api/data").then(r => r.json()),
529
- (err) => showError(err),
530
- );
531
-
532
- // Global fallback handler
533
- setGlobalErrorHandler((err, context) => {
534
- reportToSentry(err);
535
- });
536
- ```
537
-
538
- ### Loading Component
539
-
540
- ```ts
541
- import { Loading } from "sibujs";
542
-
543
- Loading(); // default spinner
544
- Loading({ text: "Loading..." }); // with text
545
- Loading({ variant: "dots" }); // dots animation
546
- Loading({ size: "lg", text: "Please wait" }); // large with text
547
- ```
548
-
549
- ### Dynamic Attribute Binding
550
-
551
- ```ts
552
- import { html, bindDynamic, signal } from "sibujs";
553
-
554
- const el = html`<div>Hover me</div>` as HTMLElement;
555
-
556
- // Both attribute name and value can be reactive
557
- const [attr, setAttr] = signal("title");
558
- const [value, setValue] = signal("Tooltip text");
559
- const teardown = bindDynamic(el, () => attr(), () => value());
560
-
561
- setAttr("aria-label"); // old "title" removed, new "aria-label" set
562
- teardown(); // stops tracking and removes the attribute
563
- ```
564
-
565
- ### Lazy Loading and Suspense
566
-
567
- ```ts
568
- import { html, lazy, Suspense } from "sibujs";
569
-
570
- const LazyDashboard = lazy(() => import("./Dashboard"));
571
-
572
- Suspense({
573
- nodes: () => LazyDashboard(),
574
- fallback: () => html`<div>Loading...</div>`,
575
- });
576
- ```
577
-
578
- ### Components and Composition
579
-
580
- ```ts
581
- import { html, getSlot, context, ErrorBoundary } from "sibujs";
582
- import type { Slots } from "sibujs";
583
-
584
- // Slots (named functions)
585
- function Card({ slots }: { slots?: Slots }) {
586
- return html`<div class="card">
587
- <div class="card-header">${getSlot(slots, "header")?.() ?? ""}</div>
588
- <div class="card-body">${getSlot(slots, "default")?.() ?? ""}</div>
589
- </div>`;
590
- }
591
-
592
- Card({
593
- slots: {
594
- header: () => html`<h2>Title</h2>`,
595
- default: () => html`<p>Body content</p>`,
596
- },
597
- });
598
-
599
- // Context (dependency injection)
600
- const ThemeCtx = context("light");
601
- ThemeCtx.provide("dark");
602
- const theme = ThemeCtx.use(); // "dark"
603
-
604
- // Error boundaries
605
- ErrorBoundary({
606
- nodes: RiskyComponent(),
607
- fallback: (err, retry) => html`<div>
608
- <p>Error: ${err.message}</p>
609
- <button on:click=${retry}>Retry</button>
610
- </div>`,
611
- });
612
- ```
613
-
614
- ### Lifecycle
615
-
616
- ```ts
617
- import { html, onMount, onUnmount, dispose } from "sibujs";
618
-
619
- function MyComponent() {
620
- const el = html`<div>Hello</div>`;
621
-
622
- onMount(() => {
623
- console.log("mounted");
624
- return () => console.log("cleanup on unmount");
625
- }, el);
626
-
627
- onUnmount(() => console.log("removed"), el);
628
-
629
- return el;
630
- }
631
-
632
- // Manual disposal of reactive bindings
633
- dispose(someElement); // tears down element + all descendants
634
- ```
635
-
636
- ---
637
-
638
- ## Plugins (`sibu/plugins`)
639
-
640
- ### Router
641
-
642
- Full client-side router with history/hash modes, guards, nested routes, lazy loading, transitions, and SSR support.
643
-
644
- ```ts
645
- import { html, mount } from "sibujs";
646
- import { createRouter, setRoutes, navigate, Route, RouterLink, lazy } from "sibujs/plugins";
647
-
648
- // Define routes
649
- setRoutes([
650
- { path: "/", component: Home },
651
- { path: "/about", component: About },
652
- {
653
- path: "/dashboard",
654
- component: Dashboard,
655
- guard: () => isLoggedIn(),
656
- redirectTo: "/login",
657
- children: [
658
- { path: "settings", component: Settings },
659
- ],
660
- },
661
- {
662
- path: "/admin",
663
- component: lazy(() => import("./Admin")), // code splitting
664
- },
665
- ]);
666
-
667
- // Navigate programmatically
668
- await navigate("/about");
669
- await navigate({ name: "user", params: { id: "42" } });
670
-
671
- // Components
672
- function App() {
673
- return html`<div>
674
- <nav>
675
- ${RouterLink({ to: "/", nodes: "Home" })}
676
- ${RouterLink({ to: "/about", nodes: "About" })}
677
- </nav>
678
- ${Route()}
679
- </div>`;
680
- }
681
-
682
- // Guards
683
- beforeEach(async (to, from) => {
684
- if (to.path === "/admin" && !isAdmin()) return "/login";
685
- return true;
686
- });
687
- ```
688
-
689
- ### Internationalization (i18n)
690
-
691
- Reactive translations with parameter interpolation.
692
-
693
- ```ts
694
- import { setLocale, registerTranslations, t, Trans } from "sibujs/plugins";
695
-
696
- registerTranslations("en", {
697
- greeting: "Hello, {name}!",
698
- items: "You have {count} items",
699
- });
700
-
701
- registerTranslations("es", {
702
- greeting: "Hola, {name}!",
703
- items: "Tienes {count} elementos",
704
- });
705
-
706
- setLocale("en");
707
-
708
- // Imperative
709
- t("greeting", { name: "Mark" }); // "Hello, Mark!"
710
-
711
- // Reactive component (auto-updates on locale change)
712
- Trans("greeting", { name: "Mark" });
713
- ```
714
-
715
- ---
716
-
717
- ## Patterns (`sibujs/patterns`)
718
-
719
- Advanced state management patterns, imported separately to keep the core lean.
720
-
721
- ### State Machines
722
-
723
- ```ts
724
- import { machine } from "sibujs/patterns";
725
-
726
- const { state, send, matches, can } = machine({
727
- initial: "idle",
728
- context: { retries: 0 },
729
- states: {
730
- idle: {
731
- on: { FETCH: "loading" },
732
- },
733
- loading: {
734
- on: {
735
- SUCCESS: "success",
736
- FAILURE: { target: "error", action: (ctx) => ({ ...ctx, retries: ctx.retries + 1 }) },
737
- },
738
- },
739
- success: { on: { RESET: "idle" } },
740
- error: {
741
- on: {
742
- RETRY: { target: "loading", guard: (ctx) => ctx.retries < 3 },
743
- },
744
- },
745
- },
746
- });
747
-
748
- send("FETCH");
749
- matches("loading"); // true
750
- can("RETRY"); // false (not in error state)
751
- ```
752
-
753
- ### Form Validation
754
-
755
- ```ts
756
- import { form, required, email, minLength } from "sibujs/ui";
757
-
758
- const myForm = form({
759
- username: { initial: "", validators: [required(), minLength(3)] },
760
- email: { initial: "", validators: [required(), email()] },
761
- });
762
-
763
- // Reactive getters
764
- myForm.fields.username.value();
765
- myForm.fields.username.error();
766
- myForm.isValid();
767
- myForm.isDirty();
768
-
769
- // Handle submission (pass callback to handleSubmit)
770
- const onSubmit = myForm.handleSubmit((values) => api.register(values));
771
- // Attach to form: on: { submit: onSubmit }
772
- ```
773
-
774
- ### Global Store
775
-
776
- ```ts
777
- import { globalStore } from "sibujs/patterns";
778
-
779
- const store = globalStore({
780
- state: { count: 0, user: null },
781
- actions: {
782
- increment: (state) => ({ ...state, count: state.count + 1 }),
783
- setUser: (state, user) => ({ ...state, user }),
784
- },
785
- middleware: [(state, action, payload, next) => { console.log(action, state); next(); }],
786
- });
787
-
788
- store.dispatch("increment");
789
- const count = store.select((s) => s.count); // reactive selector
790
- count(); // 1
791
- ```
792
-
793
- ### Persistent State
794
-
795
- ```ts
796
- import { persisted } from "sibujs/patterns";
797
-
798
- // Auto-saves to localStorage, restores on page load
799
- const [theme, setTheme] = persisted("app-theme", "light");
800
- setTheme("dark"); // saved to localStorage automatically
801
- ```
802
-
803
- ### Time Travel
804
-
805
- ```ts
806
- import { timeline } from "sibujs/patterns";
807
-
808
- const { value, set, undo, redo, canUndo, canRedo } = timeline("initial");
809
- set("second");
810
- set("third");
811
- undo(); // value() === "second"
812
- redo(); // value() === "third"
813
- ```
814
-
815
- ### Optimistic Updates
816
-
817
- ```ts
818
- import { optimistic } from "sibujs/patterns";
819
-
820
- const [likes, addLike] = optimistic(0);
821
- addLike(likes() + 1, async () => {
822
- return await api.like(postId); // reverts if this throws
823
- });
824
- ```
825
-
826
- ## Data Fetching (`sibujs/data`)
827
-
828
- ```ts
829
- import { query, mutation, infiniteQuery, resource } from "sibujs/data";
830
-
831
- // Query with caching, stale-while-revalidate, and auto-refetch
832
- const { data, loading, error, refetch } = query("users", async ({ signal }) => {
833
- const res = await fetch("/api/users", { signal });
834
- return res.json();
835
- }, {
836
- staleTime: 30_000,
837
- refetchOnWindowFocus: true,
838
- });
839
-
840
- // Cache management
841
- import { invalidateQueries, setQueryData } from "sibujs/data";
842
- invalidateQueries("users");
843
- setQueryData("users", (prev) => [...prev, newUser]);
844
-
845
- // Mutations with optimistic updates
846
- const { mutate, mutateAsync, loading: saving } = mutation(
847
- (user) => fetch("/api/users", { method: "POST", body: JSON.stringify(user) }),
848
- {
849
- onMutate: (variables) => {
850
- const prev = getQueryData("users");
851
- setQueryData("users", (old) => [...old, variables]);
852
- return prev; // context for rollback
853
- },
854
- onError: (_err, _vars, context) => setQueryData("users", context),
855
- onSuccess: () => invalidateQueries("users"),
856
- }
857
- );
858
-
859
- // Infinite / paginated queries
860
- const { pages, fetchNextPage, hasNextPage } = infiniteQuery(
861
- "feed",
862
- ({ pageParam }) => fetch(`/api/feed?page=${pageParam}`).then(r => r.json()),
863
- { getNextPageParam: (lastPage) => lastPage.nextCursor }
864
- );
865
-
866
- // Low-level resource (Solid-style)
867
- const resource = resource(
868
- () => userId(),
869
- (id, { signal }) => fetch(`/api/users/${id}`, { signal }).then(r => r.json())
870
- );
871
- resource.data(); // reactive
872
- ```
873
-
874
- ### Debounce, Throttle, and Previous
875
-
876
- ```ts
877
- import { debounce, throttle, previous } from "sibujs/data";
878
-
879
- // Debounced reactive value (updates after 300ms of inactivity)
880
- const debouncedSearch = debounce(() => searchInput(), 300);
881
-
882
- // Throttled reactive value (updates at most once per 100ms)
883
- const throttledScroll = throttle(() => scrollY(), 100);
884
-
885
- // Track previous value
886
- const prevCount = previous(count);
887
- // prevCount() is the value before the last change
888
- ```
889
-
890
- ## Browser APIs (`sibujs/browser`)
891
-
892
- All browser APIs return reactive getters and a `dispose` function for cleanup.
893
-
894
- ```ts
895
- import {
896
- media,
897
- online,
898
- clipboard,
899
- title,
900
- colorScheme,
901
- draggable,
902
- dropZone,
903
- resize,
904
- scroll,
905
- geo,
906
- battery,
907
- idle,
908
- permissions,
909
- } from "sibujs/browser";
910
-
911
- // Media queries
912
- const { matches: isMobile } = media("(max-width: 768px)");
913
-
914
- // Online/offline status
915
- const { online } = online();
916
-
917
- // Clipboard
918
- const { text, copy, copied } = clipboard();
919
- await copy("Hello!");
920
- copied(); // true (resets after 2s)
921
-
922
- // Reactive document title
923
- const disposeTitle = title(() => `(${unread()}) My App`);
924
-
925
- // Dark/light mode preference
926
- const { scheme } = colorScheme();
927
- scheme(); // "dark" | "light"
928
-
929
- // Drag and drop — accepts ref or getter
930
- const dragRef = ref<HTMLElement>();
931
- const { isDragging } = draggable(dragRef, { type: "card", id: 1 });
932
- const { isOver } = dropZone(dragRef, {
933
- onDrop: (data, event) => handleDrop(data),
934
- });
935
-
936
- // Resize observer — accepts ref or getter
937
- const elRef = ref<HTMLElement>();
938
- const { width, height } = resize(elRef);
939
-
940
- // Scroll position
941
- const { scrollX, scrollY } = scroll();
942
-
943
- // Geolocation
944
- const { latitude, longitude, error } = geo();
945
-
946
- // Battery status
947
- const { level, charging } = battery();
948
-
949
- // Idle detection
950
- const { idle } = idle(60_000); // idle after 60s
951
-
952
- // Permission status
953
- const { state: cameraPermission } = permissions("camera");
954
- ```
955
-
956
- ### Real-Time Communication
957
-
958
- ```ts
959
- import { socket, stream } from "sibujs/data";
960
- import { eventBus } from "sibujs/ui";
961
-
962
- // WebSocket with auto-reconnect and heartbeat
963
- const { data, status, send, close } = socket("wss://api.example.com/ws", {
964
- autoReconnect: true,
965
- reconnectDelay: 1000,
966
- maxReconnects: 5,
967
- heartbeat: { interval: 30_000, message: "ping" },
968
- });
969
- send("hello");
970
- status(); // "connecting" | "open" | "closing" | "closed"
971
-
972
- // Server-Sent Events (SSE)
973
- const { data: sseData, event, status: sseStatus } = stream("/api/events", {
974
- withCredentials: true,
975
- });
976
-
977
- // Typed event bus
978
- const bus = eventBus<{ notify: string; update: { id: number } }>();
979
- const off = bus.on("notify", (msg) => console.log(msg));
980
- bus.emit("notify", "Hello!");
981
- off(); // unsubscribe
982
- ```
983
-
984
- ## UI Utilities (`sibujs/ui`)
985
-
986
- ### Virtual List
987
-
988
- ```ts
989
- import { html, signal } from "sibujs";
990
- import { VirtualList } from "sibujs/ui";
991
-
992
- VirtualList({
993
- items: () => largeArray(),
994
- itemHeight: 40,
995
- containerHeight: 400,
996
- overscan: 5,
997
- renderItem: (item, index) => html`<div>${item.name}</div>`,
998
- });
999
- ```
1000
-
1001
- ## Transitions and Animations (`sibujs/motion`)
1002
-
1003
- ```ts
1004
- import { transition, TransitionGroup, viewTransition } from "sibujs/motion";
1005
-
1006
- // Single element transition
1007
- const { enter, leave } = transition(element, {
1008
- property: "opacity",
1009
- duration: 300,
1010
- easing: "ease-in-out",
1011
- });
1012
- await enter(); // fade in
1013
- await leave(); // fade out
1014
-
1015
- // Group transitions with FLIP animations
1016
- const group = TransitionGroup({
1017
- enter: (el) => el.animate([{ opacity: 0 }, { opacity: 1 }], 300).finished,
1018
- leave: (el) => el.animate([{ opacity: 1 }, { opacity: 0 }], 300).finished,
1019
- });
1020
- group.add(newElement);
1021
- await group.remove(oldElement);
1022
-
1023
- // View Transitions API (with fallback)
1024
- const { start, isTransitioning } = viewTransition(() => {
1025
- setPage("next");
1026
- });
1027
- await start();
1028
- ```
1029
-
1030
- ### Dialogs and Toasts
1031
-
1032
- ```ts
1033
- import { dialog, toast } from "sibujs/ui";
1034
-
1035
- // Dialog state
1036
- const dialog = dialog();
1037
- dialog.open();
1038
- dialog.isOpen(); // true
1039
- dialog.close(); // also closes on Escape
1040
-
1041
- // Toast notifications
1042
- const { toasts, show, dismiss, dismissAll } = toast({
1043
- duration: 5000,
1044
- maxToasts: 3,
1045
- });
1046
- const id = show("Saved successfully!", "success");
1047
- dismiss(id);
1048
- ```
1049
-
1050
- ### Pagination and Infinite Scroll
1051
-
1052
- ```ts
1053
- import { pagination, infiniteScroll } from "sibujs/ui";
1054
-
1055
- // Pagination
1056
- const { page, totalPages, next, prev, goTo, startIndex, endIndex } = pagination({
1057
- totalItems: () => items().length,
1058
- pageSize: 20,
1059
- });
1060
-
1061
- // Infinite scroll with IntersectionObserver
1062
- const { sentinelRef, loading } = infiniteScroll({
1063
- onLoadMore: () => fetchNextPage(),
1064
- hasMore: () => hasNextPage(),
1065
- threshold: 0.5,
1066
- });
1067
- ```
1068
-
1069
- ### Intersection Observer and Lazy Loading
1070
-
1071
- ```ts
1072
- import { intersection, lazyLoad } from "sibujs/ui";
1073
-
1074
- // Track element visibility
1075
- const { isIntersecting, intersectionRatio, observe } = intersection({
1076
- threshold: 0.5,
1077
- });
1078
- observe(myElement);
1079
-
1080
- // Lazy-load content when visible
1081
- const cleanup = lazyLoad(placeholder, () => {
1082
- placeholder.replaceWith(HeavyComponent());
1083
- });
1084
- ```
1085
-
1086
- ### Input Masks
1087
-
1088
- ```ts
1089
- import { inputMask, phoneMask, dateMask, creditCardMask } from "sibujs/ui";
1090
-
1091
- const phone = inputMask(phoneMask()); // (999) 999-9999
1092
- const date = inputMask(dateMask()); // 99/99/9999
1093
- const card = inputMask(creditCardMask()); // 9999 9999 9999 9999
1094
-
1095
- phone.bind(inputElement);
1096
- phone.value(); // formatted: "(555) 123-4567"
1097
- phone.rawValue(); // unformatted: "5551234567"
1098
- ```
1099
-
1100
- ### Accessibility
1101
-
1102
- ```ts
1103
- import { html } from "sibujs";
1104
- import { aria, FocusTrap, hotkey, announce } from "sibujs/ui";
1105
-
1106
- // Reactive ARIA attributes
1107
- aria(element, {
1108
- expanded: () => isOpen(),
1109
- label: "Navigation menu",
1110
- });
1111
-
1112
- // Focus trapping (modals, dialogs)
1113
- FocusTrap(modalContent, { autoFocus: true, restoreFocus: true });
1114
-
1115
- // Keyboard shortcuts
1116
- const cleanup = hotkey("s", (e) => save(), { ctrl: true });
1117
-
1118
- // Screen reader announcements
1119
- announce("Item deleted", "polite");
1120
- ```
1121
-
1122
- ### Scoped Styles
1123
-
1124
- ```ts
1125
- import { html } from "sibujs";
1126
- import { scopedStyle, withScopedStyle } from "sibujs/ui";
1127
-
1128
- // Manual scoping
1129
- const { scope, attr } = scopedStyle(`
1130
- .card { border: 1px solid #ccc; padding: 16px; }
1131
- .card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
1132
- `);
1133
-
1134
- // Auto-scoped component
1135
- const StyledCard = withScopedStyle(`
1136
- .card { background: white; border-radius: 8px; }
1137
- `, (props) => html`<div class="card">${props.content}</div>`);
1138
- ```
1139
-
1140
- ### Higher-Order Components
1141
-
1142
- ```ts
1143
- import { withDefaults, withWrapper, compose } from "sibujs/patterns";
1144
-
1145
- const Button = withDefaults(BaseButton, { variant: "primary", size: "md" });
1146
-
1147
- const LoggedButton = withWrapper(BaseButton, (Component, props) => {
1148
- console.log("rendering button", props);
1149
- return Component(props);
1150
- });
1151
-
1152
- const EnhancedButton = compose(withLogging, withTheme, withTooltip)(BaseButton);
1153
- ```
1154
-
1155
- ### Composables
1156
-
1157
- ```ts
1158
- import { html, signal } from "sibujs";
1159
- import { composable } from "sibujs/ui";
1160
-
1161
- const counterSetup = composable(() => {
1162
- const [count, setCount] = signal(0);
1163
- return { count, increment: () => setCount(c => c + 1) };
1164
- });
1165
-
1166
- // Reuse in any component
1167
- function MyComponent() {
1168
- const { count, increment } = counterSetup();
1169
- return html`<button on:click=${increment}>${() => count()}</button>`;
1170
- }
1171
- ```
1172
-
1173
- ---
1174
-
1175
- ## Widgets (`sibujs/widgets`)
1176
-
1177
- Headless UI primitives -- state logic and keyboard navigation without opinions on markup. Build your own UI on top.
1178
-
1179
- ### Tabs
1180
-
1181
- ```ts
1182
- import { tabs } from "sibujs/widgets";
1183
-
1184
- const tabs = tabs({
1185
- tabs: [
1186
- { id: "general", label: "General" },
1187
- { id: "security", label: "Security" },
1188
- { id: "billing", label: "Billing", disabled: true },
1189
- ],
1190
- defaultTab: "general",
1191
- });
1192
-
1193
- tabs.activeTab(); // "general"
1194
- tabs.setActiveTab("security");
1195
- tabs.nextTab(); // keyboard arrow navigation
1196
- tabs.prevTab();
1197
- tabs.isActive("general"); // reactive check — safe inside each()
1198
- ```
1199
-
1200
- ### Select
1201
-
1202
- ```ts
1203
- import { select } from "sibujs/widgets";
1204
-
1205
- const select = select({
1206
- items: ["Apple", "Banana", "Cherry"],
1207
- multiple: false,
1208
- });
1209
-
1210
- select.open();
1211
- select.highlightNext();
1212
- select.selectHighlighted();
1213
- select.selectedItem(); // "Apple"
1214
- select.isSelected("Apple"); // true
1215
- ```
1216
-
1217
- ### Accordion
1218
-
1219
- ```ts
1220
- import { accordion } from "sibujs/widgets";
1221
-
1222
- const accordion = accordion({
1223
- items: [
1224
- { id: "faq-1", label: "What is Sibu?" },
1225
- { id: "faq-2", label: "How does reactivity work?" },
1226
- ],
1227
- multiple: false,
1228
- });
1229
-
1230
- accordion.toggle("faq-1");
1231
- accordion.items(); // [{ id: "faq-1", label: "...", isExpanded: true }, ...]
1232
- accordion.isExpanded("faq-1"); // reactive check — safe inside each()
1233
- accordion.expandAll();
1234
- accordion.collapseAll();
1235
- ```
1236
-
1237
- ### Combobox
1238
-
1239
- ```ts
1240
- import { combobox } from "sibujs/widgets";
1241
-
1242
- const combo = combobox({
1243
- items: ["New York", "Los Angeles", "Chicago", "Houston"],
1244
- filterFn: (item, query) => item.toLowerCase().includes(query.toLowerCase()),
1245
- });
1246
-
1247
- combo.setQuery("chi");
1248
- combo.filteredItems(); // ["Chicago"]
1249
- combo.selectHighlighted();
1250
- combo.selectedItem(); // "Chicago"
1251
- ```
1252
-
1253
- ### Popover and Tooltip
1254
-
1255
- ```ts
1256
- import { popover, tooltip } from "sibujs/widgets";
1257
-
1258
- const popover = popover();
1259
- popover.toggle();
1260
- popover.isOpen(); // true
1261
-
1262
- const tooltip = tooltip({ delay: 200 });
1263
- tooltip.setContent("More info");
1264
- tooltip.show();
1265
- tooltip.isVisible(); // true (after 200ms delay)
1266
- ```
1267
-
1268
- ### File Upload
1269
-
1270
- ```ts
1271
- import { fileUpload } from "sibujs/widgets";
1272
-
1273
- const upload = fileUpload({
1274
- accept: "image/*",
1275
- multiple: true,
1276
- maxSize: 5 * 1024 * 1024, // 5 MB
1277
- onFiles: (files) => console.log("Selected:", files),
1278
- });
1279
-
1280
- upload.files(); // reactive list of File objects
1281
- upload.errors(); // validation errors (e.g., "File exceeds max size")
1282
- upload.isDragOver(); // true when dragging over drop zone
1283
- upload.clear();
1284
- ```
1285
-
1286
- ### Date Picker
1287
-
1288
- ```ts
1289
- import { datePicker } from "sibujs/widgets";
1290
-
1291
- const picker = datePicker({
1292
- minDate: new Date(2020, 0, 1),
1293
- maxDate: new Date(2030, 11, 31),
1294
- });
1295
-
1296
- picker.nextMonth();
1297
- picker.daysInMonth(); // [{ date, isCurrentMonth, isToday, isSelected, isDisabled }, ...]
1298
- picker.select(new Date(2025, 5, 15));
1299
- picker.selectedDate(); // Date
1300
- picker.isSelected(someDate); // reactive check — safe inside each()
1301
- ```
1302
-
1303
- ### Content Editable
1304
-
1305
- ```ts
1306
- import { contentEditable } from "sibujs/widgets";
1307
-
1308
- const editor = contentEditable();
1309
- editor.setContent("<b>Hello</b> world");
1310
- editor.bold(); // execCommand("bold")
1311
- editor.italic();
1312
- editor.underline();
1313
- editor.content(); // reactive HTML string
1314
- ```
1315
-
1316
- ---
1317
-
1318
- ## Web Components (`sibujs/ui`)
1319
-
1320
- ```ts
1321
- import { html, signal } from "sibujs";
1322
- import { defineElement } from "sibujs/ui";
1323
-
1324
- defineElement("my-counter", (props) => {
1325
- const [count, setCount] = signal(Number(props.initial) || 0);
1326
- return html`<button on:click=${() => setCount(c => c + 1)}>${() => count()}</button>`;
1327
- }, {
1328
- shadow: true,
1329
- observedAttributes: ["initial"],
1330
- });
1331
- ```
1332
-
1333
- ```html
1334
- <my-counter initial="5"></my-counter>
1335
- ```
1336
-
1337
- ---
1338
-
1339
- ## SSR and Static Generation (`sibujs/ssr`)
1340
-
1341
- ### Server-Side Rendering
1342
-
1343
- ```ts
1344
- import {
1345
- renderToString,
1346
- renderToStream,
1347
- renderToDocument,
1348
- hydrate,
1349
- } from "sibujs/ssr";
1350
-
1351
- // Render component to HTML string
1352
- const markup = renderToString(App());
1353
-
1354
- // Full document with head management
1355
- const page = renderToDocument(App, {
1356
- title: "My App",
1357
- meta: [{ name: "description", content: "A Sibu app" }],
1358
- scripts: ["/app.js"],
1359
- });
1360
-
1361
- // Streaming SSR
1362
- const stream = renderToStream(App());
1363
- for await (const chunk of stream) {
1364
- res.write(chunk);
1365
- }
1366
-
1367
- // Client-side hydration
1368
- hydrate(App, document.getElementById("root"));
1369
- ```
1370
-
1371
- ### Islands Architecture
1372
-
1373
- ```ts
1374
- import { island, hydrateIslands, hydrateProgressively } from "sibujs/ssr";
1375
-
1376
- // Server: mark interactive islands
1377
- const header = island("header", () => InteractiveHeader());
1378
-
1379
- // Client: hydrate only interactive parts
1380
- hydrateIslands(document.body, {
1381
- header: () => InteractiveHeader(),
1382
- sidebar: () => InteractiveSidebar(),
1383
- });
1384
-
1385
- // Progressive hydration (hydrates when scrolled into view)
1386
- hydrateProgressively(document.body, islands, { threshold: 0.1 });
1387
- ```
1388
-
1389
- ### Suspense SSR
1390
-
1391
- ```ts
1392
- import { html } from "sibujs";
1393
- import { ssrSuspense, renderToSuspenseStream, suspenseSwapScript } from "sibujs/ssr";
1394
-
1395
- const boundary = ssrSuspense({
1396
- fallback: () => html`<div>Loading...</div>`,
1397
- content: () => fetchAndRender(),
1398
- });
1399
-
1400
- // Stream HTML with out-of-order suspense resolution
1401
- const stream = renderToSuspenseStream(shell, [boundary.promise]);
1402
- ```
1403
-
1404
- ### Static Site Generation
1405
-
1406
- ```ts
1407
- import { generateStaticSite } from "sibujs/ssr";
1408
-
1409
- const result = await generateStaticSite({
1410
- routes: ["/", "/about", "/blog/1", "/blog/2"],
1411
- renderFn: async (path) => renderToDocument(App, { title: path }),
1412
- outDir: "./dist",
1413
- });
1414
-
1415
- result.pages; // [{ path: "/", html: "..." }, ...]
1416
- result.errors; // [{ path: "/blog/2", error: Error }]
1417
- ```
1418
-
1419
- ---
1420
-
1421
- ## Concurrent Rendering (`sibujs/performance`)
1422
-
1423
- ```ts
1424
- import {
1425
- startTransition,
1426
- deferredValue,
1427
- transitionState,
1428
- uniqueId,
1429
- scheduleUpdate,
1430
- yieldToMain,
1431
- processInChunks,
1432
- Priority,
1433
- } from "sibujs/performance";
1434
-
1435
- // Non-blocking state updates
1436
- startTransition(() => {
1437
- setSearchResults(filterLargeList(query()));
1438
- });
1439
-
1440
- // Deferred value (updates at lower priority)
1441
- const deferredQuery = deferredValue(() => query());
1442
-
1443
- // Transition with pending state
1444
- const [isPending, startTransition] = transitionState();
1445
-
1446
- // Unique IDs (SSR-safe)
1447
- const myId = uniqueId(); // "sibu-0"
1448
- const labelId = uniqueId("label"); // "sibu-1-label"
1449
-
1450
- // Priority-based scheduling
1451
- scheduleUpdate(Priority.USER_BLOCKING, () => updateUI());
1452
- scheduleUpdate(Priority.IDLE, () => prefetchData());
1453
-
1454
- // Yield to main thread
1455
- await yieldToMain();
1456
-
1457
- // Process large arrays without blocking
1458
- await processInChunks(bigArray, (item) => processItem(item), 50);
1459
- ```
1460
-
1461
- ---
1462
-
1463
- ## DevTools (`sibujs/devtools`)
1464
-
1465
- ### Debugging and Performance
1466
-
1467
- ```ts
1468
- import {
1469
- enableDebug,
1470
- debugLog,
1471
- perfTracker,
1472
- measureRender,
1473
- getPerformanceReport,
1474
- checkLeaks,
1475
- } from "sibujs/devtools";
1476
-
1477
- enableDebug();
1478
- debugLog("Counter", "increment", { value: 5 });
1479
-
1480
- // Measure component render times
1481
- const MeasuredList = measureRender("ItemList", ItemList);
1482
-
1483
- // Manual performance tracking
1484
- const perf = perfTracker("search");
1485
- perf.startMeasure();
1486
- // ... expensive operation
1487
- perf.endMeasure();
1488
- perf.getAverageTime();
1489
-
1490
- // Get full report
1491
- getPerformanceReport();
1492
- // { "search": { count: 10, average: 4.2, min: 2, max: 8, total: 42 } }
1493
-
1494
- // Check for cleanup leaks
1495
- checkLeaks(); // { "Counter": 2 } -- 2 unclean instances
1496
- ```
1497
-
1498
- ### DevTools Integration
1499
-
1500
- ```ts
1501
- import { initDevTools, devState, getActiveDevTools } from "sibujs/devtools";
1502
-
1503
- const devtools = initDevTools({ maxEvents: 1000 });
1504
-
1505
- // State with automatic change tracking
1506
- const [count, setCount] = devState("counter", 0);
1507
- // Changes are recorded: { type: "state-change", component: "counter", ... }
1508
-
1509
- // Register components
1510
- devtools.registerComponent("App", rootElement, { count: 0 });
1511
-
1512
- // Query events
1513
- devtools.getEvents({ type: "state-change", component: "counter" });
1514
-
1515
- // Snapshot all registered state
1516
- devtools.snapshot();
1517
- ```
1518
-
1519
- ### Hot Module Replacement
1520
-
1521
- ```ts
1522
- import { hmrState, registerHMR, createHMRBoundary } from "sibujs/devtools";
1523
-
1524
- // State that persists across HMR updates
1525
- const [count, setCount] = hmrState("counter", 0);
1526
-
1527
- // Register component for hot replacement
1528
- const { update, dispose } = registerHMR("App", App, container);
1529
-
1530
- // HMR boundary
1531
- const boundary = createHMRBoundary("feature");
1532
- const wrapped = boundary.wrap(() => FeatureComponent());
1533
- boundary.accept(() => console.log("Module updated"));
1534
- ```
1535
-
1536
- ---
1537
-
1538
- ## Ecosystem Adapters (`sibujs/ecosystem`)
1539
-
1540
- ### State Management
1541
-
1542
- ```ts
1543
- import { reduxAdapter, zustandAdapter, mobXAdapter } from "sibujs/ecosystem";
1544
-
1545
- // Use Redux store with Sibu reactivity
1546
- const { useSelector, dispatch } = reduxAdapter(reduxStore);
1547
- const count = useSelector((s) => s.counter.value);
1548
-
1549
- // Use Zustand store
1550
- const { store } = zustandAdapter(zustandStore);
1551
-
1552
- // Use MobX observables
1553
- const { useObservable } = mobXAdapter();
1554
- ```
1555
-
1556
- ### UI Framework Integration
1557
-
1558
- ```ts
1559
- import { componentAdapter, createTheme } from "sibujs/ecosystem";
1560
-
1561
- const adapter = componentAdapter();
1562
- const theme = createTheme({ colors: { primary: "#007bff" } });
1563
- ```
1564
-
1565
- ---
1566
-
1567
- ## Build (`sibujs/build`)
1568
-
1569
- Bundler plugins and deployment utilities.
1570
-
1571
- ```ts
1572
- // Vite
1573
- import { sibuVitePlugin } from "sibujs/build";
1574
- export default { plugins: [sibuVitePlugin()] };
1575
-
1576
- // Webpack
1577
- import { sibuWebpackPlugin } from "sibujs/build";
1578
- module.exports = { plugins: [sibuWebpackPlugin()] };
1579
- ```
1580
-
1581
- Additional build utilities: CDN deployment, type declaration generation, bundle analyzer, linting rules, IDE support, and static analysis tools.
1582
-
1583
- ---
1584
-
1585
- ## Testing (`sibujs/testing`)
1586
-
1587
- Component testing utilities, accessibility testing, E2E helpers, snapshot testing, and visual regression support. Works with Vitest, Jest, and Playwright.
1588
-
1589
- ```ts
1590
- import { render, fireEvent, waitFor } from "sibujs/testing";
1591
-
1592
- const { container, unmount } = render(Counter);
1593
- fireEvent.click(container.querySelector("button"));
1594
- expect(container.textContent).toContain("Count: 1");
1595
- unmount();
1596
- ```
1597
-
1598
- ---
1599
-
1600
- ## Package Structure
1601
-
1602
- Sibu is split into modular entry points. Import only what you use.
1603
-
1604
- ```
1605
- sibujs Core: signal, effect, derived, mount, each, when, html, tags, ErrorBoundary
1606
- sibujs/plugins Router, i18n
1607
- sibujs/data Data fetching: query, mutation, infiniteQuery, socket, stream
1608
- sibujs/browser Browser APIs: media, geo, resize, scroll, online, battery, ...
1609
- sibujs/patterns State patterns: machine, persisted, timeline, optimistic, globalStore
1610
- sibujs/motion Transitions: transition, TransitionGroup, viewTransition, reducedMotion
1611
- sibujs/ui Forms, a11y, dialogs, toasts, virtual lists, composables, web components
1612
- sibujs/widgets Headless UI: tabs, select, accordion, combobox, popover, datePicker, ...
1613
- sibujs/ssr SSR, hydration, islands, static site generation
1614
- sibujs/performance Concurrent rendering, scheduling, DOM recycling, chunk loading
1615
- sibujs/devtools Debugging, profiling, HMR, component introspection
1616
- sibujs/ecosystem Adapters: Redux, MobX, Zustand, Material UI, Chakra, Ant Design
1617
- sibujs/build Vite plugin, Webpack plugin, template compiler, CDN utilities
1618
- sibujs/testing Component testing utilities
1619
- ```
1620
-
1621
- The core has zero dependencies beyond TypeScript. Tree shaking works at the module level -- unused subpaths are not included in your bundle.
1622
-
1623
- ---
1624
-
1625
- ## Development
1626
-
1627
- ```bash
1628
- # Install
1629
- npm install
1630
-
1631
- # Run tests
1632
- npm test
1633
-
1634
- # Type check
1635
- npx tsc --noEmit
1636
-
1637
- # Build
1638
- npm run build
1639
- ```
1640
-
1641
- ---
1642
-
1643
- ## Contributing
1644
-
1645
- Contributions are welcome! Please read our
1646
- [Contributing Guide](.github/CONTRIBUTING.md) and
1647
- [Code of Conduct](.github/CODE_OF_CONDUCT.md)
1648
- before submitting a PR.
1649
-
1650
- ---
1651
-
1652
- ## License
1653
-
1654
- MIT -- (c) 2025-2026 [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({
29
+ nodes: [
30
+ h1({ nodes: () => `Count: ${count()}` }),
31
+ button({
32
+ nodes: "Increment",
33
+ on: { click: () => setCount(c => c + 1) }
34
+ })
35
+ ]
36
+ });
37
+ }
38
+
39
+ mount(Counter, document.getElementById("app"));
40
+ ```
41
+
42
+ ### Three Ways to Author Components
43
+
44
+ SibuJS gives you maximum flexibility with three interoperable styles:
45
+
46
+ #### 1. Tag Factory (Full Props)
47
+ Maximum control with an explicit properties object. Perfect for complex elements.
48
+
49
+ ```javascript
50
+ import { div, h1, button } from "sibujs";
51
+
52
+ const [count, setCount] = signal(0);
53
+
54
+ return div({
55
+ class: "counter",
56
+ nodes: [
57
+ h1({ nodes: () => `Count: ${count()}` }),
58
+ button({ nodes: "Increment", on: { click: () => setCount(c => c + 1) } })
59
+ ]
60
+ });
61
+ ```
62
+
63
+ #### 2. Shorthand API
64
+ Concise and readable for common layouts. Class and children passed as positional arguments.
65
+
66
+ ```javascript
67
+ import { div, h1, button } from "sibujs";
68
+
69
+ return div("counter", [
70
+ h1(() => `Count: ${count()}`),
71
+ button({ nodes: "Increment", on: { click: () => setCount(c => c + 1) } })
72
+ ]);
73
+ ```
74
+
75
+ #### 3. HTML Tagged Template
76
+ Familiar HTML-like syntax using tagged template literals. No compiler needed!
77
+
78
+ ```javascript
79
+ import { html } from "sibujs";
80
+
81
+ return html`
82
+ <div class="counter">
83
+ <h1>Count: ${() => count()}</h1>
84
+ <button on:click=${() => setCount(c => c + 1)}>Increment</button>
85
+ </div>
86
+ `;
87
+ ```
88
+
89
+ ## Learn More
90
+
91
+ For full documentation, guides, and advanced examples, visit our official website:
92
+
93
+ ### 🌐 [sibujs.dev](https://sibujs.dev/)
94
+
95
+ ---
96
+
97
+ ## Features at a Glance
98
+
99
+ - **Reactivity:** `signal`, `effect`, `derived`, `watch`, `batch`.
100
+ - **Components:** Functional, reusable, and lifecycle-aware (`onMount`, `onUnmount`).
101
+ - **Control Flow:** `when` (conditional swaps), `each` (efficient keyed lists), `match` $(pattern matching)$, `show` (toggle visibility).
102
+ - **DOM Utilities:** `Portal` (render out-of-tree), `Fragment` (group children), `Suspense` & `lazy` (async components), `ErrorBoundary`.
103
+ - **State Management:** `store` (simple state containers), `deepSignal` (object proxies), `ref`.
104
+ - **Performance:** Zero VDOM overhead, LIS-based list diffing, and optional template compilation.
105
+ - **Plugins:** Official Router (nested routes, guards), i18n (reactive translations), logic patterns (Finite State Machines).
106
+
107
+ ---
108
+
109
+ ## Ecosystem
110
+
111
+ - [SibuJS UI](https://github.com/hexplus/sibujs-ui) - Component library.
112
+
113
+ ## License
114
+
115
+ MIT © [hexplus](https://github.com/hexplus)