@objectifthunes/whiteboard
v0.13.3
Published
Pan/zoom whiteboard canvas with draggable panels, minimap, snap-to-grid, and zoom controls
Readme
@objectifthunes/whiteboard
A pan/zoom whiteboard canvas for React with draggable floating panels, a minimap, snap-to-grid, and a set of themed UI primitives.
Live docs + every export demoed at https://objectifthunes.github.io/whiteboard-demo/.
Install
pnpm add @objectifthunes/whiteboard zustandPeer dependencies: react >= 18, react-dom >= 18, zustand >= 4.
Quick start
import '@objectifthunes/whiteboard/style.css'
import { WhiteboardShell, FloatingPanel } from '@objectifthunes/whiteboard'
export function App() {
return (
<div style={{ position: 'relative', height: '100vh' }}>
<WhiteboardShell>
<FloatingPanel title="Hello" defaultPosition={{ x: 100, y: 100 }} focusable>
Drag me · scroll to zoom · double-click to focus
</FloatingPanel>
</WhiteboardShell>
</div>
)
}The shell needs a positioned, sized parent — usually position: relative; height: 100vh (or any explicit height). Auto-fit kicks in on first paint so panels appear pre-framed.
In Next.js (App Router), make the page that hosts <WhiteboardShell> a client component:
'use client'WhiteboardShell uses ResizeObserver, useEffect, Zustand subscriptions and getBoundingClientRect. For static export, gate first render behind a mounted flag.
Theming
Every component is driven by --wb-* CSS custom properties. Override on :root or scope to [data-theme="dark"]. The package ships sensible defaults.
:root {
--wb-bg: #f3f4f6;
--wb-surface: #ffffff;
--wb-text: #111827;
--wb-text-muted: #6b7280;
--wb-border: #e5e7eb;
--wb-primary: #1f2937;
--wb-danger: #dc2626;
}<ThemeToggle theme={…} onToggle={…} /> is controlled — you decide where data-theme lives (almost always <html>).
What's in the package
- Canvas:
WhiteboardShell,FloatingPanel,Minimap,ZoomBar,ConfirmDialog,PanelErrorBoundary - Store / hooks:
useWhiteboardStore,useWhiteboardLayout,computeWhiteboardFit,computeWhiteboardRectFocus,usePanelRect,belowPanel,snapToWhiteboardGrid,WHITEBOARD_GRID,cn - Primitives:
Button,ButtonRow,PanelCloseButton,ThemeToggle,OverlayIconButton,Field,Label,Input,Textarea,Select,CoordGrid,CoordInput,Alert,Pill,Chip,TagRow,LoadingState,GeneratingOverlay,EmptyState,Stack,Inline,TitleRow,SplitLayout,IconText,PageShell,PageCard,PageTitle,CardTitle,SectionTitle,SectionDescription,MutedText,ItemCard,ItemList,List,PickerCard,PickerGrid,ChoiceCard,ChoiceGroup,VerticalToolbar,AvatarBadge,CanvasStage,ImageThumb,PanelSection,PanelTitle,Checkbox,Switch,Slider,NumberField,Toolbar,Surface,Tooltip,Kbd,Divider - Skeletons:
Skeleton,LineSkeleton,TitleSkeleton,ButtonSkeleton,IconButtonSkeleton,InputSkeleton,SelectSkeleton,TextareaSkeleton,ChipSkeleton,ThumbSkeleton,AvatarSkeleton,CanvasSkeleton,PanelFormSkeleton,CardSkeleton,PickerGridSkeleton,ChoiceGroupSkeleton
See the live demo for the full prop reference and copy-pasteable examples per component.
New in 0.5
Draggable/DraggableSurface— screen-space dragging for overlay chrome, noWhiteboardShellrequired. Keep your CSS anchoring; the drag is atranslate()delta on top, so layouts stay responsive. Drags from any non-interactive area, snaps to the whiteboard grid on release, honours the globalwhiteboard-snap-nowevent, persists peridin localStorage. Double-click resets one element;resetDraggables()resets all.Toolbarreads slightly larger (roomier padding,--wb-fs-mdbuttons) — it's app chrome, not a panel row.
New in 0.4
Battle-tested additions from building a real app on the kit:
Checkbox/Switch— labeled boolean rows; the input pins its own size so host globals can never stretch it.Slider— label left, live readout right, themed range underneath. Returns numbers.NumberField— labeled numeric input; clamps to min/max, never emits NaN.Toolbar— horizontal sibling ofVerticalToolbarfor app-chrome bars (position="top" | "bottom" | "static",endslot).Surface— the plain floating overlay container (selection menus, legends, log panes) for overlays living outside aWhiteboardShell.Tooltip— CSS-only, shows on hover and keyboard focus.Kbdfor shortcut hints,Divider(h/v) for separation.- Hardening —
PickerCarddeclares its own column layout (the globalbuttonstyle can no longer collapse it into a clipped row); native text/number inputs are now styled likeselectout of the box.
Build size
42 KB JS raw / ~10 KB gzip, single 30 KB CSS file. No runtime CSS-in-JS.
License
MIT.
