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/LICENSE +21 -21
- package/README.md +115 -1654
- package/dist/chunk-3CRQALYP.js +877 -0
- package/dist/chunk-7TQKR4PP.js +294 -0
- package/dist/chunk-DTCOOBMX.js +725 -0
- package/dist/chunk-MEZVEBPN.js +2008 -0
- package/dist/chunk-N6IZB6KJ.js +567 -0
- package/dist/customElement-BKQfbSZQ.d.cts +262 -0
- package/dist/customElement-BKQfbSZQ.d.ts +262 -0
- package/dist/plugins.cjs +49 -27
- package/dist/plugins.d.cts +1 -0
- package/dist/plugins.d.ts +1 -0
- package/dist/plugins.js +49 -27
- package/dist/ssr-WKUPVSSK.js +36 -0
- package/package.json +1 -1
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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)
|