#Hooks
All hooks are included in the @ybouhjira/hyperkit package:
npm install @ybouhjira/hyperkit#Reactive State Hooks
#useBreakpoint
Returns a reactive accessor for the current breakpoint based on window width. Automatically updates on resize.
Signature:
function useBreakpoint(): Accessor<'phone' | 'tablet' | 'desktop' | 'wide' | 'tv'>;Breakpoints:
| Value | Min Width |
|---|---|
phone |
0px |
tablet |
640px |
desktop |
1024px |
wide |
1440px |
tv |
1920px |
Usage:
import { useBreakpoint } from '@ybouhjira/hyperkit';
function Layout() {
const breakpoint = useBreakpoint();
return (
<Show when={breakpoint() !== 'phone'}>
<Sidebar />
</Show>
);
}#useMode
Manages UI mode state with localStorage persistence. Modes control the overall UI density and feature visibility.
Signature:
type Mode = 'developer' | 'focus' | 'tv' | 'distraction-free';
function useMode(initial?: Mode): UseModeReturn;
interface UseModeReturn {
readonly mode: Accessor<Mode>;
readonly setMode: (mode: Mode) => void;
readonly getModeDefinition: (mode: Mode) => ModeDefinition;
}Usage:
import { useMode } from '@ybouhjira/hyperkit';
function App() {
const { mode, setMode } = useMode('developer');
return <button onClick={() => setMode('focus')}>{mode()}</button>;
}#useScrollProgress
Tracks scroll position as a normalized 0–1 signal for scroll-linked animations. Throttled via requestAnimationFrame.
Signature:
interface UseScrollProgressOptions {
target?: HTMLElement | null; // defaults to window
axis?: 'x' | 'y'; // default: 'y'
throttle?: number; // ms between updates, default: 16
}
interface UseScrollProgressReturn {
readonly progress: Accessor<number>; // 0 (top) to 1 (bottom)
readonly scrollPosition: Accessor<number>; // raw px offset
readonly scrollLength: Accessor<number>; // max scrollable px
readonly direction: Accessor<'up' | 'down' | 'none'>;
}
function useScrollProgress(options?: UseScrollProgressOptions): UseScrollProgressReturn;Usage:
import { useScrollProgress } from '@ybouhjira/hyperkit';
function ScrollBar() {
const { progress } = useScrollProgress();
return <div style={{ width: `${progress() * 100}%` }} />;
}#Animation / Physics Hooks
#useSpring
Physics-based spring animation using a damped harmonic oscillator. Driven by requestAnimationFrame, stops automatically when at rest.
Signature:
interface SpringConfig {
stiffness?: number; // default: 170
damping?: number; // default: 26
mass?: number; // default: 1
precision?: number; // convergence threshold, default: 0.01
}
interface UseSpringReturn {
readonly value: Accessor<number>;
readonly set: (target: number) => void;
readonly velocity: Accessor<number>;
readonly isAnimating: Accessor<boolean>;
readonly jump: (value: number) => void; // snap without animation
}
function useSpring(initialValue?: number, config?: SpringConfig): UseSpringReturn;Usage:
import { useSpring } from '@ybouhjira/hyperkit';
function Card() {
const spring = useSpring(0, { stiffness: 200, damping: 20 });
return (
<div
style={{ transform: `translateX(${spring.value()}px)` }}
onMouseEnter={() => spring.set(10)}
onMouseLeave={() => spring.set(0)}
/>
);
}#useMotionValue
Timed animation of a numeric value with configurable duration and CSS easing. Unlike useSpring, this has a deterministic duration suitable for UI transitions.
Signature:
interface MotionValueOptions {
duration?: number; // ms, default: 200
easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out'; // default: 'ease-out'
}
interface UseMotionValueReturn {
readonly value: Accessor<number>;
readonly set: (target: number, options?: MotionValueOptions) => void;
readonly get: () => number; // non-reactive snapshot
readonly jump: (value: number) => void;
readonly isAnimating: Accessor<boolean>;
}
function useMotionValue(
initial?: number,
defaultOptions?: MotionValueOptions
): UseMotionValueReturn;Usage:
import { useMotionValue } from '@ybouhjira/hyperkit';
function FadeIn() {
const opacity = useMotionValue(0, { duration: 300, easing: 'ease-out' });
opacity.set(1);
return <div style={{ opacity: opacity.value() }} />;
}#Gesture & Input Hooks
#useGesture
Detects drag and swipe gestures using the Pointer Events API. Handles mouse, touch, and stylus unified. Velocity is sampled between consecutive pointermove events.
Signature:
interface UseGestureOptions {
drag?: boolean; // enable drag detection (default: true)
swipe?: boolean; // enable swipe detection on drag end (default: true)
swipeThreshold?: number; // min px distance to classify as swipe (default: 50)
swipeVelocity?: number; // min px/ms velocity for swipe (default: 0.3)
onDragStart?: (state: GestureState) => void;
onDrag?: (state: GestureState) => void;
onDragEnd?: (state: GestureState) => void;
onSwipe?: (direction: 'left' | 'right' | 'up' | 'down') => void;
}
interface GestureState {
isDragging: boolean;
offsetX: number;
offsetY: number;
velocityX: number; // px/ms
velocityY: number; // px/ms
distance: number; // Euclidean distance from drag start
swipeDirection: 'left' | 'right' | 'up' | 'down' | null;
}
interface UseGestureReturn {
ref: (el: HTMLElement) => void; // assign to the target element
state: Accessor<GestureState>;
}
function useGesture(options?: UseGestureOptions): UseGestureReturn;Usage:
import { useGesture } from '@ybouhjira/hyperkit';
function Swipeable() {
const gesture = useGesture({
onSwipe: (dir) => console.log('swiped', dir),
swipeThreshold: 80,
});
return <div ref={gesture.ref}>swipe me</div>;
}#useHaptic
Haptic feedback via the Vibration API. Provides preset intensities (light, medium, heavy) and a custom pattern method.
Signature:
interface HapticOptions {
enabled?: boolean; // default: true
}
interface HapticReturn {
light: () => void;
medium: () => void;
heavy: () => void;
custom: (pattern: number | number[]) => void;
supported: boolean;
enabled: () => boolean;
setEnabled: (value: boolean) => void;
}
function useHaptic(options?: HapticOptions): HapticReturn;Usage:
import { useHaptic } from '@ybouhjira/hyperkit';
function SubmitButton() {
const haptic = useHaptic();
return <button onClick={() => haptic.medium()}>Submit</button>;
}#createHaptic
Non-hook version of useHaptic for use outside of component context. Returns the same API minus reactive signals.
Signature:
function createHaptic(options?: HapticOptions): {
light: () => void;
medium: () => void;
heavy: () => void;
custom: (pattern: number | number[]) => void;
supported: boolean;
enabled: boolean; // plain boolean, not a signal
};#Audio & Notification Hooks
#useNotificationSound
Plays a notification tone via the Web Audio API. Only plays when the browser tab is inactive (document.hidden). Returns reactive signals for enabled and volume.
Signature:
interface NotificationSoundOptions {
enabled?: boolean; // default: true
volume?: number; // 0–1, default: 0.5
frequency?: number; // Hz, default: 880 (A5)
duration?: number; // ms, default: 150
}
interface NotificationSoundReturn {
play: () => void;
setEnabled: (enabled: boolean) => void;
setVolume: (volume: number) => void;
enabled: () => boolean;
volume: () => number;
}
function useNotificationSound(options?: NotificationSoundOptions): NotificationSoundReturn;Usage:
import { useNotificationSound } from '@ybouhjira/hyperkit';
function ChatApp() {
const sound = useNotificationSound({ volume: 0.3 });
// Trigger when a new message arrives
return <button onClick={() => sound.play()}>Test sound</button>;
}#createNotificationSound
Non-hook version of useNotificationSound for use outside of component context. Uses plain variables instead of reactive signals.
Signature:
function createNotificationSound(options?: NotificationSoundOptions): NotificationSoundReturn;#Event Bus Hooks
#createEventBus
Creates a typed event bus with wildcard, once, and multi-subscriber support. Framework-agnostic — no SolidJS dependency.
Signature:
interface EventBus<Events extends Record<string, unknown>> {
emit<K extends keyof Events & string>(event: K, payload: Events[K]): void;
on<K extends keyof Events & string>(event: K, handler: (payload: Events[K]) => void): () => void;
once<K extends keyof Events & string>(
event: K,
handler: (payload: Events[K]) => void
): () => void;
onAny(handler: (event: string, payload: unknown) => void): () => void;
off<K extends keyof Events & string>(event: K, handler: (payload: Events[K]) => void): void;
clear(): void;
}
function createEventBus<Events extends Record<string, unknown>>(): EventBus<Events>;Usage:
import { createEventBus } from '@ybouhjira/hyperkit';
type AppEvents = {
'message:received': { id: string; text: string };
'session:ended': void;
};
const bus = createEventBus<AppEvents>();
bus.on('message:received', ({ id, text }) => console.log(id, text));
bus.emit('message:received', { id: '1', text: 'hello' });#useEventBus
SolidJS hook that wraps an EventBus and automatically unsubscribes all listeners on component cleanup.
Signature:
function useEventBus<Events extends Record<string, unknown>>(
bus: EventBus<Events>
): {
emit: EventBus<Events>['emit'];
on: <K extends keyof Events & string>(event: K, handler: (payload: Events[K]) => void) => void;
};Usage:
import { createEventBus, useEventBus } from '@ybouhjira/hyperkit';
const bus = createEventBus<{ ping: string }>();
function Component() {
const { on, emit } = useEventBus(bus);
on('ping', (msg) => console.log('received', msg));
return <button onClick={() => emit('ping', 'hello')}>Ping</button>;
}#Logging Hook
#useLogger
Consumes an Effect Stream<LogEntry> and feeds entries into SolidJS reactive signals. Cleans up the fiber on component unmount.
Signature:
interface UseLoggerOptions {
maxItems?: number; // max entries to keep, default: 500
onEntry?: (entry: LogEntry) => void; // called for each new entry
}
interface UseLoggerResult {
readonly entries: () => ReadonlyArray<LogEntry>;
readonly latest: () => LogEntry | undefined;
readonly active: () => boolean;
readonly stop: () => void;
}
function useLogger(stream: Stream.Stream<LogEntry>, options?: UseLoggerOptions): UseLoggerResult;Usage:
import { useLogger } from '@ybouhjira/hyperkit';
import { LoggingService } from '@ybouhjira/hyperkit';
function LogPanel() {
// Obtain the stream from LoggingService
const log = useLogger(loggingService.stream, { maxItems: 100 });
return (
<ul>
<For each={log.entries()}>
{(entry) => <li>{entry.message}</li>}
</For>
<button onClick={() => log.stop()}>Stop</button>
</ul>
);
}#Media Hook
#useVideoPreview
Extracts thumbnail, duration, and dimensions from a video source URL using a hidden <video> element and Canvas API. Reacts to src changes.
Signature:
interface UseVideoPreviewReturn {
readonly thumbnail: Accessor<string | undefined>; // Object URL (blob:)
readonly duration: Accessor<number>; // seconds
readonly width: Accessor<number>; // pixels
readonly height: Accessor<number>; // pixels
readonly loading: Accessor<boolean>;
readonly error: Accessor<string | undefined>;
}
function useVideoPreview(src: Accessor<string | undefined>): UseVideoPreviewReturn;Usage:
import { useVideoPreview } from '@ybouhjira/hyperkit';
import { createSignal } from 'solid-js';
function VideoCard() {
const [src, setSrc] = createSignal<string | undefined>();
const preview = useVideoPreview(src);
return (
<Show when={preview.thumbnail()}>
<img src={preview.thumbnail()} alt="thumbnail" />
<p>{preview.duration()}s — {preview.width()}×{preview.height()}</p>
</Show>
);
}#LLM Hook
#createLLMUIController
Manages an LLM conversation with tool/action support. Connects an LLMAdapter (e.g. OpenAI, Claude) to a reactive message list and a registry of UI actions the model can invoke.
Signature:
interface LLMUIControllerOptions {
adapter: LLMAdapter; // backend integration
systemPrompt?: string;
}
interface LLMUIControllerReturn {
messages: Accessor<LLMMessage[]>;
isProcessing: Accessor<boolean>;
error: Accessor<string | null>;
sendMessage: (content: string) => Promise<void>;
registerAction: (action: UIAction) => void;
unregisterAction: (name: string) => void;
clearMessages: () => void;
registeredActions: Accessor<UIAction[]>;
}
function createLLMUIController(options: LLMUIControllerOptions): LLMUIControllerReturn;Usage:
import { createLLMUIController } from '@ybouhjira/hyperkit';
function Chat() {
const ctrl = createLLMUIController({ adapter: myAdapter, systemPrompt: 'You are helpful.' });
ctrl.registerAction({
name: 'openFile',
description: 'Open a file in the editor',
parameters: { path: { type: 'string', description: 'File path', required: true } },
handler: ({ path }) => openFile(String(path)),
});
return <ChatWindow messages={ctrl.messages()} onSend={ctrl.sendMessage} />;
}#Effect-SolidJS Bridge Hooks
#createEffectResource
Wraps an Effect program into a SolidJS reactive resource pattern. Automatically runs the effect on mount and provides reactive signals for data, error, and loading states.
Usage:
import { createEffectResource } from '@ybouhjira/hyperkit';
import { Effect } from 'effect';
function UserProfile() {
const user = createEffectResource(
() => Effect.promise(() => fetch('/api/user').then(r => r.json())),
{
onSuccess: (data) => console.log('User loaded:', data),
onError: (err) => console.error('Failed to load user:', err),
}
);
return (
<div>
{user.loading() && <p>Loading...</p>}
{user.error() && <p>Error: {user.error()}</p>}
{user.data() && <h1>Hello, {user.data().name}!</h1>}
<button onClick={() => user.refetch()}>Reload</button>
</div>
);
}API:
interface EffectResourceOptions<A, E> {
onSuccess?: (value: A) => void;
onError?: (error: E) => void;
}
interface EffectResourceResult<A, E> {
readonly data: () => A | undefined;
readonly error: () => E | undefined;
readonly loading: () => boolean;
readonly refetch: () => void;
}
function createEffectResource<A, E>(
effectFn: () => Effect.Effect<A, E>,
options?: EffectResourceOptions<A, E>
): EffectResourceResult<A, E>;#createEffectStream
Subscribes to an Effect Stream and feeds values into SolidJS signals. Useful for real-time data, WebSocket streams, or any continuous data flow.
Usage:
import { createEffectStream } from '@ybouhjira/hyperkit';
import { Stream, Effect } from 'effect';
function LiveFeed() {
const feed = createEffectStream(
Stream.fromAsyncIterable(
new WebSocket('wss://api.example.com/feed'),
(err) => err
),
{
onItem: (msg) => console.log('New message:', msg),
onComplete: () => console.log('Stream ended'),
}
);
return (
<div>
<h2>Live Messages ({feed.items().length})</h2>
{feed.error() && <p>Error: {feed.error()}</p>}
<ul>
<For each={feed.items()}>
{(item) => <li>{item}</li>}
</For>
</ul>
{feed.active() && <p>Connected...</p>}
<button onClick={() => feed.stop()}>Disconnect</button>
</div>
);
}API:
interface EffectStreamOptions<A, E> {
onItem?: (item: A) => void;
onError?: (error: E) => void;
onComplete?: () => void;
}
interface EffectStreamResult<A, E> {
readonly items: () => ReadonlyArray<A>;
readonly latest: () => A | undefined;
readonly error: () => E | undefined;
readonly active: () => boolean;
readonly stop: () => void;
}
function createEffectStream<A, E>(
stream: Stream.Stream<A, E>,
options?: EffectStreamOptions<A, E>
): EffectStreamResult<A, E>;#Features
- Automatic cleanup: Both hooks clean up fibers on component unmount
- Cancellation:
createEffectResourcecancels previous runs when refetching - Type-safe: Full TypeScript support with proper Effect types
- Reactive: Integrates seamlessly with SolidJS reactivity system
- Tested: Comprehensive test coverage with Vitest
#Testing
All hooks include comprehensive tests:
npm test src/hooks/Test coverage includes:
- Success and error cases
- Loading states
- Callbacks (onSuccess, onError, onItem, onComplete)
- Cleanup and cancellation
- Stream stopping
#Architecture
These hooks follow SolidJS conventions:
- Return stable references (no object creation on every render)
- Use signals for reactive state
- Integrate with
onCleanupfor proper disposal - Follow the
createXnaming pattern
The Effect integration:
- Uses
Effect.runForkfor non-blocking execution - Properly handles fiber interruption for cleanup
- Supports the full Effect type system (errors, context, etc.)