@objectifthunes/whiteboard 0.2.7 → 0.3.0

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