npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mhamz.01/easyflow-whiteboard

v2.181.0

Published

A feature-rich whiteboard component built with Fabric.js and React

Readme

@mhamz.01/easyflow-whiteboard

A feature-rich, embeddable React whiteboard built on Fabric.js, Zustand, and TailwindCSS. Drop it into any React 18+ app and get a fully functional infinite canvas with drawing tools, HTML overlay nodes, and automatic persistence.


Table of Contents


Installation

npm install @mhamz.01/easyflow-whiteboard

Peer Dependencies

npm install fabric@^7 react@^18 react-dom@^18 zustand@^5 \
  @radix-ui/react-dropdown-menu@^2 \
  @radix-ui/react-label@^2 \
  @radix-ui/react-slider@^1

Quick Start

import { EasyflowWhiteboard } from '@mhamz.01/easyflow-whiteboard';
import '@mhamz.01/easyflow-whiteboard/dist/styles.css';

export default function Page() {
  return (
    <div style={{ width: '100%', height: '100vh' }}>
      <EasyflowWhiteboard />
    </div>
  );
}

Next.js note: The component uses "use client" internally. Wrap it in a dynamic import or a client boundary if you use the App Router.


Component API

<EasyflowWhiteboard />

| Prop | Type | Default | Description | |------|------|---------|-------------| | initialData | WhiteboardInitialData | — | Pre-load saved canvas JSON and HTML nodes | | onSave | (payload: WhiteboardSavePayload) => void | — | Debounced callback fired whenever the canvas changes | | saveDebounceMs | number | 2000 | Debounce delay (ms) for onSave | | availableTasks | TaskTemplate[] | [] | Tasks shown in the task picker dropdown | | availableDocuments | DocumentTemplate[] | [] | Documents shown in the document picker dropdown | | isLoadingData | boolean | false | Show toolbar skeleton while async data loads | | createNewTaskHref | string | — | URL for the "Create new task" footer link | | createNewDocumentHref | string | — | URL for the "Create new document" footer link | | editable | boolean | true | Set false to make the canvas fully read-only. See EDITABLE_MODE.md |


Types

WhiteboardInitialData

Shape of the data you pass in to restore a saved board.

interface WhiteboardInitialData {
  canvas?:    string;             // canvas.toJSON() string from a previous save
  tasks?:     TaskNodeData[];
  documents?: DocumentNodeData[];
}

WhiteboardSavePayload

Shape of the data the onSave callback receives.

interface WhiteboardSavePayload {
  canvas:    string;             // Serialized Fabric.js canvas state
  tasks:     TaskNodeData[];
  documents: DocumentNodeData[];
}

TaskTemplate / TaskNodeData

interface TaskNodeData {
  id:         string;
  type:       "task";
  title:      string;
  status:     "todo" | "in-progress" | "done";
  x:          number;            // World-space X position on the canvas
  y:          number;            // World-space Y position on the canvas
  assignee?:  string;
  project?:   string;
  priority?:  "low" | "medium" | "high";
  dueDate?:   string;
}

DocumentTemplate / DocumentNodeData

interface DocumentNodeData {
  id:          string;
  type:        "document";
  title:       string;
  project:     string;
  breadcrumb?: string[];
  preview:     string;
  updatedAt?:  string;
  x:           number;
  y:           number;
}

Store API

Access and mutate whiteboard state from anywhere in your app:

import { useWhiteboardStore } from '@mhamz.01/easyflow-whiteboard';

function MyComponent() {
  const activeTool    = useWhiteboardStore((s) => s.activeTool);
  const setActiveTool = useWhiteboardStore((s) => s.setActiveTool);

  return <button onClick={() => setActiveTool("pen")}>Pen</button>;
}

State Slices

| Field | Type | Description | |-------|------|-------------| | activeTool | Tool | Currently selected drawing tool | | setActiveTool | (tool: Tool) => void | Switch the active tool programmatically | | toolOptions | ToolOptions | Per-tool styling options (color, strokeWidth, etc.) | | setToolOption | (tool, option, value) => void | Update a single tool option | | selectedObjects | FabricObject[] | Fabric objects currently selected on canvas | | canUndo / canRedo | boolean | Whether undo/redo is available | | zoom | number | Current canvas zoom level (1 = 100%) | | bringToFront | (canvas) => void | Move selected object to the top layer | | sendToBack | (canvas) => void | Move selected object to the bottom layer |


Available Tools

| Tool ID | Shortcut | Description | |---------|----------|-------------| | select | V | Select and move Fabric objects | | pan | H | Click-drag to pan the viewport; two-finger pinch to zoom | | pen | P | Freehand drawing with configurable stroke | | rectangle | R | Draw rectangles (drag to size) | | circle | C | Draw circles (drag to size) | | frame | F | Draw white frames — always rendered behind other objects | | line | L | Draw straight lines | | arrow | A | Draw lines with a filled arrowhead | | text | T | Place editable inline text | | image | I | Upload and place an image from disk | | eraser | E | Brush-erase objects by painting over them | | undo | Ctrl+Z | Undo the last canvas change | | redo | Ctrl+Shift+Z | Redo the last undone change |


Keyboard Shortcuts

| Key | Action | |-----|--------| | V H P R C F L A T I E | Activate corresponding tool | | Delete / Backspace | Delete selected objects or HTML nodes | | Ctrl+Z | Undo | | Ctrl+Shift+Z or Ctrl+Y | Redo | | Ctrl+C | Copy selected Fabric objects | | Ctrl+V | Paste with cascading offset | | Ctrl+A | Select all HTML nodes (tasks + documents) | | Escape | Clear HTML node selection | | Ctrl++ / Ctrl+- | Zoom in / out | | Ctrl+0 | Reset zoom to 100% | | Ctrl+Scroll | Zoom to cursor | | Shift+Scroll | Horizontal pan |


Persistence Pattern

The library owns the UI and interaction. You own the data storage:

import { EasyflowWhiteboard, WhiteboardInitialData, WhiteboardSavePayload } from '@mhamz.01/easyflow-whiteboard';
import '@mhamz.01/easyflow-whiteboard/dist/styles.css';
import { useCallback, useEffect, useState } from 'react';

export default function BoardPage({ boardId }: { boardId: string }) {
  const [initialData, setInitialData] = useState<WhiteboardInitialData>();
  const [isLoading, setIsLoading] = useState(true);

  // 1. Load saved state from your database on mount
  useEffect(() => {
    fetch(`/api/boards/${boardId}`)
      .then(r => r.json())
      .then(data => {
        setInitialData({
          canvas:    data.canvasJson,
          tasks:     data.tasks,
          documents: data.documents,
        });
        setIsLoading(false);
      });
  }, [boardId]);

  // 2. Save on every debounced change
  const handleSave = useCallback(async (payload: WhiteboardSavePayload) => {
    await fetch(`/api/boards/${boardId}`, {
      method: 'PUT',
      body: JSON.stringify({
        canvasJson: payload.canvas,
        tasks:      payload.tasks,
        documents:  payload.documents,
      }),
    });
  }, [boardId]);

  return (
    <div style={{ width: '100%', height: '100vh' }}>
      <EasyflowWhiteboard
        initialData={initialData}
        onSave={handleSave}
        saveDebounceMs={2000}
        isLoadingData={isLoading}
        availableTasks={myProjectTasks}
        availableDocuments={myProjectDocs}
        createNewTaskHref="/tasks/new"
        createNewDocumentHref="/docs/new"
      />
    </div>
  );
}

Architecture Overview

EasyflowWhiteboard (whiteboard-test.tsx)
│
├── <canvas>                         Fabric.js v7 rendering surface
│   Handles: shapes, paths, images, arrows, frames, text, selection
│
├── CanvasOverlayLayer               Absolutely-positioned HTML div
│   ├── TaskNode                     Rich task card (status, assignee, priority)
│   └── DocumentNode                 Rich document card (preview, breadcrumb)
│   Reads canvas viewport transform on every `after:render` to stay aligned
│
├── WhiteboardToolbar                Fixed bottom bar
│   ├── Tool buttons (V H P R C F L A T I E)
│   ├── TaskDropdown                 Template picker → add task to canvas
│   └── DocumentDropdown            Template picker → add doc to canvas
│
├── ToolOptionsPanel                 Left sidebar (appears for active tools)
│   └── PenOptions / ShapeOptions / TextOptions / ImageOptions / ...
│
└── ZoomControls                     Top-right zoom percentage pill

Two-layer rendering

Fabric.js renders vector objects (shapes, paths, images) on a <canvas> element. An HTML <div> overlay, transformed with the same viewportTransform matrix, renders interactive React cards (tasks, documents). The two layers share a single world coordinate system — placing a node at (x: 100, y: 200) positions it exactly over canvas coordinates (100, 200).

Hook composition

The root FabricWhiteboard component delegates every behaviour to a dedicated hook:

| Hook | Responsibility | |------|---------------| | useCanvasInit | Mount canvas, hydrate saved state, handle resize | | useToolManager | Switch drawing mode and cursors on tool change | | useDrawing | Mouse-drag shape creation | | useEraser | Brush cursor, trail render, object deletion | | useMouseHandlers | Single event router with rAF throttle | | usePan | Viewport drag and pinch zoom | | useZoom | Wheel zoom, keyboard shortcuts | | useSelection | Fabric selection → store, rubber-band box | | useTextStyle | Live font/color updates on selected text | | useLiveUpdate | Live style updates on selected objects | | usePersistence | Debounced auto-save + history push | | useCopyPaste | Ctrl+C / Ctrl+V clipboard |


Directory Structure

src/
├── index.ts                              Public API — component, store, and types
├── styles.css                            Component stylesheet (TailwindCSS, scoped)
│
├── store/
│   └── whiteboard-store.ts               Zustand store (tools, history, zoom, layer order)
│
├── types/
│   └── canvas-node.ts                    TaskNodeData, DocumentNodeData, CanvasNode
│
├── lib/
│   ├── fabric-utils.ts                   Canvas setup helpers, shape updates, welcome animation
│   ├── fabric-arrow.ts                   Custom Arrow shape (Line + triangle arrowhead)
│   ├── fabric-bidirectional-arrow.ts     Custom BidirectionalArrow shape (arrowheads both ends)
│   ├── fabric-frame.ts                   Custom Frame shape (white Rect, always behind)
│   └── utils.ts                          cn() — clsx + tailwind-merge
│
├── hooks/                                Canvas behaviour hooks (consumed by whiteboard-test.tsx)
│   ├── useCanvasInit.ts                  Fabric canvas mount, hydration, resize
│   ├── useToolManager.ts                 Drawing mode + cursor switch on tool change
│   ├── useDrawing.ts                     Mouse-drag shape creation (rect, circle, etc.)
│   ├── useEraser.ts                      Eraser brush, trail, object deletion
│   ├── useMouseHandlers.ts               Single event router with rAF throttle on mouse:move
│   ├── usePan.ts                         Viewport drag and two-finger pinch zoom
│   ├── useZoom.ts                        Wheel zoom, toolbar buttons, keyboard shortcuts
│   ├── useSelection.ts                   Fabric selection events → store + rubber-band box
│   ├── useTextStyle.ts                   Live font/size/color sync to selected IText
│   ├── useLiveUpdate.ts                  Live fill/stroke sync to selected objects
│   ├── usePersistance.ts                 Debounced onSave callback + undo history push
│   └── useCopyPaste.ts                   Ctrl+C / Ctrl+V with cascading paste offset
│
└── components/
    ├── whiteboard/
    │   └── whiteboard-test.tsx           Root component — composes all hooks and layers
    │
    ├── node/                             HTML overlay: draggable cards synced to canvas
    │   ├── custom-node-overlay-layer.tsx Overlay orchestrator (renders all nodes)
    │   ├── custom-node.tsx               TaskNode — task card with status toggle
    │   ├── document-node.tsx             DocumentNode — document card with preview
    │   ├── types/
    │   │   └── overlay-types.ts          Shared interfaces (Task, Document, DragState, etc.)
    │   └── hooks/
    │       ├── useNodeState.ts           Local state + parent prop sync
    │       ├── useNodeSelection.ts       selectedIds set + selection guard refs
    │       ├── useNodeDrag.ts            Full drag lifecycle with rAF throttle
    │       ├── useFabricSync.ts          Fabric object:moving → HTML position delta
    │       ├── useWheelZoom.ts           Forwards wheel events from overlay to Fabric
    │       ├── useSelectionBox.ts        Rubber-band hit-test for HTML nodes
    │       └── useKeyboardShortcuts.ts   Ctrl+A, Escape, Delete for overlay nodes
    │
    ├── toolbar/
    │   ├── whiteboard-toolbar.tsx        Bottom tool bar — buttons, shortcuts, dropdowns
    │   ├── tooloptions-panel.tsx         Left sidebar — properties for active tool or selection
    │   ├── toolbar-button.tsx            Reusable icon button with tooltip
    │   ├── toolbar-seperator.tsx         Vertical divider between toolbar groups
    │   ├── task-dropdown.tsx             Task template picker (opens above toolbar)
    │   ├── document-dropdown.tsx         Document template picker (opens above toolbar)
    │   ├── layers-control.tsx            Bring/send layer order controls
    │   ├── options/                      Per-tool options panels
    │   │   ├── pen-option.tsx            Stroke color, width, dash style
    │   │   ├── shape-option.tsx          Fill, stroke, dash style for rect/circle/frame
    │   │   ├── text-option.tsx           Font family, size, weight, alignment, color
    │   │   ├── image-options.tsx         Opacity and filter controls for images
    │   │   ├── line-options.tsx          Stroke color, width, dash style for lines
    │   │   └── arrow-options.tsx         Stroke color, width for arrows
    │   └── toolbar-skeleton/
    │       └── toolbar-skeleton.tsx      Animated loading skeleton matching toolbar shape
    │
    ├── ui/                               Low-level Radix UI wrappers (no business logic)
    │   ├── dropdown-menu.tsx             Radix DropdownMenu
    │   ├── label.tsx                     Radix Label
    │   └── slider.tsx                    Radix Slider
    │
    └── zoomcontrol/
        └── zoom-control.tsx              Zoom percentage pill with +/− buttons

Performance Design

The library is built to stay smooth at 60fps even during fast draws and large canvases.

| Pattern | Where | Why | |---------|-------|-----| | requestRenderAll() everywhere | All hooks | Defers paint to Fabric's rAF loop; never blocks the main thread | | rAF throttle on mouse:move | useMouseHandlers | Raw pointer rate can be 200Hz; throttle to one frame | | Props in refs, effects register once | All canvas hooks | Avoids tearing down/reattaching listeners on every render | | React.memo + custom comparators | Toolbar, Overlay wrappers | Pan/zoom state changes don't re-render toolbar or node cards | | Delta accumulator in useFabricSync | Node drag | Never drops position updates even when RAF guard is active | | requestIdleCallback for JSON serialization | usePersistence | Expensive canvas.toJSON() runs during browser idle time | | History capped at 50 snapshots | whiteboard-store | Each snapshot can be 100 KB+; cap prevents memory growth |


Built With

| Dependency | Role | |-----------|------| | Fabric.js v7 | Canvas rendering engine | | Zustand v5 | Lightweight state management | | Radix UI | Accessible headless UI primitives | | Lucide React | Icon set | | TailwindCSS v4 | Utility-first styling | | clsx + tailwind-merge | Safe class name composition |


License

MIT © Muhammad Hamza