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

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
@@ -25,13 +25,17 @@ the stylesheet explicitly.
25
25
  `ScrawlDefaultUI` supplies the SDK-owned interaction chrome for tools, ink style,
26
26
  undo and redo, zoom and fit, search, JSON import, and PNG, SVG, or JSON export.
27
27
  Every control calls a public grouped controller capability. The default keyboard
28
- surface is scoped to the focused Board: `P` selects marker, `H` selects
29
- highlighter, `V` selects selection, `E` selects eraser, `N` selects note, `T`
30
- selects text, `F` fits content, `Mod+Z` undoes, `Mod+Shift+Z` redoes, `Mod+F`
31
- opens Board search, and the arrow keys nudge the current selection by a small
32
- step (`Shift` for a bigger one) across any mix of selected object types, as one
33
- undo step. Dialogs trap focus, close with Escape, and restore focus to
34
- their opener. Hosts that replace the default UI can remove it independently while
28
+ surface is scoped to the focused Board and centralized in one dispatch table
29
+ (`interaction/keyboard/`, closing issue #27), not scattered across React
30
+ handlers: `P` selects marker, `H` selects highlighter, `V` selects selection,
31
+ `E` selects eraser, `N` selects note, `T` selects text, `F` fits content,
32
+ `Mod+Z` undoes, `Mod+Shift+Z` redoes, `Mod+C` copies the selection, `Mod+X`
33
+ cuts it, `Mod+V` pastes, `Mod+D` duplicates it, `Mod+A` selects every
34
+ top-level (non-hidden) object, `Mod+F` opens Board search, and the arrow keys
35
+ nudge the current selection by a small step (`Shift` for a bigger one) across
36
+ any mix of selected object types, as one undo step. Dialogs trap focus, close
37
+ with Escape, and restore focus to their opener. Hosts that replace the default UI
38
+ can remove it independently while
35
39
  retaining `ScrawlProvider` and `ScrawlCanvas`, or disable individual `tools`,
36
40
  `history`, `view`, `style`, `search`, `import`, and `export` regions with the
37
41
  `ScrawlDefaultUI` `regions` prop.
package/dist/browser.d.ts CHANGED
@@ -152,6 +152,21 @@ interface CustomObjectDefinition<Props extends JsonValue = JsonValue> {
152
152
  /** One pure, synchronous step per consecutive schema version. */
153
153
  migrate?: Readonly<Record<number, (oldProps: JsonValue) => JsonValue>>;
154
154
  describe(object: ReadonlyCustomObject<Props>, context: ObjectDescribeContext): BoardScene;
155
+ /**
156
+ * Optional point-level hit-test precision (Phase 8). Every custom object
157
+ * hit-tests against its bounding box (`fallback.bounds`) by default — this
158
+ * lets a non-rectangular shape (e.g. a circular card, an L-shaped region)
159
+ * reject a point that's inside that box but outside its actual visible
160
+ * silhouette, tightening a click/marquee/raycast hit to the shape's real
161
+ * outline. `point` is in this object's own local space — the same
162
+ * untransformed space `describe`'s returned geometry already lives in
163
+ * (the caller inverse-transforms the pointer's board point through
164
+ * `object.transform` before calling this). Absent means every point
165
+ * inside the bounding box hits, matching pre-Phase-8 behavior exactly.
166
+ * Rejecting a point here does not fall through to whatever's underneath —
167
+ * the gesture simply misses this object, same as clicking empty space.
168
+ */
169
+ hitTest?(object: ReadonlyCustomObject<Props>, point: BoardPoint): boolean;
155
170
  }
156
171
  interface SceneNodeBase {
157
172
  key: string;
@@ -185,6 +200,15 @@ interface SceneGroup extends SceneNodeBase {
185
200
  kind: "group";
186
201
  children: readonly BoardScene[];
187
202
  }
203
+ /**
204
+ * **No renderer or SVG-export interpreter exists for this node kind yet**
205
+ * (tracked as deferred work — see `renderer/shapes/customObjects.ts`'s
206
+ * `"path"` case). Returning a `ScenePath` from `describe()` renders nothing,
207
+ * exports nothing, and contributes no hit-test bounds — it neither errors
208
+ * nor emits a diagnostic. Until an interpreter ships, build custom shapes
209
+ * from `SceneRect`/`SceneEllipse`/`SceneGroup`/`SceneText`/`SceneImage`
210
+ * instead.
211
+ */
188
212
  interface ScenePath extends SceneNodeBase {
189
213
  kind: "path";
190
214
  /** SVG-style path data, board-local coordinates. */
@@ -340,8 +364,23 @@ interface Lockable {
340
364
  lockedByName?: string;
341
365
  }
342
366
 
367
+ /**
368
+ * Per-object visibility (Phase 8) — mirrors `itemLock.ts`'s `Lockable`
369
+ * pattern exactly, but simpler: unlike a lock, hidden state carries no
370
+ * holder/ownership concept, so there's no analogue to `LockHolder`/
371
+ * `canUnlockItem`. A hidden object stays fully present in the Document
372
+ * (still serializes, persists, syncs, undoes/redoes) — it just skips
373
+ * rendering and hit-testing/selection candidacy. `hidden` absent or
374
+ * `false` means visible; this keeps every pre-Phase-8 document (which has
375
+ * no `hidden` field on any object at all) implicitly fully visible with
376
+ * zero migration needed.
377
+ */
378
+ interface Hideable {
379
+ hidden?: boolean;
380
+ }
381
+
343
382
  /** A kitchen timer sitting on the board. Remaining time is derived, not ticked. */
344
- interface KitchenTimer extends Lockable {
383
+ interface KitchenTimer extends Lockable, Hideable {
345
384
  id: string;
346
385
  x: number;
347
386
  y: number;
@@ -355,7 +394,7 @@ interface KitchenTimer extends Lockable {
355
394
  runningSince?: number;
356
395
  }
357
396
 
358
- interface RectangleObject extends Lockable {
397
+ interface RectangleObject extends Lockable, Hideable {
359
398
  id: string;
360
399
  x: number;
361
400
  y: number;
@@ -377,7 +416,7 @@ interface RectangleObject extends Lockable {
377
416
  */
378
417
  rotation?: number;
379
418
  }
380
- interface EllipseObject extends Lockable {
419
+ interface EllipseObject extends Lockable, Hideable {
381
420
  id: string;
382
421
  x: number;
383
422
  y: number;
@@ -393,7 +432,7 @@ interface EllipseObject extends Lockable {
393
432
  }
394
433
  /** `"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. */
395
434
  type ArrowHeadStyle = "triangle" | "none";
396
- interface LineObject extends Lockable {
435
+ interface LineObject extends Lockable, Hideable {
397
436
  id: string;
398
437
  start: BoardPoint;
399
438
  end: BoardPoint;
@@ -401,7 +440,7 @@ interface LineObject extends Lockable {
401
440
  strokeWidth?: number;
402
441
  opacity?: number;
403
442
  }
404
- interface ArrowObject extends Lockable {
443
+ interface ArrowObject extends Lockable, Hideable {
405
444
  id: string;
406
445
  start: BoardPoint;
407
446
  end: BoardPoint;
@@ -420,7 +459,7 @@ interface ArrowObject extends Lockable {
420
459
  * `polygonStartAngle`, which encodes each side count's own vertex
421
460
  * orientation so the outline always matches the legacy drag-preview shape.
422
461
  */
423
- interface PolygonObject extends Lockable {
462
+ interface PolygonObject extends Lockable, Hideable {
424
463
  id: string;
425
464
  x: number;
426
465
  y: number;
@@ -435,7 +474,7 @@ interface PolygonObject extends Lockable {
435
474
  rotation?: number;
436
475
  }
437
476
  /** 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`). */
438
- interface StarObject extends Lockable {
477
+ interface StarObject extends Lockable, Hideable {
439
478
  id: string;
440
479
  x: number;
441
480
  y: number;
@@ -452,7 +491,7 @@ interface StarObject extends Lockable {
452
491
  rotation?: number;
453
492
  }
454
493
  /** 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. */
455
- interface HeartObject extends Lockable {
494
+ interface HeartObject extends Lockable, Hideable {
456
495
  id: string;
457
496
  x: number;
458
497
  y: number;
@@ -477,7 +516,7 @@ interface HeartObject extends Lockable {
477
516
  * a group into its leaf members is always done by the caller (recursively,
478
517
  * with cycle protection), never assumed here.
479
518
  */
480
- interface GroupObject extends Lockable {
519
+ interface GroupObject extends Lockable, Hideable {
481
520
  id: string;
482
521
  children: string[];
483
522
  }
@@ -502,7 +541,7 @@ interface StrokePoint extends BoardPoint {
502
541
  * geometric outline, not an expressive ink mark.
503
542
  */
504
543
  type StrokeTool = "marker" | "highlighter" | "shape";
505
- interface Stroke extends Lockable {
544
+ interface Stroke extends Lockable, Hideable {
506
545
  id: string;
507
546
  color: string;
508
547
  baseWidth: number;
@@ -518,7 +557,7 @@ interface Stroke extends Lockable {
518
557
  clusterId?: string;
519
558
  }
520
559
  type SerializedPoint = [number, number, number, number];
521
- interface SerializedStroke extends Lockable {
560
+ interface SerializedStroke extends Lockable, Hideable {
522
561
  id: string;
523
562
  color: string;
524
563
  baseWidth: number;
@@ -582,7 +621,7 @@ interface NoteVote {
582
621
  * A sticky note: content floating above the board at a z-offset (pillar 3 —
583
622
  * depth as an organizational axis). Center position in board space.
584
623
  */
585
- interface StickyNote extends Lockable {
624
+ interface StickyNote extends Lockable, Hideable {
586
625
  id: string;
587
626
  x: number;
588
627
  y: number;
@@ -600,7 +639,7 @@ interface StickyNote extends Lockable {
600
639
  * top-left corner; lines flow downward (-y). Text joins the clustering
601
640
  * system like handwriting (build prompt §6.4).
602
641
  */
603
- interface TextBlock extends Lockable {
642
+ interface TextBlock extends Lockable, Hideable {
604
643
  id: string;
605
644
  x: number;
606
645
  y: number;
@@ -614,7 +653,7 @@ interface TextBlock extends Lockable {
614
653
  * Interactive structured table on the board. Position (x, y) is top-left in board units.
615
654
  * Cells are indexed as `${row},${col}` keys mapping to cell text content.
616
655
  */
617
- interface TableBlock extends Lockable {
656
+ interface TableBlock extends Lockable, Hideable {
618
657
  id: string;
619
658
  x: number;
620
659
  y: number;
@@ -631,7 +670,7 @@ interface TableBlock extends Lockable {
631
670
  * An imported image block on the board plane.
632
671
  * Coordinates (x, y) represent the center of the image in board space.
633
672
  */
634
- interface ImageBlock extends Lockable {
673
+ interface ImageBlock extends Lockable, Hideable {
635
674
  id: string;
636
675
  /**
637
676
  * A legacy, read-only data URL (or, historically, an arbitrary string) —
@@ -849,6 +888,8 @@ interface BoardEventMap {
849
888
  "asset-diagnostic": AssetDiagnostic;
850
889
  /** A batch of Ops was reconciled (not applied as-sent) by the persistence adapter (ticket #24). */
851
890
  "persistence-diagnostic": PersistenceDiagnostic;
891
+ /** 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. */
892
+ "collaboration-ops-acknowledged": CollaborationAckDiagnostic;
852
893
  audit: unknown;
853
894
  error: BoardControllerError;
854
895
  disposed: undefined;
@@ -912,9 +953,12 @@ interface PresenceView {
912
953
  readonly height: number;
913
954
  }
914
955
  /**
915
- * A Host-owned collaborator, synced in for cursor/roster rendering only.
916
- * Presence is ephemeral — it never touches the Document, Ops, undo/redo,
917
- * or persistence so this is a read/query capability, not an adapter.
956
+ * A collaborator, synced in for cursor/roster rendering only. Presence is
957
+ * ephemeral — it never touches the Document, Ops, undo/redo, or persistence
958
+ * (ADR 0006/0007). Two ways a roster gets populated (`presence.sync`
959
+ * directly, or a `CollaborationAdapter`'s optional presence channel —
960
+ * Phase 6, ADR 0015) both feed the exact same read/query capability below;
961
+ * a Host picks one, not both, for a given controller.
918
962
  */
919
963
  interface PresenceUser {
920
964
  readonly id: string;
@@ -923,6 +967,14 @@ interface PresenceUser {
923
967
  readonly tool?: string;
924
968
  readonly cursor?: PresenceCursor;
925
969
  readonly view?: PresenceView;
970
+ /** Host-supplied extras (avatar URL, role, etc.) — opaque to Scrawl, never interpreted. */
971
+ readonly metadata?: Record<string, unknown>;
972
+ }
973
+ /** This client's own local presence, published via `presence.broadcast()` (Phase 6). */
974
+ interface LocalPresence {
975
+ readonly cursor?: PresenceCursor | null;
976
+ readonly view?: PresenceView | null;
977
+ readonly tool?: string;
926
978
  }
927
979
  /**
928
980
  * The Custom arm wraps `CustomBoardObject` under the same `type` discriminant
@@ -1048,10 +1100,34 @@ type LoadResult = {
1048
1100
  } | {
1049
1101
  state: "missing";
1050
1102
  };
1103
+ /**
1104
+ * Result of a whole-document `PersistenceAdapter.replace()` call (ADR 0006:
1105
+ * "Whole-document writes survive only for create, clear-board and import,
1106
+ * where replacing everything is the actual intent"). Revision-gated, unlike
1107
+ * `applyOps` — `conflict` means `baseRevision` was stale (someone else's
1108
+ * write landed first); the caller must reload and never overwrites blind.
1109
+ */
1110
+ type ReplaceResult = {
1111
+ state: "applied";
1112
+ revision: string;
1113
+ } | {
1114
+ state: "conflict";
1115
+ currentRevision: string;
1116
+ };
1117
+ /**
1118
+ * The one sanctioned seam for persisting a Board's Document to a Host's own
1119
+ * storage — implement this against a database, an HTTP API, IndexedDB
1120
+ * (see `@scrawl-board/board/local`'s `createIndexedDBPersistence`), or
1121
+ * anything else. `load()` fetches the current state on connect; `applyOps()`
1122
+ * streams incremental Ops as edits happen; `replace()` is only for
1123
+ * whole-document writes (create, clear-board, import — see ADR 0006) and is
1124
+ * revision-gated so a stale write never silently clobbers a newer one.
1125
+ * Passed via `createBoardController({ adapters: { persistence } })`.
1126
+ */
1051
1127
  interface PersistenceAdapter {
1052
1128
  load(context: DocumentContext): Promise<LoadResult>;
1053
1129
  applyOps(context: DocumentContext, ops: readonly ControllerOp[]): Promise<ApplyOpsResult>;
1054
- replace(context: DocumentContext, document: CurrentSerializedDocument, baseRevision: string): Promise<unknown>;
1130
+ replace(context: DocumentContext, document: CurrentSerializedDocument, baseRevision: string): Promise<ReplaceResult>;
1055
1131
  }
1056
1132
  /**
1057
1133
  * `"reconcile"` (ticket #24) means the server authoritatively resolved the
@@ -1075,6 +1151,10 @@ interface PersistenceDiagnostic {
1075
1151
  rejectedOpIds: readonly string[];
1076
1152
  revision: string;
1077
1153
  }
1154
+ /** Emitted as `"collaboration-ops-acknowledged"` (Phase 7) — the server has confirmed receipt of these op ids on the live pipe. */
1155
+ interface CollaborationAckDiagnostic {
1156
+ opIds: readonly string[];
1157
+ }
1078
1158
  interface ControllerOp {
1079
1159
  id: string;
1080
1160
  schemaVersion: 1;
@@ -1091,7 +1171,39 @@ interface ControllerOp {
1091
1171
  | "order";
1092
1172
  objectId: string;
1093
1173
  payload?: unknown;
1174
+ /**
1175
+ * This op's position in its own originating client's local sequence
1176
+ * (Phase 7) — 1, 2, 3, ... per controller instance, distinct from `id`
1177
+ * (an opaque, globally-unique identifier used for dedup/ack, not
1178
+ * ordering) and from a server's own authoritative ordering (e.g.
1179
+ * `referenceCollaborationServer.ts`'s per-room `version` counter).
1180
+ * Present on every op this SDK originates locally; a remote peer's op
1181
+ * carries whatever its own origin set, unchanged — never renumbered in
1182
+ * transit. Absent on an op minted by decoding the legacy wire envelope
1183
+ * (`scrawlOpEnvelope.ts`), which predates this field and has no
1184
+ * per-client sequence concept of its own.
1185
+ */
1186
+ clientSequence?: number;
1187
+ /**
1188
+ * The `CollaboratorIdentity.id` of this op's originating client (Phase
1189
+ * 7) — set for every op this SDK originates locally when `identity` is
1190
+ * configured, omitted entirely otherwise (never sent as `undefined`).
1191
+ * The explicit foundation for a future per-author undo filter (a local
1192
+ * user's own undo should only ever touch their own ops) — no undo-stack
1193
+ * behavior itself changes this phase.
1194
+ */
1195
+ clientId?: string;
1094
1196
  }
1197
+ /**
1198
+ * The one sanctioned seam for real-time multiplayer — implement this against
1199
+ * a Host's own collaboration backend (WebSocket relay, CRDT server, etc.).
1200
+ * `connect()` is called once per controller with the local user's
1201
+ * `identity` and a `receive` callback the adapter invokes with incoming
1202
+ * Ops, presence updates, acks, and connection status; it resolves with a
1203
+ * `CollaborationSession` the controller uses to send local Ops and presence
1204
+ * back out. Passed via `createBoardController({ adapters: { collaboration } })`;
1205
+ * omit it entirely to run single-player.
1206
+ */
1095
1207
  interface CollaborationAdapter {
1096
1208
  connect(options: DocumentContext & {
1097
1209
  identity: CollaboratorIdentity;
@@ -1102,14 +1214,72 @@ interface CollaboratorIdentity {
1102
1214
  id: string;
1103
1215
  name: string;
1104
1216
  color?: string;
1217
+ /** Host-supplied extras (avatar URL, role, etc.) — opaque to Scrawl, forwarded into any resulting `PresenceUser` unread and never interpreted. */
1218
+ metadata?: Record<string, unknown>;
1105
1219
  }
1106
1220
  interface CollaborationReceiver {
1107
1221
  ops(ops: readonly ControllerOp[]): void;
1222
+ /**
1223
+ * The current presence roster (Phase 6, ADR 0015) — always a full
1224
+ * replacement, never a delta, matching `presence.sync`'s existing
1225
+ * semantics exactly (an adapter that aggregates wire deltas into a full
1226
+ * roster before calling this is the adapter's own job, not the
1227
+ * controller's). Required on this interface (not optional) because a
1228
+ * Host only ever *consumes* `CollaborationReceiver` — never implements
1229
+ * it — so adding a required method here cannot break an existing custom
1230
+ * `CollaborationAdapter`. An adapter with no presence support simply
1231
+ * never calls it.
1232
+ */
1233
+ presence(users: readonly PresenceUser[]): void;
1234
+ /**
1235
+ * The server has confirmed receipt of these op ids (Phase 7) —
1236
+ * distinguishes "sent" from "server accepted," which `sendOps` alone
1237
+ * (fire-and-forget) cannot. Required for the same reason `presence` is:
1238
+ * Hosts only ever consume this interface, never implement it, so this
1239
+ * cannot break an existing custom `CollaborationAdapter`. An adapter with
1240
+ * no ack support simply never calls it — the collaboration pipe still
1241
+ * works exactly as it did before this existed, just without the
1242
+ * bookkeeping/observability this enables.
1243
+ */
1244
+ acknowledged(opIds: readonly string[]): void;
1108
1245
  status(state: "online" | "reconnecting" | "offline"): void;
1109
1246
  error(cause: unknown): void;
1110
1247
  }
1248
+ /**
1249
+ * Result of `CollaborationSession.requestSync()` (Phase 7). `"ops"` means
1250
+ * the adapter's own live-pipe cache fully covered the gap since the
1251
+ * caller's last known revision — apply `ops` and the client is caught up,
1252
+ * no persistence reload needed. `"unavailable"` means it couldn't (gap too
1253
+ * large, server restarted, or the adapter has no retained history at all)
1254
+ * — the caller must fall back to a persistence-backed reload. This is a
1255
+ * best-effort *liveness* cache, deliberately never a durable source of
1256
+ * truth (ADR 0006's "collaboration is never a second source of document
1257
+ * truth" — see ADR 0015's own extension of that principle to presence,
1258
+ * now extended once more, the same way, to this).
1259
+ */
1260
+ type CollaborationSyncResult = {
1261
+ state: "ops";
1262
+ ops: readonly ControllerOp[];
1263
+ serverRevision: string;
1264
+ } | {
1265
+ state: "unavailable";
1266
+ };
1111
1267
  interface CollaborationSession {
1112
1268
  sendOps(ops: readonly ControllerOp[]): void;
1269
+ /**
1270
+ * Publishes this client's own local presence (Phase 6, ADR 0015) —
1271
+ * best-effort, unordered, never persisted, never an Op. Optional: an
1272
+ * adapter that doesn't support presence simply omits this method, and
1273
+ * `presence.broadcast()` becomes a silent no-op.
1274
+ */
1275
+ updatePresence?(presence: LocalPresence): void;
1276
+ /**
1277
+ * Requests an incremental catch-up after a reconnect (Phase 7) — optional;
1278
+ * an adapter that doesn't support this simply omits the method, and the
1279
+ * caller (`resyncAfterReconnect`) goes straight to its existing
1280
+ * persistence-backed full reload, unchanged from Phase 6.
1281
+ */
1282
+ requestSync?(): Promise<CollaborationSyncResult>;
1113
1283
  close(): Promise<void>;
1114
1284
  }
1115
1285
  interface CreateBoardControllerOptions {
@@ -1149,6 +1319,87 @@ interface CreateBoardControllerOptions {
1149
1319
  * doesn't use the React theme system can set this directly instead.
1150
1320
  */
1151
1321
  boardTheme?: BoardThemeOptions;
1322
+ /**
1323
+ * Debounced auto-flush of pending persistence Ops after document changes
1324
+ * settle (Phase 5). Enabled by default (1000ms debounce) whenever
1325
+ * `adapters.persistence` is configured — today, without this, a Host must
1326
+ * call `flush()` manually after every edit for anything to persist. Pass
1327
+ * `false` to opt out entirely and drive `flush()` yourself, preserving
1328
+ * prior behavior exactly. Never fires on a per-change basis — rapid edits
1329
+ * coalesce into one flush of their final state (ADR 0006).
1330
+ */
1331
+ autosave?: boolean | {
1332
+ debounceMs?: number;
1333
+ };
1334
+ /**
1335
+ * Throttle for `presence.broadcast()` (Phase 6, ADR 0015) — the minimum
1336
+ * interval between outgoing presence updates sent via the configured
1337
+ * `CollaborationAdapter`. Defaults to 50ms. A trailing throttle: the
1338
+ * latest value passed to `broadcast()` always eventually sends, even if
1339
+ * calls arrive faster than this interval.
1340
+ */
1341
+ presenceThrottleMs?: number;
1342
+ /**
1343
+ * Caps how many `ControllerOp`s can sit queued, unsent, for the
1344
+ * persistence pipe (`pendingOps`) or the collaboration pipe
1345
+ * (`pendingCollaborationOps`) at once (Phase 7) — each pipe is capped
1346
+ * independently. Prevents unbounded memory growth from a long-lived
1347
+ * offline session or a stuck adapter. Exceeding it never fails or drops
1348
+ * the local edit itself (the Document already applied it optimistically)
1349
+ * — only queueing for that one pipe is skipped, and a
1350
+ * `{code:"queue-overflow", retryable:false}` error is emitted so a Host
1351
+ * can react. Defaults to 1000 — the Phase 9 collaboration coalescing
1352
+ * above (`collaborationCoalesceMs`) already keeps a busy drag from
1353
+ * approaching this on its own, so hitting it in practice means a pipe
1354
+ * has been offline/stuck for a genuinely long editing session.
1355
+ *
1356
+ * **Recovery** (Phase 9): the dropped op itself is gone from that one
1357
+ * pipe's queue — there is no automatic backfill, and the live
1358
+ * controller keeps running with that pipe now silently missing one
1359
+ * edit. Two things stay true regardless: (1) the in-memory Document is
1360
+ * never affected — a queue-overflow can never corrupt or roll back a
1361
+ * local edit, only skip sending it; (2) staleness is per-object, not
1362
+ * permanent — any *later* edit to that same object produces a brand
1363
+ * new, undropped Op carrying its full current state, which naturally
1364
+ * supersedes the gap (the Op model is already last-write-wins/
1365
+ * idempotent, so a superseding Op doesn't need the earlier one to have
1366
+ * arrived). The real risk is an object that's dropped and never edited
1367
+ * again before the controller is disposed or the page reloads — a Host
1368
+ * that needs strict durability should treat `queue-overflow` as a
1369
+ * signal to check `persistence.state`/`pendingOps` pressure (via
1370
+ * `usePersistenceStatus`/`getSnapshot().connection.persistence`)
1371
+ * before disposing, not assume disposing and reconnecting alone
1372
+ * repairs the gap (a fresh `load()` only returns what the backend
1373
+ * already has, which is exactly what's missing the dropped edit).
1374
+ */
1375
+ maxPendingOps?: number;
1376
+ /**
1377
+ * Coalescing window (ms) for outgoing collaboration Ops (Phase 9) — same
1378
+ * trailing-throttle shape as `presenceThrottleMs`: the first Op after an
1379
+ * idle period sends immediately, and subsequent Ops for the *same*
1380
+ * object within this window replace each other (latest value wins,
1381
+ * matching the already-idempotent Op model) rather than each triggering
1382
+ * its own send. A multi-second drag that previously sent one full Op per
1383
+ * pointer-move now sends at most one per window per touched object.
1384
+ * Persistence (`pendingOps`) is unaffected — it already debounces via
1385
+ * `autosave`, so this option only changes live collaboration traffic.
1386
+ * Defaults to 50ms.
1387
+ */
1388
+ collaborationCoalesceMs?: number;
1389
+ /**
1390
+ * Caps how many resolved objects a single `content.copy`/`content.cut`
1391
+ * (or their Cmd/Ctrl+C/X keyboard equivalents) will hold in the
1392
+ * in-memory clipboard at once (Phase 9) — `expandSelection` recursively
1393
+ * expands groups, so an unbounded selection (a huge group, or thousands
1394
+ * of individually selected strokes) could otherwise clone and retain an
1395
+ * arbitrarily large snapshot indefinitely, until the next copy/cut
1396
+ * replaces it. Exceeding it rejects the whole copy/cut (nothing is
1397
+ * cloned, and — for cut — nothing is removed from the Document either,
1398
+ * never a partial copy of an arbitrary subset) and emits a
1399
+ * `{code:"clipboard-overflow", retryable:false}` error. Defaults to
1400
+ * 5000.
1401
+ */
1402
+ maxClipboardItems?: number;
1152
1403
  }
1153
1404
  interface BoardController {
1154
1405
  readonly document: ReadonlyBoardDocument;
@@ -1180,6 +1431,15 @@ interface BoardController {
1180
1431
  };
1181
1432
  readonly view: {
1182
1433
  fit(): void;
1434
+ /**
1435
+ * Frame the current selection (Phase 8), the same way `fit()` frames the
1436
+ * whole board. A no-op with nothing selected — deliberately doesn't fall
1437
+ * back to `fit()`'s "frame everything," which would be a surprising
1438
+ * result for an empty selection. On a headless board this can only
1439
+ * re-center the view (no viewport to compute a real zoom-to-fit from),
1440
+ * matching `fit()`'s own headless limitation exactly.
1441
+ */
1442
+ zoomToSelection(): void;
1183
1443
  zoomTo(value: number): void;
1184
1444
  centerOn(point: BoardPoint): void;
1185
1445
  get(): BoardView;
@@ -1251,6 +1511,14 @@ interface BoardController {
1251
1511
  * aren't listed separately. `[]` when the clipboard is empty.
1252
1512
  */
1253
1513
  paste(): readonly string[];
1514
+ /**
1515
+ * Select every top-level object (Phase 8) — a group's own id stands for
1516
+ * its children, which aren't selected separately, matching `paste`'s own
1517
+ * "what the user sees" id list. Hidden objects are excluded, consistent
1518
+ * with them already being excluded from marquee selection. Works with
1519
+ * no canvas/engine, same as {@link toggleSelectionVisibility}.
1520
+ */
1521
+ selectAll(): void;
1254
1522
  table: {
1255
1523
  addRow(tableId: string): void;
1256
1524
  addCol(tableId: string): void;
@@ -1269,6 +1537,21 @@ interface BoardController {
1269
1537
  * another collaborator who isn't the current lock holder.
1270
1538
  */
1271
1539
  toggleSelectionLock(): void;
1540
+ /**
1541
+ * Toggle hidden state for the current selection, as one undo entry
1542
+ * (Phase 8). If any selected object is hidden, shows every selected
1543
+ * object; otherwise hides them all — same "any wins" semantics as
1544
+ * {@link toggleSelectionLock}. Hidden objects stay fully present in the
1545
+ * document (they still serialize, persist, sync, undo/redo) — they just
1546
+ * stop rendering and stop being hit-testable/selectable via pointer
1547
+ * interaction. Unlike `toggleSelectionLock`, this works on a headless
1548
+ * board too: it only touches `selection`/the document, no canvas or
1549
+ * engine involved. Custom objects have no visibility concept (no
1550
+ * `Hideable` field) and are silently skipped, matching how
1551
+ * `toggleSelectionLock` already excludes them. A no-op with nothing
1552
+ * selected or when the selection is only custom objects.
1553
+ */
1554
+ toggleSelectionVisibility(): void;
1272
1555
  };
1273
1556
  readonly query: {
1274
1557
  get(id: string): DeepReadonly<BoardObject> | undefined;
@@ -1297,6 +1580,15 @@ interface BoardController {
1297
1580
  follow(view: PresenceView): void;
1298
1581
  /** Ease the camera to a peer's view; returns false (no-op) while mid-stroke. */
1299
1582
  gather(view: PresenceView): boolean;
1583
+ /**
1584
+ * Publishes this client's own cursor/tool/view for other collaborators
1585
+ * (Phase 6, ADR 0015), via the configured `CollaborationAdapter` —
1586
+ * throttled internally (`presenceThrottleMs` option, default 50ms) so
1587
+ * a raw pointermove stream never becomes a message-per-event flood. A
1588
+ * no-op if no collaboration adapter is configured, or if the
1589
+ * configured one doesn't implement `updatePresence`.
1590
+ */
1591
+ broadcast(local: LocalPresence): void;
1300
1592
  };
1301
1593
  readonly export: {
1302
1594
  svg(): string;
@@ -1322,7 +1614,19 @@ interface BoardController {
1322
1614
  }>;
1323
1615
  dispose(): Promise<void>;
1324
1616
  }
1617
+ /**
1618
+ * Creates a {@link BoardController} — the SDK's canonical, capability-grouped
1619
+ * entry point (`content`, `tools`, `style`, `history`, `view`, `query`,
1620
+ * `comments`, `presence`, `export`, `assets`, plus top-level `getSnapshot`/
1621
+ * `subscribe`/`on`/`setReadOnly`/`flush`/`dispose`) for driving a Board
1622
+ * imperatively from any JS/TS runtime. Supply a `canvas` to render, or omit
1623
+ * it to run headless (SSR, tests, or a document/history-only integration).
1624
+ * Persistence and Collaboration are opt-in via `options.adapters` — without
1625
+ * them the controller runs entirely in memory. Call `dispose()` when done to
1626
+ * release the renderer, adapters, and any pending timers.
1627
+ */
1325
1628
  declare function createBoardController(options: CreateBoardControllerOptions): BoardController;
1629
+ /** @deprecated Use `BoardController.getSnapshot()`'s return type instead. */
1326
1630
  type LocalBoardSnapshot = {
1327
1631
  documentId: string;
1328
1632
  selectedStrokeId: string | null;
@@ -1331,10 +1635,12 @@ type LocalBoardSnapshot = {
1331
1635
  canRedo: boolean;
1332
1636
  disposed: boolean;
1333
1637
  };
1638
+ /** @deprecated Use `CreateBoardControllerOptions` with `createBoardController` instead. */
1334
1639
  type LocalBoardOptions = {
1335
1640
  documentId: string;
1336
1641
  initialDocument?: SerializedBoardDocument;
1337
1642
  };
1643
+ /** @deprecated Use `BoardController` (from `createBoardController`) instead — this stroke-only, single-tool surface predates the full capability-grouped controller. */
1338
1644
  type LocalBoard = {
1339
1645
  drawStroke(stroke: Stroke): void;
1340
1646
  selectAt(point: BoardPoint): string | null;
@@ -1345,7 +1651,20 @@ type LocalBoard = {
1345
1651
  subscribe(listener: () => void): () => void;
1346
1652
  dispose(): Promise<void>;
1347
1653
  };
1654
+ /** @deprecated Use `createBoardController` instead — this is a thin, stroke-only wrapper kept for the original Phase 1 tracer's compatibility. */
1348
1655
  declare function createLocalBoard(options: LocalBoardOptions): LocalBoard;
1349
1656
 
1350
- export { AssetResolutionError, STAMPS, SUPPORTED_ASSET_MEDIA_TYPES, assetRef, clampAssetCacheBytes, cloneCustomObject, createBoardController, createLocalBoard, isAssetRef, isStampKind, stampDataUrl };
1351
- export type { ApplyOpsResult, AssetDiagnostic, AssetIngestRequest, AssetIngestResult, AssetIngestor, AssetKind, AssetPurpose, AssetRef, AssetResolutionErrorCode, AssetResolveRequest, AssetResolveResult, AssetResolver, BoardController, BoardControllerError, BoardEventMap, BoardKeyInput, BoardObject, BoardObjectInput, BoardObjectPatch, BoardPointerInput, BoardScene, BoardSnapshot, BoardStyle, BoardThemeOptions, BoardView, BuiltInTool, CollaborationAdapter, CollaborationReceiver, CollaborationSession, CollaborationSnapshot, CollaboratorIdentity, CommentMarker, ControllerOp, CreateBoardControllerOptions, CustomBoardObject, CustomObjectAddInput, CustomObjectDefinition, CustomTool, CustomToolDefinition, DeepReadonly, DocumentContext, ExtensionCommand, ExtensionDiagnostic, ExtensionHitResult, ExtensionId, ExtensionRequirement, FocusedItem, InputModifiers, JsonObject, JsonValue, LoadResult, LocalBoard, LocalBoardOptions, LocalBoardSnapshot, Mat2x3, ObjectDescribeContext, ObjectIntent, ObjectType, PersistenceAdapter, PersistenceDiagnostic, PersistenceSnapshot, PresenceCursor, PresenceUser, PresenceView, QueryableBoardObject, ReadonlyBoardDocument, ReadonlyCustomObject, ReadonlyDocumentChange, SceneEllipse, SceneGroup, SceneImage, ScenePath, SceneRect, SceneText, ScrawlExtension, ScreenPoint, ScreenRect, StampKind, SupportedAssetMediaType, ToolCancelReason, ToolCapabilities, ToolCursor, ToolId };
1657
+ interface CreateMemoryPersistenceOptions {
1658
+ /** Pre-seeded documents, keyed by document id as if a prior session had already saved them. Seeded documents start at revision "1". */
1659
+ seed?: Record<string, CurrentSerializedDocument>;
1660
+ }
1661
+ /**
1662
+ * Creates a real, in-memory `PersistenceAdapter`. One instance can back
1663
+ * multiple documents (keyed by `DocumentContext.documentId`, like every
1664
+ * other adapter in this package). State lives only in this instance —
1665
+ * discarded on garbage collection, never written to disk.
1666
+ */
1667
+ declare function createMemoryPersistence(options?: CreateMemoryPersistenceOptions): PersistenceAdapter;
1668
+
1669
+ export { AssetResolutionError, STAMPS, SUPPORTED_ASSET_MEDIA_TYPES, assetRef, clampAssetCacheBytes, cloneCustomObject, createBoardController, createLocalBoard, createMemoryPersistence, isAssetRef, isStampKind, stampDataUrl };
1670
+ export type { ApplyOpsResult, AssetDiagnostic, AssetExportFailure, AssetIngestRequest, AssetIngestResult, AssetIngestor, AssetKind, AssetPurpose, AssetRef, AssetResolutionErrorCode, AssetResolveRequest, AssetResolveResult, AssetResolver, BoardController, BoardControllerError, BoardEventMap, BoardKeyInput, BoardObject, BoardObjectInput, BoardObjectPatch, BoardPointerInput, BoardScene, BoardSnapshot, BoardStyle, BoardThemeOptions, BoardView, BuiltInTool, CollaborationAckDiagnostic, CollaborationAdapter, CollaborationReceiver, CollaborationSession, CollaborationSnapshot, CollaborationSyncResult, CollaboratorIdentity, CommentMarker, ControllerOp, CreateBoardControllerOptions, CreateMemoryPersistenceOptions, CustomBoardObject, CustomObjectAddInput, CustomObjectDefinition, CustomTool, CustomToolDefinition, DeepReadonly, DocumentContext, ExportDocumentSVGOptions, ExportDocumentSVGResult, ExtensionCommand, ExtensionDiagnostic, ExtensionHitResult, ExtensionId, ExtensionRequirement, FocusedItem, InputModifiers, JsonObject, JsonValue, LoadResult, LocalBoard, LocalBoardOptions, LocalBoardSnapshot, LocalPresence, Mat2x3, ObjectDescribeContext, ObjectIntent, ObjectType, PersistenceAdapter, PersistenceDiagnostic, PersistenceSnapshot, PresenceCursor, PresenceUser, PresenceView, QueryableBoardObject, ReadonlyBoardDocument, ReadonlyCustomObject, ReadonlyDocumentChange, ReplaceResult, SceneEllipse, SceneGroup, SceneImage, ScenePath, SceneRect, SceneText, ScrawlExtension, ScreenPoint, ScreenRect, SearchHit, SearchHitKind, SearchableComment, StampKind, SupportedAssetMediaType, ToolCancelReason, ToolCapabilities, ToolCursor, ToolId };