npm package: @ninesstudios/whiteboard
Description: Whiteboard drawing application
Author: Nines Studios
Latest version: 0.2.5

# Whiteboard

A tailored React Whiteboard component that supports drawing, shapes, text, images, and more. It comes with built-in state management and a customizable toolbar.

## Features

- **Drawing Tools**: Pencil, Text, Rectangle, Circle, Triangle, Diamond, Star, Heart, Hexagon, Octagon, Arrow.
- **Image Support**: Insert and manipulate images on the canvas.
- **Canvas Controls**: Pan and Zoom functionality.
- **Grid System**: Toggleable background grid.
- **Selection**: Select, move, and modify elements.
- **Export**: Export the whiteboard content as a PNG image.
- **Locking Mechanism**: Set the whiteboard to read-only mode with custom messages.
- **Action Handling**: Hook into internal actions for external syncing or logging.

## Installation

Install the package and its peer dependencies:

```bash
npm install whiteboard react react-dom
```

> **Note**: If this package is not yet published to npm, you can install it locally or link it.

## Usage

### Basic Usage

Import the `Whiteboard` component and the stylesheet. The component takes up 100% of the parent container's width and height.

```jsx
import React from 'react';
import Whiteboard from 'whiteboard';
import 'whiteboard/style.css'; // Import styles

function App() {
  return (
    <div style={{ width: '100vw', height: '100vh', border: '1px solid #ccc' }}>
      <Whiteboard />
    </div>
  );
}

export default App;
```

### Advanced Usage

You can control the whiteboard state and handle events using props and a ref.

```jsx
import React, { useRef } from 'react';
import Whiteboard from 'whiteboard';
import 'whiteboard/style.css';

function App() {
  const whiteboardRef = useRef(null);

  const handleAction = (action) => {
    console.log('Action occurred:', action);
    // Send action to server for real-time collaboration
  };

  const handleLockChange = (isLocked) => {
    console.log('Lock state changed:', isLocked);
  };

  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <Whiteboard
        ref={whiteboardRef}
        onAction={handleAction}
        isLocked={false}
        lockText="This board is currently locked"
        onLockChange={handleLockChange}
      />
    </div>
  );
}

// Example: read & replace elements
// const elements = whiteboardRef.current?.getElements();
// whiteboardRef.current?.setElements(elements);

// Example: clear all elements
// whiteboardRef.current?.clearElements();
```

### Programmatic Updates (applyAction)

You can programmatically apply actions to the whiteboard using the `applyAction` method exposed via the ref. This is essential for implementing real-time collaboration where you need to apply actions received from a server.

```jsx
import React, { useEffect, useRef } from 'react';
import Whiteboard from 'whiteboard';

function App() {
  const whiteboardRef = useRef(null);

  useEffect(() => {
    // Example: mimicking receiving an action from a websocket
    const mockIncomingAction = {
      type: 'whiteboard/addElement',
      payload: {
        id: 'remote-1',
        type: 'rectangle',
        x: 200,
        y: 200,
        width: 100,
        height: 100,
        fillColor: '#FF0000'
      }
    };

    // Apply the action to the whiteboard
    if (whiteboardRef.current) {
        // In a real app, this would be inside a socket event listener
        setTimeout(() => {
            whiteboardRef.current.applyAction(mockIncomingAction);
        }, 1000);
    }
  }, []);

  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <Whiteboard ref={whiteboardRef} />
    </div>
  );
}
```

## API Reference

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `onAction` | `(action: object) => void` | `undefined` | Callback fired for content-changing actions (e.g., add/remove/move elements, text edits). It does not emit high-frequency UI actions like pan/zoom/selection/tool changes. |
| `isLocked` | `boolean` | `false` | When `true`, disables all editing tools and shows a lock indicator. |
| `lockText` | `string` | `undefined` | Custom text to display on the lock indicator when `isLocked` is true. |
| `onLockChange` | `(isLocked: boolean) => void` | `undefined` | Callback fired when the lock state changes. |
| `onImageUpload` | `(file: File) => Promise<string>` | `undefined` | Async callback to handle image uploads. Must return a Promise that resolves to the image URL. |

### Handling Image Uploads

By default, the whiteboard creates temporary Blob URLs for uploaded images. This works great for local sessions but won't work for real-time collaboration because Blob URLs are local to the browser tab.

To support multiplayer image sharing, you must provide the `onImageUpload` prop. This function should upload the file to your server (e.g., S3, Cloudinary) and return the public URL.

```jsx
const handleImageUpload = async (file) => {
  // 1. Upload file to your server
  const formData = new FormData();
  formData.append('file', file);
  
  const response = await fetch('https://your-api.com/upload', {
    method: 'POST',
    body: formData
  });
  
  const data = await response.json();
  
  // 2. Return the public URL
  return data.url; 
};

<Whiteboard onImageUpload={handleImageUpload} />
```

### Ref Methods

You can access these methods by passing a `ref` to the `Whiteboard` component.

| Method | Arguments | Description |
|--------|-----------|-------------|
| `applyAction` | `(action: object)` | Dispatches an action to the internal Redux store. Useful for applying remote updates. |
| `getElems` / `getElements` | `()` | Returns the current `elements` array from the internal store. |
| `setElems` / `setElements` | `(elements: object[])` | Replaces the entire `elements` array in the internal store. |
| `clearElems` / `clearElements` | `()` | Clears all elements from the internal store. |
| `canvas` | - | Returns the underlying HTMLCanvasElement. |

### Redux Actions Helper

For convenience (e.g., building your own `applyAction` payloads), the package exports the internal Redux action creators:

```js
import { whiteboardActions } from 'whiteboard';

// Example: build an action object to send/apply
const action = whiteboardActions.clearElements();
// action.type === 'whiteboard/clearElements'
```

#### All available actions

| Action creator | `action.type` | Payload |
|---|---|---|
| `toggleGrid()` | `whiteboard/toggleGrid` | _none_ |
| `setGridSize(size)` | `whiteboard/setGridSize` | `number` |
| `setGridColor(color)` | `whiteboard/setGridColor` | `string` |
| `setSelectedTool(tool)` | `whiteboard/setSelectedTool` | `string` |
| `setToolProperty({ tool, property, value })` | `whiteboard/setToolProperty` | `{ tool: string, property: string, value: any }` |
| `addElement(element)` | `whiteboard/addElement` | `Element` (see “addElement payload” below) |
| `updateElement({ id, updates })` | `whiteboard/updateElement` | `{ id: string, updates: object }` |
| `removeElement(id)` | `whiteboard/removeElement` | `string` |
| `setSelectedElement(idOrNull)` | `whiteboard/setSelectedElement` | `string \| null` |
| `clearElements()` | `whiteboard/clearElements` | _none_ |
| `setElements(elements)` | `whiteboard/setElements` | `object[]` |
| `setPan({ x, y })` | `whiteboard/setPan` | `{ x: number, y: number }` |
| `setZoom(zoom)` | `whiteboard/setZoom` | `number` |
| `resetViewport()` | `whiteboard/resetViewport` | _none_ |
| `fitToView({ canvasWidth, canvasHeight, padding? })` | `whiteboard/fitToView` | `{ canvasWidth: number, canvasHeight: number, padding?: number }` |
| `bringToFront(id)` | `whiteboard/bringToFront` | `string` |
| `sendToBack(id)` | `whiteboard/sendToBack` | `string` |
| `setLocked(isLocked)` | `whiteboard/setLocked` | `boolean` |
| `setWatermarkEnabled(visible)` | `whiteboard/setWatermarkEnabled` | `boolean` |
| `setWatermarkText(text)` | `whiteboard/setWatermarkText` | `string` |

#### `addElement(element)` payload

`addElement` expects a full element object. Coordinates are **world coordinates** (the board handles pan/zoom internally).

Common fields (all element types):

| Field | Type | Notes |
|---|---|---|
| `id` | `string` | Must be unique (commonly `Date.now().toString()`). |
| `type` | `string` | One of the supported element types below. |

Element schemas by `type`:

- **rectangle / triangle / diamond / hexagon / octagon**
  - Required: `x: number`, `y: number`, `width: number`, `height: number`
  - Styling: `strokeColor: string`, `fillColor: string`, `lineWidth: number`

- **circle**
  - Required: `centerX: number`, `centerY: number`, `radius: number`
  - Styling: `strokeColor: string`, `fillColor: string`, `lineWidth: number`

- **pencil**
  - Required: `points: Array<{ x: number, y: number }>`
  - Styling: `strokeColor: string`, `lineWidth: number`

- **star / heart**
  - Required: `centerX: number`, `centerY: number`, `size: number`
  - Styling: `strokeColor: string`, `fillColor: string`, `lineWidth: number`

- **arrow**
  - Required: `startX: number`, `startY: number`, `endX: number`, `endY: number`
  - Styling: `strokeColor: string`, `fillColor: string`, `lineWidth: number`

- **text**
  - Required: `x: number`, `y: number`, `text: string`
  - Styling: `fontSize: number`, `fontColor: string`

- **image**
  - Required: `x: number`, `y: number`, `width: number`, `height: number`, `src: string`
  - Notes: `src` is typically a Data URL (from `FileReader.readAsDataURL`).

Example (rectangle):

```js
import { whiteboardActions } from 'whiteboard';

const action = whiteboardActions.addElement({
  id: '1',
  type: 'rectangle',
  x: 100,
  y: 80,
  width: 200,
  height: 120,
  strokeColor: '#000000',
  fillColor: '#ffffff',
  lineWidth: 2,
});
```

## License

MIT