@scrawl-board/board 0.1.0-beta.7 → 0.1.0-beta.8

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/dist/react.d.ts CHANGED
@@ -155,6 +155,21 @@ interface CustomObjectDefinition<Props extends JsonValue = JsonValue> {
155
155
  /** One pure, synchronous step per consecutive schema version. */
156
156
  migrate?: Readonly<Record<number, (oldProps: JsonValue) => JsonValue>>;
157
157
  describe(object: ReadonlyCustomObject<Props>, context: ObjectDescribeContext): BoardScene;
158
+ /**
159
+ * Optional point-level hit-test precision (Phase 8). Every custom object
160
+ * hit-tests against its bounding box (`fallback.bounds`) by default — this
161
+ * lets a non-rectangular shape (e.g. a circular card, an L-shaped region)
162
+ * reject a point that's inside that box but outside its actual visible
163
+ * silhouette, tightening a click/marquee/raycast hit to the shape's real
164
+ * outline. `point` is in this object's own local space — the same
165
+ * untransformed space `describe`'s returned geometry already lives in
166
+ * (the caller inverse-transforms the pointer's board point through
167
+ * `object.transform` before calling this). Absent means every point
168
+ * inside the bounding box hits, matching pre-Phase-8 behavior exactly.
169
+ * Rejecting a point here does not fall through to whatever's underneath —
170
+ * the gesture simply misses this object, same as clicking empty space.
171
+ */
172
+ hitTest?(object: ReadonlyCustomObject<Props>, point: BoardPoint): boolean;
158
173
  }
159
174
  interface SceneNodeBase {
160
175
  key: string;
@@ -188,6 +203,15 @@ interface SceneGroup extends SceneNodeBase {
188
203
  kind: "group";
189
204
  children: readonly BoardScene[];
190
205
  }
206
+ /**
207
+ * **No renderer or SVG-export interpreter exists for this node kind yet**
208
+ * (tracked as deferred work — see `renderer/shapes/customObjects.ts`'s
209
+ * `"path"` case). Returning a `ScenePath` from `describe()` renders nothing,
210
+ * exports nothing, and contributes no hit-test bounds — it neither errors
211
+ * nor emits a diagnostic. Until an interpreter ships, build custom shapes
212
+ * from `SceneRect`/`SceneEllipse`/`SceneGroup`/`SceneText`/`SceneImage`
213
+ * instead.
214
+ */
191
215
  interface ScenePath extends SceneNodeBase {
192
216
  kind: "path";
193
217
  /** SVG-style path data, board-local coordinates. */
@@ -343,8 +367,23 @@ interface Lockable {
343
367
  lockedByName?: string;
344
368
  }
345
369
 
370
+ /**
371
+ * Per-object visibility (Phase 8) — mirrors `itemLock.ts`'s `Lockable`
372
+ * pattern exactly, but simpler: unlike a lock, hidden state carries no
373
+ * holder/ownership concept, so there's no analogue to `LockHolder`/
374
+ * `canUnlockItem`. A hidden object stays fully present in the Document
375
+ * (still serializes, persists, syncs, undoes/redoes) — it just skips
376
+ * rendering and hit-testing/selection candidacy. `hidden` absent or
377
+ * `false` means visible; this keeps every pre-Phase-8 document (which has
378
+ * no `hidden` field on any object at all) implicitly fully visible with
379
+ * zero migration needed.
380
+ */
381
+ interface Hideable {
382
+ hidden?: boolean;
383
+ }
384
+
346
385
  /** A kitchen timer sitting on the board. Remaining time is derived, not ticked. */
347
- interface KitchenTimer extends Lockable {
386
+ interface KitchenTimer extends Lockable, Hideable {
348
387
  id: string;
349
388
  x: number;
350
389
  y: number;
@@ -358,7 +397,7 @@ interface KitchenTimer extends Lockable {
358
397
  runningSince?: number;
359
398
  }
360
399
 
361
- interface RectangleObject extends Lockable {
400
+ interface RectangleObject extends Lockable, Hideable {
362
401
  id: string;
363
402
  x: number;
364
403
  y: number;
@@ -380,7 +419,7 @@ interface RectangleObject extends Lockable {
380
419
  */
381
420
  rotation?: number;
382
421
  }
383
- interface EllipseObject extends Lockable {
422
+ interface EllipseObject extends Lockable, Hideable {
384
423
  id: string;
385
424
  x: number;
386
425
  y: number;
@@ -396,7 +435,7 @@ interface EllipseObject extends Lockable {
396
435
  }
397
436
  /** `"none"` is a plain line with no arrowhead; today's only real head shape is `"triangle"`. New head shapes extend this union without touching `ArrowObject`'s own fields. */
398
437
  type ArrowHeadStyle = "triangle" | "none";
399
- interface LineObject extends Lockable {
438
+ interface LineObject extends Lockable, Hideable {
400
439
  id: string;
401
440
  start: BoardPoint;
402
441
  end: BoardPoint;
@@ -404,7 +443,7 @@ interface LineObject extends Lockable {
404
443
  strokeWidth?: number;
405
444
  opacity?: number;
406
445
  }
407
- interface ArrowObject extends Lockable {
446
+ interface ArrowObject extends Lockable, Hideable {
408
447
  id: string;
409
448
  start: BoardPoint;
410
449
  end: BoardPoint;
@@ -423,7 +462,7 @@ interface ArrowObject extends Lockable {
423
462
  * `polygonStartAngle`, which encodes each side count's own vertex
424
463
  * orientation so the outline always matches the legacy drag-preview shape.
425
464
  */
426
- interface PolygonObject extends Lockable {
465
+ interface PolygonObject extends Lockable, Hideable {
427
466
  id: string;
428
467
  x: number;
429
468
  y: number;
@@ -438,7 +477,7 @@ interface PolygonObject extends Lockable {
438
477
  rotation?: number;
439
478
  }
440
479
  /** Same bounding-box/rotation convention as Rectangle; a 5-pointed star with a tuned inner-radius ratio, matching the legacy tool's own default (see `polygonGeometry.ts`'s `starPoints`). */
441
- interface StarObject extends Lockable {
480
+ interface StarObject extends Lockable, Hideable {
442
481
  id: string;
443
482
  x: number;
444
483
  y: number;
@@ -455,7 +494,7 @@ interface StarObject extends Lockable {
455
494
  rotation?: number;
456
495
  }
457
496
  /** Same bounding-box/rotation convention as Rectangle; the standard parametric heart curve (see `polygonGeometry.ts`'s `heartPoints`), no extra parameters beyond the shared shape fields. */
458
- interface HeartObject extends Lockable {
497
+ interface HeartObject extends Lockable, Hideable {
459
498
  id: string;
460
499
  x: number;
461
500
  y: number;
@@ -480,7 +519,7 @@ interface HeartObject extends Lockable {
480
519
  * a group into its leaf members is always done by the caller (recursively,
481
520
  * with cycle protection), never assumed here.
482
521
  */
483
- interface GroupObject extends Lockable {
522
+ interface GroupObject extends Lockable, Hideable {
484
523
  id: string;
485
524
  children: string[];
486
525
  }
@@ -505,7 +544,7 @@ interface StrokePoint extends BoardPoint {
505
544
  * geometric outline, not an expressive ink mark.
506
545
  */
507
546
  type StrokeTool = "marker" | "highlighter" | "shape";
508
- interface Stroke extends Lockable {
547
+ interface Stroke extends Lockable, Hideable {
509
548
  id: string;
510
549
  color: string;
511
550
  baseWidth: number;
@@ -521,7 +560,7 @@ interface Stroke extends Lockable {
521
560
  clusterId?: string;
522
561
  }
523
562
  type SerializedPoint = [number, number, number, number];
524
- interface SerializedStroke extends Lockable {
563
+ interface SerializedStroke extends Lockable, Hideable {
525
564
  id: string;
526
565
  color: string;
527
566
  baseWidth: number;
@@ -585,7 +624,7 @@ interface NoteVote {
585
624
  * A sticky note: content floating above the board at a z-offset (pillar 3 —
586
625
  * depth as an organizational axis). Center position in board space.
587
626
  */
588
- interface StickyNote extends Lockable {
627
+ interface StickyNote extends Lockable, Hideable {
589
628
  id: string;
590
629
  x: number;
591
630
  y: number;
@@ -603,7 +642,7 @@ interface StickyNote extends Lockable {
603
642
  * top-left corner; lines flow downward (-y). Text joins the clustering
604
643
  * system like handwriting (build prompt §6.4).
605
644
  */
606
- interface TextBlock extends Lockable {
645
+ interface TextBlock extends Lockable, Hideable {
607
646
  id: string;
608
647
  x: number;
609
648
  y: number;
@@ -617,7 +656,7 @@ interface TextBlock extends Lockable {
617
656
  * Interactive structured table on the board. Position (x, y) is top-left in board units.
618
657
  * Cells are indexed as `${row},${col}` keys mapping to cell text content.
619
658
  */
620
- interface TableBlock extends Lockable {
659
+ interface TableBlock extends Lockable, Hideable {
621
660
  id: string;
622
661
  x: number;
623
662
  y: number;
@@ -634,7 +673,7 @@ interface TableBlock extends Lockable {
634
673
  * An imported image block on the board plane.
635
674
  * Coordinates (x, y) represent the center of the image in board space.
636
675
  */
637
- interface ImageBlock extends Lockable {
676
+ interface ImageBlock extends Lockable, Hideable {
638
677
  id: string;
639
678
  /**
640
679
  * A legacy, read-only data URL (or, historically, an arbitrary string) —
@@ -845,6 +884,8 @@ interface BoardEventMap {
845
884
  "asset-diagnostic": AssetDiagnostic;
846
885
  /** A batch of Ops was reconciled (not applied as-sent) by the persistence adapter (ticket #24). */
847
886
  "persistence-diagnostic": PersistenceDiagnostic;
887
+ /** The collaboration server has confirmed receipt of these op ids (Phase 7) — observability only; the Document was already correct via optimistic local apply before this ever fires. */
888
+ "collaboration-ops-acknowledged": CollaborationAckDiagnostic;
848
889
  audit: unknown;
849
890
  error: BoardControllerError;
850
891
  disposed: undefined;
@@ -908,9 +949,12 @@ interface PresenceView {
908
949
  readonly height: number;
909
950
  }
910
951
  /**
911
- * A Host-owned collaborator, synced in for cursor/roster rendering only.
912
- * Presence is ephemeral — it never touches the Document, Ops, undo/redo,
913
- * or persistence — so this is a read/query capability, not an adapter.
952
+ * A collaborator, synced in for cursor/roster rendering only. Presence is
953
+ * ephemeral — it never touches the Document, Ops, undo/redo, or persistence
954
+ * (ADR 0006/0007). Two ways a roster gets populated (`presence.sync`
955
+ * directly, or a `CollaborationAdapter`'s optional presence channel —
956
+ * Phase 6, ADR 0015) both feed the exact same read/query capability below;
957
+ * a Host picks one, not both, for a given controller.
914
958
  */
915
959
  interface PresenceUser {
916
960
  readonly id: string;
@@ -919,6 +963,14 @@ interface PresenceUser {
919
963
  readonly tool?: string;
920
964
  readonly cursor?: PresenceCursor;
921
965
  readonly view?: PresenceView;
966
+ /** Host-supplied extras (avatar URL, role, etc.) — opaque to Scrawl, never interpreted. */
967
+ readonly metadata?: Record<string, unknown>;
968
+ }
969
+ /** This client's own local presence, published via `presence.broadcast()` (Phase 6). */
970
+ interface LocalPresence {
971
+ readonly cursor?: PresenceCursor | null;
972
+ readonly view?: PresenceView | null;
973
+ readonly tool?: string;
922
974
  }
923
975
  /**
924
976
  * The Custom arm wraps `CustomBoardObject` under the same `type` discriminant
@@ -1044,10 +1096,34 @@ type LoadResult = {
1044
1096
  } | {
1045
1097
  state: "missing";
1046
1098
  };
1099
+ /**
1100
+ * Result of a whole-document `PersistenceAdapter.replace()` call (ADR 0006:
1101
+ * "Whole-document writes survive only for create, clear-board and import,
1102
+ * where replacing everything is the actual intent"). Revision-gated, unlike
1103
+ * `applyOps` — `conflict` means `baseRevision` was stale (someone else's
1104
+ * write landed first); the caller must reload and never overwrites blind.
1105
+ */
1106
+ type ReplaceResult = {
1107
+ state: "applied";
1108
+ revision: string;
1109
+ } | {
1110
+ state: "conflict";
1111
+ currentRevision: string;
1112
+ };
1113
+ /**
1114
+ * The one sanctioned seam for persisting a Board's Document to a Host's own
1115
+ * storage — implement this against a database, an HTTP API, IndexedDB
1116
+ * (see `@scrawl-board/board/local`'s `createIndexedDBPersistence`), or
1117
+ * anything else. `load()` fetches the current state on connect; `applyOps()`
1118
+ * streams incremental Ops as edits happen; `replace()` is only for
1119
+ * whole-document writes (create, clear-board, import — see ADR 0006) and is
1120
+ * revision-gated so a stale write never silently clobbers a newer one.
1121
+ * Passed via `createBoardController({ adapters: { persistence } })`.
1122
+ */
1047
1123
  interface PersistenceAdapter {
1048
1124
  load(context: DocumentContext): Promise<LoadResult>;
1049
1125
  applyOps(context: DocumentContext, ops: readonly ControllerOp[]): Promise<ApplyOpsResult>;
1050
- replace(context: DocumentContext, document: CurrentSerializedDocument, baseRevision: string): Promise<unknown>;
1126
+ replace(context: DocumentContext, document: CurrentSerializedDocument, baseRevision: string): Promise<ReplaceResult>;
1051
1127
  }
1052
1128
  /**
1053
1129
  * `"reconcile"` (ticket #24) means the server authoritatively resolved the
@@ -1071,6 +1147,10 @@ interface PersistenceDiagnostic {
1071
1147
  rejectedOpIds: readonly string[];
1072
1148
  revision: string;
1073
1149
  }
1150
+ /** Emitted as `"collaboration-ops-acknowledged"` (Phase 7) — the server has confirmed receipt of these op ids on the live pipe. */
1151
+ interface CollaborationAckDiagnostic {
1152
+ opIds: readonly string[];
1153
+ }
1074
1154
  interface ControllerOp {
1075
1155
  id: string;
1076
1156
  schemaVersion: 1;
@@ -1087,7 +1167,39 @@ interface ControllerOp {
1087
1167
  | "order";
1088
1168
  objectId: string;
1089
1169
  payload?: unknown;
1170
+ /**
1171
+ * This op's position in its own originating client's local sequence
1172
+ * (Phase 7) — 1, 2, 3, ... per controller instance, distinct from `id`
1173
+ * (an opaque, globally-unique identifier used for dedup/ack, not
1174
+ * ordering) and from a server's own authoritative ordering (e.g.
1175
+ * `referenceCollaborationServer.ts`'s per-room `version` counter).
1176
+ * Present on every op this SDK originates locally; a remote peer's op
1177
+ * carries whatever its own origin set, unchanged — never renumbered in
1178
+ * transit. Absent on an op minted by decoding the legacy wire envelope
1179
+ * (`scrawlOpEnvelope.ts`), which predates this field and has no
1180
+ * per-client sequence concept of its own.
1181
+ */
1182
+ clientSequence?: number;
1183
+ /**
1184
+ * The `CollaboratorIdentity.id` of this op's originating client (Phase
1185
+ * 7) — set for every op this SDK originates locally when `identity` is
1186
+ * configured, omitted entirely otherwise (never sent as `undefined`).
1187
+ * The explicit foundation for a future per-author undo filter (a local
1188
+ * user's own undo should only ever touch their own ops) — no undo-stack
1189
+ * behavior itself changes this phase.
1190
+ */
1191
+ clientId?: string;
1090
1192
  }
1193
+ /**
1194
+ * The one sanctioned seam for real-time multiplayer — implement this against
1195
+ * a Host's own collaboration backend (WebSocket relay, CRDT server, etc.).
1196
+ * `connect()` is called once per controller with the local user's
1197
+ * `identity` and a `receive` callback the adapter invokes with incoming
1198
+ * Ops, presence updates, acks, and connection status; it resolves with a
1199
+ * `CollaborationSession` the controller uses to send local Ops and presence
1200
+ * back out. Passed via `createBoardController({ adapters: { collaboration } })`;
1201
+ * omit it entirely to run single-player.
1202
+ */
1091
1203
  interface CollaborationAdapter {
1092
1204
  connect(options: DocumentContext & {
1093
1205
  identity: CollaboratorIdentity;
@@ -1098,14 +1210,72 @@ interface CollaboratorIdentity {
1098
1210
  id: string;
1099
1211
  name: string;
1100
1212
  color?: string;
1213
+ /** Host-supplied extras (avatar URL, role, etc.) — opaque to Scrawl, forwarded into any resulting `PresenceUser` unread and never interpreted. */
1214
+ metadata?: Record<string, unknown>;
1101
1215
  }
1102
1216
  interface CollaborationReceiver {
1103
1217
  ops(ops: readonly ControllerOp[]): void;
1218
+ /**
1219
+ * The current presence roster (Phase 6, ADR 0015) — always a full
1220
+ * replacement, never a delta, matching `presence.sync`'s existing
1221
+ * semantics exactly (an adapter that aggregates wire deltas into a full
1222
+ * roster before calling this is the adapter's own job, not the
1223
+ * controller's). Required on this interface (not optional) because a
1224
+ * Host only ever *consumes* `CollaborationReceiver` — never implements
1225
+ * it — so adding a required method here cannot break an existing custom
1226
+ * `CollaborationAdapter`. An adapter with no presence support simply
1227
+ * never calls it.
1228
+ */
1229
+ presence(users: readonly PresenceUser[]): void;
1230
+ /**
1231
+ * The server has confirmed receipt of these op ids (Phase 7) —
1232
+ * distinguishes "sent" from "server accepted," which `sendOps` alone
1233
+ * (fire-and-forget) cannot. Required for the same reason `presence` is:
1234
+ * Hosts only ever consume this interface, never implement it, so this
1235
+ * cannot break an existing custom `CollaborationAdapter`. An adapter with
1236
+ * no ack support simply never calls it — the collaboration pipe still
1237
+ * works exactly as it did before this existed, just without the
1238
+ * bookkeeping/observability this enables.
1239
+ */
1240
+ acknowledged(opIds: readonly string[]): void;
1104
1241
  status(state: "online" | "reconnecting" | "offline"): void;
1105
1242
  error(cause: unknown): void;
1106
1243
  }
1244
+ /**
1245
+ * Result of `CollaborationSession.requestSync()` (Phase 7). `"ops"` means
1246
+ * the adapter's own live-pipe cache fully covered the gap since the
1247
+ * caller's last known revision — apply `ops` and the client is caught up,
1248
+ * no persistence reload needed. `"unavailable"` means it couldn't (gap too
1249
+ * large, server restarted, or the adapter has no retained history at all)
1250
+ * — the caller must fall back to a persistence-backed reload. This is a
1251
+ * best-effort *liveness* cache, deliberately never a durable source of
1252
+ * truth (ADR 0006's "collaboration is never a second source of document
1253
+ * truth" — see ADR 0015's own extension of that principle to presence,
1254
+ * now extended once more, the same way, to this).
1255
+ */
1256
+ type CollaborationSyncResult = {
1257
+ state: "ops";
1258
+ ops: readonly ControllerOp[];
1259
+ serverRevision: string;
1260
+ } | {
1261
+ state: "unavailable";
1262
+ };
1107
1263
  interface CollaborationSession {
1108
1264
  sendOps(ops: readonly ControllerOp[]): void;
1265
+ /**
1266
+ * Publishes this client's own local presence (Phase 6, ADR 0015) —
1267
+ * best-effort, unordered, never persisted, never an Op. Optional: an
1268
+ * adapter that doesn't support presence simply omits this method, and
1269
+ * `presence.broadcast()` becomes a silent no-op.
1270
+ */
1271
+ updatePresence?(presence: LocalPresence): void;
1272
+ /**
1273
+ * Requests an incremental catch-up after a reconnect (Phase 7) — optional;
1274
+ * an adapter that doesn't support this simply omits the method, and the
1275
+ * caller (`resyncAfterReconnect`) goes straight to its existing
1276
+ * persistence-backed full reload, unchanged from Phase 6.
1277
+ */
1278
+ requestSync?(): Promise<CollaborationSyncResult>;
1109
1279
  close(): Promise<void>;
1110
1280
  }
1111
1281
  interface CreateBoardControllerOptions {
@@ -1145,6 +1315,87 @@ interface CreateBoardControllerOptions {
1145
1315
  * doesn't use the React theme system can set this directly instead.
1146
1316
  */
1147
1317
  boardTheme?: BoardThemeOptions;
1318
+ /**
1319
+ * Debounced auto-flush of pending persistence Ops after document changes
1320
+ * settle (Phase 5). Enabled by default (1000ms debounce) whenever
1321
+ * `adapters.persistence` is configured — today, without this, a Host must
1322
+ * call `flush()` manually after every edit for anything to persist. Pass
1323
+ * `false` to opt out entirely and drive `flush()` yourself, preserving
1324
+ * prior behavior exactly. Never fires on a per-change basis — rapid edits
1325
+ * coalesce into one flush of their final state (ADR 0006).
1326
+ */
1327
+ autosave?: boolean | {
1328
+ debounceMs?: number;
1329
+ };
1330
+ /**
1331
+ * Throttle for `presence.broadcast()` (Phase 6, ADR 0015) — the minimum
1332
+ * interval between outgoing presence updates sent via the configured
1333
+ * `CollaborationAdapter`. Defaults to 50ms. A trailing throttle: the
1334
+ * latest value passed to `broadcast()` always eventually sends, even if
1335
+ * calls arrive faster than this interval.
1336
+ */
1337
+ presenceThrottleMs?: number;
1338
+ /**
1339
+ * Caps how many `ControllerOp`s can sit queued, unsent, for the
1340
+ * persistence pipe (`pendingOps`) or the collaboration pipe
1341
+ * (`pendingCollaborationOps`) at once (Phase 7) — each pipe is capped
1342
+ * independently. Prevents unbounded memory growth from a long-lived
1343
+ * offline session or a stuck adapter. Exceeding it never fails or drops
1344
+ * the local edit itself (the Document already applied it optimistically)
1345
+ * — only queueing for that one pipe is skipped, and a
1346
+ * `{code:"queue-overflow", retryable:false}` error is emitted so a Host
1347
+ * can react. Defaults to 1000 — the Phase 9 collaboration coalescing
1348
+ * above (`collaborationCoalesceMs`) already keeps a busy drag from
1349
+ * approaching this on its own, so hitting it in practice means a pipe
1350
+ * has been offline/stuck for a genuinely long editing session.
1351
+ *
1352
+ * **Recovery** (Phase 9): the dropped op itself is gone from that one
1353
+ * pipe's queue — there is no automatic backfill, and the live
1354
+ * controller keeps running with that pipe now silently missing one
1355
+ * edit. Two things stay true regardless: (1) the in-memory Document is
1356
+ * never affected — a queue-overflow can never corrupt or roll back a
1357
+ * local edit, only skip sending it; (2) staleness is per-object, not
1358
+ * permanent — any *later* edit to that same object produces a brand
1359
+ * new, undropped Op carrying its full current state, which naturally
1360
+ * supersedes the gap (the Op model is already last-write-wins/
1361
+ * idempotent, so a superseding Op doesn't need the earlier one to have
1362
+ * arrived). The real risk is an object that's dropped and never edited
1363
+ * again before the controller is disposed or the page reloads — a Host
1364
+ * that needs strict durability should treat `queue-overflow` as a
1365
+ * signal to check `persistence.state`/`pendingOps` pressure (via
1366
+ * `usePersistenceStatus`/`getSnapshot().connection.persistence`)
1367
+ * before disposing, not assume disposing and reconnecting alone
1368
+ * repairs the gap (a fresh `load()` only returns what the backend
1369
+ * already has, which is exactly what's missing the dropped edit).
1370
+ */
1371
+ maxPendingOps?: number;
1372
+ /**
1373
+ * Coalescing window (ms) for outgoing collaboration Ops (Phase 9) — same
1374
+ * trailing-throttle shape as `presenceThrottleMs`: the first Op after an
1375
+ * idle period sends immediately, and subsequent Ops for the *same*
1376
+ * object within this window replace each other (latest value wins,
1377
+ * matching the already-idempotent Op model) rather than each triggering
1378
+ * its own send. A multi-second drag that previously sent one full Op per
1379
+ * pointer-move now sends at most one per window per touched object.
1380
+ * Persistence (`pendingOps`) is unaffected — it already debounces via
1381
+ * `autosave`, so this option only changes live collaboration traffic.
1382
+ * Defaults to 50ms.
1383
+ */
1384
+ collaborationCoalesceMs?: number;
1385
+ /**
1386
+ * Caps how many resolved objects a single `content.copy`/`content.cut`
1387
+ * (or their Cmd/Ctrl+C/X keyboard equivalents) will hold in the
1388
+ * in-memory clipboard at once (Phase 9) — `expandSelection` recursively
1389
+ * expands groups, so an unbounded selection (a huge group, or thousands
1390
+ * of individually selected strokes) could otherwise clone and retain an
1391
+ * arbitrarily large snapshot indefinitely, until the next copy/cut
1392
+ * replaces it. Exceeding it rejects the whole copy/cut (nothing is
1393
+ * cloned, and — for cut — nothing is removed from the Document either,
1394
+ * never a partial copy of an arbitrary subset) and emits a
1395
+ * `{code:"clipboard-overflow", retryable:false}` error. Defaults to
1396
+ * 5000.
1397
+ */
1398
+ maxClipboardItems?: number;
1148
1399
  }
1149
1400
  interface BoardController {
1150
1401
  readonly document: ReadonlyBoardDocument;
@@ -1176,6 +1427,15 @@ interface BoardController {
1176
1427
  };
1177
1428
  readonly view: {
1178
1429
  fit(): void;
1430
+ /**
1431
+ * Frame the current selection (Phase 8), the same way `fit()` frames the
1432
+ * whole board. A no-op with nothing selected — deliberately doesn't fall
1433
+ * back to `fit()`'s "frame everything," which would be a surprising
1434
+ * result for an empty selection. On a headless board this can only
1435
+ * re-center the view (no viewport to compute a real zoom-to-fit from),
1436
+ * matching `fit()`'s own headless limitation exactly.
1437
+ */
1438
+ zoomToSelection(): void;
1179
1439
  zoomTo(value: number): void;
1180
1440
  centerOn(point: BoardPoint): void;
1181
1441
  get(): BoardView;
@@ -1247,6 +1507,14 @@ interface BoardController {
1247
1507
  * aren't listed separately. `[]` when the clipboard is empty.
1248
1508
  */
1249
1509
  paste(): readonly string[];
1510
+ /**
1511
+ * Select every top-level object (Phase 8) — a group's own id stands for
1512
+ * its children, which aren't selected separately, matching `paste`'s own
1513
+ * "what the user sees" id list. Hidden objects are excluded, consistent
1514
+ * with them already being excluded from marquee selection. Works with
1515
+ * no canvas/engine, same as {@link toggleSelectionVisibility}.
1516
+ */
1517
+ selectAll(): void;
1250
1518
  table: {
1251
1519
  addRow(tableId: string): void;
1252
1520
  addCol(tableId: string): void;
@@ -1265,6 +1533,21 @@ interface BoardController {
1265
1533
  * another collaborator who isn't the current lock holder.
1266
1534
  */
1267
1535
  toggleSelectionLock(): void;
1536
+ /**
1537
+ * Toggle hidden state for the current selection, as one undo entry
1538
+ * (Phase 8). If any selected object is hidden, shows every selected
1539
+ * object; otherwise hides them all — same "any wins" semantics as
1540
+ * {@link toggleSelectionLock}. Hidden objects stay fully present in the
1541
+ * document (they still serialize, persist, sync, undo/redo) — they just
1542
+ * stop rendering and stop being hit-testable/selectable via pointer
1543
+ * interaction. Unlike `toggleSelectionLock`, this works on a headless
1544
+ * board too: it only touches `selection`/the document, no canvas or
1545
+ * engine involved. Custom objects have no visibility concept (no
1546
+ * `Hideable` field) and are silently skipped, matching how
1547
+ * `toggleSelectionLock` already excludes them. A no-op with nothing
1548
+ * selected or when the selection is only custom objects.
1549
+ */
1550
+ toggleSelectionVisibility(): void;
1268
1551
  };
1269
1552
  readonly query: {
1270
1553
  get(id: string): DeepReadonly<BoardObject> | undefined;
@@ -1293,6 +1576,15 @@ interface BoardController {
1293
1576
  follow(view: PresenceView): void;
1294
1577
  /** Ease the camera to a peer's view; returns false (no-op) while mid-stroke. */
1295
1578
  gather(view: PresenceView): boolean;
1579
+ /**
1580
+ * Publishes this client's own cursor/tool/view for other collaborators
1581
+ * (Phase 6, ADR 0015), via the configured `CollaborationAdapter` —
1582
+ * throttled internally (`presenceThrottleMs` option, default 50ms) so
1583
+ * a raw pointermove stream never becomes a message-per-event flood. A
1584
+ * no-op if no collaboration adapter is configured, or if the
1585
+ * configured one doesn't implement `updatePresence`.
1586
+ */
1587
+ broadcast(local: LocalPresence): void;
1296
1588
  };
1297
1589
  readonly export: {
1298
1590
  svg(): string;
@@ -1318,6 +1610,7 @@ interface BoardController {
1318
1610
  }>;
1319
1611
  dispose(): Promise<void>;
1320
1612
  }
1613
+ /** @deprecated Use `BoardController.getSnapshot()`'s return type instead. */
1321
1614
  type LocalBoardSnapshot = {
1322
1615
  documentId: string;
1323
1616
  selectedStrokeId: string | null;
@@ -1326,6 +1619,7 @@ type LocalBoardSnapshot = {
1326
1619
  canRedo: boolean;
1327
1620
  disposed: boolean;
1328
1621
  };
1622
+ /** @deprecated Use `BoardController` (from `createBoardController`) instead — this stroke-only, single-tool surface predates the full capability-grouped controller. */
1329
1623
  type LocalBoard = {
1330
1624
  drawStroke(stroke: Stroke): void;
1331
1625
  selectAt(point: BoardPoint): string | null;
@@ -1579,6 +1873,16 @@ interface ScrawlProviderProps {
1579
1873
  className?: string;
1580
1874
  style?: ThemeStyle;
1581
1875
  }
1876
+ /**
1877
+ * Wraps a Board `controller` you created yourself (via `createBoardController`)
1878
+ * so its descendants can use `useScrawlController`/`useScrawlSnapshot`/etc.
1879
+ * and render its default UI pieces (`DefaultBoardChrome`, `StyleShelf`, ...).
1880
+ * Prefer `Scrawl` unless you need to construct or own the controller's
1881
+ * lifecycle yourself (e.g. you create it outside React, or need it before
1882
+ * first render). Set `disposeOnUnmount` to have this provider call
1883
+ * `controller.dispose()` on unmount; otherwise disposal remains your own
1884
+ * responsibility.
1885
+ */
1582
1886
  declare function ScrawlProvider({ controller, children, preset, theme, portalContainer: customPortal, disposeOnUnmount, onThemeDiagnostic, className, style }: ScrawlProviderProps): react.JSX.Element;
1583
1887
  interface ScrawlProps extends Omit<CreateBoardControllerOptions, "canvas"> {
1584
1888
  children?: ReactNode;
@@ -1599,6 +1903,17 @@ interface ScrawlProps extends Omit<CreateBoardControllerOptions, "canvas"> {
1599
1903
  */
1600
1904
  icons?: Partial<Record<BuiltInTool, ReactNode>>;
1601
1905
  }
1906
+ /**
1907
+ * The fastest path to an embedded Board: creates and owns a
1908
+ * `BoardController` for you (constructed once, disposed on unmount) and
1909
+ * renders it into a canvas. Render with no `children` to get the SDK's
1910
+ * default toolbar/UI chrome, or supply your own `children` (using the
1911
+ * `useScrawlController`/`useScrawlSnapshot` hooks, or the exported
1912
+ * `DefaultBoardChrome`/`StyleShelf`/etc. pieces) to build a custom UI on
1913
+ * top of the same controller. Accepts every `CreateBoardControllerOptions`
1914
+ * field except `canvas` (pass `canvas={null}` for a headless board, or an
1915
+ * existing `<canvas>` element to control its identity yourself).
1916
+ */
1602
1917
  declare function Scrawl({ children, preset, theme, portalContainer, className, style, onReady, onError, onThemeDiagnostic, canvas: suppliedCanvas, icons, ...options }: ScrawlProps): react.JSX.Element;
1603
1918
  interface ScrawlCanvasProps {
1604
1919
  element?: HTMLCanvasElement;
@@ -1624,6 +1939,38 @@ declare function ScrawlPortal({ children }: {
1624
1939
  declare function useScrawlController(): BoardController;
1625
1940
  declare function useScrawlTheme(): ScrawlResolvedTheme;
1626
1941
  declare function useScrawlSnapshot(): BoardSnapshot;
1942
+ /**
1943
+ * A thin, purely-derived convenience over
1944
+ * `useScrawlSnapshot().connection.persistence` (Phase 5) — for a Host
1945
+ * component that only cares about save status (e.g. a
1946
+ * "Saving…"/"Saved"/"Offline" indicator) and would otherwise re-derive
1947
+ * this same field access itself. Adds no state and no behavior of its
1948
+ * own: React still owns none of persistence, exactly as before — this
1949
+ * hook only re-reads what the controller already tracks.
1950
+ *
1951
+ * Deliberately does *not* go through `useScrawlSnapshot()` (Phase 9):
1952
+ * `changed()` rebuilds the whole `BoardSnapshot` — including a fresh
1953
+ * `connection.persistence` object — on every document mutation, even ones
1954
+ * that never touch persistence at all, so a component using only this
1955
+ * hook would otherwise re-render on every stroke draw. `useSyncStatus`
1956
+ * below memoizes on the value (`state`/`error`), not just the object
1957
+ * reference, and returns the same cached value across renders where
1958
+ * nothing relevant changed — the same shape `usePresence` already uses
1959
+ * for its own independently-scoped store.
1960
+ */
1961
+ declare function usePersistenceStatus(): PersistenceSnapshot;
1962
+ /** The `CollaborationSnapshot` equivalent of `usePersistenceStatus` (Phase 6) — same value-memoized convenience over `useScrawlSnapshot().connection.collaboration` (Phase 9: see `usePersistenceStatus`'s doc comment for why it doesn't derive from the full snapshot). */
1963
+ declare function useCollaborationStatus(): CollaborationSnapshot;
1964
+ /**
1965
+ * The current presence roster (Phase 6), live-updating — wraps
1966
+ * `controller.presence.list()`/`subscribe` the same way `useScrawlSnapshot`
1967
+ * wraps the controller's main snapshot, via `useSyncExternalStore`. Does
1968
+ * not re-render on every remote pointer move by itself; it re-renders
1969
+ * whenever the roster the controller already tracks changes, at whatever
1970
+ * rate that arrives at (throttled adapter-side, per `presenceThrottleMs`).
1971
+ */
1972
+ declare function usePresence(): readonly PresenceUser[];
1973
+ /** @deprecated Use `Scrawl` instead — this predates the full capability-grouped `BoardController` and only exposes the stroke-only `LocalBoard` surface. */
1627
1974
  type ScrawlBoardProps = {
1628
1975
  documentId: string;
1629
1976
  initialDocument?: SerializedBoardDocument;
@@ -1631,7 +1978,8 @@ type ScrawlBoardProps = {
1631
1978
  className?: string;
1632
1979
  style?: CSSProperties;
1633
1980
  };
1981
+ /** @deprecated Use `Scrawl` instead — this predates the full capability-grouped `BoardController` and only exposes the stroke-only `LocalBoard` surface. */
1634
1982
  declare function ScrawlBoard({ documentId, initialDocument, onReady, className, style }: ScrawlBoardProps): react.JSX.Element;
1635
1983
 
1636
- export { AssetResolutionError, DefaultBoardChrome, FocusedItemToolbar, InlineEditors, MultiplayerCursors, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, StyleShelf, assetRef, clampAssetCacheBytes, cloneCustomObject, isAssetRef, resolveScrawlTheme, scrawlThemePresets, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
1637
- export type { AssetDiagnostic, AssetIngestRequest, AssetIngestResult, AssetIngestor, AssetKind, AssetPurpose, AssetRef, AssetResolutionErrorCode, AssetResolveRequest, AssetResolveResult, AssetResolver, BoardController, BoardKeyInput, BoardPointerInput, BoardScene, BoardSlotProps, BoardSnapshot, BoardStyle, BoardThemeOptions, CommentMarker, CreateBoardControllerOptions, CustomBoardObject, CustomObjectAddInput, CustomObjectDefinition, CustomTool, CustomToolDefinition, DefaultBoardChromeProps, DefaultUIRegion, DefaultUISlot, DefaultUISlots, DialogSlotProps, ExtensionCommand, ExtensionDiagnostic, ExtensionHitResult, ExtensionId, ExtensionRequirement, FocusedItem, FocusedItemToolbarProps, InlineEditorsProps, InputModifiers, JsonObject, JsonValue, LocalBoard, LocalBoardSnapshot, Mat2x3, MultiplayerCursorsProps, ObjectDescribeContext, ObjectIntent, ObjectType, PresenceCursor, PresenceUser, PresenceView, QueryableBoardObject, ReadonlyCustomObject, SceneEllipse, SceneGroup, SceneImage, ScenePath, SceneRect, SceneText, ScrawlBoardProps, ScrawlCanvasProps, ScrawlDefaultUIProps, ScrawlDensity, ScrawlExtension, ScrawlGridMode, ScrawlProps, ScrawlProviderProps, ScrawlResolvedTheme, ScrawlSurfaceTexture, ScrawlTheme, ScrawlThemeDiagnostic, ScrawlThemePreset, StyleShelfProps, SupportedAssetMediaType, ToolCancelReason, ToolCapabilities, ToolCursor, ToolId };
1984
+ export { AssetResolutionError, DefaultBoardChrome, FocusedItemToolbar, InlineEditors, MultiplayerCursors, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, StyleShelf, assetRef, clampAssetCacheBytes, cloneCustomObject, isAssetRef, resolveScrawlTheme, scrawlThemePresets, useCollaborationStatus, usePersistenceStatus, usePresence, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
1985
+ export type { AssetDiagnostic, AssetExportFailure, AssetIngestRequest, AssetIngestResult, AssetIngestor, AssetKind, AssetPurpose, AssetRef, AssetResolutionErrorCode, AssetResolveRequest, AssetResolveResult, AssetResolver, BoardController, BoardKeyInput, BoardPointerInput, BoardScene, BoardSlotProps, BoardSnapshot, BoardStyle, BoardThemeOptions, CollaborationSnapshot, CommentMarker, CreateBoardControllerOptions, CustomBoardObject, CustomObjectAddInput, CustomObjectDefinition, CustomTool, CustomToolDefinition, DefaultBoardChromeProps, DefaultUIRegion, DefaultUISlot, DefaultUISlots, DialogSlotProps, ExportDocumentSVGOptions, ExportDocumentSVGResult, ExtensionCommand, ExtensionDiagnostic, ExtensionHitResult, ExtensionId, ExtensionRequirement, FocusedItem, FocusedItemToolbarProps, InlineEditorsProps, InputModifiers, JsonObject, JsonValue, LocalBoard, LocalBoardSnapshot, LocalPresence, Mat2x3, MultiplayerCursorsProps, ObjectDescribeContext, ObjectIntent, ObjectType, PresenceCursor, PresenceUser, PresenceView, QueryableBoardObject, ReadonlyCustomObject, SceneEllipse, SceneGroup, SceneImage, ScenePath, SceneRect, SceneText, ScrawlBoardProps, ScrawlCanvasProps, ScrawlDefaultUIProps, ScrawlDensity, ScrawlExtension, ScrawlGridMode, ScrawlProps, ScrawlProviderProps, ScrawlResolvedTheme, ScrawlSurfaceTexture, ScrawlTheme, ScrawlThemeDiagnostic, ScrawlThemePreset, SearchHit, SearchHitKind, SearchableComment, StyleShelfProps, SupportedAssetMediaType, ToolCancelReason, ToolCapabilities, ToolCursor, ToolId };