@scrawl-board/board 0.1.0-beta.6 → 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/README.md +11 -5
- package/dist/browser.d.ts +593 -15
- package/dist/browser.js +6277 -34044
- package/dist/core.d.ts +711 -16
- package/dist/core.js +2015 -51
- package/dist/index.d.ts +1171 -26
- package/dist/index.js +6209 -33607
- package/dist/local.d.ts +469 -0
- package/dist/local.js +234 -0
- package/dist/react.d.ts +635 -19
- package/dist/react.js +6142 -33785
- package/package.json +17 -3
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,6 +394,133 @@ interface KitchenTimer extends Lockable {
|
|
|
355
394
|
runningSince?: number;
|
|
356
395
|
}
|
|
357
396
|
|
|
397
|
+
interface RectangleObject extends Lockable, Hideable {
|
|
398
|
+
id: string;
|
|
399
|
+
x: number;
|
|
400
|
+
y: number;
|
|
401
|
+
width: number;
|
|
402
|
+
height: number;
|
|
403
|
+
fill?: string;
|
|
404
|
+
stroke?: string;
|
|
405
|
+
strokeWidth?: number;
|
|
406
|
+
/** Corner radius in board units; clamped to at most half the shorter side at render time. */
|
|
407
|
+
cornerRadius?: number;
|
|
408
|
+
/** `[0, 1]`; undefined means fully opaque (Phase 4). */
|
|
409
|
+
opacity?: number;
|
|
410
|
+
/**
|
|
411
|
+
* Radians, about the shape's own center `(x + width/2, y - height/2)`.
|
|
412
|
+
* Undefined means 0 (Phase 3). `x`/`y`/`width`/`height` stay in the
|
|
413
|
+
* shape's own unrotated local frame — rotation is a separate, applied-last
|
|
414
|
+
* transform, not baked into them, matching how Stroke/CustomBoardObject
|
|
415
|
+
* keep geometry and placement independent via their own `matrix`.
|
|
416
|
+
*/
|
|
417
|
+
rotation?: number;
|
|
418
|
+
}
|
|
419
|
+
interface EllipseObject extends Lockable, Hideable {
|
|
420
|
+
id: string;
|
|
421
|
+
x: number;
|
|
422
|
+
y: number;
|
|
423
|
+
width: number;
|
|
424
|
+
height: number;
|
|
425
|
+
fill?: string;
|
|
426
|
+
stroke?: string;
|
|
427
|
+
strokeWidth?: number;
|
|
428
|
+
/** `[0, 1]`; undefined means fully opaque (Phase 4). */
|
|
429
|
+
opacity?: number;
|
|
430
|
+
/** Radians, about the shape's own center — see RectangleObject's `rotation` doc. */
|
|
431
|
+
rotation?: number;
|
|
432
|
+
}
|
|
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. */
|
|
434
|
+
type ArrowHeadStyle = "triangle" | "none";
|
|
435
|
+
interface LineObject extends Lockable, Hideable {
|
|
436
|
+
id: string;
|
|
437
|
+
start: BoardPoint;
|
|
438
|
+
end: BoardPoint;
|
|
439
|
+
stroke?: string;
|
|
440
|
+
strokeWidth?: number;
|
|
441
|
+
opacity?: number;
|
|
442
|
+
}
|
|
443
|
+
interface ArrowObject extends Lockable, Hideable {
|
|
444
|
+
id: string;
|
|
445
|
+
start: BoardPoint;
|
|
446
|
+
end: BoardPoint;
|
|
447
|
+
head?: ArrowHeadStyle;
|
|
448
|
+
stroke?: string;
|
|
449
|
+
strokeWidth?: number;
|
|
450
|
+
opacity?: number;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Triangle(3)/Diamond(4)/Pentagon(5)/Hexagon(6)/Octagon(8) as one shared
|
|
454
|
+
* type instead of five near-duplicate interfaces — a regular N-gon
|
|
455
|
+
* inscribed in the same `x`/`y`/`width`/`height`/`rotation` bounding box
|
|
456
|
+
* Rectangle already uses, parameterized by `sides`. Diamond is exactly a
|
|
457
|
+
* 4-sided regular polygon with vertex 0 pointing right (not up, like
|
|
458
|
+
* Triangle/Pentagon/Hexagon) — see `polygonGeometry.ts`'s
|
|
459
|
+
* `polygonStartAngle`, which encodes each side count's own vertex
|
|
460
|
+
* orientation so the outline always matches the legacy drag-preview shape.
|
|
461
|
+
*/
|
|
462
|
+
interface PolygonObject extends Lockable, Hideable {
|
|
463
|
+
id: string;
|
|
464
|
+
x: number;
|
|
465
|
+
y: number;
|
|
466
|
+
width: number;
|
|
467
|
+
height: number;
|
|
468
|
+
sides: 3 | 4 | 5 | 6 | 8;
|
|
469
|
+
fill?: string;
|
|
470
|
+
stroke?: string;
|
|
471
|
+
strokeWidth?: number;
|
|
472
|
+
opacity?: number;
|
|
473
|
+
/** Radians, about the shape's own center — see RectangleObject's `rotation` doc. */
|
|
474
|
+
rotation?: number;
|
|
475
|
+
}
|
|
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`). */
|
|
477
|
+
interface StarObject extends Lockable, Hideable {
|
|
478
|
+
id: string;
|
|
479
|
+
x: number;
|
|
480
|
+
y: number;
|
|
481
|
+
width: number;
|
|
482
|
+
height: number;
|
|
483
|
+
/** Vertex count; today's only shipped preset is 5, matching the legacy tool. */
|
|
484
|
+
points: number;
|
|
485
|
+
/** `(0, 1)` — inner vertex radius as a fraction of the outer radius. */
|
|
486
|
+
innerRadiusRatio: number;
|
|
487
|
+
fill?: string;
|
|
488
|
+
stroke?: string;
|
|
489
|
+
strokeWidth?: number;
|
|
490
|
+
opacity?: number;
|
|
491
|
+
rotation?: number;
|
|
492
|
+
}
|
|
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. */
|
|
494
|
+
interface HeartObject extends Lockable, Hideable {
|
|
495
|
+
id: string;
|
|
496
|
+
x: number;
|
|
497
|
+
y: number;
|
|
498
|
+
width: number;
|
|
499
|
+
height: number;
|
|
500
|
+
fill?: string;
|
|
501
|
+
stroke?: string;
|
|
502
|
+
strokeWidth?: number;
|
|
503
|
+
opacity?: number;
|
|
504
|
+
rotation?: number;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* A logical grouping of other board objects (Phase 3 — Selection,
|
|
508
|
+
* Transformation & Grouping). Deliberately has no `x`/`y`/`transform` of its
|
|
509
|
+
* own — a group's bounds are always derived on demand from its (recursively
|
|
510
|
+
* resolved) children, and "moving/rotating/scaling the group" is exactly a
|
|
511
|
+
* multi-object transform applied to those children, nothing more. A group
|
|
512
|
+
* has no renderer/mesh of its own; its only visual presence is the
|
|
513
|
+
* selection gizmo's bounding box while it's the current selection.
|
|
514
|
+
*
|
|
515
|
+
* `children` may itself contain other group ids (nested groups) — expanding
|
|
516
|
+
* a group into its leaf members is always done by the caller (recursively,
|
|
517
|
+
* with cycle protection), never assumed here.
|
|
518
|
+
*/
|
|
519
|
+
interface GroupObject extends Lockable, Hideable {
|
|
520
|
+
id: string;
|
|
521
|
+
children: string[];
|
|
522
|
+
}
|
|
523
|
+
|
|
358
524
|
interface BoardPoint {
|
|
359
525
|
x: number;
|
|
360
526
|
y: number;
|
|
@@ -375,7 +541,7 @@ interface StrokePoint extends BoardPoint {
|
|
|
375
541
|
* geometric outline, not an expressive ink mark.
|
|
376
542
|
*/
|
|
377
543
|
type StrokeTool = "marker" | "highlighter" | "shape";
|
|
378
|
-
interface Stroke extends Lockable {
|
|
544
|
+
interface Stroke extends Lockable, Hideable {
|
|
379
545
|
id: string;
|
|
380
546
|
color: string;
|
|
381
547
|
baseWidth: number;
|
|
@@ -391,7 +557,7 @@ interface Stroke extends Lockable {
|
|
|
391
557
|
clusterId?: string;
|
|
392
558
|
}
|
|
393
559
|
type SerializedPoint = [number, number, number, number];
|
|
394
|
-
interface SerializedStroke extends Lockable {
|
|
560
|
+
interface SerializedStroke extends Lockable, Hideable {
|
|
395
561
|
id: string;
|
|
396
562
|
color: string;
|
|
397
563
|
baseWidth: number;
|
|
@@ -417,6 +583,31 @@ interface SerializedDocument {
|
|
|
417
583
|
timers?: KitchenTimer[];
|
|
418
584
|
/** Absent in documents saved before Custom board objects existed (ticket #22). */
|
|
419
585
|
customObjects?: CustomBoardObject[];
|
|
586
|
+
/** Absent in documents saved before semantic Rectangle objects existed (Phase 2). */
|
|
587
|
+
rectangles?: RectangleObject[];
|
|
588
|
+
/** Absent in documents saved before semantic Ellipse objects existed (Phase 2). */
|
|
589
|
+
ellipses?: EllipseObject[];
|
|
590
|
+
/** Absent in documents saved before Groups existed (Phase 3). */
|
|
591
|
+
groups?: GroupObject[];
|
|
592
|
+
/** Absent in documents saved before semantic Line objects existed (Phase 4). */
|
|
593
|
+
lines?: LineObject[];
|
|
594
|
+
/** Absent in documents saved before semantic Arrow objects existed (Phase 4). */
|
|
595
|
+
arrows?: ArrowObject[];
|
|
596
|
+
/** Absent in documents saved before semantic Polygon objects existed (Phase 4). */
|
|
597
|
+
polygons?: PolygonObject[];
|
|
598
|
+
/** Absent in documents saved before semantic Star objects existed (Phase 4). */
|
|
599
|
+
stars?: StarObject[];
|
|
600
|
+
/** Absent in documents saved before semantic Heart objects existed (Phase 4). */
|
|
601
|
+
hearts?: HeartObject[];
|
|
602
|
+
/**
|
|
603
|
+
* Every content-object id (every type above except comments, which are
|
|
604
|
+
* host-synced and never enter this schema) in paint order, back to front.
|
|
605
|
+
* Absent in documents saved before per-object z-order existed (Phase 3) —
|
|
606
|
+
* migration synthesizes a default order preserving the old fixed-Z-band
|
|
607
|
+
* visual stacking exactly, so an existing document never visibly changes
|
|
608
|
+
* on load; only an explicit reorder action touches this from then on.
|
|
609
|
+
*/
|
|
610
|
+
objectOrder?: string[];
|
|
420
611
|
}
|
|
421
612
|
/**
|
|
422
613
|
* One collaborator's vote on a note. One per person; toggling removes it.
|
|
@@ -430,7 +621,7 @@ interface NoteVote {
|
|
|
430
621
|
* A sticky note: content floating above the board at a z-offset (pillar 3 —
|
|
431
622
|
* depth as an organizational axis). Center position in board space.
|
|
432
623
|
*/
|
|
433
|
-
interface StickyNote extends Lockable {
|
|
624
|
+
interface StickyNote extends Lockable, Hideable {
|
|
434
625
|
id: string;
|
|
435
626
|
x: number;
|
|
436
627
|
y: number;
|
|
@@ -448,7 +639,7 @@ interface StickyNote extends Lockable {
|
|
|
448
639
|
* top-left corner; lines flow downward (-y). Text joins the clustering
|
|
449
640
|
* system like handwriting (build prompt §6.4).
|
|
450
641
|
*/
|
|
451
|
-
interface TextBlock extends Lockable {
|
|
642
|
+
interface TextBlock extends Lockable, Hideable {
|
|
452
643
|
id: string;
|
|
453
644
|
x: number;
|
|
454
645
|
y: number;
|
|
@@ -462,7 +653,7 @@ interface TextBlock extends Lockable {
|
|
|
462
653
|
* Interactive structured table on the board. Position (x, y) is top-left in board units.
|
|
463
654
|
* Cells are indexed as `${row},${col}` keys mapping to cell text content.
|
|
464
655
|
*/
|
|
465
|
-
interface TableBlock extends Lockable {
|
|
656
|
+
interface TableBlock extends Lockable, Hideable {
|
|
466
657
|
id: string;
|
|
467
658
|
x: number;
|
|
468
659
|
y: number;
|
|
@@ -479,7 +670,7 @@ interface TableBlock extends Lockable {
|
|
|
479
670
|
* An imported image block on the board plane.
|
|
480
671
|
* Coordinates (x, y) represent the center of the image in board space.
|
|
481
672
|
*/
|
|
482
|
-
interface ImageBlock extends Lockable {
|
|
673
|
+
interface ImageBlock extends Lockable, Hideable {
|
|
483
674
|
id: string;
|
|
484
675
|
/**
|
|
485
676
|
* A legacy, read-only data URL (or, historically, an arbitrary string) —
|
|
@@ -616,7 +807,7 @@ interface BoardSnapshot {
|
|
|
616
807
|
* here, matching the engine's own internal selection-badge behavior.
|
|
617
808
|
*/
|
|
618
809
|
interface FocusedItem {
|
|
619
|
-
readonly type: "stroke" | "note" | "text" | "table" | "image" | "timer" | "custom";
|
|
810
|
+
readonly type: "stroke" | "note" | "text" | "table" | "image" | "timer" | "rectangle" | "ellipse" | "group" | "line" | "arrow" | "polygon" | "star" | "heart" | "custom";
|
|
620
811
|
readonly id: string;
|
|
621
812
|
readonly locked: boolean;
|
|
622
813
|
readonly lockedBy?: string;
|
|
@@ -697,6 +888,8 @@ interface BoardEventMap {
|
|
|
697
888
|
"asset-diagnostic": AssetDiagnostic;
|
|
698
889
|
/** A batch of Ops was reconciled (not applied as-sent) by the persistence adapter (ticket #24). */
|
|
699
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;
|
|
700
893
|
audit: unknown;
|
|
701
894
|
error: BoardControllerError;
|
|
702
895
|
disposed: undefined;
|
|
@@ -760,9 +953,12 @@ interface PresenceView {
|
|
|
760
953
|
readonly height: number;
|
|
761
954
|
}
|
|
762
955
|
/**
|
|
763
|
-
* A
|
|
764
|
-
*
|
|
765
|
-
*
|
|
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.
|
|
766
962
|
*/
|
|
767
963
|
interface PresenceUser {
|
|
768
964
|
readonly id: string;
|
|
@@ -771,6 +967,14 @@ interface PresenceUser {
|
|
|
771
967
|
readonly tool?: string;
|
|
772
968
|
readonly cursor?: PresenceCursor;
|
|
773
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;
|
|
774
978
|
}
|
|
775
979
|
/**
|
|
776
980
|
* The Custom arm wraps `CustomBoardObject` under the same `type` discriminant
|
|
@@ -798,6 +1002,22 @@ type BoardObject = ({
|
|
|
798
1002
|
} & ImageBlock) | ({
|
|
799
1003
|
type: "timer";
|
|
800
1004
|
} & KitchenTimer) | ({
|
|
1005
|
+
type: "rectangle";
|
|
1006
|
+
} & RectangleObject) | ({
|
|
1007
|
+
type: "ellipse";
|
|
1008
|
+
} & EllipseObject) | ({
|
|
1009
|
+
type: "group";
|
|
1010
|
+
} & GroupObject) | ({
|
|
1011
|
+
type: "line";
|
|
1012
|
+
} & LineObject) | ({
|
|
1013
|
+
type: "arrow";
|
|
1014
|
+
} & ArrowObject) | ({
|
|
1015
|
+
type: "polygon";
|
|
1016
|
+
} & PolygonObject) | ({
|
|
1017
|
+
type: "star";
|
|
1018
|
+
} & StarObject) | ({
|
|
1019
|
+
type: "heart";
|
|
1020
|
+
} & HeartObject) | ({
|
|
801
1021
|
type: "custom";
|
|
802
1022
|
customType: ObjectType;
|
|
803
1023
|
} & Omit<CustomBoardObject, "type">);
|
|
@@ -826,6 +1046,30 @@ type BoardObjectInput = {
|
|
|
826
1046
|
type: "timer";
|
|
827
1047
|
id?: string;
|
|
828
1048
|
} & Omit<KitchenTimer, "id">) | ({
|
|
1049
|
+
type: "rectangle";
|
|
1050
|
+
id?: string;
|
|
1051
|
+
} & Omit<RectangleObject, "id">) | ({
|
|
1052
|
+
type: "ellipse";
|
|
1053
|
+
id?: string;
|
|
1054
|
+
} & Omit<EllipseObject, "id">) | ({
|
|
1055
|
+
type: "group";
|
|
1056
|
+
id?: string;
|
|
1057
|
+
} & Omit<GroupObject, "id">) | ({
|
|
1058
|
+
type: "line";
|
|
1059
|
+
id?: string;
|
|
1060
|
+
} & Omit<LineObject, "id">) | ({
|
|
1061
|
+
type: "arrow";
|
|
1062
|
+
id?: string;
|
|
1063
|
+
} & Omit<ArrowObject, "id">) | ({
|
|
1064
|
+
type: "polygon";
|
|
1065
|
+
id?: string;
|
|
1066
|
+
} & Omit<PolygonObject, "id">) | ({
|
|
1067
|
+
type: "star";
|
|
1068
|
+
id?: string;
|
|
1069
|
+
} & Omit<StarObject, "id">) | ({
|
|
1070
|
+
type: "heart";
|
|
1071
|
+
id?: string;
|
|
1072
|
+
} & Omit<HeartObject, "id">) | ({
|
|
829
1073
|
type: "custom";
|
|
830
1074
|
id?: string;
|
|
831
1075
|
customType: ObjectType;
|
|
@@ -856,10 +1100,34 @@ type LoadResult = {
|
|
|
856
1100
|
} | {
|
|
857
1101
|
state: "missing";
|
|
858
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
|
+
*/
|
|
859
1127
|
interface PersistenceAdapter {
|
|
860
1128
|
load(context: DocumentContext): Promise<LoadResult>;
|
|
861
1129
|
applyOps(context: DocumentContext, ops: readonly ControllerOp[]): Promise<ApplyOpsResult>;
|
|
862
|
-
replace(context: DocumentContext, document: CurrentSerializedDocument, baseRevision: string): Promise<
|
|
1130
|
+
replace(context: DocumentContext, document: CurrentSerializedDocument, baseRevision: string): Promise<ReplaceResult>;
|
|
863
1131
|
}
|
|
864
1132
|
/**
|
|
865
1133
|
* `"reconcile"` (ticket #24) means the server authoritatively resolved the
|
|
@@ -883,14 +1151,59 @@ interface PersistenceDiagnostic {
|
|
|
883
1151
|
rejectedOpIds: readonly string[];
|
|
884
1152
|
revision: string;
|
|
885
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
|
+
}
|
|
886
1158
|
interface ControllerOp {
|
|
887
1159
|
id: string;
|
|
888
1160
|
schemaVersion: 1;
|
|
889
1161
|
kind: "upsert" | "restore" | "remove";
|
|
890
|
-
objectType: "stroke" | "note" | "text" | "table" | "image" | "timer" | "custom"
|
|
1162
|
+
objectType: "stroke" | "note" | "text" | "table" | "image" | "timer" | "rectangle" | "ellipse" | "group" | "line" | "arrow" | "polygon" | "star" | "heart" | "custom"
|
|
1163
|
+
/**
|
|
1164
|
+
* A whole-document paint-order sync (Phase 3), not a per-object type —
|
|
1165
|
+
* `objectId` is always the fixed sentinel `"order"` and `payload` is
|
|
1166
|
+
* `{ order: string[] }`. The only `objectType` with no matching
|
|
1167
|
+
* `BoardObject`/document collection; kept in this same union (rather
|
|
1168
|
+
* than a separate wire message) so it flows through the existing
|
|
1169
|
+
* `PersistenceAdapter`/`CollaborationAdapter` opaquely, unchanged.
|
|
1170
|
+
*/
|
|
1171
|
+
| "order";
|
|
891
1172
|
objectId: string;
|
|
892
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;
|
|
893
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
|
+
*/
|
|
894
1207
|
interface CollaborationAdapter {
|
|
895
1208
|
connect(options: DocumentContext & {
|
|
896
1209
|
identity: CollaboratorIdentity;
|
|
@@ -901,14 +1214,72 @@ interface CollaboratorIdentity {
|
|
|
901
1214
|
id: string;
|
|
902
1215
|
name: string;
|
|
903
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>;
|
|
904
1219
|
}
|
|
905
1220
|
interface CollaborationReceiver {
|
|
906
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;
|
|
907
1245
|
status(state: "online" | "reconnecting" | "offline"): void;
|
|
908
1246
|
error(cause: unknown): void;
|
|
909
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
|
+
};
|
|
910
1267
|
interface CollaborationSession {
|
|
911
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>;
|
|
912
1283
|
close(): Promise<void>;
|
|
913
1284
|
}
|
|
914
1285
|
interface CreateBoardControllerOptions {
|
|
@@ -948,6 +1319,87 @@ interface CreateBoardControllerOptions {
|
|
|
948
1319
|
* doesn't use the React theme system can set this directly instead.
|
|
949
1320
|
*/
|
|
950
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;
|
|
951
1403
|
}
|
|
952
1404
|
interface BoardController {
|
|
953
1405
|
readonly document: ReadonlyBoardDocument;
|
|
@@ -979,6 +1431,15 @@ interface BoardController {
|
|
|
979
1431
|
};
|
|
980
1432
|
readonly view: {
|
|
981
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;
|
|
982
1443
|
zoomTo(value: number): void;
|
|
983
1444
|
centerOn(point: BoardPoint): void;
|
|
984
1445
|
get(): BoardView;
|
|
@@ -1000,6 +1461,64 @@ interface BoardController {
|
|
|
1000
1461
|
* Unknown ids are silently skipped, matching `remove`'s convention.
|
|
1001
1462
|
*/
|
|
1002
1463
|
duplicate(ids: readonly string[]): readonly string[];
|
|
1464
|
+
/**
|
|
1465
|
+
* Creates a new Group referencing `ids` as its children and returns its
|
|
1466
|
+
* id, as one undoable step. Unknown ids are silently skipped, matching
|
|
1467
|
+
* `duplicate`/`remove`'s convention. A child id that's itself a group
|
|
1468
|
+
* makes a nested group — expanding nested groups into their leaf
|
|
1469
|
+
* members is always the caller's job, never assumed here (matches the
|
|
1470
|
+
* document-model `GroupObject` itself).
|
|
1471
|
+
*/
|
|
1472
|
+
group(ids: readonly string[]): string;
|
|
1473
|
+
/**
|
|
1474
|
+
* Dissolves one group, returning its immediate children's ids (a nested
|
|
1475
|
+
* subgroup among them stays intact, itself still a group) — the group
|
|
1476
|
+
* record itself is removed, the children are untouched. A no-op
|
|
1477
|
+
* (returns `[]`) if `groupId` isn't a group.
|
|
1478
|
+
*/
|
|
1479
|
+
ungroup(groupId: string): readonly string[];
|
|
1480
|
+
/**
|
|
1481
|
+
* Aligns every given object's matching edge/center to the corresponding
|
|
1482
|
+
* edge/center of their combined bounding box, as one undoable step.
|
|
1483
|
+
* `"top"`/`"bottom"` follow board space's Y-up convention (`"top"` is
|
|
1484
|
+
* the larger Y). Ids that don't resolve, or resolve to a Group (which
|
|
1485
|
+
* has no position of its own), are skipped. A no-op under 2 resolvable
|
|
1486
|
+
* ids — there's nothing to align relative to.
|
|
1487
|
+
*/
|
|
1488
|
+
align(ids: readonly string[], edge: "left" | "right" | "top" | "bottom" | "centerX" | "centerY"): void;
|
|
1489
|
+
/**
|
|
1490
|
+
* Spaces the middle objects' centers evenly between the first and last
|
|
1491
|
+
* (sorted along `axis`), as one undoable step — the two endpoints don't
|
|
1492
|
+
* move. Ids that don't resolve, or resolve to a Group, are skipped. A
|
|
1493
|
+
* no-op under 3 resolvable ids — there's no "middle" to distribute.
|
|
1494
|
+
*/
|
|
1495
|
+
distribute(ids: readonly string[], axis: "x" | "y"): void;
|
|
1496
|
+
/**
|
|
1497
|
+
* Snapshots `ids` (recursively expanded through any group, same as
|
|
1498
|
+
* `duplicate`) into an internal in-memory clipboard — never
|
|
1499
|
+
* `navigator.clipboard`, scoped to this one controller instance and
|
|
1500
|
+
* replaced wholesale by the next `copy`/`cut`. Read-only; works even
|
|
1501
|
+
* on a read-only board.
|
|
1502
|
+
*/
|
|
1503
|
+
copy(ids: readonly string[]): void;
|
|
1504
|
+
/** `copy`, then removes every resolved object (recursively through any group) as one undoable step. */
|
|
1505
|
+
cut(ids: readonly string[]): void;
|
|
1506
|
+
/**
|
|
1507
|
+
* Clones the current clipboard contents onto the board as one undoable
|
|
1508
|
+
* step, offset the same small cascade `duplicate` uses (no cursor
|
|
1509
|
+
* position to paste relative to yet). Returns the new top-level ids —
|
|
1510
|
+
* a pasted group's own id stands for its (also-pasted) children, which
|
|
1511
|
+
* aren't listed separately. `[]` when the clipboard is empty.
|
|
1512
|
+
*/
|
|
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;
|
|
1003
1522
|
table: {
|
|
1004
1523
|
addRow(tableId: string): void;
|
|
1005
1524
|
addCol(tableId: string): void;
|
|
@@ -1010,6 +1529,29 @@ interface BoardController {
|
|
|
1010
1529
|
};
|
|
1011
1530
|
select(ids: readonly string[]): void;
|
|
1012
1531
|
import(document: SerializedBoardDocument): readonly string[];
|
|
1532
|
+
/**
|
|
1533
|
+
* Toggle lock state for the current selection (or focused note/text/
|
|
1534
|
+
* table/image/timer), matching whatever a single Host lock/unlock
|
|
1535
|
+
* control already does per object type. A no-op with nothing selected,
|
|
1536
|
+
* on a headless board, or when every actionable target is locked by
|
|
1537
|
+
* another collaborator who isn't the current lock holder.
|
|
1538
|
+
*/
|
|
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;
|
|
1013
1555
|
};
|
|
1014
1556
|
readonly query: {
|
|
1015
1557
|
get(id: string): DeepReadonly<BoardObject> | undefined;
|
|
@@ -1038,6 +1580,15 @@ interface BoardController {
|
|
|
1038
1580
|
follow(view: PresenceView): void;
|
|
1039
1581
|
/** Ease the camera to a peer's view; returns false (no-op) while mid-stroke. */
|
|
1040
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;
|
|
1041
1592
|
};
|
|
1042
1593
|
readonly export: {
|
|
1043
1594
|
svg(): string;
|
|
@@ -1063,7 +1614,19 @@ interface BoardController {
|
|
|
1063
1614
|
}>;
|
|
1064
1615
|
dispose(): Promise<void>;
|
|
1065
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
|
+
*/
|
|
1066
1628
|
declare function createBoardController(options: CreateBoardControllerOptions): BoardController;
|
|
1629
|
+
/** @deprecated Use `BoardController.getSnapshot()`'s return type instead. */
|
|
1067
1630
|
type LocalBoardSnapshot = {
|
|
1068
1631
|
documentId: string;
|
|
1069
1632
|
selectedStrokeId: string | null;
|
|
@@ -1072,10 +1635,12 @@ type LocalBoardSnapshot = {
|
|
|
1072
1635
|
canRedo: boolean;
|
|
1073
1636
|
disposed: boolean;
|
|
1074
1637
|
};
|
|
1638
|
+
/** @deprecated Use `CreateBoardControllerOptions` with `createBoardController` instead. */
|
|
1075
1639
|
type LocalBoardOptions = {
|
|
1076
1640
|
documentId: string;
|
|
1077
1641
|
initialDocument?: SerializedBoardDocument;
|
|
1078
1642
|
};
|
|
1643
|
+
/** @deprecated Use `BoardController` (from `createBoardController`) instead — this stroke-only, single-tool surface predates the full capability-grouped controller. */
|
|
1079
1644
|
type LocalBoard = {
|
|
1080
1645
|
drawStroke(stroke: Stroke): void;
|
|
1081
1646
|
selectAt(point: BoardPoint): string | null;
|
|
@@ -1086,7 +1651,20 @@ type LocalBoard = {
|
|
|
1086
1651
|
subscribe(listener: () => void): () => void;
|
|
1087
1652
|
dispose(): Promise<void>;
|
|
1088
1653
|
};
|
|
1654
|
+
/** @deprecated Use `createBoardController` instead — this is a thin, stroke-only wrapper kept for the original Phase 1 tracer's compatibility. */
|
|
1089
1655
|
declare function createLocalBoard(options: LocalBoardOptions): LocalBoard;
|
|
1090
1656
|
|
|
1091
|
-
|
|
1092
|
-
|
|
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 };
|