@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 +11 -7
- package/dist/browser.d.ts +340 -21
- package/dist/browser.js +1543 -35810
- package/dist/core.d.ts +115 -30
- package/dist/core.js +260 -124
- package/dist/index.d.ts +460 -52
- package/dist/index.js +1731 -35807
- package/dist/local.d.ts +469 -0
- package/dist/local.js +234 -0
- package/dist/react.d.ts +375 -38
- package/dist/react.js +1597 -35849
- package/dist/styles.css +47 -47
- package/package.json +17 -3
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
916
|
-
*
|
|
917
|
-
*
|
|
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<
|
|
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
|
-
|
|
1351
|
-
|
|
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 };
|