Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
95db6ce
Updated CHANGELOG and package.json
hexplus Mar 28, 2026
56080d8
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 28, 2026
7eeec49
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 28, 2026
14a9cd4
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
9487727
ci: use npm install instead of npm ci
hexplus Mar 29, 2026
6b4bd83
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
0b9a0cc
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
0777184
trusted-publisher
hexplus Mar 29, 2026
4d46e82
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
bea9788
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
825a8dc
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
55c4436
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
0d2c7e0
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
8da81e8
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
325ce5d
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
0cad329
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 1, 2026
aea6787
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 4, 2026
00e5e88
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 7, 2026
b10a2c5
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 7, 2026
639eae0
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 9, 2026
2f4de42
feat(sibujs): v1.3.0 — 27 new reactive/DOM primitives, OWASP security…
hexplus Apr 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,23 @@ export * from "./src/browser/dragDrop";
export * from "./src/browser/title";
export * from "./src/browser/colorScheme";
export * from "./src/browser/format";
export * from "./src/browser/visibility";
export * from "./src/browser/network";
export * from "./src/browser/mouse";
export * from "./src/browser/swipe";
export * from "./src/browser/windowSize";
export * from "./src/browser/urlState";
export * from "./src/browser/broadcast";
export * from "./src/browser/fullscreen";
export * from "./src/browser/wakeLock";
export * from "./src/browser/animationFrame";
export * from "./src/browser/mutationObserver";
export * from "./src/browser/bounds";
export * from "./src/browser/keyboard";
export * from "./src/browser/speech";
export * from "./src/browser/gamepad";
export * from "./src/browser/pointerLock";
export * from "./src/browser/vibrate";
export * from "./src/browser/favicon";
export * from "./src/browser/textSelection";
export * from "./src/browser/imageLoader";
1 change: 1 addition & 0 deletions devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from "./src/devtools/debugValue";
export * from "./src/devtools/componentProfiler";
export * from "./src/devtools/devtoolsOverlay";
export * from "./src/devtools/introspect";
export * from "./src/devtools/signalGraph";
23 changes: 23 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@
export * from "./src/core/rendering/html";
export { tagFactory, SVG_NS } from "./src/core/rendering/tagFactory";
export type { TagProps } from "./src/core/rendering/tagFactory";
// Per-element typed prop interfaces for common form/media/link elements
export type {
AnchorProps,
AudioProps,
ButtonProps,
FormProps,
ImgProps,
InputProps,
InputType,
LabelProps,
MediaProps,
OptionProps,
SelectProps,
TextareaProps,
TypedTagFunction,
VideoProps,
} from "./src/core/rendering/tagPropTypes";

// htm — tagged template literal for HTML-like syntax (no compiler)
export { html } from "./src/core/rendering/htm";
Expand All @@ -28,6 +45,7 @@ export * from "./src/core/rendering/directives";
export * from "./src/core/rendering/keepAlive";
export * from "./src/core/rendering/action";
export * from "./src/core/rendering/catch";
export * from "./src/core/rendering/createId";

// Disposal (reactive binding cleanup)
export * from "./src/core/rendering/dispose";
Expand All @@ -44,16 +62,20 @@ export * from "./src/core/signals/memoFn";
export * from "./src/core/signals/array";
export * from "./src/core/signals/deepSignal";
export * from "./src/core/signals/writable";
export * from "./src/core/signals/asyncDerived";

// Lifecycle & context
export * from "./src/core/rendering/lifecycle";
export * from "./src/core/rendering/context";
export * from "./src/core/strict";

// SSR context
export * from "./src/core/ssr-context";

// Reactivity primitives
export * from "./src/reactivity/batch";
export * from "./src/reactivity/nextTick";
export * from "./src/reactivity/concurrent";
export { untracked } from "./src/reactivity/track";
export { bindDynamic } from "./src/reactivity/bindAttribute";

Expand All @@ -63,4 +85,5 @@ export type { SuspenseProps } from "./src/core/rendering/lazy";

// Components
export * from "./src/components/ErrorBoundary";
export * from "./src/components/ErrorDisplay";
export * from "./src/components/Loading";
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sibujs",
"version": "1.2.0",
"version": "1.3.0",
"description": "A lightweight, function-based frontend framework that combines the best of React, Svelte, and Vue — with zero VDOM and maximum simplicity. Designed for developers who want fine-grained reactivity and full control without compilation or magic.",
"keywords": [
"frontend",
Expand Down
91 changes: 91 additions & 0 deletions src/browser/animationFrame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { signal } from "../core/signals/signal";

export interface AnimationFrameOptions {
/** Maximum FPS. Frames that would exceed this are skipped. Default: unlimited. */
fpsLimit?: number;
/** Start immediately. Default: true. */
immediate?: boolean;
}

/**
* animationFrame emits a reactive `delta` (ms since previous frame) and
* `elapsed` (ms since start) tracked via `requestAnimationFrame`. Useful for
* declarative animations, game loops, or real-time visual updates — without
* forcing callers to manage the rAF id manually.
*
* The loop is paused automatically when `pause()` is called and resumed with
* `resume()`. `dispose()` cancels the loop permanently.
*
* @example
* ```ts
* const frame = animationFrame();
* effect(() => {
* const dt = frame.delta();
* setAngle((a) => (a + dt * 0.1) % 360);
* });
* ```
*/
export function animationFrame(options: AnimationFrameOptions = {}): {
delta: () => number;
elapsed: () => number;
running: () => boolean;
pause: () => void;
resume: () => void;
dispose: () => void;
} {
const [delta, setDelta] = signal(0);
const [elapsed, setElapsed] = signal(0);
const [running, setRunning] = signal(false);

if (typeof requestAnimationFrame === "undefined") {
return {
delta,
elapsed,
running,
pause: () => {},
resume: () => {},
dispose: () => {},
};
}

let id: number | null = null;
let prev = -1;
let start = -1;
const minFrameMs = options.fpsLimit ? 1000 / options.fpsLimit : 0;

const step = (now: number) => {
if (start < 0) start = now;
const firstTick = prev < 0;
const dt = firstTick ? 0 : now - prev;
if (firstTick || dt >= minFrameMs) {
setDelta(dt);
setElapsed(now - start);
prev = now;
}
id = requestAnimationFrame(step);
};

function resume() {
if (id !== null) return;
setRunning(true);
id = requestAnimationFrame(step);
}

function pause() {
if (id !== null) {
cancelAnimationFrame(id);
id = null;
}
setRunning(false);
prev = -1;
start = -1;
}

function dispose() {
pause();
}

if (options.immediate !== false) resume();

return { delta, elapsed, running, pause, resume, dispose };
}
94 changes: 94 additions & 0 deletions src/browser/bounds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { signal } from "../core/signals/signal";

export interface BoundsRect {
x: number;
y: number;
width: number;
height: number;
top: number;
left: number;
right: number;
bottom: number;
}

const ZERO: BoundsRect = {
x: 0,
y: 0,
width: 0,
height: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
};

function readRect(el: Element): BoundsRect {
const r = el.getBoundingClientRect();
return {
x: r.x,
y: r.y,
width: r.width,
height: r.height,
top: r.top,
left: r.left,
right: r.right,
bottom: r.bottom,
};
}

/**
* bounds tracks an element's `getBoundingClientRect()` as a reactive signal.
* Updates when the element resizes OR when the window scrolls (so absolute
* top/left stay accurate for overlays, tooltips, and popovers).
*
* Implementation detail: uses a `ResizeObserver` for size changes and a
* passive window `scroll` listener for position changes. Both are torn down
* on `dispose()`.
*
* @example
* ```ts
* const el = div({ class: "anchor" });
* const rect = bounds(el);
* effect(() => {
* const r = rect.rect();
* positionTooltip(r.left, r.bottom);
* });
* ```
*/
export function bounds(target: Element): {
rect: () => BoundsRect;
refresh: () => void;
dispose: () => void;
} {
const [rect, setRect] = signal<BoundsRect>(ZERO);

if (typeof window === "undefined" || !target) {
return {
rect,
refresh: () => {},
dispose: () => {},
};
}

function refresh() {
setRect(readRect(target));
}

refresh();

let resizeObserver: ResizeObserver | null = null;
if (typeof ResizeObserver !== "undefined") {
resizeObserver = new ResizeObserver(refresh);
resizeObserver.observe(target);
}

const onScroll = () => refresh();
window.addEventListener("scroll", onScroll, { passive: true, capture: true });

function dispose() {
resizeObserver?.disconnect();
window.removeEventListener("scroll", onScroll, { capture: true });
}

return { rect, refresh, dispose };
}
52 changes: 52 additions & 0 deletions src/browser/broadcast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { signal } from "../core/signals/signal";

/**
* broadcast wraps the BroadcastChannel API as a reactive signal.
* Unlike the `storage` event (which only fires for localStorage writes and
* sends only the serialized value), a `BroadcastChannel` can send arbitrary
* structured-cloneable payloads between same-origin tabs, iframes, and
* workers — instantly, without touching storage.
*
* Returns the last received message as a reactive signal plus a `post()`
* sender and `dispose()` cleanup. The `post()` call does NOT echo back into
* the local `last()` signal — BroadcastChannel doesn't deliver to its own
* sender, matching the browser's native behavior.
*
* @param channelName Name of the broadcast channel
* @returns `{ last, post, dispose }`
*
* @example
* ```ts
* const chat = broadcast<{ user: string; text: string }>("chat");
* chat.post({ user: "alice", text: "hi" });
* effect(() => {
* const m = chat.last();
* if (m) renderIncoming(m);
* });
* ```
*/
export function broadcast<T = unknown>(
channelName: string,
): { last: () => T | null; post: (message: T) => void; dispose: () => void } {
if (typeof BroadcastChannel === "undefined") {
const [last] = signal<T | null>(null);
return { last, post: () => {}, dispose: () => {} };
}

const [last, setLast] = signal<T | null>(null);
const channel = new BroadcastChannel(channelName);

const handler = (ev: MessageEvent) => setLast(ev.data as T);
channel.addEventListener("message", handler);

function post(message: T): void {
channel.postMessage(message);
}

function dispose(): void {
channel.removeEventListener("message", handler);
channel.close();
}

return { last, post, dispose };
}
46 changes: 46 additions & 0 deletions src/browser/favicon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* favicon sets or updates the page favicon at runtime.
*
* Passes a `url` (to set `href`) or accepts an inline SVG string via
* `data:image/svg+xml` encoding. Useful for notification badges, theme
* switching, dynamic status indicators.
*
* Ensures a `<link rel="icon">` exists — creates one if missing, updates
* the `href` otherwise.
*
* @param url Favicon URL or `data:` URI
*
* @example
* ```ts
* favicon("/icons/default.png");
* // Unread count badge on the favicon
* effect(() => {
* const n = unreadCount();
* favicon(n > 0 ? "/icons/badge.png" : "/icons/default.png");
* });
* ```
*/
export function favicon(url: string): void {
if (typeof document === "undefined") return;
let link = document.querySelector<HTMLLinkElement>("link[rel='icon']");
if (!link) {
link = document.createElement("link");
link.rel = "icon";
document.head.appendChild(link);
}
link.href = url;
}

/**
* Encode an SVG string into a `data:image/svg+xml` URI suitable for use
* with `favicon()`. Handles URL encoding of special characters so inline
* SVG content can be embedded safely.
*
* @example
* ```ts
* favicon(svgFavicon(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5" fill="red"/></svg>`));
* ```
*/
export function svgFavicon(svg: string): string {
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
}
Loading
Loading