#Composition Patterns
Real-world multi-component patterns showing how to compose HyperKit components into production UIs. Each pattern includes: description, components used, and a complete copy-pasteable SolidJS component.
Rules enforced in every example:
- SolidJS primitives only (
createSignal,Show,For— never React hooks)- All spacing/color via
--sk-*CSS tokens — never rawpxor hex values- All imports from
@ybouhjira/hyperkit- No AI attribution
#1. Page Layout with Header
Standard app page: fixed header bar + scrollable content area. The root fills the viewport height using
Flex direction="column", the header stays pinned at the top, and the Stack below it grows and scrolls.
Components: Flex, Stack, Text, Button
import { JSX } from 'solid-js';
import { Flex, Stack, Text, Button } from '@ybouhjira/hyperkit';
interface AppPageProps {
title: string;
onNew?: () => void;
onSettings?: () => void;
children: JSX.Element;
}
export function AppPage(props: AppPageProps) {
return (
<Flex direction="column" style={{ height: '100vh', overflow: 'hidden' }}>
{/* Fixed header */}
<Flex
align="center"
justify="between"
style={{
padding: 'var(--sk-space-sm) var(--sk-space-md)',
'border-bottom': '1px solid var(--sk-border)',
background: 'var(--sk-bg-secondary)',
'flex-shrink': '0',
}}
>
<Text as="h1" size="lg" weight="semibold" color="primary">
{props.title}
</Text>
<Flex gap="sm" align="center">
<Button variant="ghost" size="sm" onClick={props.onSettings}>
Settings
</Button>
<Button size="sm" onClick={props.onNew}>
New Item
</Button>
</Flex>
</Flex>
{/* Scrollable content */}
<Stack
spacing="md"
style={{
flex: '1',
padding: 'var(--sk-space-lg)',
overflow: 'auto',
}}
>
{props.children}
</Stack>
</Flex>
);
}#2. Metric Dashboard Grid
Responsive grid of KPI cards. Pass columns as a number for equal-width 1fr columns. Use MetricCard
for each metric — it handles icon, trend arrow, and color variant automatically.
Components: Grid, MetricCard
import { MetricCard, Grid } from '@ybouhjira/hyperkit';
interface Metric {
label: string;
value: string | number;
trend?: string;
trendDirection?: 'up' | 'down' | 'neutral';
variant?: 'default' | 'success' | 'warning' | 'danger' | 'info' | 'accent';
}
const METRICS: Metric[] = [
{ label: 'Total Users', value: '12,400', trend: '+8%', trendDirection: 'up', variant: 'success' },
{ label: 'Active Sessions', value: 342, trend: '+3%', trendDirection: 'up', variant: 'accent' },
{ label: 'Error Rate', value: '0.4%', trend: '-0.1%', trendDirection: 'down', variant: 'danger' },
{
label: 'Avg Response',
value: '180ms',
trend: '+12ms',
trendDirection: 'down',
variant: 'warning',
},
];
export function MetricDashboard() {
return (
<Grid columns={4} gap="md" style={{ padding: 'var(--sk-space-lg)' }}>
{METRICS.map((m) => (
<MetricCard
label={m.label}
value={m.value}
trend={m.trend}
trendDirection={m.trendDirection}
variant={m.variant}
/>
))}
</Grid>
);
}#3. Searchable Data Table
Sortable, filterable table with a SearchInput above it. State is pure SolidJS signals — no external store
needed for basic use. The Table generic parameter T keeps everything typed.
Components: Stack, SearchInput, Table, Badge, Text
import { createSignal, createMemo, For } from 'solid-js';
import { Stack, SearchInput, Table, Badge, Text } from '@ybouhjira/hyperkit';
import type { TableColumn } from '@ybouhjira/hyperkit';
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'editor' | 'viewer';
status: 'active' | 'inactive';
}
const USERS: User[] = [
{ id: '1', name: 'Alice Martin', email: 'alice@example.com', role: 'admin', status: 'active' },
{ id: '2', name: 'Bob Chen', email: 'bob@example.com', role: 'editor', status: 'active' },
{ id: '3', name: 'Carol Davis', email: 'carol@example.com', role: 'viewer', status: 'inactive' },
];
const COLUMNS: TableColumn<User>[] = [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'email', header: 'Email', sortable: true },
{ key: 'role', header: 'Role', render: (u) => <Badge>{u.role}</Badge> },
{
key: 'status',
header: 'Status',
render: (u) => (
<Badge variant={u.status === 'active' ? 'success' : 'default'}>{u.status}</Badge>
),
},
];
export function UserTable() {
const [query, setQuery] = createSignal('');
const [sortColumn, setSortColumn] = createSignal<string>('name');
const [sortDir, setSortDir] = createSignal<'asc' | 'desc'>('asc');
const [selected, setSelected] = createSignal<string | null>(null);
const filtered = createMemo(() => {
const q = query().toLowerCase();
return USERS.filter(
(u) => u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)
);
});
const handleSort = (col: string, dir: 'asc' | 'desc') => {
setSortColumn(col);
setSortDir(dir);
};
return (
<Stack gap="md" style={{ padding: 'var(--sk-space-lg)' }}>
<SearchInput
placeholder="Search users…"
value={query()}
onChange={setQuery}
onClear={() => setQuery('')}
shortcut="/"
/>
<Text size="sm" color="secondary">
{filtered().length} result{filtered().length !== 1 ? 's' : ''}
</Text>
<Table<User>
columns={COLUMNS}
data={filtered()}
getRowKey={(u) => u.id}
selectedKey={selected()}
onRowClick={(u) => setSelected(u.id)}
sortColumn={sortColumn()}
sortDirection={sortDir()}
onSort={handleSort}
emptyState="No users match your search."
/>
</Stack>
);
}#4. Form with Validation
Multi-field form with inline error messages. Each field uses a createSignal pair for value + error.
Validation fires on submit — no third-party form library needed for simple cases.
Components: Stack, Input, Select, Switch, Button, Text
import { createSignal } from 'solid-js';
import { Stack, Input, Select, Switch, Button, Text, Flex } from '@ybouhjira/hyperkit';
const ROLE_OPTIONS = [
{ value: 'admin', label: 'Admin' },
{ value: 'editor', label: 'Editor' },
{ value: 'viewer', label: 'Viewer' },
];
export function CreateUserForm(props: { onSubmit: (data: unknown) => void }) {
const [name, setName] = createSignal('');
const [email, setEmail] = createSignal('');
const [role, setRole] = createSignal('viewer');
const [active, setActive] = createSignal(true);
const [nameErr, setNameErr] = createSignal('');
const [emailErr, setEmailErr] = createSignal('');
const [loading, setLoading] = createSignal(false);
const validate = () => {
let valid = true;
if (!name().trim()) {
setNameErr('Name is required');
valid = false;
} else setNameErr('');
if (!email().includes('@')) {
setEmailErr('Enter a valid email');
valid = false;
} else setEmailErr('');
return valid;
};
const handleSubmit = async () => {
if (!validate()) return;
setLoading(true);
await props.onSubmit({ name: name(), email: email(), role: role(), active: active() });
setLoading(false);
};
return (
<Stack
gap="md"
style={{
'max-width': '480px',
padding: 'var(--sk-space-xl)',
background: 'var(--sk-bg-secondary)',
'border-radius': 'var(--sk-radius-lg)',
border: '1px solid var(--sk-border)',
}}
>
<Text as="h2" size="xl" weight="semibold">
Create User
</Text>
<Stack gap="xs">
<Text as="label" size="sm" weight="medium" color="secondary">
Full Name
</Text>
<Input
id="name"
placeholder="Alice Martin"
value={name()}
onInput={setName}
error={nameErr()}
/>
</Stack>
<Stack gap="xs">
<Text as="label" size="sm" weight="medium" color="secondary">
Email Address
</Text>
<Input
id="email"
type="email"
placeholder="alice@example.com"
value={email()}
onInput={setEmail}
error={emailErr()}
/>
</Stack>
<Stack gap="xs">
<Text as="label" size="sm" weight="medium" color="secondary">
Role
</Text>
<Select options={ROLE_OPTIONS} value={role()} onChange={setRole} />
</Stack>
<Switch
label="Active account"
description="User can log in immediately"
checked={active()}
onChange={setActive}
/>
<Flex justify="end" gap="sm" style={{ 'margin-top': 'var(--sk-space-sm)' }}>
<Button variant="ghost">Cancel</Button>
<Button onClick={handleSubmit} loading={loading()}>
Create User
</Button>
</Flex>
</Stack>
);
}#5. Settings Page with Tabs
Multi-section settings page using Tabs. Each tab contains its own settings group. The items array
pattern keeps all tab configuration co-located and type-safe.
Components: Flex, Stack, Tabs, Input, Select, Switch, RangeSlider, Text, Button
import { createSignal } from 'solid-js';
import {
Flex,
Stack,
Tabs,
Input,
Select,
Switch,
RangeSlider,
Text,
Button,
} from '@ybouhjira/hyperkit';
import type { TabItem } from '@ybouhjira/hyperkit';
const THEME_OPTIONS = [
{ value: 'dark', label: 'Dark' },
{ value: 'light', label: 'Light' },
{ value: 'system', label: 'System' },
];
const LANG_OPTIONS = [
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'French' },
{ value: 'ar', label: 'Arabic' },
];
export function SettingsPage() {
const [displayName, setDisplayName] = createSignal('Alice');
const [email, setEmail] = createSignal('alice@example.com');
const [theme, setTheme] = createSignal('dark');
const [language, setLanguage] = createSignal('en');
const [fontSize, setFontSize] = createSignal<[number, number]>([14, 14]);
const [sounds, setSounds] = createSignal(true);
const [notifications, setNotif] = createSignal(true);
const tabs: TabItem[] = [
{
value: 'profile',
label: 'Profile',
content: (
<Stack gap="md">
<Stack gap="xs">
<Text as="label" size="sm" weight="medium" color="secondary">
Display Name
</Text>
<Input value={displayName()} onInput={setDisplayName} placeholder="Your name" />
</Stack>
<Stack gap="xs">
<Text as="label" size="sm" weight="medium" color="secondary">
Email
</Text>
<Input type="email" value={email()} onInput={setEmail} />
</Stack>
<Button>Save Profile</Button>
</Stack>
),
},
{
value: 'appearance',
label: 'Appearance',
content: (
<Stack gap="md">
<Stack gap="xs">
<Text as="label" size="sm" weight="medium" color="secondary">
Theme
</Text>
<Select options={THEME_OPTIONS} value={theme()} onChange={setTheme} />
</Stack>
<Stack gap="xs">
<Text as="label" size="sm" weight="medium" color="secondary">
Language
</Text>
<Select options={LANG_OPTIONS} value={language()} onChange={setLanguage} />
</Stack>
<RangeSlider
label="Font size"
min={10}
max={24}
step={1}
value={fontSize()}
onChange={setFontSize}
/>
</Stack>
),
},
{
value: 'notifications',
label: 'Notifications',
content: (
<Stack gap="md">
<Switch
label="Desktop notifications"
description="Show pop-up alerts for important events"
checked={notifications()}
onChange={setNotif}
/>
<Switch
label="Sound effects"
description="Play audio for incoming messages"
checked={sounds()}
onChange={setSounds}
/>
</Stack>
),
},
];
return (
<Flex
direction="column"
style={{
'max-width': '640px',
margin: '0 auto',
padding: 'var(--sk-space-xl)',
}}
>
<Text as="h1" size="2xl" weight="bold" mb="lg">
Settings
</Text>
<Tabs items={tabs} />
</Flex>
);
}#6. Chat Interface
Full chat layout: message list scrolls from the bottom, MessageInput handles slash commands, file
attachments, and voice. Use createSignal for the message history array.
Components: Flex, Stack, Text, MessageInput, ToastProvider, useToast
import { createSignal, For } from 'solid-js';
import { Flex, Stack, Text, MessageInput, ToastProvider, useToast } from '@ybouhjira/hyperkit';
import type { FileAttachment, SlashCommand } from '@ybouhjira/hyperkit';
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
}
const SLASH_COMMANDS: SlashCommand[] = [
{ id: 'clear', name: 'clear', description: 'Clear the conversation', icon: '🗑️' },
{ id: 'export', name: 'export', description: 'Export chat as markdown', icon: '📄' },
];
function ChatApp() {
const toast = useToast();
const [messages, setMessages] = createSignal<Message[]>([
{ id: '1', role: 'assistant', content: 'Hello! How can I help?' },
]);
const [streaming, setStreaming] = createSignal(false);
const handleSend = async (text: string, attachments?: FileAttachment[]) => {
if (!text.trim()) return;
const userMsg: Message = { id: Date.now().toString(), role: 'user', content: text };
setMessages((prev) => [...prev, userMsg]);
setStreaming(true);
// Simulate async response
await new Promise((r) => setTimeout(r, 800));
const reply: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: `Echo: ${text}${attachments?.length ? ` (+${attachments.length} file(s))` : ''}`,
};
setMessages((prev) => [...prev, reply]);
setStreaming(false);
};
const handleInterrupt = () => {
setStreaming(false);
toast.warning('Generation stopped');
};
return (
<Flex direction="column" style={{ height: '100vh', background: 'var(--sk-bg-primary)' }}>
{/* Header */}
<Flex
align="center"
style={{
padding: 'var(--sk-space-sm) var(--sk-space-md)',
'border-bottom': '1px solid var(--sk-border)',
background: 'var(--sk-bg-secondary)',
}}
>
<Text size="lg" weight="semibold">
Chat
</Text>
</Flex>
{/* Message list */}
<Stack
gap="sm"
style={{
flex: '1',
overflow: 'auto',
padding: 'var(--sk-space-md)',
'flex-direction': 'column-reverse',
}}
>
<For each={[...messages()].reverse()}>
{(msg) => (
<Flex justify={msg.role === 'user' ? 'end' : 'start'} style={{ width: '100%' }}>
<Text
as="p"
size="base"
style={{
'max-width': '72%',
padding: 'var(--sk-space-sm) var(--sk-space-md)',
background: msg.role === 'user' ? 'var(--sk-accent)' : 'var(--sk-bg-secondary)',
'border-radius': 'var(--sk-radius-lg)',
color:
msg.role === 'user' ? 'var(--sk-text-on-accent)' : 'var(--sk-text-primary)',
}}
>
{msg.content}
</Text>
</Flex>
)}
</For>
</Stack>
{/* Input */}
<MessageInput
onSend={handleSend}
onInterrupt={handleInterrupt}
isStreaming={streaming()}
enableAttachments
enableVoice
enableMarkdownToolbar
slashCommands={SLASH_COMMANDS}
placeholder="Message…"
style={{ 'border-top': '1px solid var(--sk-border)' }}
/>
</Flex>
);
}
export function Chat() {
return (
<ToastProvider position="top-right">
<ChatApp />
</ToastProvider>
);
}#7. File Browser with Split Pane
Left pane shows a FileExplorer, right pane shows metadata for the selected file. The two panes are
separated by a CSS flex layout with a fixed left width.
Components: Flex, Stack, FileExplorer, Text, Badge, EmptyState
import { createSignal } from 'solid-js';
import { Show } from 'solid-js';
import { Flex, Stack, FileExplorer, Text, Badge, EmptyState } from '@ybouhjira/hyperkit';
import type { FileItem } from '@ybouhjira/hyperkit';
const DEMO_FILES: FileItem[] = [
{ name: 'src', path: '/src', isDirectory: true },
{
name: 'index.ts',
path: '/src/index.ts',
isDirectory: false,
size: 4096,
modifiedAt: new Date(),
},
{ name: 'README.md', path: '/README.md', isDirectory: false, size: 1024, modifiedAt: new Date() },
];
export function FileBrowser() {
const [currentPath, setCurrentPath] = createSignal('/');
const [selected, setSelected] = createSignal<FileItem | null>(null);
return (
<Flex style={{ height: '100vh', overflow: 'hidden' }}>
{/* Left: file tree */}
<div
style={{
width: '280px',
'flex-shrink': '0',
'border-right': '1px solid var(--sk-border)',
overflow: 'auto',
}}
>
<FileExplorer
items={DEMO_FILES}
currentPath={currentPath()}
onNavigate={setCurrentPath}
onSelect={setSelected}
showToolbar
showStatusBar
/>
</div>
{/* Right: detail pane */}
<Flex
direction="column"
align="center"
justify="center"
style={{ flex: '1', padding: 'var(--sk-space-xl)' }}
>
<Show
when={selected()}
fallback={
<EmptyState
icon="file"
title="No file selected"
description="Click a file in the tree to view its details."
/>
}
>
{(file) => (
<Stack
gap="md"
style={{
'max-width': '480px',
width: '100%',
padding: 'var(--sk-space-xl)',
background: 'var(--sk-bg-secondary)',
'border-radius': 'var(--sk-radius-lg)',
border: '1px solid var(--sk-border)',
}}
>
<Text as="h2" size="xl" weight="semibold">
{file().name}
</Text>
<Flex gap="sm">
<Badge variant={file().isDirectory ? 'info' : 'default'}>
{file().isDirectory ? 'Directory' : 'File'}
</Badge>
</Flex>
<Text size="sm" color="secondary">
Path: {file().path}
</Text>
<Show when={file().size != null}>
<Text size="sm" color="secondary">
Size: {file().size} bytes
</Text>
</Show>
<Show when={file().modifiedAt != null}>
<Text size="sm" color="secondary">
Modified: {file().modifiedAt?.toLocaleDateString()}
</Text>
</Show>
</Stack>
)}
</Show>
</Flex>
</Flex>
);
}#8. Confirmation Dialog
Danger-confirm dialog with a loading state during the async operation. The ConfirmDialog handles the
button layout and danger variant styling — no need to build a custom dialog.
Components: ConfirmDialog, Stack, Text, Button, useToast, ToastProvider
import { createSignal } from 'solid-js';
import { ConfirmDialog, Stack, Text, Button, ToastProvider, useToast } from '@ybouhjira/hyperkit';
function DeleteItemButton(props: { itemName: string; onDelete: () => Promise<void> }) {
const toast = useToast();
const [open, setOpen] = createSignal(false);
const [loading, setLoading] = createSignal(false);
const handleConfirm = async () => {
setLoading(true);
try {
await props.onDelete();
toast.success(`"${props.itemName}" deleted`);
setOpen(false);
} catch {
toast.error('Failed to delete. Please try again.');
} finally {
setLoading(false);
}
};
return (
<>
<Button variant="danger" onClick={() => setOpen(true)}>
Delete {props.itemName}
</Button>
<ConfirmDialog
open={open()}
onClose={() => setOpen(false)}
onConfirm={handleConfirm}
title={`Delete "${props.itemName}"?`}
variant="danger"
confirmLabel="Yes, delete"
cancelLabel="Keep it"
loading={loading()}
>
<Stack gap="sm">
<Text>This action is permanent and cannot be undone.</Text>
<Text size="sm" color="secondary">
All associated data will be removed from the system.
</Text>
</Stack>
</ConfirmDialog>
</>
);
}
export function DeleteDemo() {
return (
<ToastProvider>
<DeleteItemButton
itemName="Project Alpha"
onDelete={() => new Promise((r) => setTimeout(r, 1200))}
/>
</ToastProvider>
);
}#9. Toast Notification Center
ToastProvider wraps the app once. useToast() gives access to success, error, info, and
warning anywhere in the tree. Toasts auto-dismiss (configurable duration) or stay with persistent.
Components: ToastProvider, useToast, Stack, Button, Flex
import { ToastProvider, useToast, Stack, Button, Flex, Text } from '@ybouhjira/hyperkit';
function NotificationDemo() {
const toast = useToast();
return (
<Stack gap="lg" style={{ padding: 'var(--sk-space-xl)' }}>
<Text as="h2" size="xl" weight="semibold">
Toast Examples
</Text>
<Flex gap="sm" wrap="wrap">
<Button variant="primary" onClick={() => toast.success('File saved successfully', 'Saved')}>
Success
</Button>
<Button
variant="danger"
onClick={() => toast.error('Upload failed — check your connection', 'Error')}
>
Error
</Button>
<Button variant="secondary" onClick={() => toast.warning('Storage is 90% full', 'Warning')}>
Warning
</Button>
<Button
variant="ghost"
onClick={() => toast.info('New version available (v2.6.0)', 'Info')}
>
Info
</Button>
<Button
variant="outline"
onClick={() =>
toast.show({
title: 'Persistent',
description: 'This notification stays until dismissed.',
variant: 'info',
persistent: true,
})
}
>
Persistent
</Button>
</Flex>
</Stack>
);
}
export function App() {
return (
<ToastProvider position="top-right">
<NotificationDemo />
</ToastProvider>
);
}#10. Empty State with CTA
Full-page empty state for list views. EmptyState handles the centered layout, icon, title, description,
and optional action — compose it with a Button via the action prop.
Components: EmptyState, Button, Flex
import { createSignal } from 'solid-js';
import { Show } from 'solid-js';
import { Flex, EmptyState, Button } from '@ybouhjira/hyperkit';
interface Props {
onCreate: () => void;
}
export function ProjectListEmpty(props: Props) {
const [loading, setLoading] = createSignal(false);
const handleCreate = async () => {
setLoading(true);
await props.onCreate();
setLoading(false);
};
return (
<Flex align="center" justify="center" style={{ height: '100%', 'min-height': '400px' }}>
<EmptyState
icon="folder"
title="No projects yet"
description="Create your first project to get started. Projects help you organise work across teams."
action={
<Button onClick={handleCreate} loading={loading()}>
Create Project
</Button>
}
/>
</Flex>
);
}#11. Draggable Dashboard
DashboardContainer manages grid positions, drag-to-reorder, and optional localStorage persistence via
storageKey. Each card is a component that receives { config, isEditing } — use isEditing to show
resize handles.
Components: DashboardContainer, MetricCard, Stack, Text, Button, Flex
import { createSignal } from 'solid-js';
import { DashboardContainer, MetricCard, Stack, Text, Button, Flex } from '@ybouhjira/hyperkit';
import type { DashboardCardConfig, DashboardCardProps } from '@ybouhjira/hyperkit';
// Card components must accept DashboardCardProps
const RequestsCard = (_props: DashboardCardProps) => (
<MetricCard
label="Requests / min"
value="4,200"
trend="+12%"
trendDirection="up"
variant="accent"
/>
);
const ErrorRateCard = (_props: DashboardCardProps) => (
<MetricCard
label="Error rate"
value="0.3%"
trend="-0.05%"
trendDirection="down"
variant="success"
/>
);
const LatencyCard = (_props: DashboardCardProps) => (
<MetricCard
label="P99 latency"
value="240ms"
trend="+8ms"
trendDirection="down"
variant="warning"
/>
);
const UptimeCard = (_props: DashboardCardProps) => (
<MetricCard label="Uptime" value="99.97%" variant="success" />
);
const CARDS: DashboardCardConfig[] = [
{
id: 'requests',
title: 'Requests',
component: RequestsCard,
defaultSize: { cols: 3, rows: 2 },
category: 'traffic',
},
{
id: 'errors',
title: 'Errors',
component: ErrorRateCard,
defaultSize: { cols: 3, rows: 2 },
category: 'reliability',
},
{
id: 'latency',
title: 'Latency',
component: LatencyCard,
defaultSize: { cols: 3, rows: 2 },
category: 'performance',
},
{
id: 'uptime',
title: 'Uptime',
component: UptimeCard,
defaultSize: { cols: 3, rows: 2 },
category: 'reliability',
},
];
export function MonitoringDashboard() {
const [editable, setEditable] = createSignal(false);
return (
<Stack gap="md" style={{ padding: 'var(--sk-space-lg)' }}>
<Flex justify="between" align="center">
<Text as="h1" size="2xl" weight="bold">
Monitoring
</Text>
<Button variant={editable() ? 'primary' : 'ghost'} onClick={() => setEditable((v) => !v)}>
{editable() ? 'Done' : 'Customise'}
</Button>
</Flex>
<DashboardContainer
cards={CARDS}
columns={12}
rowHeight="md"
gap="md"
editable={editable()}
storageKey="monitoring-dashboard-v1"
/>
</Stack>
);
}#12. Command Palette
Global keyboard-triggered command palette. Register it once near the app root, listen for Ctrl+K / Cmd+K
with useShortcut, and pass your action list. Categories are automatically grouped.
Components: CommandPalette, useShortcut, KeyboardProvider
import { createSignal } from 'solid-js';
import { CommandPalette, KeyboardProvider, useShortcut } from '@ybouhjira/hyperkit';
import type { CommandAction } from '@ybouhjira/hyperkit';
const ACTIONS: CommandAction[] = [
{
id: 'new-project',
label: 'New Project',
icon: '➕',
category: 'Projects',
shortcut: '⌘N',
handler: () => console.log('new project'),
},
{
id: 'open-search',
label: 'Search Files',
icon: '🔍',
category: 'Navigation',
shortcut: '⌘P',
handler: () => console.log('search'),
},
{
id: 'open-settings',
label: 'Open Settings',
icon: '⚙️',
category: 'Navigation',
shortcut: '⌘,',
handler: () => console.log('settings'),
},
{
id: 'toggle-theme',
label: 'Toggle Theme',
icon: '🌙',
category: 'Appearance',
handler: () => console.log('toggle theme'),
},
];
function AppCommandPalette() {
const [open, setOpen] = createSignal(false);
useShortcut('mod+k', () => setOpen((v) => !v), { description: 'Open command palette' });
return (
<CommandPalette
open={open()}
onOpenChange={setOpen}
actions={ACTIONS}
placeholder="Type a command or search…"
/>
);
}
export function App() {
return (
<KeyboardProvider>
<AppCommandPalette />
{/* rest of app */}
</KeyboardProvider>
);
}#13. IDE-Like Panel Layout
PanelContainer drives a full IDE layout — left sidebar, center editor, right tools, bottom terminal.
Each panel's render function returns its content. The layout persists to localStorage via storageKey.
Components: PanelContainer, MenuBar, StatusBar
import { Flex } from '@ybouhjira/hyperkit';
import { PanelContainer, MenuBar, StatusBar } from '@ybouhjira/hyperkit';
import type { PanelConfig, MenuDefinition, StatusBarItem } from '@ybouhjira/hyperkit';
const PANELS: PanelConfig[] = [
{
id: 'explorer',
title: 'Explorer',
icon: '📁',
defaultPosition: 'left',
defaultSize: 240,
minSize: 160,
collapsible: true,
draggable: true,
render: () => <div style={{ padding: 'var(--sk-space-md)' }}>File tree goes here</div>,
},
{
id: 'editor',
title: 'Editor',
defaultPosition: 'center',
showHeader: false,
render: () => (
<div
style={{
flex: '1',
padding: 'var(--sk-space-md)',
'font-family': 'monospace',
color: 'var(--sk-text-primary)',
}}
>
{'// Start coding…'}
</div>
),
},
{
id: 'terminal',
title: 'Terminal',
icon: '>_',
defaultPosition: 'bottom',
defaultSize: 200,
minSize: 120,
collapsible: true,
draggable: true,
render: () => (
<div
style={{
padding: 'var(--sk-space-sm) var(--sk-space-md)',
'font-family': 'monospace',
'font-size': 'var(--sk-font-size-sm)',
color: 'var(--sk-text-primary)',
}}
>
$ _
</div>
),
},
];
const MENUS: MenuDefinition[] = [
{
id: 'file',
label: 'File',
items: [
{ id: 'new', label: 'New File', shortcut: '⌘N', handler: () => {} },
{ id: 'open', label: 'Open…', shortcut: '⌘O', handler: () => {} },
{ id: 'sep', label: '', type: 'separator' },
{ id: 'save', label: 'Save', shortcut: '⌘S', handler: () => {} },
],
},
{
id: 'view',
label: 'View',
items: [
{ id: 'explorer', label: 'Toggle Explorer', shortcut: '⌘B', handler: () => {} },
{ id: 'terminal', label: 'Toggle Terminal', shortcut: '⌘`', handler: () => {} },
],
},
];
const STATUS_ITEMS: StatusBarItem[] = [
{ id: 'branch', text: 'main', icon: '⎇', align: 'left', priority: 1 },
{ id: 'lang', text: 'TypeScript', align: 'right', priority: 1 },
{ id: 'line', text: 'Ln 1, Col 1', align: 'right', priority: 2 },
];
export function IDE() {
return (
<Flex direction="column" style={{ height: '100vh', overflow: 'hidden' }}>
<MenuBar menus={MENUS} />
<PanelContainer
panels={PANELS}
storageKey="ide-layout-v1"
style={{ flex: '1', overflow: 'hidden' }}
/>
<StatusBar items={STATUS_ITEMS} />
</Flex>
);
}#14. Tabbed File Editor (TabBar)
TabBar provides an IDE-style tab strip with close buttons, dirty indicators, and keyboard navigation.
Pair it with createSignal to manage the active tab and the open file list.
Components: Flex, TabBar, Stack, Text
import { createSignal } from 'solid-js';
import { Show } from 'solid-js';
import { Flex, TabBar, Stack, Text } from '@ybouhjira/hyperkit';
import type { TabBarTab } from '@ybouhjira/hyperkit';
const INITIAL_TABS: TabBarTab[] = [
{ id: 'index', label: 'index.ts', icon: 'ts', dirty: false },
{ id: 'app', label: 'App.tsx', icon: 'tsx', dirty: true },
{ id: 'styles', label: 'styles.css', icon: 'css', dirty: false },
];
export function TabbedEditor() {
const [tabs, setTabs] = createSignal<TabBarTab[]>(INITIAL_TABS);
const [active, setActive] = createSignal('index');
const closeTab = (id: string) => {
setTabs((prev) => prev.filter((t) => t.id !== id));
if (active() === id) {
const remaining = tabs().filter((t) => t.id !== id);
setActive(remaining[remaining.length - 1]?.id ?? '');
}
};
const addTab = () => {
const id = `new-${Date.now()}`;
setTabs((prev) => [...prev, { id, label: 'untitled.ts', icon: 'ts', dirty: true }]);
setActive(id);
};
const activeTab = () => tabs().find((t) => t.id === active());
return (
<Flex direction="column" style={{ height: '100%' }}>
<TabBar
tabs={tabs()}
activeId={active()}
onSelect={setActive}
onClose={closeTab}
onAdd={addTab}
/>
<Flex
align="center"
justify="center"
style={{ flex: '1', background: 'var(--sk-bg-primary)' }}
>
<Show when={activeTab()} fallback={<Text color="muted">No file open</Text>}>
{(tab) => (
<Stack gap="sm" align="center">
<Text size="xl" weight="semibold" color="secondary">
{tab().label}
</Text>
<Text size="sm" color="muted">
{tab().dirty ? 'Unsaved changes' : 'No changes'}
</Text>
</Stack>
)}
</Show>
</Flex>
</Flex>
);
}#15. Skeleton Loading Pattern
Show skeleton placeholders while data loads. Use Show to swap between skeleton and real content based on
a loading signal. Each variant (rect, circle, text) maps to a common UI element.
Components: Show, Stack, Card, Flex, Skeleton, Text
import { createSignal, onMount } from 'solid-js';
import { Show } from 'solid-js';
import { Stack, Card, Flex, Skeleton, Text, Badge } from '@ybouhjira/hyperkit';
interface Post {
id: string;
title: string;
author: string;
preview: string;
tag: string;
}
function PostCardSkeleton() {
return (
<Card padding="md">
<Flex gap="sm" align="center" style={{ 'margin-bottom': 'var(--sk-space-sm)' }}>
<Skeleton variant="circle" size={36} />
<Stack gap="xs" style={{ flex: '1' }}>
<Skeleton variant="rect" width="40%" height={14} />
<Skeleton variant="rect" width="25%" height={12} />
</Stack>
</Flex>
<Skeleton variant="rect" height={20} style={{ 'margin-bottom': 'var(--sk-space-xs)' }} />
<Skeleton variant="text" lines={3} />
</Card>
);
}
function PostCard(props: { post: Post }) {
return (
<Card padding="md" hoverable>
<Flex gap="sm" align="center" style={{ 'margin-bottom': 'var(--sk-space-sm)' }}>
<div
style={{
width: '36px',
height: '36px',
'border-radius': '50%',
background: 'var(--sk-accent)',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
}}
>
<Text size="sm" color="primary" weight="bold">
{props.post.author[0]}
</Text>
</div>
<Stack gap="xs" style={{ flex: '1' }}>
<Text size="sm" weight="semibold">
{props.post.author}
</Text>
<Badge>{props.post.tag}</Badge>
</Stack>
</Flex>
<Text as="h3" size="lg" weight="semibold" mb="xs">
{props.post.title}
</Text>
<Text size="sm" color="secondary">
{props.post.preview}
</Text>
</Card>
);
}
const DEMO_POSTS: Post[] = [
{
id: '1',
title: 'Getting started with HyperKit',
author: 'Alice',
preview: 'HyperKit provides a rich set of composable UI primitives…',
tag: 'Tutorial',
},
{
id: '2',
title: 'Building an IDE layout',
author: 'Bob',
preview: 'PanelContainer makes multi-pane layouts effortless…',
tag: 'Guide',
},
];
export function PostList() {
const [loading, setLoading] = createSignal(true);
const [posts, setPosts] = createSignal<Post[]>([]);
onMount(async () => {
await new Promise((r) => setTimeout(r, 1500));
setPosts(DEMO_POSTS);
setLoading(false);
});
return (
<Stack
gap="md"
style={{ 'max-width': '560px', margin: '0 auto', padding: 'var(--sk-space-xl)' }}
>
<Text as="h1" size="2xl" weight="bold">
Latest Posts
</Text>
<Show
when={!loading()}
fallback={
<Stack gap="md">
<PostCardSkeleton />
<PostCardSkeleton />
<PostCardSkeleton />
</Stack>
}
>
<Stack gap="md">
{posts().map((p) => (
<PostCard post={p} />
))}
</Stack>
</Show>
</Stack>
);
}#16. Context Menu on Right-Click
Wrap any element in SKContextMenu. The items array drives the menu — use type: "separator" and
type: "label" for visual grouping, variant: "danger" for destructive actions.
Components: SKContextMenu, Card, Text, Stack
import { createSignal } from 'solid-js';
import { SKContextMenu, Card, Text, Stack, Badge } from '@ybouhjira/hyperkit';
import type { ContextMenuItem } from '@ybouhjira/hyperkit';
export function FileCard(props: { name: string; path: string }) {
const [copied, setCopied] = createSignal(false);
const copyPath = () => {
void navigator.clipboard.writeText(props.path);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
const menuItems: ContextMenuItem[] = [
{ label: 'Open', icon: 'folder-open', onClick: () => console.log('open') },
{ label: 'Open in Tab', icon: 'plus', onClick: () => console.log('new tab') },
{ type: 'separator' },
{ label: 'Copy Path', icon: 'copy', onClick: copyPath },
{ label: 'Rename…', icon: 'edit', onClick: () => console.log('rename') },
{ type: 'separator' },
{ label: 'Delete', icon: 'trash', onClick: () => console.log('delete'), variant: 'danger' },
];
return (
<SKContextMenu items={menuItems}>
<Card padding="md" hoverable>
<Stack gap="xs">
<Text weight="medium">{props.name}</Text>
<Text size="sm" color="muted">
{props.path}
</Text>
<Show when={copied()}>
<Badge variant="success">Path copied!</Badge>
</Show>
</Stack>
</Card>
</SKContextMenu>
);
}
// Suppress TS import — Show is from solid-js
import { Show } from 'solid-js';#17. Responsive Navigation with Sidebar
Sidebar collapses to a toggle button on narrow viewports. Use useBreakpoint to detect phone width and
switch to a compact nav automatically.
Components: Flex, Sidebar, Stack, Text, Button, useBreakpoint
import { createSignal } from 'solid-js';
import { Show } from 'solid-js';
import { Flex, Sidebar, Stack, Text, Button, useBreakpoint } from '@ybouhjira/hyperkit';
const NAV_ITEMS = [
{ id: 'dashboard', label: 'Dashboard', icon: '📊' },
{ id: 'projects', label: 'Projects', icon: '📁' },
{ id: 'settings', label: 'Settings', icon: '⚙️' },
];
export function ResponsiveLayout() {
const bp = useBreakpoint();
const [sidebarOpen, setSidebarOpen] = createSignal(true);
const [active, setActive] = createSignal('dashboard');
const isMobile = () => bp() === 'phone' || bp() === 'tablet';
return (
<Flex style={{ height: '100vh' }}>
<Sidebar
open={isMobile() ? sidebarOpen() : true}
onToggle={isMobile() ? () => setSidebarOpen((v) => !v) : undefined}
width="220px"
header={
<Flex align="center" style={{ padding: 'var(--sk-space-md)' }}>
<Text size="lg" weight="bold">
MyApp
</Text>
</Flex>
}
>
<Stack gap="xs" style={{ padding: 'var(--sk-space-sm)' }}>
{NAV_ITEMS.map((item) => (
<Button
variant={active() === item.id ? 'secondary' : 'ghost'}
fullWidth
onClick={() => {
setActive(item.id);
if (isMobile()) setSidebarOpen(false);
}}
style={{ 'justify-content': 'flex-start', gap: 'var(--sk-space-sm)' }}
>
{item.icon} {item.label}
</Button>
))}
</Stack>
</Sidebar>
{/* Main content */}
<Flex
direction="column"
style={{ flex: '1', overflow: 'auto', padding: 'var(--sk-space-lg)' }}
>
<Show when={isMobile()}>
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarOpen((v) => !v)}
style={{ 'align-self': 'flex-start', 'margin-bottom': 'var(--sk-space-md)' }}
>
☰ Menu
</Button>
</Show>
<Text as="h1" size="2xl" weight="bold">
{NAV_ITEMS.find((n) => n.id === active())?.label}
</Text>
</Flex>
</Flex>
);
}#18. Theme Configurator with Live Preview
Live color/font settings panel. ColorInput and RangeSlider emit changes instantly. Wrap with
ThemeProvider to apply them — the whole subtree re-renders with the new theme variables.
Components: ThemeProvider, Stack, Flex, ColorInput, RangeSlider, Select, Text, Card, Button
import { createSignal } from 'solid-js';
import { ThemeProvider, Stack, Flex, ColorInput, Text, Card, Button } from '@ybouhjira/hyperkit';
import type { ThemeConfig } from '@ybouhjira/hyperkit';
const DEFAULT_THEME: ThemeConfig = {
colors: {
'bg-primary': '#0d0d0d',
'bg-secondary': '#1a1a1a',
accent: '#6366f1',
'text-primary': '#f5f5f5',
'text-secondary': '#a3a3a3',
border: '#2a2a2a',
},
fonts: { sans: 'Inter, sans-serif' },
radius: { base: '8px' },
};
export function ThemeConfigurator() {
const [accentColor, setAccentColor] = createSignal(DEFAULT_THEME.colors?.['accent'] as string);
const [bgColor, setBgColor] = createSignal(DEFAULT_THEME.colors?.['bg-primary'] as string);
const [textColor, setTextColor] = createSignal(DEFAULT_THEME.colors?.['text-primary'] as string);
const liveTheme = (): ThemeConfig => ({
...DEFAULT_THEME,
colors: {
...DEFAULT_THEME.colors,
accent: accentColor(),
'bg-primary': bgColor(),
'text-primary': textColor(),
},
});
return (
<Flex style={{ height: '100vh' }}>
{/* Config panel */}
<Stack
gap="lg"
style={{
width: '320px',
'flex-shrink': '0',
padding: 'var(--sk-space-xl)',
'border-right': '1px solid var(--sk-border)',
overflow: 'auto',
}}
>
<Text as="h2" size="xl" weight="bold">
Theme Editor
</Text>
<ColorInput
label="Accent color"
value={accentColor()}
onChange={setAccentColor}
format="hex"
presets={['#6366f1', '#ec4899', '#f97316', '#22c55e', '#06b6d4']}
/>
<ColorInput label="Background" value={bgColor()} onChange={setBgColor} format="hex" />
<ColorInput label="Text color" value={textColor()} onChange={setTextColor} format="hex" />
<Button
onClick={() => {
setAccentColor(DEFAULT_THEME.colors?.['accent'] as string);
setBgColor(DEFAULT_THEME.colors?.['bg-primary'] as string);
setTextColor(DEFAULT_THEME.colors?.['text-primary'] as string);
}}
variant="ghost"
fullWidth
>
Reset to defaults
</Button>
</Stack>
{/* Live preview */}
<ThemeProvider theme={liveTheme()} style={{ flex: '1', overflow: 'auto' }}>
<Flex
align="center"
justify="center"
style={{
height: '100%',
background: 'var(--sk-bg-primary)',
padding: 'var(--sk-space-xl)',
}}
>
<Card padding="lg" style={{ 'max-width': '400px', width: '100%' }}>
<Stack gap="md">
<Text as="h3" size="xl" weight="bold">
Preview Card
</Text>
<Text color="secondary">
This card reflects your theme changes live. Accent, background, and text colors
update instantly.
</Text>
<Flex gap="sm">
<Button>Primary Action</Button>
<Button variant="ghost">Cancel</Button>
</Flex>
</Stack>
</Card>
</Flex>
</ThemeProvider>
</Flex>
);
}#19. Progress Wizard / Multi-Step Flow
Multi-step form with ProgressBar showing completion. Each step is a separate component. The wizard
tracks the current step index and navigates forward/back.
Components: Stack, Flex, ProgressBar, Text, Button, Input, Badge
import { createSignal } from 'solid-js';
import { Show, Switch, Match } from 'solid-js';
import { Stack, Flex, ProgressBar, Text, Button, Input, Badge } from '@ybouhjira/hyperkit';
type StepId = 'account' | 'profile' | 'confirm';
const STEPS: { id: StepId; label: string }[] = [
{ id: 'account', label: 'Account' },
{ id: 'profile', label: 'Profile' },
{ id: 'confirm', label: 'Confirm' },
];
export function OnboardingWizard() {
const [stepIndex, setStepIndex] = createSignal(0);
const [email, setEmail] = createSignal('');
const [password, setPassword] = createSignal('');
const [username, setUsername] = createSignal('');
const [done, setDone] = createSignal(false);
const progress = () => ((stepIndex() + 1) / STEPS.length) * 100;
const currentStep = () => STEPS[stepIndex()];
const next = () => setStepIndex((i) => Math.min(i + 1, STEPS.length - 1));
const back = () => setStepIndex((i) => Math.max(i - 1, 0));
return (
<Flex
align="center"
justify="center"
style={{ height: '100vh', background: 'var(--sk-bg-primary)' }}
>
<Stack
gap="xl"
style={{
width: '480px',
padding: 'var(--sk-space-xl)',
background: 'var(--sk-bg-secondary)',
'border-radius': 'var(--sk-radius-xl)',
border: '1px solid var(--sk-border)',
}}
>
<Show when={!done()}>
{/* Step indicators */}
<Flex gap="sm" align="center">
{STEPS.map((s, i) => (
<Badge
variant={i <= stepIndex() ? 'info' : 'default'}
style={{ 'font-size': 'var(--sk-font-size-xs)' }}
>
{s.label}
</Badge>
))}
</Flex>
<ProgressBar value={progress()} size="sm" />
<Text as="h2" size="2xl" weight="bold">
{currentStep()?.label}
</Text>
{/* Step content */}
<Switch>
<Match when={stepIndex() === 0}>
<Stack gap="md">
<Input
id="email"
type="email"
placeholder="Email"
value={email()}
onInput={setEmail}
/>
<Input
id="password"
type="password"
placeholder="Password"
value={password()}
onInput={setPassword}
/>
</Stack>
</Match>
<Match when={stepIndex() === 1}>
<Stack gap="md">
<Input
id="username"
placeholder="Username"
value={username()}
onInput={setUsername}
/>
</Stack>
</Match>
<Match when={stepIndex() === 2}>
<Stack gap="sm">
<Text size="sm" color="secondary">
Email: {email()}
</Text>
<Text size="sm" color="secondary">
Username: {username()}
</Text>
</Stack>
</Match>
</Switch>
{/* Navigation */}
<Flex justify="between">
<Button variant="ghost" disabled={stepIndex() === 0} onClick={back}>
Back
</Button>
<Button
onClick={() => {
if (stepIndex() === STEPS.length - 1) {
setDone(true);
} else {
next();
}
}}
>
{stepIndex() === STEPS.length - 1 ? 'Finish' : 'Continue'}
</Button>
</Flex>
</Show>
<Show when={done()}>
<Stack gap="md" align="center">
<Text size="3xl">🎉</Text>
<Text as="h2" size="2xl" weight="bold">
All set!
</Text>
<Text color="secondary">Your account has been created successfully.</Text>
</Stack>
</Show>
</Stack>
</Flex>
);
}#20. Card with Collapsible Section
Collapsible wraps expandable card sections. Combine it with Card, Flex, and a chevron Button for
a clean accordion-style UI — useful for long settings pages or FAQ sections.
Components: Card, Flex, Stack, Text, Button, Collapsible
import { createSignal, For } from 'solid-js';
import { Card, Flex, Stack, Text, Button, Collapsible } from '@ybouhjira/hyperkit';
interface Section {
id: string;
title: string;
body: string;
}
const FAQ_SECTIONS: Section[] = [
{
id: 'billing',
title: 'How does billing work?',
body: 'You are charged monthly. Cancel anytime from the settings page.',
},
{
id: 'data',
title: 'Where is my data stored?',
body: 'All data is encrypted at rest and in transit. Servers are hosted in the EU.',
},
{
id: 'support',
title: 'How do I contact support?',
body: 'Use the chat widget in the bottom-right corner, or email support@example.com.',
},
];
function FAQItem(props: { section: Section }) {
const [open, setOpen] = createSignal(false);
return (
<Card padding="md">
<Flex
align="center"
justify="between"
style={{ cursor: 'pointer' }}
onClick={() => setOpen((v) => !v)}
>
<Text weight="medium">{props.section.title}</Text>
<Text
color="muted"
style={{ transition: 'transform 0.2s', transform: open() ? 'rotate(180deg)' : 'none' }}
>
▾
</Text>
</Flex>
<Collapsible open={open()}>
<Text color="secondary" size="sm" style={{ 'padding-top': 'var(--sk-space-sm)' }}>
{props.section.body}
</Text>
</Collapsible>
</Card>
);
}
export function FAQ() {
return (
<Stack
gap="sm"
style={{ 'max-width': '600px', margin: '0 auto', padding: 'var(--sk-space-xl)' }}
>
<Text as="h1" size="2xl" weight="bold" mb="md">
Frequently Asked Questions
</Text>
<For each={FAQ_SECTIONS}>{(section) => <FAQItem section={section} />}</For>
</Stack>
);
}#21. Logging & Observability
Structured, Effect-native logging with in-memory history, redaction, transports, and reactive SolidJS
consumption. All logs flow through Effect's Logger layer — Effect.log() / Effect.logError() etc.
are captured automatically with no extra instrumentation.
Components: Stack, Flex, Text, Badge, Card, For, Show, ScrollArea
Hook: useLogger
Service: makeLoggingLayer, LoggingService
#Basic setup
import { makeLoggingLayer } from '@ybouhjira/hyperkit';
import { Effect } from 'effect';
// Minimal: in-memory capture only (default 500 entries)
const LogLayer = makeLoggingLayer();
// Usage — any Effect.log* call is captured automatically
const program = Effect.gen(function* () {
yield* Effect.log('Server started');
yield* Effect.logInfo('Listening on port 3000');
yield* Effect.logWarning('High memory usage detected');
yield* Effect.logError('Database connection failed');
});
Effect.runPromise(program.pipe(Effect.provide(LogLayer)));#Enterprise setup with transports
import { makeLoggingLayer, ConsoleTransport, HttpTransport } from '@ybouhjira/hyperkit';
import { LogLevel } from 'effect';
const LogLayer = makeLoggingLayer({
// Merged into every log entry as annotations
context: { service: 'my-app', version: '1.0.0', env: 'production' },
// Annotation keys to mask — case-insensitive, value replaced with [REDACTED]
redact: ['password', 'token', 'authorization', 'apiKey'],
// Max entries kept in memory (accessible via LoggingService.getHistory)
maxHistory: 1000,
// Only send 20% of sessions to external transports (reduces noise/cost)
sampling: { rate: 0.2 },
transports: [
// Pretty-print warnings+ to the browser/node console
ConsoleTransport({ format: 'pretty', minLevel: LogLevel.Warning }),
// Batch-POST all logs to your backend every 5 seconds
HttpTransport({ url: '/api/logs', batchWindow: '5 seconds' }),
],
});#Consuming logs in SolidJS UI
import { createSignal, For, Show } from 'solid-js';
import { Stack, Flex, Text, Badge, Card, ScrollArea, useLogger } from '@ybouhjira/hyperkit';
import { LoggingService, LogEntry } from '@ybouhjira/hyperkit';
import { Effect } from 'effect';
// Resolve the service from the Effect runtime and pass its stream to useLogger
function LogViewer() {
// Obtain stream from LoggingService — wire this up from your Effect runtime
const loggingService = getLoggingService(); // your app's runtime accessor
const { entries, latest, active, stop } = useLogger(loggingService.stream, {
maxEntries: 200,
});
const levelVariant = (level: string) => {
switch (level.toLowerCase()) {
case 'error':
return 'danger';
case 'warning':
return 'warning';
case 'info':
return 'info';
default:
return 'default';
}
};
return (
<Stack
spacing="sm"
style={{
height: '100%',
padding: 'var(--sk-space-md)',
background: 'var(--sk-bg-primary)',
}}
>
{/* Header */}
<Flex align="center" justify="between">
<Text size="lg" weight="semibold" color="primary">
Log Viewer
</Text>
<Flex gap="sm" align="center">
<Badge variant={active() ? 'success' : 'default'}>{active() ? 'Live' : 'Stopped'}</Badge>
<Text size="sm" color="muted">
{entries().length} entries
</Text>
</Flex>
</Flex>
{/* Latest entry summary */}
<Show when={latest()}>
{(entry) => (
<Card
style={{
padding: 'var(--sk-space-sm) var(--sk-space-md)',
background: 'var(--sk-bg-secondary)',
}}
>
<Text size="sm" color="secondary">
Latest: {entry().message}
</Text>
</Card>
)}
</Show>
{/* Scrollable log list */}
<ScrollArea style={{ flex: '1' }}>
<Stack spacing="xs">
<For each={entries()}>
{(entry: LogEntry) => (
<Flex align="start" gap="sm" style={{ padding: 'var(--sk-space-xs) 0' }}>
<Badge
variant={levelVariant(entry.level._tag)}
size="sm"
style={{ 'flex-shrink': '0', 'min-width': '60px' }}
>
{entry.level._tag}
</Badge>
<Text size="sm" color="muted" style={{ 'flex-shrink': '0', 'min-width': '80px' }}>
{new Date(entry.timestamp).toLocaleTimeString()}
</Text>
<Text size="sm" color="primary" style={{ flex: '1' }}>
{entry.message}
</Text>
</Flex>
)}
</For>
</Stack>
</ScrollArea>
</Stack>
);
}#Custom transport
Use SimpleTransport to wrap any Effect Logger instance as a HyperKit transport.
import { SimpleTransport } from '@ybouhjira/hyperkit';
import { Logger } from 'effect';
// Write to an external sink (e.g., DataDog, Grafana Loki, local file)
const myTransport = SimpleTransport(
Logger.make(({ logLevel, message, annotations, fiberId, spans }) => {
const payload = {
level: logLevel._tag,
msg: Array.isArray(message) ? message.join(' ') : String(message),
annotations: Object.fromEntries(annotations),
fiberId: fiberId.toString(),
spans: spans.toJSON(),
};
// Fire-and-forget — sync transports only
externalSink.write(payload);
})
);
const LogLayer = makeLoggingLayer({ transports: [myTransport] });#Quick Cheat Sheet
| Pattern | Key Components |
|---|---|
| Page layout | Flex direction="column" + Stack scrollable body |
| Metric grid | Grid columns={N} + MetricCard |
| Searchable table | SearchInput + createSignal filter + Table<T> |
| Form | Stack + Input + Select + Switch + Button |
| Settings | Tabs items={[…]} with content per tab |
| Chat | Flex column + For messages + MessageInput |
| File browser | Flex split + FileExplorer + detail pane |
| Confirm dialog | ConfirmDialog with variant="danger" + loading |
| Toasts | ToastProvider at root + useToast() anywhere |
| Empty state | EmptyState icon title description action |
| Draggable dashboard | DashboardContainer cards storageKey editable |
| Command palette | CommandPalette + useShortcut("mod+k") |
| IDE layout | PanelContainer panels storageKey + MenuBar + StatusBar |
| File tabs | TabBar tabs activeId onSelect onClose onAdd |
| Skeletons | Show when={!loading()} fallback={<Skeleton…>} |
| Context menu | SKContextMenu items={[…]} wrapping any element |
| Responsive nav | Sidebar + useBreakpoint() |
| Theme editor | ThemeProvider theme={liveTheme()} wrapping preview |
| Multi-step wizard | ProgressBar + Switch/Match per step |
| Collapsible card | Collapsible open={open()} inside Card |
| Logging / observability | makeLoggingLayer + useLogger(service.stream) + <For each={entries()}> |
#Design Tokens Reference
All patterns use these CSS variables — never raw values:
Spacing: --sk-space-xs --sk-space-sm --sk-space-md --sk-space-lg --sk-space-xl --sk-space-2xl
Colors: --sk-bg-primary --sk-bg-secondary --sk-bg-tertiary --sk-accent --sk-border
Text: --sk-text-primary --sk-text-secondary --sk-text-muted --sk-text-on-accent
Radius: --sk-radius-sm --sk-radius-md --sk-radius-lg --sk-radius-xl
Font: --sk-font-size-xs --sk-font-size-sm --sk-font-size-base --sk-font-size-lg --sk-font-size-xlSpaceTokens accepted by gap, mb, mt: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'