@objectifthunes/whiteboard 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1274 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,1274 @@
|
|
|
1
|
+
# @objectifthunes/whiteboard
|
|
2
|
+
|
|
3
|
+
A pan / zoom **whiteboard canvas for React** with draggable floating panels, a minimap, snap-to-grid, and a complete set of UI primitives — all themed via CSS custom properties.
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="https://raw.githubusercontent.com/MaxouJS/whiteboard/main/docs/images/overview.png" width="49%" alt="All panels, light mode" />
|
|
7
|
+
<img src="https://raw.githubusercontent.com/MaxouJS/whiteboard/main/docs/images/dark.png" width="49%" alt="All panels, dark mode" />
|
|
8
|
+
</p>
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="https://raw.githubusercontent.com/MaxouJS/whiteboard/main/docs/images/detail.png" width="98%" alt="Panels zoomed in" />
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
> One package, zero design system lock-in: drop `<WhiteboardShell>` into any React app and start placing panels.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Highlights
|
|
18
|
+
|
|
19
|
+
- **Pan & zoom canvas** — drag to pan, scroll/pinch to zoom, pointer-capture safe.
|
|
20
|
+
- **Floating panels** — drag to reposition, double-click to focus, snap to grid.
|
|
21
|
+
- **Minimap** — live overview with click / drag navigation and per-panel double-click focus.
|
|
22
|
+
- **ZoomBar** — zoom in/out, fit to content, reset positions, snap toggle, extra slots.
|
|
23
|
+
- **Zustand store** — full programmatic control (`fitToContent`, `focusPanel`, `resetWidgets`, …).
|
|
24
|
+
- **45+ UI components** — buttons, forms, alerts, typography, skeletons, dialogs, cards, navigation.
|
|
25
|
+
- **Light / dark theme** — CSS custom properties + the built-in `<ThemeToggle />`.
|
|
26
|
+
- **Tiny** — 42 kB raw / 10 kB gzipped JS, single CSS file.
|
|
27
|
+
- **Tree-shakeable ESM** + full TypeScript declarations.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install @objectifthunes/whiteboard
|
|
35
|
+
# peer deps
|
|
36
|
+
npm install react react-dom zustand
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Peer-dependency ranges: `react >= 18`, `react-dom >= 18`, `zustand >= 4`.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import '@objectifthunes/whiteboard/style.css'
|
|
47
|
+
import {
|
|
48
|
+
WhiteboardShell,
|
|
49
|
+
FloatingPanel,
|
|
50
|
+
ThemeToggle,
|
|
51
|
+
} from '@objectifthunes/whiteboard'
|
|
52
|
+
|
|
53
|
+
export function App() {
|
|
54
|
+
return (
|
|
55
|
+
<div style={{ position: 'relative', height: '100vh' }}>
|
|
56
|
+
<WhiteboardShell extraActions={<ThemeToggle />}>
|
|
57
|
+
<FloatingPanel
|
|
58
|
+
title="My Panel"
|
|
59
|
+
defaultPosition={{ x: 100, y: 100 }}
|
|
60
|
+
focusable
|
|
61
|
+
>
|
|
62
|
+
Drag me · Scroll to zoom · Double-click to focus
|
|
63
|
+
</FloatingPanel>
|
|
64
|
+
</WhiteboardShell>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Component Index
|
|
75
|
+
|
|
76
|
+
**Whiteboard:** [WhiteboardShell](#whiteboardshell) · [FloatingPanel](#floatingpanel) · [Minimap](#minimap) · [ZoomBar](#zoombar) · [ConfirmDialog](#confirmdialog) · [PanelErrorBoundary](#panelerrorboundary)
|
|
77
|
+
|
|
78
|
+
**Store & hooks:** [useWhiteboardStore](#usewhiteboardstore) · [useWhiteboardLayout](#usewhiteboardlayout) · [Geometry helpers](#geometry-helpers) · [Panel-rect helpers](#panel-rect-helpers)
|
|
79
|
+
|
|
80
|
+
**UI — Buttons:** [Button](#button) · [ButtonRow](#buttonrow) · [PanelCloseButton](#panelclosebutton)
|
|
81
|
+
|
|
82
|
+
**UI — Forms:** [Field](#field) · [Label](#label) · [Input](#input) · [Textarea](#textarea) · [Select](#select) · [CoordGrid · CoordInput](#coordgrid--coordinput)
|
|
83
|
+
|
|
84
|
+
**UI — Status & Feedback:** [Alert](#alert) · [Pill](#pill) · [Chip](#chip) · [TagRow](#tagrow) · [LoadingState](#loadingstate)
|
|
85
|
+
|
|
86
|
+
**UI — Layout:** [Stack](#stack) · [Inline](#inline) · [TitleRow](#titlerow) · [SplitLayout](#splitlayout) · [IconText](#icontext) · [PageShell · PageCard](#pageshell--pagecard)
|
|
87
|
+
|
|
88
|
+
**UI — Typography:** [PageTitle](#pagetitle) · [StoryTitle](#storytitle) · [AssetTitle](#assettitle) · [SectionTitle · SectionDescription](#sectiontitle--sectiondescription) · [MutedText](#mutedtext)
|
|
89
|
+
|
|
90
|
+
**UI — Cards & Lists:** [ItemCard](#itemcard) · [ItemList](#itemlist) · [List](#list) · [PickerCard](#pickercard) · [PickerGrid](#pickergrid) · [ChoiceCard](#choicecard) · [ChoiceGroup](#choicegroup)
|
|
91
|
+
|
|
92
|
+
**UI — Overlays:** [GeneratingOverlay](#generatingoverlay) · [EmptyState](#emptystate) · [PanelErrorBoundary](#panelerrorboundary)
|
|
93
|
+
|
|
94
|
+
**UI — Navigation & Canvas:** [VerticalToolbar](#verticaltoolbar) · [AvatarBadge](#avatarbadge) · [CanvasStage](#canvasstage) · [OverlayIconButton](#overlayiconbutton) · [ImageThumb](#imagethumb)
|
|
95
|
+
|
|
96
|
+
**UI — Skeletons:** [Skeleton](#skeleton-base) · [Primitive skeletons](#primitive-skeletons) · [Widget skeletons](#widget-skeletons) · [ChoiceGroupSkeleton](#choicegroupskeleton)
|
|
97
|
+
|
|
98
|
+
**UI — Panel sections:** [PanelSection](#panelsection) · [PanelTitle](#paneltitle)
|
|
99
|
+
|
|
100
|
+
**Theming:** [ThemeToggle](#themetoggle) · [CSS custom properties](#css--theming)
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Whiteboard Components
|
|
105
|
+
|
|
106
|
+
### `WhiteboardShell`
|
|
107
|
+
|
|
108
|
+
The root container. Handles pan, zoom, viewport tracking, auto-fit on first mount, session reset on unmount, and renders the `ZoomBar` + `Minimap`.
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
<WhiteboardShell
|
|
112
|
+
showMinimap={true} // default: true
|
|
113
|
+
minimapLoading={false} // show a spinner inside the minimap
|
|
114
|
+
extraActions={<ThemeToggle />} // any node rendered inside the ZoomBar
|
|
115
|
+
>
|
|
116
|
+
{/* FloatingPanels go here */}
|
|
117
|
+
</WhiteboardShell>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| Prop | Type | Default | Description |
|
|
121
|
+
| ---------------- | ----------- | ------- | -------------------------------------------- |
|
|
122
|
+
| `children` | `ReactNode` | — | Panels (any node) rendered on the canvas. |
|
|
123
|
+
| `showMinimap` | `boolean` | `true` | Render the minimap in the bottom corner. |
|
|
124
|
+
| `minimapLoading` | `boolean` | `false` | Show a spinner instead of the minimap rects. |
|
|
125
|
+
| `extraActions` | `ReactNode` | — | Extra controls rendered at the bottom of the ZoomBar (theme toggle, custom action). |
|
|
126
|
+
|
|
127
|
+
**Interactions out of the box**
|
|
128
|
+
|
|
129
|
+
- Drag canvas background → pan
|
|
130
|
+
- Scroll wheel → zoom toward cursor (clamped `0.2`–`3`)
|
|
131
|
+
- Right-click is consumed (no native context menu over the canvas)
|
|
132
|
+
- ResizeObserver keeps the viewport size in sync
|
|
133
|
+
- `resetSession` runs on unmount so the next mount starts clean
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### `FloatingPanel`
|
|
138
|
+
|
|
139
|
+
A draggable card placed on the canvas.
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<FloatingPanel
|
|
143
|
+
title="Settings"
|
|
144
|
+
defaultPosition={{ x: 100, y: 80 }}
|
|
145
|
+
width={320} // default: 300
|
|
146
|
+
focusable // adds the focus button in the header
|
|
147
|
+
focusPadding={40} // padding when focusing (default: 40)
|
|
148
|
+
focusMaxScale={1.5} // max zoom when focusing (default: 1.5)
|
|
149
|
+
headerActions={<Button>…</Button>}
|
|
150
|
+
trackRect={rectRef} // MutableRefObject<PanelRect>, kept in sync
|
|
151
|
+
>
|
|
152
|
+
{/* any content */}
|
|
153
|
+
</FloatingPanel>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Prop | Type | Default | Description |
|
|
157
|
+
| ----------------- | ----------------------------------- | ------- | ----------- |
|
|
158
|
+
| `title` | `ReactNode` | — | Header label. |
|
|
159
|
+
| `defaultPosition` | `{ x: number, y: number }` | — | Initial world-space position. |
|
|
160
|
+
| `width` | `number` | `300` | Pixel width. Height is content-driven. |
|
|
161
|
+
| `focusable` | `boolean` | `false` | Renders the corner focus button. |
|
|
162
|
+
| `focusPadding` | `number` | `40` | Padding used by the focus camera. |
|
|
163
|
+
| `focusMaxScale` | `number` | `1.5` | Camera scale ceiling when focusing. |
|
|
164
|
+
| `headerActions` | `ReactNode` | — | Buttons rendered next to the focus button. |
|
|
165
|
+
| `trackRect` | `MutableRefObject<PanelRect>` | — | Ref kept in sync with current `{x, y, width, height, focusPadding, focusMaxScale}`. |
|
|
166
|
+
| `className` | `string` | — | Extra class merged onto the panel root. |
|
|
167
|
+
|
|
168
|
+
Double-click the panel or press the focus button to zoom the camera to that panel. Snap-to-grid (toggled in the `ZoomBar`) also realigns the panel on activation.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
### `Minimap`
|
|
173
|
+
|
|
174
|
+
Lives in the bottom-right corner, scaled to panel content bounds. Click to pan, drag to scrub, scroll to zoom, double-click a panel rect to focus it.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
<WhiteboardShell showMinimap minimapLoading={false} />
|
|
178
|
+
// Renders <Minimap loading={false} /> internally
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
You normally don't render `<Minimap>` directly — `WhiteboardShell` does it for you.
|
|
182
|
+
|
|
183
|
+
| Prop | Type | Default | Description |
|
|
184
|
+
| --------- | --------- | ------- | -------------------------------------- |
|
|
185
|
+
| `loading` | `boolean` | `false` | Show a spinner instead of panel rects. |
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### `ZoomBar`
|
|
190
|
+
|
|
191
|
+
Vertical bar in the right edge with: zoom out, current %, zoom in, **Fit to content**, **Reset positions**, **Snap to grid** toggle, and any `extraActions` you pass in.
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
<ZoomBar extraActions={<ThemeToggle />} />
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Like `<Minimap>`, this is rendered by `WhiteboardShell` — only render it directly if you're building a custom shell. The component reads & writes directly to `useWhiteboardStore`.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### `ConfirmDialog`
|
|
202
|
+
|
|
203
|
+
A portaled, accessible confirmation dialog. Closes on `Escape`, click outside, or the Cancel button.
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
<ConfirmDialog
|
|
207
|
+
open={open}
|
|
208
|
+
title="Delete scene?"
|
|
209
|
+
message="This cannot be undone."
|
|
210
|
+
confirmLabel="Delete"
|
|
211
|
+
loading={deleting}
|
|
212
|
+
error={errorMessage}
|
|
213
|
+
onConfirm={handleDelete}
|
|
214
|
+
onCancel={() => setOpen(false)}
|
|
215
|
+
/>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
| Prop | Type | Default | Description |
|
|
219
|
+
| -------------- | ------------------- | ----------- | -------------------------------------------- |
|
|
220
|
+
| `open` | `boolean` | — | Controls visibility. |
|
|
221
|
+
| `title` | `string` | — | Dialog title (also `aria-label`). |
|
|
222
|
+
| `message` | `string` | — | Body text. |
|
|
223
|
+
| `confirmLabel` | `string` | `'Confirm'` | Confirm button label. |
|
|
224
|
+
| `loading` | `boolean` | `false` | Disables confirm and shows `Deleting…`. |
|
|
225
|
+
| `error` | `string \| null` | — | Inline error shown above the actions. |
|
|
226
|
+
| `onConfirm` | `() => void` | — | Confirm handler. |
|
|
227
|
+
| `onCancel` | `() => void` | — | Cancel handler (also fires on Escape / backdrop). |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
### `PanelErrorBoundary`
|
|
232
|
+
|
|
233
|
+
A class-based error boundary that renders a friendly fallback + Retry button when a panel's subtree throws.
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
<PanelErrorBoundary fallbackMessage="This panel crashed.">
|
|
237
|
+
<PotentiallyBrokenWidget />
|
|
238
|
+
</PanelErrorBoundary>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
| Prop | Type | Default |
|
|
242
|
+
| ----------------- | -------- | ------------------------ |
|
|
243
|
+
| `fallbackMessage` | `string` | `'This panel crashed.'` |
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Store & Hooks
|
|
248
|
+
|
|
249
|
+
### `useWhiteboardStore`
|
|
250
|
+
|
|
251
|
+
A Zustand store that exposes camera, viewport, panels registry, and actions.
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import { useWhiteboardStore } from '@objectifthunes/whiteboard'
|
|
255
|
+
|
|
256
|
+
const offset = useWhiteboardStore(s => s.offset) // { x, y }
|
|
257
|
+
const scale = useWhiteboardStore(s => s.scale) // 0.2 — 3
|
|
258
|
+
const snapToGrid = useWhiteboardStore(s => s.snapToGrid)
|
|
259
|
+
const setSnapToGrid = useWhiteboardStore(s => s.setSnapToGrid)
|
|
260
|
+
const fitToContent = useWhiteboardStore(s => s.fitToContent)
|
|
261
|
+
const focusPanel = useWhiteboardStore(s => s.focusPanel)
|
|
262
|
+
const resetWidgets = useWhiteboardStore(s => s.resetWidgets)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Selectors (state)**
|
|
266
|
+
|
|
267
|
+
| Key | Type |
|
|
268
|
+
| ------------------ | --------------------------------------- |
|
|
269
|
+
| `offset` | `{ x: number, y: number }` |
|
|
270
|
+
| `scale` | `number` (clamped 0.2–3) |
|
|
271
|
+
| `viewportSize` | `{ width: number, height: number }` |
|
|
272
|
+
| `snapToGrid` | `boolean` |
|
|
273
|
+
| `snapGridSize` | `number` (defaults to `20`) |
|
|
274
|
+
| `panels` | `Map<string, PanelRect>` |
|
|
275
|
+
| `resetFns` | `Map<string, () => void>` |
|
|
276
|
+
| `registryVersion` | `number` (bumps on registry change) |
|
|
277
|
+
|
|
278
|
+
**Actions**
|
|
279
|
+
|
|
280
|
+
| Method | Description |
|
|
281
|
+
| --------------------- | ---------------------------------------------------------- |
|
|
282
|
+
| `setOffset(v)` | Set camera offset (value or updater). |
|
|
283
|
+
| `setScale(v)` | Set camera scale. |
|
|
284
|
+
| `setViewportSize(v)` | Called automatically by the shell. |
|
|
285
|
+
| `setSnapToGrid(v)` | Toggle snap mode. Dispatches `whiteboard-snap-now` so existing panels realign. |
|
|
286
|
+
| `register(id, rect)` | Add/replace a panel rect. Called by `FloatingPanel`. |
|
|
287
|
+
| `unregister(id)` | Remove a panel rect. |
|
|
288
|
+
| `registerReset(id, fn)` / `unregisterReset(id)` | Register a reset handler for `resetWidgets`. |
|
|
289
|
+
| `fitToContent()` | Reframe the camera so all registered panels fit (with padding). |
|
|
290
|
+
| `focusPanel(rect, { padding?, maxScale? })` | Reframe the camera onto a single rect. |
|
|
291
|
+
| `resetWidgets()` | Call every panel's reset handler then re-fit. |
|
|
292
|
+
| `resetSession()` | Discard all state. Called on shell unmount. |
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
### `useWhiteboardLayout`
|
|
297
|
+
|
|
298
|
+
Compute snap-aligned default positions from a map of panel widths. Returns world-space coords that you feed into each panel's `defaultPosition`.
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
const { positions, panelWidth, layout } = useWhiteboardLayout({
|
|
302
|
+
widths: { settings: 320, preview: 480, layers: 280 },
|
|
303
|
+
startX: 40,
|
|
304
|
+
y: 60,
|
|
305
|
+
gap: 20,
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// positions.settings → { x: 40, y: 60 }
|
|
309
|
+
// positions.preview → { x: 380, y: 60 }
|
|
310
|
+
// positions.layers → { x: 880, y: 60 }
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
All inputs are normalized to `WHITEBOARD_GRID` (20 px) so panels stay snapped even with snap mode off.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
### Geometry helpers
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
import {
|
|
321
|
+
computeWhiteboardFit, // ({ panels, viewportSize, padding? }) → { scale, offset } | null
|
|
322
|
+
computeWhiteboardRectFocus, // ({ rect, viewportSize, padding?, maxScale? }) → { scale, offset }
|
|
323
|
+
snapToWhiteboardGrid, // (n: number) → nearest multiple of 20
|
|
324
|
+
WHITEBOARD_GRID, // constant: 20
|
|
325
|
+
} from '@objectifthunes/whiteboard'
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
These are the same primitives that power `fitToContent` and `focusPanel`. Use them when you need to drive the camera from a custom source (URL state, animation, etc).
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
### Panel-rect helpers
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
import { usePanelRect, belowPanel } from '@objectifthunes/whiteboard'
|
|
336
|
+
|
|
337
|
+
const settingsRect = usePanelRect({ x: 100, y: 100 })
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<>
|
|
341
|
+
<FloatingPanel title="Settings" defaultPosition={{ x: 100, y: 100 }} trackRect={settingsRect}>…</FloatingPanel>
|
|
342
|
+
<FloatingPanel title="Preview" defaultPosition={belowPanel(settingsRect.current)}>…</FloatingPanel>
|
|
343
|
+
</>
|
|
344
|
+
)
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
- `usePanelRect(initial)` → `MutableRefObject<PanelRect>` with `width`/`height` filled after first measurement.
|
|
348
|
+
- `belowPanel(rect, gap = WHITEBOARD_GRID)` → `{ x, y }` for the next panel directly below.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
### `cn(...)`
|
|
353
|
+
|
|
354
|
+
Tiny class-name joiner, exported for users who don't already pull in `clsx` / `classnames`.
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
import { cn } from '@objectifthunes/whiteboard'
|
|
358
|
+
cn('btn', isPrimary && 'btn--primary', className)
|
|
359
|
+
// → "btn btn--primary <className>"
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## UI — Buttons
|
|
365
|
+
|
|
366
|
+
<p align="center">
|
|
367
|
+
<img src="https://raw.githubusercontent.com/MaxouJS/whiteboard/main/docs/images/panel-button.png" width="380" alt="Button, ButtonRow, PanelCloseButton" />
|
|
368
|
+
</p>
|
|
369
|
+
|
|
370
|
+
### `Button`
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
<Button>Primary</Button>
|
|
374
|
+
<Button variant="secondary">Secondary</Button>
|
|
375
|
+
<Button variant="danger">Delete</Button>
|
|
376
|
+
<Button loading loadingText="Saving…">Save</Button>
|
|
377
|
+
<Button disabled>Disabled</Button>
|
|
378
|
+
<Button iconOnly aria-label="Delete"><TrashIcon /></Button>
|
|
379
|
+
<Button fullWidth>Stretch</Button>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
| Prop | Type | Default |
|
|
383
|
+
| ------------- | ------------------------------------- | ----------- |
|
|
384
|
+
| `variant` | `'primary' \| 'secondary' \| 'danger'`| `'primary'` |
|
|
385
|
+
| `fullWidth` | `boolean` | `false` |
|
|
386
|
+
| `iconOnly` | `boolean` | `false` |
|
|
387
|
+
| `loading` | `boolean` | `false` |
|
|
388
|
+
| `loadingText` | `string` | — |
|
|
389
|
+
| All native `<button>` props | … | — |
|
|
390
|
+
|
|
391
|
+
`loading` automatically disables the button and prefixes a spinner.
|
|
392
|
+
|
|
393
|
+
### `ButtonRow`
|
|
394
|
+
|
|
395
|
+
A horizontal row that gives equal sizing + a consistent gap to all children.
|
|
396
|
+
|
|
397
|
+
```tsx
|
|
398
|
+
<ButtonRow>
|
|
399
|
+
<Button variant="secondary">Cancel</Button>
|
|
400
|
+
<Button>Confirm</Button>
|
|
401
|
+
</ButtonRow>
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
| Prop | Type | Default |
|
|
405
|
+
| ----- | ------------- | ------- |
|
|
406
|
+
| `as` | `ElementType` | `'div'` |
|
|
407
|
+
|
|
408
|
+
### `PanelCloseButton`
|
|
409
|
+
|
|
410
|
+
A pre-built secondary button with an `X` icon. Use it inside a panel's `headerActions`.
|
|
411
|
+
|
|
412
|
+
```tsx
|
|
413
|
+
<PanelCloseButton onClick={onClose} label="Close" />
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
| Prop | Type | Default |
|
|
417
|
+
| --------- | ------------ | ---------- |
|
|
418
|
+
| `onClick` | `() => void` | — |
|
|
419
|
+
| `label` | `string` | `'Close'` |
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## UI — Forms
|
|
424
|
+
|
|
425
|
+
<p align="center">
|
|
426
|
+
<img src="https://raw.githubusercontent.com/MaxouJS/whiteboard/main/docs/images/panel-forms.png" width="380" alt="Field, Input, Textarea, Select, CoordGrid" />
|
|
427
|
+
</p>
|
|
428
|
+
|
|
429
|
+
### `Field`
|
|
430
|
+
|
|
431
|
+
Wraps a control with a label, optional hint and inline error.
|
|
432
|
+
|
|
433
|
+
```tsx
|
|
434
|
+
<Field label="Name" htmlFor="name" hint="Display name" error={errors.name}>
|
|
435
|
+
<Input id="name" placeholder="John Doe" />
|
|
436
|
+
</Field>
|
|
437
|
+
|
|
438
|
+
<Field as="fieldset" label="Notifications" layout="control">
|
|
439
|
+
{/* checkbox stack */}
|
|
440
|
+
</Field>
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
| Prop | Type | Default |
|
|
444
|
+
| --------- | -------------------------- | --------- |
|
|
445
|
+
| `label` | `ReactNode` | — |
|
|
446
|
+
| `htmlFor` | `string` | — |
|
|
447
|
+
| `hint` | `ReactNode` | — |
|
|
448
|
+
| `error` | `ReactNode` | — |
|
|
449
|
+
| `layout` | `'stack' \| 'control'` | `'stack'` |
|
|
450
|
+
| `as` | `ElementType` | `'div'` |
|
|
451
|
+
|
|
452
|
+
### `Label`
|
|
453
|
+
|
|
454
|
+
Bare styled `<label>`. Most of the time you'll get one for free from `<Field>`.
|
|
455
|
+
|
|
456
|
+
```tsx
|
|
457
|
+
<Label htmlFor="email">Email</Label>
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### `Input`
|
|
461
|
+
|
|
462
|
+
Styled `<input>`. Accepts all native input props; supports refs.
|
|
463
|
+
|
|
464
|
+
```tsx
|
|
465
|
+
<Input type="email" placeholder="you@example.com" />
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### `Textarea`
|
|
469
|
+
|
|
470
|
+
Styled `<textarea>`. Supports refs.
|
|
471
|
+
|
|
472
|
+
```tsx
|
|
473
|
+
<Textarea rows={4} placeholder="About you…" />
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### `Select`
|
|
477
|
+
|
|
478
|
+
Styled `<select>`. Supports refs.
|
|
479
|
+
|
|
480
|
+
```tsx
|
|
481
|
+
<Select defaultValue="light">
|
|
482
|
+
<option value="light">Light</option>
|
|
483
|
+
<option value="dark">Dark</option>
|
|
484
|
+
</Select>
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### `CoordGrid` / `CoordInput`
|
|
488
|
+
|
|
489
|
+
Two-column grid for `x / y / z / scale`-style numeric inputs.
|
|
490
|
+
|
|
491
|
+
```tsx
|
|
492
|
+
<CoordGrid>
|
|
493
|
+
<CoordInput axis="X" value={x} onChange={e => setX(+e.target.value)} />
|
|
494
|
+
<CoordInput axis="Y" value={y} onChange={e => setY(+e.target.value)} />
|
|
495
|
+
<CoordInput axis="Z" value={z} onChange={e => setZ(+e.target.value)} />
|
|
496
|
+
<CoordInput axis="Scale" value={s} onChange={e => setS(+e.target.value)} />
|
|
497
|
+
</CoordGrid>
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
`CoordInput` forces `type="number"` and `step="0.01"`. Pass any other native input prop through.
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## UI — Status & Feedback
|
|
505
|
+
|
|
506
|
+
<p align="center">
|
|
507
|
+
<img src="https://raw.githubusercontent.com/MaxouJS/whiteboard/main/docs/images/panel-status.png" width="380" alt="Alert, Pill, Chip, TagRow, LoadingState" />
|
|
508
|
+
</p>
|
|
509
|
+
|
|
510
|
+
### `Alert`
|
|
511
|
+
|
|
512
|
+
A one-line status message with four tones.
|
|
513
|
+
|
|
514
|
+
```tsx
|
|
515
|
+
<Alert tone="info">Everything looks good.</Alert>
|
|
516
|
+
<Alert tone="success">Saved.</Alert>
|
|
517
|
+
<Alert tone="error">Something went wrong.</Alert>
|
|
518
|
+
<Alert tone="muted">No results.</Alert>
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
| Prop | Type | Default |
|
|
522
|
+
| ------ | --------------------------------------------- | -------- |
|
|
523
|
+
| `tone` | `'info' \| 'success' \| 'error' \| 'muted'` | `'info'` |
|
|
524
|
+
|
|
525
|
+
### `Pill`
|
|
526
|
+
|
|
527
|
+
A compact rounded label.
|
|
528
|
+
|
|
529
|
+
```tsx
|
|
530
|
+
<Pill>Draft</Pill>
|
|
531
|
+
<Pill tone="success">Published</Pill>
|
|
532
|
+
<Pill tone="warning">Review</Pill>
|
|
533
|
+
<Pill tone="danger">Rejected</Pill>
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
| Prop | Type | Default |
|
|
537
|
+
| ------ | ------------------------------------------------- | ----------- |
|
|
538
|
+
| `tone` | `'default' \| 'success' \| 'warning' \| 'danger'` | `'default'` |
|
|
539
|
+
|
|
540
|
+
### `Chip`
|
|
541
|
+
|
|
542
|
+
A subtle tag-style label, perfect for tech tags or filters.
|
|
543
|
+
|
|
544
|
+
```tsx
|
|
545
|
+
<Chip>React</Chip>
|
|
546
|
+
<Chip>TypeScript</Chip>
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### `TagRow`
|
|
550
|
+
|
|
551
|
+
A horizontal wrapping row for chips, pills, or any small inline items.
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
<TagRow>
|
|
555
|
+
<Chip>react</Chip>
|
|
556
|
+
<Chip>typescript</Chip>
|
|
557
|
+
<Chip>vite</Chip>
|
|
558
|
+
</TagRow>
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### `LoadingState`
|
|
562
|
+
|
|
563
|
+
An inline spinner + label.
|
|
564
|
+
|
|
565
|
+
```tsx
|
|
566
|
+
<LoadingState label="Fetching…" />
|
|
567
|
+
<LoadingState /> {/* defaults to "Loading..." */}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
| Prop | Type | Default |
|
|
571
|
+
| ------- | -------- | -------------- |
|
|
572
|
+
| `label` | `string` | `'Loading...'` |
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## UI — Layout
|
|
577
|
+
|
|
578
|
+
<p align="center">
|
|
579
|
+
<img src="https://raw.githubusercontent.com/MaxouJS/whiteboard/main/docs/images/panel-layout.png" width="380" alt="Stack, Inline, TitleRow, SplitLayout, IconText" />
|
|
580
|
+
</p>
|
|
581
|
+
|
|
582
|
+
### `Stack`
|
|
583
|
+
|
|
584
|
+
Vertical flex container with a consistent gap.
|
|
585
|
+
|
|
586
|
+
```tsx
|
|
587
|
+
<Stack size="md">{/* default */}
|
|
588
|
+
<SectionTitle>Settings</SectionTitle>
|
|
589
|
+
<p>…</p>
|
|
590
|
+
</Stack>
|
|
591
|
+
|
|
592
|
+
<Stack size="sm" as="section">…</Stack>
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
| Prop | Type | Default |
|
|
596
|
+
| ------ | --------------- | ------- |
|
|
597
|
+
| `size` | `'sm' \| 'md'` | `'md'` |
|
|
598
|
+
| `as` | `ElementType` | `'div'` |
|
|
599
|
+
|
|
600
|
+
### `Inline`
|
|
601
|
+
|
|
602
|
+
Horizontal row with controllable justification.
|
|
603
|
+
|
|
604
|
+
```tsx
|
|
605
|
+
<Inline justify="start">…</Inline>
|
|
606
|
+
<Inline justify="between">…</Inline>
|
|
607
|
+
<Inline justify="end">…</Inline>
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
| Prop | Type | Default |
|
|
611
|
+
| --------- | --------------------------------- | --------- |
|
|
612
|
+
| `justify` | `'start' \| 'between' \| 'end'` | `'start'` |
|
|
613
|
+
| `as` | `ElementType` | `'div'` |
|
|
614
|
+
|
|
615
|
+
### `TitleRow`
|
|
616
|
+
|
|
617
|
+
A `space-between` row tailored for `title + action` pairs.
|
|
618
|
+
|
|
619
|
+
```tsx
|
|
620
|
+
<TitleRow>
|
|
621
|
+
<SectionTitle>Layers</SectionTitle>
|
|
622
|
+
<Button variant="secondary">Add</Button>
|
|
623
|
+
</TitleRow>
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### `SplitLayout`
|
|
627
|
+
|
|
628
|
+
Two-column responsive grid (image + content, avatar + meta, etc.).
|
|
629
|
+
|
|
630
|
+
```tsx
|
|
631
|
+
<SplitLayout variant="element">
|
|
632
|
+
<ImageThumb src={thumb} alt="Cover" size="md" />
|
|
633
|
+
<Stack size="sm">
|
|
634
|
+
<AssetTitle>Hero</AssetTitle>
|
|
635
|
+
<MutedText>Updated 5 min ago</MutedText>
|
|
636
|
+
</Stack>
|
|
637
|
+
</SplitLayout>
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
| Prop | Type | Default |
|
|
641
|
+
| --------- | ------------------------------------------ | ------- |
|
|
642
|
+
| `variant` | `'element' \| 'character' \| 'user'` | — |
|
|
643
|
+
|
|
644
|
+
### `IconText`
|
|
645
|
+
|
|
646
|
+
Inline icon + text helper.
|
|
647
|
+
|
|
648
|
+
```tsx
|
|
649
|
+
<IconText icon={<CheckIcon size={14} />}>Connected</IconText>
|
|
650
|
+
<IconText as="span" icon={<CalendarIcon size={14} />}>Due Friday</IconText>
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
| Prop | Type | Default |
|
|
654
|
+
| ------ | -------------- | -------- |
|
|
655
|
+
| `icon` | `ReactNode` | — |
|
|
656
|
+
| `as` | `ElementType` | `'span'` |
|
|
657
|
+
|
|
658
|
+
### `PageShell` / `PageCard`
|
|
659
|
+
|
|
660
|
+
Page-level containers — `PageShell` is a `<main>` with consistent padding; `PageCard` is a bordered card. Use these outside the whiteboard for full-page screens.
|
|
661
|
+
|
|
662
|
+
```tsx
|
|
663
|
+
<PageShell>
|
|
664
|
+
<PageCard>
|
|
665
|
+
<h1>Account</h1>
|
|
666
|
+
…
|
|
667
|
+
</PageCard>
|
|
668
|
+
</PageShell>
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## UI — Typography
|
|
674
|
+
|
|
675
|
+
<p align="center">
|
|
676
|
+
<img src="https://raw.githubusercontent.com/MaxouJS/whiteboard/main/docs/images/panel-typography.png" width="380" alt="PageTitle, StoryTitle, AssetTitle, SectionTitle, SectionDescription, MutedText" />
|
|
677
|
+
</p>
|
|
678
|
+
|
|
679
|
+
All typography primitives accept `as` for the rendered tag and pass through any HTML attributes.
|
|
680
|
+
|
|
681
|
+
### `PageTitle`
|
|
682
|
+
|
|
683
|
+
Top-level page heading.
|
|
684
|
+
|
|
685
|
+
```tsx
|
|
686
|
+
<PageTitle>Account settings</PageTitle>
|
|
687
|
+
<PageTitle as="h2">Embedded title</PageTitle>
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### `StoryTitle`
|
|
691
|
+
|
|
692
|
+
Slightly smaller hero-style title for story / item cards.
|
|
693
|
+
|
|
694
|
+
```tsx
|
|
695
|
+
<StoryTitle>The night the lights went out</StoryTitle>
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### `AssetTitle`
|
|
699
|
+
|
|
700
|
+
Compact title used in lists. Set `clamp` to truncate at one line.
|
|
701
|
+
|
|
702
|
+
```tsx
|
|
703
|
+
<AssetTitle clamp>Very, very, very long asset name</AssetTitle>
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
| Prop | Type | Default |
|
|
707
|
+
| ------- | --------- | ------- |
|
|
708
|
+
| `clamp` | `boolean` | `false` |
|
|
709
|
+
|
|
710
|
+
### `SectionTitle` / `SectionDescription`
|
|
711
|
+
|
|
712
|
+
The label + supporting copy you place above a panel section.
|
|
713
|
+
|
|
714
|
+
```tsx
|
|
715
|
+
<SectionTitle>Assets</SectionTitle>
|
|
716
|
+
<SectionDescription>Drag an asset onto the canvas.</SectionDescription>
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### `MutedText`
|
|
720
|
+
|
|
721
|
+
De-emphasized paragraph for hints and metadata.
|
|
722
|
+
|
|
723
|
+
```tsx
|
|
724
|
+
<MutedText size="xs">Last edited 5 min ago</MutedText>
|
|
725
|
+
<MutedText>Helper copy.</MutedText>
|
|
726
|
+
<MutedText size="md">Wider helper copy.</MutedText>
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
| Prop | Type | Default |
|
|
730
|
+
| ------ | ----------------------- | ------- |
|
|
731
|
+
| `size` | `'xs' \| 'sm' \| 'md'` | `'sm'` |
|
|
732
|
+
|
|
733
|
+
---
|
|
734
|
+
|
|
735
|
+
## UI — Cards & Lists
|
|
736
|
+
|
|
737
|
+
<p align="center">
|
|
738
|
+
<img src="https://raw.githubusercontent.com/MaxouJS/whiteboard/main/docs/images/panel-cards.png" width="380" alt="ItemCard, ItemList, PickerGrid, ChoiceGroup" />
|
|
739
|
+
</p>
|
|
740
|
+
|
|
741
|
+
### `ItemCard`
|
|
742
|
+
|
|
743
|
+
Bordered card used inside lists — elements, facts, secrets, users, characters, assets.
|
|
744
|
+
|
|
745
|
+
```tsx
|
|
746
|
+
<ItemCard as="li">
|
|
747
|
+
<Inline justify="between">
|
|
748
|
+
<span>Character sprite</span>
|
|
749
|
+
<Pill tone="success">Active</Pill>
|
|
750
|
+
</Inline>
|
|
751
|
+
</ItemCard>
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### `ItemList`
|
|
755
|
+
|
|
756
|
+
Vertical list container with a consistent gap. Pairs with `ItemCard`.
|
|
757
|
+
|
|
758
|
+
```tsx
|
|
759
|
+
<ItemList as="ul">
|
|
760
|
+
<ItemCard as="li">First</ItemCard>
|
|
761
|
+
<ItemCard as="li">Second</ItemCard>
|
|
762
|
+
</ItemList>
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
### `List`
|
|
766
|
+
|
|
767
|
+
A `list-style: none` reset, useful when you need an unstyled `<ul>` / `<ol>`.
|
|
768
|
+
|
|
769
|
+
```tsx
|
|
770
|
+
<List as="ul">
|
|
771
|
+
<li>Alpha</li>
|
|
772
|
+
<li>Beta</li>
|
|
773
|
+
</List>
|
|
774
|
+
|
|
775
|
+
<List reset={false}>…still has bullets</List>
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
| Prop | Type | Default |
|
|
779
|
+
| ------- | ------------- | ------- |
|
|
780
|
+
| `as` | `ElementType` | `'ul'` |
|
|
781
|
+
| `reset` | `boolean` | `true` |
|
|
782
|
+
|
|
783
|
+
### `PickerCard`
|
|
784
|
+
|
|
785
|
+
A clickable card used inside `PickerGrid`. Defaults to `<button>` (set `as="div"` for skeletons).
|
|
786
|
+
|
|
787
|
+
```tsx
|
|
788
|
+
<PickerCard onClick={() => select('a')}>Option A</PickerCard>
|
|
789
|
+
<PickerCard as="div" className="picker-card--skeleton">…</PickerCard>
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
### `PickerGrid`
|
|
793
|
+
|
|
794
|
+
Responsive grid of `PickerCard`s.
|
|
795
|
+
|
|
796
|
+
```tsx
|
|
797
|
+
<PickerGrid variant="elements">
|
|
798
|
+
<PickerCard onClick={…}>A</PickerCard>
|
|
799
|
+
<PickerCard onClick={…}>B</PickerCard>
|
|
800
|
+
</PickerGrid>
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
| Prop | Type | Default |
|
|
804
|
+
| --------- | ------------------------------------------ | ------- |
|
|
805
|
+
| `variant` | `'elements' \| 'characters' \| 'library'` | — |
|
|
806
|
+
|
|
807
|
+
### `ChoiceCard`
|
|
808
|
+
|
|
809
|
+
Single selectable card. Manage `active` from a controlled state.
|
|
810
|
+
|
|
811
|
+
```tsx
|
|
812
|
+
<ChoiceCard active={value === 'a'} onClick={() => setValue('a')}>
|
|
813
|
+
Option A
|
|
814
|
+
</ChoiceCard>
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
| Prop | Type | Default |
|
|
818
|
+
| -------- | --------- | ------- |
|
|
819
|
+
| `active` | `boolean` | `false` |
|
|
820
|
+
|
|
821
|
+
### `ChoiceGroup`
|
|
822
|
+
|
|
823
|
+
Radio-style list of `ChoiceCard`s built from a typed options array.
|
|
824
|
+
|
|
825
|
+
```tsx
|
|
826
|
+
type Direction = 'left' | 'right'
|
|
827
|
+
|
|
828
|
+
<ChoiceGroup<Direction>
|
|
829
|
+
options={[
|
|
830
|
+
{ value: 'left', label: 'Left to right' },
|
|
831
|
+
{ value: 'right', label: 'Right to left', description: 'Arabic, Hebrew' },
|
|
832
|
+
]}
|
|
833
|
+
value={direction}
|
|
834
|
+
onChange={setDirection}
|
|
835
|
+
/>
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
Each option:
|
|
839
|
+
|
|
840
|
+
```ts
|
|
841
|
+
type ChoiceOption<T> = {
|
|
842
|
+
value: T
|
|
843
|
+
label: ReactNode
|
|
844
|
+
description?: ReactNode
|
|
845
|
+
disabled?: boolean
|
|
846
|
+
}
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
## UI — Overlays
|
|
852
|
+
|
|
853
|
+
<p align="center">
|
|
854
|
+
<img src="https://raw.githubusercontent.com/MaxouJS/whiteboard/main/docs/images/panel-overlays.png" width="380" alt="ConfirmDialog, GeneratingOverlay, EmptyState, PanelErrorBoundary" />
|
|
855
|
+
</p>
|
|
856
|
+
|
|
857
|
+
> `ConfirmDialog` and `PanelErrorBoundary` are documented under [Whiteboard Components](#confirmdialog) — they're not purely UI primitives.
|
|
858
|
+
|
|
859
|
+
### `GeneratingOverlay`
|
|
860
|
+
|
|
861
|
+
Cover content with a semi-transparent layer + spinner while work is in progress.
|
|
862
|
+
|
|
863
|
+
```tsx
|
|
864
|
+
<GeneratingOverlay isGenerating={loading} message="Building…">
|
|
865
|
+
<YourContent />
|
|
866
|
+
</GeneratingOverlay>
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
| Prop | Type | Default |
|
|
870
|
+
| -------------- | ----------- | ----------------------------- |
|
|
871
|
+
| `isGenerating` | `boolean` | — |
|
|
872
|
+
| `message` | `string` | `'Generating, please wait…'` |
|
|
873
|
+
|
|
874
|
+
### `EmptyState`
|
|
875
|
+
|
|
876
|
+
Friendly placeholder for empty lists / first-run states.
|
|
877
|
+
|
|
878
|
+
```tsx
|
|
879
|
+
<EmptyState
|
|
880
|
+
title="No items yet"
|
|
881
|
+
description="Create your first item to get started."
|
|
882
|
+
action={<Button>Create item</Button>}
|
|
883
|
+
/>
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
| Prop | Type | Default |
|
|
887
|
+
| ------------- | ----------- | ------- |
|
|
888
|
+
| `title` | `ReactNode` | — |
|
|
889
|
+
| `description` | `ReactNode` | — |
|
|
890
|
+
| `action` | `ReactNode` | — |
|
|
891
|
+
|
|
892
|
+
---
|
|
893
|
+
|
|
894
|
+
## UI — Navigation & Canvas
|
|
895
|
+
|
|
896
|
+
<p align="center">
|
|
897
|
+
<img src="https://raw.githubusercontent.com/MaxouJS/whiteboard/main/docs/images/panel-navigation.png" width="380" alt="VerticalToolbar, AvatarBadge, CanvasStage, OverlayIconButton" />
|
|
898
|
+
</p>
|
|
899
|
+
|
|
900
|
+
### `VerticalToolbar`
|
|
901
|
+
|
|
902
|
+
A fixed vertical strip of icon buttons (think app-shell sidebar).
|
|
903
|
+
|
|
904
|
+
```tsx
|
|
905
|
+
<VerticalToolbar
|
|
906
|
+
position="left" // 'left' | 'right' | 'static'
|
|
907
|
+
bottom={<button className="vertical-toolbar__icon-btn">⎋</button>}
|
|
908
|
+
>
|
|
909
|
+
<a className="vertical-toolbar__icon-btn is-active" href="/"><DashboardIcon /></a>
|
|
910
|
+
<a className="vertical-toolbar__icon-btn" href="/stories"><BookIcon /></a>
|
|
911
|
+
</VerticalToolbar>
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
| Prop | Type | Default |
|
|
915
|
+
| ---------- | --------------------------------- | ------- |
|
|
916
|
+
| `position` | `'left' \| 'right' \| 'static'` | `'left'`|
|
|
917
|
+
| `bottom` | `ReactNode` | — |
|
|
918
|
+
|
|
919
|
+
### `AvatarBadge`
|
|
920
|
+
|
|
921
|
+
A circular initials/avatar badge.
|
|
922
|
+
|
|
923
|
+
```tsx
|
|
924
|
+
<AvatarBadge>MG</AvatarBadge>
|
|
925
|
+
<AvatarBadge title="Maxence">M</AvatarBadge>
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
### `CanvasStage`
|
|
929
|
+
|
|
930
|
+
A 16:9 bordered media container, ideal for previews and editor canvases.
|
|
931
|
+
|
|
932
|
+
```tsx
|
|
933
|
+
<CanvasStage hint="Tap to interact">
|
|
934
|
+
<YourMedia />
|
|
935
|
+
</CanvasStage>
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
| Prop | Type | Default |
|
|
939
|
+
| ------- | -------- | ------- |
|
|
940
|
+
| `hint` | `string` | — |
|
|
941
|
+
|
|
942
|
+
### `OverlayIconButton`
|
|
943
|
+
|
|
944
|
+
A secondary icon-only button positioned over media (zoom, prev/next, etc.). Stops pointer & wheel propagation so it doesn't pan the underlying canvas.
|
|
945
|
+
|
|
946
|
+
```tsx
|
|
947
|
+
<CanvasStage>
|
|
948
|
+
<OverlayIconButton placement="top-right" aria-label="Zoom">
|
|
949
|
+
<ZoomInIcon />
|
|
950
|
+
</OverlayIconButton>
|
|
951
|
+
<OverlayIconButton placement="bottom-left" aria-label="Previous">
|
|
952
|
+
<ChevronLeftIcon />
|
|
953
|
+
</OverlayIconButton>
|
|
954
|
+
<OverlayIconButton placement="bottom-right" aria-label="Next">
|
|
955
|
+
<ChevronRightIcon />
|
|
956
|
+
</OverlayIconButton>
|
|
957
|
+
</CanvasStage>
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
| Prop | Type | Default |
|
|
961
|
+
| ----------- | ------------------------------------------------- | ------- |
|
|
962
|
+
| `placement` | `'top-right' \| 'bottom-left' \| 'bottom-right'` | — |
|
|
963
|
+
| (all other `Button` props except `variant` / `iconOnly`) | — | — |
|
|
964
|
+
|
|
965
|
+
### `ImageThumb`
|
|
966
|
+
|
|
967
|
+
Image thumbnail with placeholder + error fallback.
|
|
968
|
+
|
|
969
|
+
```tsx
|
|
970
|
+
<ImageThumb src={url} alt="Cover" size="md" fit="cover" />
|
|
971
|
+
<ImageThumb src={null} alt="Empty" placeholder={<MutedText>No image</MutedText>} />
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
| Prop | Type | Default |
|
|
975
|
+
| -------------- | ----------------------------- | ------------- |
|
|
976
|
+
| `src` | `string \| null` | — |
|
|
977
|
+
| `alt` | `string` | — |
|
|
978
|
+
| `size` | `'sm' \| 'md' \| 'fluid'` | `'md'` |
|
|
979
|
+
| `fit` | `'contain' \| 'cover'` | `'contain'` |
|
|
980
|
+
| `placeholder` | `ReactNode` | `'No image'` |
|
|
981
|
+
| `onImageError` | `() => void` | — |
|
|
982
|
+
|
|
983
|
+
---
|
|
984
|
+
|
|
985
|
+
## UI — Skeletons
|
|
986
|
+
|
|
987
|
+
<p align="center">
|
|
988
|
+
<img src="https://raw.githubusercontent.com/MaxouJS/whiteboard/main/docs/images/panel-skeletons.png" width="820" alt="Skeleton primitives and widget skeletons" />
|
|
989
|
+
</p>
|
|
990
|
+
|
|
991
|
+
### `Skeleton` (base)
|
|
992
|
+
|
|
993
|
+
Animated placeholder. Use the primitives below for common shapes, or compose your own.
|
|
994
|
+
|
|
995
|
+
```tsx
|
|
996
|
+
<Skeleton style={{ width: 120, height: 16 }} />
|
|
997
|
+
<Skeleton radius="md" style={{ width: 80, height: 80 }} />
|
|
998
|
+
<Skeleton radius="pill" style={{ width: 64, height: 24 }} />
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
| Prop | Type | Default |
|
|
1002
|
+
| -------- | -------------------------- | ------- |
|
|
1003
|
+
| `radius` | `'sm' \| 'md' \| 'pill'` | `'sm'` |
|
|
1004
|
+
| `as` | `ElementType` | `'div'` |
|
|
1005
|
+
|
|
1006
|
+
### Primitive skeletons
|
|
1007
|
+
|
|
1008
|
+
Pre-sized shapes that line up with the corresponding component:
|
|
1009
|
+
|
|
1010
|
+
```tsx
|
|
1011
|
+
import {
|
|
1012
|
+
TitleSkeleton, // matches an asset / section title line
|
|
1013
|
+
LineSkeleton, // <LineSkeleton short /> for a half-width line
|
|
1014
|
+
InputSkeleton, // matches <Input>
|
|
1015
|
+
SelectSkeleton, // matches <Select>
|
|
1016
|
+
TextareaSkeleton, // matches <Textarea>
|
|
1017
|
+
ButtonSkeleton, // matches <Button>
|
|
1018
|
+
IconButtonSkeleton, // matches <Button iconOnly>
|
|
1019
|
+
ChipSkeleton, // matches <Chip> / <Pill>
|
|
1020
|
+
ThumbSkeleton, // matches <ImageThumb>
|
|
1021
|
+
AvatarSkeleton, // matches <AvatarBadge>
|
|
1022
|
+
CanvasSkeleton, // matches <CanvasStage>
|
|
1023
|
+
} from '@objectifthunes/whiteboard'
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
All accept native `div` props (`className`, `style`, …).
|
|
1027
|
+
|
|
1028
|
+
### Widget skeletons
|
|
1029
|
+
|
|
1030
|
+
Higher-level placeholders that mirror common card layouts.
|
|
1031
|
+
|
|
1032
|
+
```tsx
|
|
1033
|
+
import {
|
|
1034
|
+
PanelFormSkeleton, // <PanelFormSkeleton inputs={3} showButton />
|
|
1035
|
+
StoryCardSkeleton, // matches a hero story card
|
|
1036
|
+
UserCardSkeleton, // matches a user row
|
|
1037
|
+
UserListSkeleton, // <UserListSkeleton count={5} />
|
|
1038
|
+
AssetCardSkeleton, // matches an asset card with thumb + chips + actions
|
|
1039
|
+
PickerGridSkeleton, // <PickerGridSkeleton count={8} gridClass="picker-grid--elements" />
|
|
1040
|
+
} from '@objectifthunes/whiteboard'
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
`PanelFormSkeleton`
|
|
1044
|
+
|
|
1045
|
+
| Prop | Type | Default |
|
|
1046
|
+
| ------------ | --------- | ------- |
|
|
1047
|
+
| `inputs` | `number` | `1` |
|
|
1048
|
+
| `showButton` | `boolean` | `true` |
|
|
1049
|
+
|
|
1050
|
+
`UserListSkeleton`
|
|
1051
|
+
|
|
1052
|
+
| Prop | Type | Default |
|
|
1053
|
+
| ------- | -------- | ------- |
|
|
1054
|
+
| `count` | `number` | `3` |
|
|
1055
|
+
|
|
1056
|
+
`PickerGridSkeleton`
|
|
1057
|
+
|
|
1058
|
+
| Prop | Type | Default |
|
|
1059
|
+
| ----------- | -------- | ------- |
|
|
1060
|
+
| `count` | `number` | `8` |
|
|
1061
|
+
| `gridClass` | `string` | — |
|
|
1062
|
+
|
|
1063
|
+
### `ChoiceGroupSkeleton`
|
|
1064
|
+
|
|
1065
|
+
A skeleton form for `ChoiceGroup`.
|
|
1066
|
+
|
|
1067
|
+
```tsx
|
|
1068
|
+
<ChoiceGroupSkeleton count={4} withDescription />
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
| Prop | Type | Default |
|
|
1072
|
+
| ----------------- | --------- | ------- |
|
|
1073
|
+
| `count` | `number` | `4` |
|
|
1074
|
+
| `withDescription` | `boolean` | `false` |
|
|
1075
|
+
|
|
1076
|
+
---
|
|
1077
|
+
|
|
1078
|
+
## UI — Panel sections
|
|
1079
|
+
|
|
1080
|
+
### `PanelSection`
|
|
1081
|
+
|
|
1082
|
+
A semantic `<section>` with optional heading, description, and a right-aligned actions slot.
|
|
1083
|
+
|
|
1084
|
+
```tsx
|
|
1085
|
+
<PanelSection
|
|
1086
|
+
heading="Assets"
|
|
1087
|
+
description="Drag an asset onto the canvas."
|
|
1088
|
+
actions={<Button variant="secondary">Upload</Button>}
|
|
1089
|
+
>
|
|
1090
|
+
<ItemList>…</ItemList>
|
|
1091
|
+
</PanelSection>
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
| Prop | Type |
|
|
1095
|
+
| ------------- | ----------- |
|
|
1096
|
+
| `heading` | `ReactNode` |
|
|
1097
|
+
| `description` | `ReactNode` |
|
|
1098
|
+
| `actions` | `ReactNode` |
|
|
1099
|
+
|
|
1100
|
+
### `PanelTitle`
|
|
1101
|
+
|
|
1102
|
+
Title with a leading icon. Pass a **component type** (not an element):
|
|
1103
|
+
|
|
1104
|
+
```tsx
|
|
1105
|
+
import { LayersIcon } from 'lucide-react'
|
|
1106
|
+
|
|
1107
|
+
<FloatingPanel title={<PanelTitle icon={LayersIcon} label="Layers" />} … />
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
| Prop | Type |
|
|
1111
|
+
| ------- | ---------------------------------------------------------- |
|
|
1112
|
+
| `icon` | `ComponentType<{ size?: number; className?: string }>` |
|
|
1113
|
+
| `label` | `string` |
|
|
1114
|
+
|
|
1115
|
+
---
|
|
1116
|
+
|
|
1117
|
+
## Theming
|
|
1118
|
+
|
|
1119
|
+
### `ThemeToggle`
|
|
1120
|
+
|
|
1121
|
+
Controlled toggle that renders a moon / sun icon. Wire it to your own theme state.
|
|
1122
|
+
|
|
1123
|
+
```tsx
|
|
1124
|
+
const [theme, setTheme] = useState<'light' | 'dark'>('light')
|
|
1125
|
+
|
|
1126
|
+
useEffect(() => {
|
|
1127
|
+
document.documentElement.dataset.theme = theme
|
|
1128
|
+
}, [theme])
|
|
1129
|
+
|
|
1130
|
+
<WhiteboardShell
|
|
1131
|
+
extraActions={
|
|
1132
|
+
<ThemeToggle
|
|
1133
|
+
theme={theme}
|
|
1134
|
+
onToggle={() => setTheme(t => t === 'light' ? 'dark' : 'light')}
|
|
1135
|
+
/>
|
|
1136
|
+
}
|
|
1137
|
+
/>
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
| Prop | Type | Default |
|
|
1141
|
+
| ----------- | --------------------- | --------- |
|
|
1142
|
+
| `theme` | `'light' \| 'dark'` | `'light'` |
|
|
1143
|
+
| `onToggle` | `() => void` | — |
|
|
1144
|
+
| `lightIcon` | `ReactNode` | sun |
|
|
1145
|
+
| `darkIcon` | `ReactNode` | moon |
|
|
1146
|
+
| `className` | `string` | — |
|
|
1147
|
+
|
|
1148
|
+
### CSS & theming
|
|
1149
|
+
|
|
1150
|
+
The package ships a single stylesheet. Import it once at the root:
|
|
1151
|
+
|
|
1152
|
+
```tsx
|
|
1153
|
+
import '@objectifthunes/whiteboard/style.css'
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
Theming uses CSS custom properties on `[data-theme]`. The built-in styles expose tokens for surfaces, borders, text, and accents — you can override any of them on `:root` or scoped to `[data-theme="dark"]`:
|
|
1157
|
+
|
|
1158
|
+
```css
|
|
1159
|
+
:root {
|
|
1160
|
+
--wb-bg: #ffffff;
|
|
1161
|
+
--wb-fg: #0b0b0c;
|
|
1162
|
+
--wb-muted: #6c6c70;
|
|
1163
|
+
--wb-border: #e5e5e7;
|
|
1164
|
+
--wb-accent: #2563eb;
|
|
1165
|
+
/* …and more — inspect dist/whiteboard.css for the full set */
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
[data-theme='dark'] {
|
|
1169
|
+
--wb-bg: #0b0b0c;
|
|
1170
|
+
--wb-fg: #f5f5f7;
|
|
1171
|
+
--wb-muted: #9a9a9f;
|
|
1172
|
+
--wb-border: #2a2a2c;
|
|
1173
|
+
}
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
`<ThemeToggle />` is intentionally **uncontrolled** of the DOM — you decide where the `data-theme` attribute lives (usually `<html>`).
|
|
1177
|
+
|
|
1178
|
+
---
|
|
1179
|
+
|
|
1180
|
+
## TypeScript
|
|
1181
|
+
|
|
1182
|
+
All exports ship with `.d.ts` declarations. Notable re-exported types:
|
|
1183
|
+
|
|
1184
|
+
```ts
|
|
1185
|
+
import type { PanelRect, WhiteboardStore, ChoiceOption } from '@objectifthunes/whiteboard'
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
`PanelRect`:
|
|
1189
|
+
|
|
1190
|
+
```ts
|
|
1191
|
+
type PanelRect = {
|
|
1192
|
+
x: number
|
|
1193
|
+
y: number
|
|
1194
|
+
width: number
|
|
1195
|
+
height: number
|
|
1196
|
+
focusPadding?: number
|
|
1197
|
+
focusMaxScale?: number
|
|
1198
|
+
}
|
|
1199
|
+
```
|
|
1200
|
+
|
|
1201
|
+
`ChoiceOption<T extends string>`:
|
|
1202
|
+
|
|
1203
|
+
```ts
|
|
1204
|
+
type ChoiceOption<T extends string> = {
|
|
1205
|
+
value: T
|
|
1206
|
+
label: ReactNode
|
|
1207
|
+
description?: ReactNode
|
|
1208
|
+
disabled?: boolean
|
|
1209
|
+
}
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
---
|
|
1213
|
+
|
|
1214
|
+
## Recipes
|
|
1215
|
+
|
|
1216
|
+
### Track a panel's rect, then place another below it
|
|
1217
|
+
|
|
1218
|
+
```tsx
|
|
1219
|
+
const settingsRect = usePanelRect({ x: 40, y: 60 })
|
|
1220
|
+
|
|
1221
|
+
<FloatingPanel title="Settings" defaultPosition={{ x: 40, y: 60 }} trackRect={settingsRect}>
|
|
1222
|
+
…
|
|
1223
|
+
</FloatingPanel>
|
|
1224
|
+
|
|
1225
|
+
<FloatingPanel title="Layers" defaultPosition={belowPanel(settingsRect.current)}>
|
|
1226
|
+
…
|
|
1227
|
+
</FloatingPanel>
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
### Programmatic camera control
|
|
1231
|
+
|
|
1232
|
+
```tsx
|
|
1233
|
+
const fitToContent = useWhiteboardStore(s => s.fitToContent)
|
|
1234
|
+
const focusPanel = useWhiteboardStore(s => s.focusPanel)
|
|
1235
|
+
|
|
1236
|
+
<Button onClick={fitToContent}>Fit all</Button>
|
|
1237
|
+
<Button onClick={() => focusPanel({ x: 0, y: 0, width: 600, height: 400 }, { maxScale: 2 })}>
|
|
1238
|
+
Zoom hero
|
|
1239
|
+
</Button>
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
### Loading → real content swap
|
|
1243
|
+
|
|
1244
|
+
```tsx
|
|
1245
|
+
<FloatingPanel title="Assets" defaultPosition={pos}>
|
|
1246
|
+
{loading ? (
|
|
1247
|
+
<PanelFormSkeleton inputs={2} />
|
|
1248
|
+
) : (
|
|
1249
|
+
<PanelSection heading="Assets">
|
|
1250
|
+
<ItemList>…</ItemList>
|
|
1251
|
+
</PanelSection>
|
|
1252
|
+
)}
|
|
1253
|
+
</FloatingPanel>
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
### Custom shell without minimap
|
|
1257
|
+
|
|
1258
|
+
```tsx
|
|
1259
|
+
<WhiteboardShell showMinimap={false}>
|
|
1260
|
+
<FloatingPanel title="Solo" defaultPosition={{ x: 80, y: 80 }}>…</FloatingPanel>
|
|
1261
|
+
</WhiteboardShell>
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
---
|
|
1265
|
+
|
|
1266
|
+
## Browser support
|
|
1267
|
+
|
|
1268
|
+
Modern evergreen browsers. Uses `PointerEvent` and `ResizeObserver`; both are available everywhere except very old IE/Edge legacy.
|
|
1269
|
+
|
|
1270
|
+
---
|
|
1271
|
+
|
|
1272
|
+
## License
|
|
1273
|
+
|
|
1274
|
+
MIT © ObjectifThunes
|