@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/react.d.ts
CHANGED
|
@@ -155,6 +155,21 @@ interface CustomObjectDefinition<Props extends JsonValue = JsonValue> {
|
|
|
155
155
|
/** One pure, synchronous step per consecutive schema version. */
|
|
156
156
|
migrate?: Readonly<Record<number, (oldProps: JsonValue) => JsonValue>>;
|
|
157
157
|
describe(object: ReadonlyCustomObject<Props>, context: ObjectDescribeContext): BoardScene;
|
|
158
|
+
/**
|
|
159
|
+
* Optional point-level hit-test precision (Phase 8). Every custom object
|
|
160
|
+
* hit-tests against its bounding box (`fallback.bounds`) by default — this
|
|
161
|
+
* lets a non-rectangular shape (e.g. a circular card, an L-shaped region)
|
|
162
|
+
* reject a point that's inside that box but outside its actual visible
|
|
163
|
+
* silhouette, tightening a click/marquee/raycast hit to the shape's real
|
|
164
|
+
* outline. `point` is in this object's own local space — the same
|
|
165
|
+
* untransformed space `describe`'s returned geometry already lives in
|
|
166
|
+
* (the caller inverse-transforms the pointer's board point through
|
|
167
|
+
* `object.transform` before calling this). Absent means every point
|
|
168
|
+
* inside the bounding box hits, matching pre-Phase-8 behavior exactly.
|
|
169
|
+
* Rejecting a point here does not fall through to whatever's underneath —
|
|
170
|
+
* the gesture simply misses this object, same as clicking empty space.
|
|
171
|
+
*/
|
|
172
|
+
hitTest?(object: ReadonlyCustomObject<Props>, point: BoardPoint): boolean;
|
|
158
173
|
}
|
|
159
174
|
interface SceneNodeBase {
|
|
160
175
|
key: string;
|
|
@@ -188,6 +203,15 @@ interface SceneGroup extends SceneNodeBase {
|
|
|
188
203
|
kind: "group";
|
|
189
204
|
children: readonly BoardScene[];
|
|
190
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* **No renderer or SVG-export interpreter exists for this node kind yet**
|
|
208
|
+
* (tracked as deferred work — see `renderer/shapes/customObjects.ts`'s
|
|
209
|
+
* `"path"` case). Returning a `ScenePath` from `describe()` renders nothing,
|
|
210
|
+
* exports nothing, and contributes no hit-test bounds — it neither errors
|
|
211
|
+
* nor emits a diagnostic. Until an interpreter ships, build custom shapes
|
|
212
|
+
* from `SceneRect`/`SceneEllipse`/`SceneGroup`/`SceneText`/`SceneImage`
|
|
213
|
+
* instead.
|
|
214
|
+
*/
|
|
191
215
|
interface ScenePath extends SceneNodeBase {
|
|
192
216
|
kind: "path";
|
|
193
217
|
/** SVG-style path data, board-local coordinates. */
|
|
@@ -343,8 +367,23 @@ interface Lockable {
|
|
|
343
367
|
lockedByName?: string;
|
|
344
368
|
}
|
|
345
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Per-object visibility (Phase 8) — mirrors `itemLock.ts`'s `Lockable`
|
|
372
|
+
* pattern exactly, but simpler: unlike a lock, hidden state carries no
|
|
373
|
+
* holder/ownership concept, so there's no analogue to `LockHolder`/
|
|
374
|
+
* `canUnlockItem`. A hidden object stays fully present in the Document
|
|
375
|
+
* (still serializes, persists, syncs, undoes/redoes) — it just skips
|
|
376
|
+
* rendering and hit-testing/selection candidacy. `hidden` absent or
|
|
377
|
+
* `false` means visible; this keeps every pre-Phase-8 document (which has
|
|
378
|
+
* no `hidden` field on any object at all) implicitly fully visible with
|
|
379
|
+
* zero migration needed.
|
|
380
|
+
*/
|
|
381
|
+
interface Hideable {
|
|
382
|
+
hidden?: boolean;
|
|
383
|
+
}
|
|
384
|
+
|
|
346
385
|
/** A kitchen timer sitting on the board. Remaining time is derived, not ticked. */
|
|
347
|
-
interface KitchenTimer extends Lockable {
|
|
386
|
+
interface KitchenTimer extends Lockable, Hideable {
|
|
348
387
|
id: string;
|
|
349
388
|
x: number;
|
|
350
389
|
y: number;
|
|
@@ -358,6 +397,133 @@ interface KitchenTimer extends Lockable {
|
|
|
358
397
|
runningSince?: number;
|
|
359
398
|
}
|
|
360
399
|
|
|
400
|
+
interface RectangleObject extends Lockable, Hideable {
|
|
401
|
+
id: string;
|
|
402
|
+
x: number;
|
|
403
|
+
y: number;
|
|
404
|
+
width: number;
|
|
405
|
+
height: number;
|
|
406
|
+
fill?: string;
|
|
407
|
+
stroke?: string;
|
|
408
|
+
strokeWidth?: number;
|
|
409
|
+
/** Corner radius in board units; clamped to at most half the shorter side at render time. */
|
|
410
|
+
cornerRadius?: number;
|
|
411
|
+
/** `[0, 1]`; undefined means fully opaque (Phase 4). */
|
|
412
|
+
opacity?: number;
|
|
413
|
+
/**
|
|
414
|
+
* Radians, about the shape's own center `(x + width/2, y - height/2)`.
|
|
415
|
+
* Undefined means 0 (Phase 3). `x`/`y`/`width`/`height` stay in the
|
|
416
|
+
* shape's own unrotated local frame — rotation is a separate, applied-last
|
|
417
|
+
* transform, not baked into them, matching how Stroke/CustomBoardObject
|
|
418
|
+
* keep geometry and placement independent via their own `matrix`.
|
|
419
|
+
*/
|
|
420
|
+
rotation?: number;
|
|
421
|
+
}
|
|
422
|
+
interface EllipseObject extends Lockable, Hideable {
|
|
423
|
+
id: string;
|
|
424
|
+
x: number;
|
|
425
|
+
y: number;
|
|
426
|
+
width: number;
|
|
427
|
+
height: number;
|
|
428
|
+
fill?: string;
|
|
429
|
+
stroke?: string;
|
|
430
|
+
strokeWidth?: number;
|
|
431
|
+
/** `[0, 1]`; undefined means fully opaque (Phase 4). */
|
|
432
|
+
opacity?: number;
|
|
433
|
+
/** Radians, about the shape's own center — see RectangleObject's `rotation` doc. */
|
|
434
|
+
rotation?: number;
|
|
435
|
+
}
|
|
436
|
+
/** `"none"` is a plain line with no arrowhead; today's only real head shape is `"triangle"`. New head shapes extend this union without touching `ArrowObject`'s own fields. */
|
|
437
|
+
type ArrowHeadStyle = "triangle" | "none";
|
|
438
|
+
interface LineObject extends Lockable, Hideable {
|
|
439
|
+
id: string;
|
|
440
|
+
start: BoardPoint;
|
|
441
|
+
end: BoardPoint;
|
|
442
|
+
stroke?: string;
|
|
443
|
+
strokeWidth?: number;
|
|
444
|
+
opacity?: number;
|
|
445
|
+
}
|
|
446
|
+
interface ArrowObject extends Lockable, Hideable {
|
|
447
|
+
id: string;
|
|
448
|
+
start: BoardPoint;
|
|
449
|
+
end: BoardPoint;
|
|
450
|
+
head?: ArrowHeadStyle;
|
|
451
|
+
stroke?: string;
|
|
452
|
+
strokeWidth?: number;
|
|
453
|
+
opacity?: number;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Triangle(3)/Diamond(4)/Pentagon(5)/Hexagon(6)/Octagon(8) as one shared
|
|
457
|
+
* type instead of five near-duplicate interfaces — a regular N-gon
|
|
458
|
+
* inscribed in the same `x`/`y`/`width`/`height`/`rotation` bounding box
|
|
459
|
+
* Rectangle already uses, parameterized by `sides`. Diamond is exactly a
|
|
460
|
+
* 4-sided regular polygon with vertex 0 pointing right (not up, like
|
|
461
|
+
* Triangle/Pentagon/Hexagon) — see `polygonGeometry.ts`'s
|
|
462
|
+
* `polygonStartAngle`, which encodes each side count's own vertex
|
|
463
|
+
* orientation so the outline always matches the legacy drag-preview shape.
|
|
464
|
+
*/
|
|
465
|
+
interface PolygonObject extends Lockable, Hideable {
|
|
466
|
+
id: string;
|
|
467
|
+
x: number;
|
|
468
|
+
y: number;
|
|
469
|
+
width: number;
|
|
470
|
+
height: number;
|
|
471
|
+
sides: 3 | 4 | 5 | 6 | 8;
|
|
472
|
+
fill?: string;
|
|
473
|
+
stroke?: string;
|
|
474
|
+
strokeWidth?: number;
|
|
475
|
+
opacity?: number;
|
|
476
|
+
/** Radians, about the shape's own center — see RectangleObject's `rotation` doc. */
|
|
477
|
+
rotation?: number;
|
|
478
|
+
}
|
|
479
|
+
/** Same bounding-box/rotation convention as Rectangle; a 5-pointed star with a tuned inner-radius ratio, matching the legacy tool's own default (see `polygonGeometry.ts`'s `starPoints`). */
|
|
480
|
+
interface StarObject extends Lockable, Hideable {
|
|
481
|
+
id: string;
|
|
482
|
+
x: number;
|
|
483
|
+
y: number;
|
|
484
|
+
width: number;
|
|
485
|
+
height: number;
|
|
486
|
+
/** Vertex count; today's only shipped preset is 5, matching the legacy tool. */
|
|
487
|
+
points: number;
|
|
488
|
+
/** `(0, 1)` — inner vertex radius as a fraction of the outer radius. */
|
|
489
|
+
innerRadiusRatio: number;
|
|
490
|
+
fill?: string;
|
|
491
|
+
stroke?: string;
|
|
492
|
+
strokeWidth?: number;
|
|
493
|
+
opacity?: number;
|
|
494
|
+
rotation?: number;
|
|
495
|
+
}
|
|
496
|
+
/** Same bounding-box/rotation convention as Rectangle; the standard parametric heart curve (see `polygonGeometry.ts`'s `heartPoints`), no extra parameters beyond the shared shape fields. */
|
|
497
|
+
interface HeartObject extends Lockable, Hideable {
|
|
498
|
+
id: string;
|
|
499
|
+
x: number;
|
|
500
|
+
y: number;
|
|
501
|
+
width: number;
|
|
502
|
+
height: number;
|
|
503
|
+
fill?: string;
|
|
504
|
+
stroke?: string;
|
|
505
|
+
strokeWidth?: number;
|
|
506
|
+
opacity?: number;
|
|
507
|
+
rotation?: number;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* A logical grouping of other board objects (Phase 3 — Selection,
|
|
511
|
+
* Transformation & Grouping). Deliberately has no `x`/`y`/`transform` of its
|
|
512
|
+
* own — a group's bounds are always derived on demand from its (recursively
|
|
513
|
+
* resolved) children, and "moving/rotating/scaling the group" is exactly a
|
|
514
|
+
* multi-object transform applied to those children, nothing more. A group
|
|
515
|
+
* has no renderer/mesh of its own; its only visual presence is the
|
|
516
|
+
* selection gizmo's bounding box while it's the current selection.
|
|
517
|
+
*
|
|
518
|
+
* `children` may itself contain other group ids (nested groups) — expanding
|
|
519
|
+
* a group into its leaf members is always done by the caller (recursively,
|
|
520
|
+
* with cycle protection), never assumed here.
|
|
521
|
+
*/
|
|
522
|
+
interface GroupObject extends Lockable, Hideable {
|
|
523
|
+
id: string;
|
|
524
|
+
children: string[];
|
|
525
|
+
}
|
|
526
|
+
|
|
361
527
|
interface BoardPoint {
|
|
362
528
|
x: number;
|
|
363
529
|
y: number;
|
|
@@ -378,7 +544,7 @@ interface StrokePoint extends BoardPoint {
|
|
|
378
544
|
* geometric outline, not an expressive ink mark.
|
|
379
545
|
*/
|
|
380
546
|
type StrokeTool = "marker" | "highlighter" | "shape";
|
|
381
|
-
interface Stroke extends Lockable {
|
|
547
|
+
interface Stroke extends Lockable, Hideable {
|
|
382
548
|
id: string;
|
|
383
549
|
color: string;
|
|
384
550
|
baseWidth: number;
|
|
@@ -394,7 +560,7 @@ interface Stroke extends Lockable {
|
|
|
394
560
|
clusterId?: string;
|
|
395
561
|
}
|
|
396
562
|
type SerializedPoint = [number, number, number, number];
|
|
397
|
-
interface SerializedStroke extends Lockable {
|
|
563
|
+
interface SerializedStroke extends Lockable, Hideable {
|
|
398
564
|
id: string;
|
|
399
565
|
color: string;
|
|
400
566
|
baseWidth: number;
|
|
@@ -420,6 +586,31 @@ interface SerializedDocument {
|
|
|
420
586
|
timers?: KitchenTimer[];
|
|
421
587
|
/** Absent in documents saved before Custom board objects existed (ticket #22). */
|
|
422
588
|
customObjects?: CustomBoardObject[];
|
|
589
|
+
/** Absent in documents saved before semantic Rectangle objects existed (Phase 2). */
|
|
590
|
+
rectangles?: RectangleObject[];
|
|
591
|
+
/** Absent in documents saved before semantic Ellipse objects existed (Phase 2). */
|
|
592
|
+
ellipses?: EllipseObject[];
|
|
593
|
+
/** Absent in documents saved before Groups existed (Phase 3). */
|
|
594
|
+
groups?: GroupObject[];
|
|
595
|
+
/** Absent in documents saved before semantic Line objects existed (Phase 4). */
|
|
596
|
+
lines?: LineObject[];
|
|
597
|
+
/** Absent in documents saved before semantic Arrow objects existed (Phase 4). */
|
|
598
|
+
arrows?: ArrowObject[];
|
|
599
|
+
/** Absent in documents saved before semantic Polygon objects existed (Phase 4). */
|
|
600
|
+
polygons?: PolygonObject[];
|
|
601
|
+
/** Absent in documents saved before semantic Star objects existed (Phase 4). */
|
|
602
|
+
stars?: StarObject[];
|
|
603
|
+
/** Absent in documents saved before semantic Heart objects existed (Phase 4). */
|
|
604
|
+
hearts?: HeartObject[];
|
|
605
|
+
/**
|
|
606
|
+
* Every content-object id (every type above except comments, which are
|
|
607
|
+
* host-synced and never enter this schema) in paint order, back to front.
|
|
608
|
+
* Absent in documents saved before per-object z-order existed (Phase 3) —
|
|
609
|
+
* migration synthesizes a default order preserving the old fixed-Z-band
|
|
610
|
+
* visual stacking exactly, so an existing document never visibly changes
|
|
611
|
+
* on load; only an explicit reorder action touches this from then on.
|
|
612
|
+
*/
|
|
613
|
+
objectOrder?: string[];
|
|
423
614
|
}
|
|
424
615
|
/**
|
|
425
616
|
* One collaborator's vote on a note. One per person; toggling removes it.
|
|
@@ -433,7 +624,7 @@ interface NoteVote {
|
|
|
433
624
|
* A sticky note: content floating above the board at a z-offset (pillar 3 —
|
|
434
625
|
* depth as an organizational axis). Center position in board space.
|
|
435
626
|
*/
|
|
436
|
-
interface StickyNote extends Lockable {
|
|
627
|
+
interface StickyNote extends Lockable, Hideable {
|
|
437
628
|
id: string;
|
|
438
629
|
x: number;
|
|
439
630
|
y: number;
|
|
@@ -451,7 +642,7 @@ interface StickyNote extends Lockable {
|
|
|
451
642
|
* top-left corner; lines flow downward (-y). Text joins the clustering
|
|
452
643
|
* system like handwriting (build prompt §6.4).
|
|
453
644
|
*/
|
|
454
|
-
interface TextBlock extends Lockable {
|
|
645
|
+
interface TextBlock extends Lockable, Hideable {
|
|
455
646
|
id: string;
|
|
456
647
|
x: number;
|
|
457
648
|
y: number;
|
|
@@ -465,7 +656,7 @@ interface TextBlock extends Lockable {
|
|
|
465
656
|
* Interactive structured table on the board. Position (x, y) is top-left in board units.
|
|
466
657
|
* Cells are indexed as `${row},${col}` keys mapping to cell text content.
|
|
467
658
|
*/
|
|
468
|
-
interface TableBlock extends Lockable {
|
|
659
|
+
interface TableBlock extends Lockable, Hideable {
|
|
469
660
|
id: string;
|
|
470
661
|
x: number;
|
|
471
662
|
y: number;
|
|
@@ -482,7 +673,7 @@ interface TableBlock extends Lockable {
|
|
|
482
673
|
* An imported image block on the board plane.
|
|
483
674
|
* Coordinates (x, y) represent the center of the image in board space.
|
|
484
675
|
*/
|
|
485
|
-
interface ImageBlock extends Lockable {
|
|
676
|
+
interface ImageBlock extends Lockable, Hideable {
|
|
486
677
|
id: string;
|
|
487
678
|
/**
|
|
488
679
|
* A legacy, read-only data URL (or, historically, an arbitrary string) —
|
|
@@ -612,7 +803,7 @@ interface BoardSnapshot {
|
|
|
612
803
|
* here, matching the engine's own internal selection-badge behavior.
|
|
613
804
|
*/
|
|
614
805
|
interface FocusedItem {
|
|
615
|
-
readonly type: "stroke" | "note" | "text" | "table" | "image" | "timer" | "custom";
|
|
806
|
+
readonly type: "stroke" | "note" | "text" | "table" | "image" | "timer" | "rectangle" | "ellipse" | "group" | "line" | "arrow" | "polygon" | "star" | "heart" | "custom";
|
|
616
807
|
readonly id: string;
|
|
617
808
|
readonly locked: boolean;
|
|
618
809
|
readonly lockedBy?: string;
|
|
@@ -693,6 +884,8 @@ interface BoardEventMap {
|
|
|
693
884
|
"asset-diagnostic": AssetDiagnostic;
|
|
694
885
|
/** A batch of Ops was reconciled (not applied as-sent) by the persistence adapter (ticket #24). */
|
|
695
886
|
"persistence-diagnostic": PersistenceDiagnostic;
|
|
887
|
+
/** The collaboration server has confirmed receipt of these op ids (Phase 7) — observability only; the Document was already correct via optimistic local apply before this ever fires. */
|
|
888
|
+
"collaboration-ops-acknowledged": CollaborationAckDiagnostic;
|
|
696
889
|
audit: unknown;
|
|
697
890
|
error: BoardControllerError;
|
|
698
891
|
disposed: undefined;
|
|
@@ -756,9 +949,12 @@ interface PresenceView {
|
|
|
756
949
|
readonly height: number;
|
|
757
950
|
}
|
|
758
951
|
/**
|
|
759
|
-
* A
|
|
760
|
-
*
|
|
761
|
-
*
|
|
952
|
+
* A collaborator, synced in for cursor/roster rendering only. Presence is
|
|
953
|
+
* ephemeral — it never touches the Document, Ops, undo/redo, or persistence
|
|
954
|
+
* (ADR 0006/0007). Two ways a roster gets populated (`presence.sync`
|
|
955
|
+
* directly, or a `CollaborationAdapter`'s optional presence channel —
|
|
956
|
+
* Phase 6, ADR 0015) both feed the exact same read/query capability below;
|
|
957
|
+
* a Host picks one, not both, for a given controller.
|
|
762
958
|
*/
|
|
763
959
|
interface PresenceUser {
|
|
764
960
|
readonly id: string;
|
|
@@ -767,6 +963,14 @@ interface PresenceUser {
|
|
|
767
963
|
readonly tool?: string;
|
|
768
964
|
readonly cursor?: PresenceCursor;
|
|
769
965
|
readonly view?: PresenceView;
|
|
966
|
+
/** Host-supplied extras (avatar URL, role, etc.) — opaque to Scrawl, never interpreted. */
|
|
967
|
+
readonly metadata?: Record<string, unknown>;
|
|
968
|
+
}
|
|
969
|
+
/** This client's own local presence, published via `presence.broadcast()` (Phase 6). */
|
|
970
|
+
interface LocalPresence {
|
|
971
|
+
readonly cursor?: PresenceCursor | null;
|
|
972
|
+
readonly view?: PresenceView | null;
|
|
973
|
+
readonly tool?: string;
|
|
770
974
|
}
|
|
771
975
|
/**
|
|
772
976
|
* The Custom arm wraps `CustomBoardObject` under the same `type` discriminant
|
|
@@ -794,6 +998,22 @@ type BoardObject = ({
|
|
|
794
998
|
} & ImageBlock) | ({
|
|
795
999
|
type: "timer";
|
|
796
1000
|
} & KitchenTimer) | ({
|
|
1001
|
+
type: "rectangle";
|
|
1002
|
+
} & RectangleObject) | ({
|
|
1003
|
+
type: "ellipse";
|
|
1004
|
+
} & EllipseObject) | ({
|
|
1005
|
+
type: "group";
|
|
1006
|
+
} & GroupObject) | ({
|
|
1007
|
+
type: "line";
|
|
1008
|
+
} & LineObject) | ({
|
|
1009
|
+
type: "arrow";
|
|
1010
|
+
} & ArrowObject) | ({
|
|
1011
|
+
type: "polygon";
|
|
1012
|
+
} & PolygonObject) | ({
|
|
1013
|
+
type: "star";
|
|
1014
|
+
} & StarObject) | ({
|
|
1015
|
+
type: "heart";
|
|
1016
|
+
} & HeartObject) | ({
|
|
797
1017
|
type: "custom";
|
|
798
1018
|
customType: ObjectType;
|
|
799
1019
|
} & Omit<CustomBoardObject, "type">);
|
|
@@ -822,6 +1042,30 @@ type BoardObjectInput = {
|
|
|
822
1042
|
type: "timer";
|
|
823
1043
|
id?: string;
|
|
824
1044
|
} & Omit<KitchenTimer, "id">) | ({
|
|
1045
|
+
type: "rectangle";
|
|
1046
|
+
id?: string;
|
|
1047
|
+
} & Omit<RectangleObject, "id">) | ({
|
|
1048
|
+
type: "ellipse";
|
|
1049
|
+
id?: string;
|
|
1050
|
+
} & Omit<EllipseObject, "id">) | ({
|
|
1051
|
+
type: "group";
|
|
1052
|
+
id?: string;
|
|
1053
|
+
} & Omit<GroupObject, "id">) | ({
|
|
1054
|
+
type: "line";
|
|
1055
|
+
id?: string;
|
|
1056
|
+
} & Omit<LineObject, "id">) | ({
|
|
1057
|
+
type: "arrow";
|
|
1058
|
+
id?: string;
|
|
1059
|
+
} & Omit<ArrowObject, "id">) | ({
|
|
1060
|
+
type: "polygon";
|
|
1061
|
+
id?: string;
|
|
1062
|
+
} & Omit<PolygonObject, "id">) | ({
|
|
1063
|
+
type: "star";
|
|
1064
|
+
id?: string;
|
|
1065
|
+
} & Omit<StarObject, "id">) | ({
|
|
1066
|
+
type: "heart";
|
|
1067
|
+
id?: string;
|
|
1068
|
+
} & Omit<HeartObject, "id">) | ({
|
|
825
1069
|
type: "custom";
|
|
826
1070
|
id?: string;
|
|
827
1071
|
customType: ObjectType;
|
|
@@ -852,10 +1096,34 @@ type LoadResult = {
|
|
|
852
1096
|
} | {
|
|
853
1097
|
state: "missing";
|
|
854
1098
|
};
|
|
1099
|
+
/**
|
|
1100
|
+
* Result of a whole-document `PersistenceAdapter.replace()` call (ADR 0006:
|
|
1101
|
+
* "Whole-document writes survive only for create, clear-board and import,
|
|
1102
|
+
* where replacing everything is the actual intent"). Revision-gated, unlike
|
|
1103
|
+
* `applyOps` — `conflict` means `baseRevision` was stale (someone else's
|
|
1104
|
+
* write landed first); the caller must reload and never overwrites blind.
|
|
1105
|
+
*/
|
|
1106
|
+
type ReplaceResult = {
|
|
1107
|
+
state: "applied";
|
|
1108
|
+
revision: string;
|
|
1109
|
+
} | {
|
|
1110
|
+
state: "conflict";
|
|
1111
|
+
currentRevision: string;
|
|
1112
|
+
};
|
|
1113
|
+
/**
|
|
1114
|
+
* The one sanctioned seam for persisting a Board's Document to a Host's own
|
|
1115
|
+
* storage — implement this against a database, an HTTP API, IndexedDB
|
|
1116
|
+
* (see `@scrawl-board/board/local`'s `createIndexedDBPersistence`), or
|
|
1117
|
+
* anything else. `load()` fetches the current state on connect; `applyOps()`
|
|
1118
|
+
* streams incremental Ops as edits happen; `replace()` is only for
|
|
1119
|
+
* whole-document writes (create, clear-board, import — see ADR 0006) and is
|
|
1120
|
+
* revision-gated so a stale write never silently clobbers a newer one.
|
|
1121
|
+
* Passed via `createBoardController({ adapters: { persistence } })`.
|
|
1122
|
+
*/
|
|
855
1123
|
interface PersistenceAdapter {
|
|
856
1124
|
load(context: DocumentContext): Promise<LoadResult>;
|
|
857
1125
|
applyOps(context: DocumentContext, ops: readonly ControllerOp[]): Promise<ApplyOpsResult>;
|
|
858
|
-
replace(context: DocumentContext, document: CurrentSerializedDocument, baseRevision: string): Promise<
|
|
1126
|
+
replace(context: DocumentContext, document: CurrentSerializedDocument, baseRevision: string): Promise<ReplaceResult>;
|
|
859
1127
|
}
|
|
860
1128
|
/**
|
|
861
1129
|
* `"reconcile"` (ticket #24) means the server authoritatively resolved the
|
|
@@ -879,14 +1147,59 @@ interface PersistenceDiagnostic {
|
|
|
879
1147
|
rejectedOpIds: readonly string[];
|
|
880
1148
|
revision: string;
|
|
881
1149
|
}
|
|
1150
|
+
/** Emitted as `"collaboration-ops-acknowledged"` (Phase 7) — the server has confirmed receipt of these op ids on the live pipe. */
|
|
1151
|
+
interface CollaborationAckDiagnostic {
|
|
1152
|
+
opIds: readonly string[];
|
|
1153
|
+
}
|
|
882
1154
|
interface ControllerOp {
|
|
883
1155
|
id: string;
|
|
884
1156
|
schemaVersion: 1;
|
|
885
1157
|
kind: "upsert" | "restore" | "remove";
|
|
886
|
-
objectType: "stroke" | "note" | "text" | "table" | "image" | "timer" | "custom"
|
|
1158
|
+
objectType: "stroke" | "note" | "text" | "table" | "image" | "timer" | "rectangle" | "ellipse" | "group" | "line" | "arrow" | "polygon" | "star" | "heart" | "custom"
|
|
1159
|
+
/**
|
|
1160
|
+
* A whole-document paint-order sync (Phase 3), not a per-object type —
|
|
1161
|
+
* `objectId` is always the fixed sentinel `"order"` and `payload` is
|
|
1162
|
+
* `{ order: string[] }`. The only `objectType` with no matching
|
|
1163
|
+
* `BoardObject`/document collection; kept in this same union (rather
|
|
1164
|
+
* than a separate wire message) so it flows through the existing
|
|
1165
|
+
* `PersistenceAdapter`/`CollaborationAdapter` opaquely, unchanged.
|
|
1166
|
+
*/
|
|
1167
|
+
| "order";
|
|
887
1168
|
objectId: string;
|
|
888
1169
|
payload?: unknown;
|
|
1170
|
+
/**
|
|
1171
|
+
* This op's position in its own originating client's local sequence
|
|
1172
|
+
* (Phase 7) — 1, 2, 3, ... per controller instance, distinct from `id`
|
|
1173
|
+
* (an opaque, globally-unique identifier used for dedup/ack, not
|
|
1174
|
+
* ordering) and from a server's own authoritative ordering (e.g.
|
|
1175
|
+
* `referenceCollaborationServer.ts`'s per-room `version` counter).
|
|
1176
|
+
* Present on every op this SDK originates locally; a remote peer's op
|
|
1177
|
+
* carries whatever its own origin set, unchanged — never renumbered in
|
|
1178
|
+
* transit. Absent on an op minted by decoding the legacy wire envelope
|
|
1179
|
+
* (`scrawlOpEnvelope.ts`), which predates this field and has no
|
|
1180
|
+
* per-client sequence concept of its own.
|
|
1181
|
+
*/
|
|
1182
|
+
clientSequence?: number;
|
|
1183
|
+
/**
|
|
1184
|
+
* The `CollaboratorIdentity.id` of this op's originating client (Phase
|
|
1185
|
+
* 7) — set for every op this SDK originates locally when `identity` is
|
|
1186
|
+
* configured, omitted entirely otherwise (never sent as `undefined`).
|
|
1187
|
+
* The explicit foundation for a future per-author undo filter (a local
|
|
1188
|
+
* user's own undo should only ever touch their own ops) — no undo-stack
|
|
1189
|
+
* behavior itself changes this phase.
|
|
1190
|
+
*/
|
|
1191
|
+
clientId?: string;
|
|
889
1192
|
}
|
|
1193
|
+
/**
|
|
1194
|
+
* The one sanctioned seam for real-time multiplayer — implement this against
|
|
1195
|
+
* a Host's own collaboration backend (WebSocket relay, CRDT server, etc.).
|
|
1196
|
+
* `connect()` is called once per controller with the local user's
|
|
1197
|
+
* `identity` and a `receive` callback the adapter invokes with incoming
|
|
1198
|
+
* Ops, presence updates, acks, and connection status; it resolves with a
|
|
1199
|
+
* `CollaborationSession` the controller uses to send local Ops and presence
|
|
1200
|
+
* back out. Passed via `createBoardController({ adapters: { collaboration } })`;
|
|
1201
|
+
* omit it entirely to run single-player.
|
|
1202
|
+
*/
|
|
890
1203
|
interface CollaborationAdapter {
|
|
891
1204
|
connect(options: DocumentContext & {
|
|
892
1205
|
identity: CollaboratorIdentity;
|
|
@@ -897,14 +1210,72 @@ interface CollaboratorIdentity {
|
|
|
897
1210
|
id: string;
|
|
898
1211
|
name: string;
|
|
899
1212
|
color?: string;
|
|
1213
|
+
/** Host-supplied extras (avatar URL, role, etc.) — opaque to Scrawl, forwarded into any resulting `PresenceUser` unread and never interpreted. */
|
|
1214
|
+
metadata?: Record<string, unknown>;
|
|
900
1215
|
}
|
|
901
1216
|
interface CollaborationReceiver {
|
|
902
1217
|
ops(ops: readonly ControllerOp[]): void;
|
|
1218
|
+
/**
|
|
1219
|
+
* The current presence roster (Phase 6, ADR 0015) — always a full
|
|
1220
|
+
* replacement, never a delta, matching `presence.sync`'s existing
|
|
1221
|
+
* semantics exactly (an adapter that aggregates wire deltas into a full
|
|
1222
|
+
* roster before calling this is the adapter's own job, not the
|
|
1223
|
+
* controller's). Required on this interface (not optional) because a
|
|
1224
|
+
* Host only ever *consumes* `CollaborationReceiver` — never implements
|
|
1225
|
+
* it — so adding a required method here cannot break an existing custom
|
|
1226
|
+
* `CollaborationAdapter`. An adapter with no presence support simply
|
|
1227
|
+
* never calls it.
|
|
1228
|
+
*/
|
|
1229
|
+
presence(users: readonly PresenceUser[]): void;
|
|
1230
|
+
/**
|
|
1231
|
+
* The server has confirmed receipt of these op ids (Phase 7) —
|
|
1232
|
+
* distinguishes "sent" from "server accepted," which `sendOps` alone
|
|
1233
|
+
* (fire-and-forget) cannot. Required for the same reason `presence` is:
|
|
1234
|
+
* Hosts only ever consume this interface, never implement it, so this
|
|
1235
|
+
* cannot break an existing custom `CollaborationAdapter`. An adapter with
|
|
1236
|
+
* no ack support simply never calls it — the collaboration pipe still
|
|
1237
|
+
* works exactly as it did before this existed, just without the
|
|
1238
|
+
* bookkeeping/observability this enables.
|
|
1239
|
+
*/
|
|
1240
|
+
acknowledged(opIds: readonly string[]): void;
|
|
903
1241
|
status(state: "online" | "reconnecting" | "offline"): void;
|
|
904
1242
|
error(cause: unknown): void;
|
|
905
1243
|
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Result of `CollaborationSession.requestSync()` (Phase 7). `"ops"` means
|
|
1246
|
+
* the adapter's own live-pipe cache fully covered the gap since the
|
|
1247
|
+
* caller's last known revision — apply `ops` and the client is caught up,
|
|
1248
|
+
* no persistence reload needed. `"unavailable"` means it couldn't (gap too
|
|
1249
|
+
* large, server restarted, or the adapter has no retained history at all)
|
|
1250
|
+
* — the caller must fall back to a persistence-backed reload. This is a
|
|
1251
|
+
* best-effort *liveness* cache, deliberately never a durable source of
|
|
1252
|
+
* truth (ADR 0006's "collaboration is never a second source of document
|
|
1253
|
+
* truth" — see ADR 0015's own extension of that principle to presence,
|
|
1254
|
+
* now extended once more, the same way, to this).
|
|
1255
|
+
*/
|
|
1256
|
+
type CollaborationSyncResult = {
|
|
1257
|
+
state: "ops";
|
|
1258
|
+
ops: readonly ControllerOp[];
|
|
1259
|
+
serverRevision: string;
|
|
1260
|
+
} | {
|
|
1261
|
+
state: "unavailable";
|
|
1262
|
+
};
|
|
906
1263
|
interface CollaborationSession {
|
|
907
1264
|
sendOps(ops: readonly ControllerOp[]): void;
|
|
1265
|
+
/**
|
|
1266
|
+
* Publishes this client's own local presence (Phase 6, ADR 0015) —
|
|
1267
|
+
* best-effort, unordered, never persisted, never an Op. Optional: an
|
|
1268
|
+
* adapter that doesn't support presence simply omits this method, and
|
|
1269
|
+
* `presence.broadcast()` becomes a silent no-op.
|
|
1270
|
+
*/
|
|
1271
|
+
updatePresence?(presence: LocalPresence): void;
|
|
1272
|
+
/**
|
|
1273
|
+
* Requests an incremental catch-up after a reconnect (Phase 7) — optional;
|
|
1274
|
+
* an adapter that doesn't support this simply omits the method, and the
|
|
1275
|
+
* caller (`resyncAfterReconnect`) goes straight to its existing
|
|
1276
|
+
* persistence-backed full reload, unchanged from Phase 6.
|
|
1277
|
+
*/
|
|
1278
|
+
requestSync?(): Promise<CollaborationSyncResult>;
|
|
908
1279
|
close(): Promise<void>;
|
|
909
1280
|
}
|
|
910
1281
|
interface CreateBoardControllerOptions {
|
|
@@ -944,6 +1315,87 @@ interface CreateBoardControllerOptions {
|
|
|
944
1315
|
* doesn't use the React theme system can set this directly instead.
|
|
945
1316
|
*/
|
|
946
1317
|
boardTheme?: BoardThemeOptions;
|
|
1318
|
+
/**
|
|
1319
|
+
* Debounced auto-flush of pending persistence Ops after document changes
|
|
1320
|
+
* settle (Phase 5). Enabled by default (1000ms debounce) whenever
|
|
1321
|
+
* `adapters.persistence` is configured — today, without this, a Host must
|
|
1322
|
+
* call `flush()` manually after every edit for anything to persist. Pass
|
|
1323
|
+
* `false` to opt out entirely and drive `flush()` yourself, preserving
|
|
1324
|
+
* prior behavior exactly. Never fires on a per-change basis — rapid edits
|
|
1325
|
+
* coalesce into one flush of their final state (ADR 0006).
|
|
1326
|
+
*/
|
|
1327
|
+
autosave?: boolean | {
|
|
1328
|
+
debounceMs?: number;
|
|
1329
|
+
};
|
|
1330
|
+
/**
|
|
1331
|
+
* Throttle for `presence.broadcast()` (Phase 6, ADR 0015) — the minimum
|
|
1332
|
+
* interval between outgoing presence updates sent via the configured
|
|
1333
|
+
* `CollaborationAdapter`. Defaults to 50ms. A trailing throttle: the
|
|
1334
|
+
* latest value passed to `broadcast()` always eventually sends, even if
|
|
1335
|
+
* calls arrive faster than this interval.
|
|
1336
|
+
*/
|
|
1337
|
+
presenceThrottleMs?: number;
|
|
1338
|
+
/**
|
|
1339
|
+
* Caps how many `ControllerOp`s can sit queued, unsent, for the
|
|
1340
|
+
* persistence pipe (`pendingOps`) or the collaboration pipe
|
|
1341
|
+
* (`pendingCollaborationOps`) at once (Phase 7) — each pipe is capped
|
|
1342
|
+
* independently. Prevents unbounded memory growth from a long-lived
|
|
1343
|
+
* offline session or a stuck adapter. Exceeding it never fails or drops
|
|
1344
|
+
* the local edit itself (the Document already applied it optimistically)
|
|
1345
|
+
* — only queueing for that one pipe is skipped, and a
|
|
1346
|
+
* `{code:"queue-overflow", retryable:false}` error is emitted so a Host
|
|
1347
|
+
* can react. Defaults to 1000 — the Phase 9 collaboration coalescing
|
|
1348
|
+
* above (`collaborationCoalesceMs`) already keeps a busy drag from
|
|
1349
|
+
* approaching this on its own, so hitting it in practice means a pipe
|
|
1350
|
+
* has been offline/stuck for a genuinely long editing session.
|
|
1351
|
+
*
|
|
1352
|
+
* **Recovery** (Phase 9): the dropped op itself is gone from that one
|
|
1353
|
+
* pipe's queue — there is no automatic backfill, and the live
|
|
1354
|
+
* controller keeps running with that pipe now silently missing one
|
|
1355
|
+
* edit. Two things stay true regardless: (1) the in-memory Document is
|
|
1356
|
+
* never affected — a queue-overflow can never corrupt or roll back a
|
|
1357
|
+
* local edit, only skip sending it; (2) staleness is per-object, not
|
|
1358
|
+
* permanent — any *later* edit to that same object produces a brand
|
|
1359
|
+
* new, undropped Op carrying its full current state, which naturally
|
|
1360
|
+
* supersedes the gap (the Op model is already last-write-wins/
|
|
1361
|
+
* idempotent, so a superseding Op doesn't need the earlier one to have
|
|
1362
|
+
* arrived). The real risk is an object that's dropped and never edited
|
|
1363
|
+
* again before the controller is disposed or the page reloads — a Host
|
|
1364
|
+
* that needs strict durability should treat `queue-overflow` as a
|
|
1365
|
+
* signal to check `persistence.state`/`pendingOps` pressure (via
|
|
1366
|
+
* `usePersistenceStatus`/`getSnapshot().connection.persistence`)
|
|
1367
|
+
* before disposing, not assume disposing and reconnecting alone
|
|
1368
|
+
* repairs the gap (a fresh `load()` only returns what the backend
|
|
1369
|
+
* already has, which is exactly what's missing the dropped edit).
|
|
1370
|
+
*/
|
|
1371
|
+
maxPendingOps?: number;
|
|
1372
|
+
/**
|
|
1373
|
+
* Coalescing window (ms) for outgoing collaboration Ops (Phase 9) — same
|
|
1374
|
+
* trailing-throttle shape as `presenceThrottleMs`: the first Op after an
|
|
1375
|
+
* idle period sends immediately, and subsequent Ops for the *same*
|
|
1376
|
+
* object within this window replace each other (latest value wins,
|
|
1377
|
+
* matching the already-idempotent Op model) rather than each triggering
|
|
1378
|
+
* its own send. A multi-second drag that previously sent one full Op per
|
|
1379
|
+
* pointer-move now sends at most one per window per touched object.
|
|
1380
|
+
* Persistence (`pendingOps`) is unaffected — it already debounces via
|
|
1381
|
+
* `autosave`, so this option only changes live collaboration traffic.
|
|
1382
|
+
* Defaults to 50ms.
|
|
1383
|
+
*/
|
|
1384
|
+
collaborationCoalesceMs?: number;
|
|
1385
|
+
/**
|
|
1386
|
+
* Caps how many resolved objects a single `content.copy`/`content.cut`
|
|
1387
|
+
* (or their Cmd/Ctrl+C/X keyboard equivalents) will hold in the
|
|
1388
|
+
* in-memory clipboard at once (Phase 9) — `expandSelection` recursively
|
|
1389
|
+
* expands groups, so an unbounded selection (a huge group, or thousands
|
|
1390
|
+
* of individually selected strokes) could otherwise clone and retain an
|
|
1391
|
+
* arbitrarily large snapshot indefinitely, until the next copy/cut
|
|
1392
|
+
* replaces it. Exceeding it rejects the whole copy/cut (nothing is
|
|
1393
|
+
* cloned, and — for cut — nothing is removed from the Document either,
|
|
1394
|
+
* never a partial copy of an arbitrary subset) and emits a
|
|
1395
|
+
* `{code:"clipboard-overflow", retryable:false}` error. Defaults to
|
|
1396
|
+
* 5000.
|
|
1397
|
+
*/
|
|
1398
|
+
maxClipboardItems?: number;
|
|
947
1399
|
}
|
|
948
1400
|
interface BoardController {
|
|
949
1401
|
readonly document: ReadonlyBoardDocument;
|
|
@@ -975,6 +1427,15 @@ interface BoardController {
|
|
|
975
1427
|
};
|
|
976
1428
|
readonly view: {
|
|
977
1429
|
fit(): void;
|
|
1430
|
+
/**
|
|
1431
|
+
* Frame the current selection (Phase 8), the same way `fit()` frames the
|
|
1432
|
+
* whole board. A no-op with nothing selected — deliberately doesn't fall
|
|
1433
|
+
* back to `fit()`'s "frame everything," which would be a surprising
|
|
1434
|
+
* result for an empty selection. On a headless board this can only
|
|
1435
|
+
* re-center the view (no viewport to compute a real zoom-to-fit from),
|
|
1436
|
+
* matching `fit()`'s own headless limitation exactly.
|
|
1437
|
+
*/
|
|
1438
|
+
zoomToSelection(): void;
|
|
978
1439
|
zoomTo(value: number): void;
|
|
979
1440
|
centerOn(point: BoardPoint): void;
|
|
980
1441
|
get(): BoardView;
|
|
@@ -996,6 +1457,64 @@ interface BoardController {
|
|
|
996
1457
|
* Unknown ids are silently skipped, matching `remove`'s convention.
|
|
997
1458
|
*/
|
|
998
1459
|
duplicate(ids: readonly string[]): readonly string[];
|
|
1460
|
+
/**
|
|
1461
|
+
* Creates a new Group referencing `ids` as its children and returns its
|
|
1462
|
+
* id, as one undoable step. Unknown ids are silently skipped, matching
|
|
1463
|
+
* `duplicate`/`remove`'s convention. A child id that's itself a group
|
|
1464
|
+
* makes a nested group — expanding nested groups into their leaf
|
|
1465
|
+
* members is always the caller's job, never assumed here (matches the
|
|
1466
|
+
* document-model `GroupObject` itself).
|
|
1467
|
+
*/
|
|
1468
|
+
group(ids: readonly string[]): string;
|
|
1469
|
+
/**
|
|
1470
|
+
* Dissolves one group, returning its immediate children's ids (a nested
|
|
1471
|
+
* subgroup among them stays intact, itself still a group) — the group
|
|
1472
|
+
* record itself is removed, the children are untouched. A no-op
|
|
1473
|
+
* (returns `[]`) if `groupId` isn't a group.
|
|
1474
|
+
*/
|
|
1475
|
+
ungroup(groupId: string): readonly string[];
|
|
1476
|
+
/**
|
|
1477
|
+
* Aligns every given object's matching edge/center to the corresponding
|
|
1478
|
+
* edge/center of their combined bounding box, as one undoable step.
|
|
1479
|
+
* `"top"`/`"bottom"` follow board space's Y-up convention (`"top"` is
|
|
1480
|
+
* the larger Y). Ids that don't resolve, or resolve to a Group (which
|
|
1481
|
+
* has no position of its own), are skipped. A no-op under 2 resolvable
|
|
1482
|
+
* ids — there's nothing to align relative to.
|
|
1483
|
+
*/
|
|
1484
|
+
align(ids: readonly string[], edge: "left" | "right" | "top" | "bottom" | "centerX" | "centerY"): void;
|
|
1485
|
+
/**
|
|
1486
|
+
* Spaces the middle objects' centers evenly between the first and last
|
|
1487
|
+
* (sorted along `axis`), as one undoable step — the two endpoints don't
|
|
1488
|
+
* move. Ids that don't resolve, or resolve to a Group, are skipped. A
|
|
1489
|
+
* no-op under 3 resolvable ids — there's no "middle" to distribute.
|
|
1490
|
+
*/
|
|
1491
|
+
distribute(ids: readonly string[], axis: "x" | "y"): void;
|
|
1492
|
+
/**
|
|
1493
|
+
* Snapshots `ids` (recursively expanded through any group, same as
|
|
1494
|
+
* `duplicate`) into an internal in-memory clipboard — never
|
|
1495
|
+
* `navigator.clipboard`, scoped to this one controller instance and
|
|
1496
|
+
* replaced wholesale by the next `copy`/`cut`. Read-only; works even
|
|
1497
|
+
* on a read-only board.
|
|
1498
|
+
*/
|
|
1499
|
+
copy(ids: readonly string[]): void;
|
|
1500
|
+
/** `copy`, then removes every resolved object (recursively through any group) as one undoable step. */
|
|
1501
|
+
cut(ids: readonly string[]): void;
|
|
1502
|
+
/**
|
|
1503
|
+
* Clones the current clipboard contents onto the board as one undoable
|
|
1504
|
+
* step, offset the same small cascade `duplicate` uses (no cursor
|
|
1505
|
+
* position to paste relative to yet). Returns the new top-level ids —
|
|
1506
|
+
* a pasted group's own id stands for its (also-pasted) children, which
|
|
1507
|
+
* aren't listed separately. `[]` when the clipboard is empty.
|
|
1508
|
+
*/
|
|
1509
|
+
paste(): readonly string[];
|
|
1510
|
+
/**
|
|
1511
|
+
* Select every top-level object (Phase 8) — a group's own id stands for
|
|
1512
|
+
* its children, which aren't selected separately, matching `paste`'s own
|
|
1513
|
+
* "what the user sees" id list. Hidden objects are excluded, consistent
|
|
1514
|
+
* with them already being excluded from marquee selection. Works with
|
|
1515
|
+
* no canvas/engine, same as {@link toggleSelectionVisibility}.
|
|
1516
|
+
*/
|
|
1517
|
+
selectAll(): void;
|
|
999
1518
|
table: {
|
|
1000
1519
|
addRow(tableId: string): void;
|
|
1001
1520
|
addCol(tableId: string): void;
|
|
@@ -1006,6 +1525,29 @@ interface BoardController {
|
|
|
1006
1525
|
};
|
|
1007
1526
|
select(ids: readonly string[]): void;
|
|
1008
1527
|
import(document: SerializedBoardDocument): readonly string[];
|
|
1528
|
+
/**
|
|
1529
|
+
* Toggle lock state for the current selection (or focused note/text/
|
|
1530
|
+
* table/image/timer), matching whatever a single Host lock/unlock
|
|
1531
|
+
* control already does per object type. A no-op with nothing selected,
|
|
1532
|
+
* on a headless board, or when every actionable target is locked by
|
|
1533
|
+
* another collaborator who isn't the current lock holder.
|
|
1534
|
+
*/
|
|
1535
|
+
toggleSelectionLock(): void;
|
|
1536
|
+
/**
|
|
1537
|
+
* Toggle hidden state for the current selection, as one undo entry
|
|
1538
|
+
* (Phase 8). If any selected object is hidden, shows every selected
|
|
1539
|
+
* object; otherwise hides them all — same "any wins" semantics as
|
|
1540
|
+
* {@link toggleSelectionLock}. Hidden objects stay fully present in the
|
|
1541
|
+
* document (they still serialize, persist, sync, undo/redo) — they just
|
|
1542
|
+
* stop rendering and stop being hit-testable/selectable via pointer
|
|
1543
|
+
* interaction. Unlike `toggleSelectionLock`, this works on a headless
|
|
1544
|
+
* board too: it only touches `selection`/the document, no canvas or
|
|
1545
|
+
* engine involved. Custom objects have no visibility concept (no
|
|
1546
|
+
* `Hideable` field) and are silently skipped, matching how
|
|
1547
|
+
* `toggleSelectionLock` already excludes them. A no-op with nothing
|
|
1548
|
+
* selected or when the selection is only custom objects.
|
|
1549
|
+
*/
|
|
1550
|
+
toggleSelectionVisibility(): void;
|
|
1009
1551
|
};
|
|
1010
1552
|
readonly query: {
|
|
1011
1553
|
get(id: string): DeepReadonly<BoardObject> | undefined;
|
|
@@ -1034,6 +1576,15 @@ interface BoardController {
|
|
|
1034
1576
|
follow(view: PresenceView): void;
|
|
1035
1577
|
/** Ease the camera to a peer's view; returns false (no-op) while mid-stroke. */
|
|
1036
1578
|
gather(view: PresenceView): boolean;
|
|
1579
|
+
/**
|
|
1580
|
+
* Publishes this client's own cursor/tool/view for other collaborators
|
|
1581
|
+
* (Phase 6, ADR 0015), via the configured `CollaborationAdapter` —
|
|
1582
|
+
* throttled internally (`presenceThrottleMs` option, default 50ms) so
|
|
1583
|
+
* a raw pointermove stream never becomes a message-per-event flood. A
|
|
1584
|
+
* no-op if no collaboration adapter is configured, or if the
|
|
1585
|
+
* configured one doesn't implement `updatePresence`.
|
|
1586
|
+
*/
|
|
1587
|
+
broadcast(local: LocalPresence): void;
|
|
1037
1588
|
};
|
|
1038
1589
|
readonly export: {
|
|
1039
1590
|
svg(): string;
|
|
@@ -1059,6 +1610,7 @@ interface BoardController {
|
|
|
1059
1610
|
}>;
|
|
1060
1611
|
dispose(): Promise<void>;
|
|
1061
1612
|
}
|
|
1613
|
+
/** @deprecated Use `BoardController.getSnapshot()`'s return type instead. */
|
|
1062
1614
|
type LocalBoardSnapshot = {
|
|
1063
1615
|
documentId: string;
|
|
1064
1616
|
selectedStrokeId: string | null;
|
|
@@ -1067,6 +1619,7 @@ type LocalBoardSnapshot = {
|
|
|
1067
1619
|
canRedo: boolean;
|
|
1068
1620
|
disposed: boolean;
|
|
1069
1621
|
};
|
|
1622
|
+
/** @deprecated Use `BoardController` (from `createBoardController`) instead — this stroke-only, single-tool surface predates the full capability-grouped controller. */
|
|
1070
1623
|
type LocalBoard = {
|
|
1071
1624
|
drawStroke(stroke: Stroke): void;
|
|
1072
1625
|
selectAt(point: BoardPoint): string | null;
|
|
@@ -1253,10 +1806,19 @@ interface FocusedItemToolbarProps {
|
|
|
1253
1806
|
snapshot: BoardSnapshot;
|
|
1254
1807
|
}
|
|
1255
1808
|
/**
|
|
1256
|
-
* Floating toolbar above the focused note, shape,
|
|
1257
|
-
* Size (note/text) or Width (shape),
|
|
1258
|
-
* Table/image/timer/
|
|
1259
|
-
*
|
|
1809
|
+
* Floating toolbar above the focused note, shape, text block, or semantic
|
|
1810
|
+
* Rectangle/Ellipse — Colour, Size (note/text) or Width (shape), Fill/Stroke
|
|
1811
|
+
* (Rectangle/Ellipse), Lock/Unlock, Duplicate, Delete. Table/image/timer/
|
|
1812
|
+
* custom objects and plain (non-shape) ink strokes never get a toolbar here
|
|
1813
|
+
* — out of scope for this destination.
|
|
1814
|
+
*
|
|
1815
|
+
* Rectangle/Ellipse (Phase 2) are a second, parallel "shape" concept from
|
|
1816
|
+
* the legacy ink-stroke shape below: both draw from the same toolbar
|
|
1817
|
+
* buttons and look the same to a user, but a Rectangle/Ellipse is a real
|
|
1818
|
+
* BoardObject with its own resize handles (independent width/height,
|
|
1819
|
+
* `ShapeResizeHandle`), while a legacy shape-stroke keeps going through the
|
|
1820
|
+
* native selection gizmo. This duality is deliberate and temporary — see
|
|
1821
|
+
* docs/reports/phase-2-document-object-model.md.
|
|
1260
1822
|
*
|
|
1261
1823
|
* Lock/Unlock carries no per-user ownership gating: nothing else in this
|
|
1262
1824
|
* SDK enforces lock ownership either (`content.update` never checks
|
|
@@ -1311,6 +1873,16 @@ interface ScrawlProviderProps {
|
|
|
1311
1873
|
className?: string;
|
|
1312
1874
|
style?: ThemeStyle;
|
|
1313
1875
|
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Wraps a Board `controller` you created yourself (via `createBoardController`)
|
|
1878
|
+
* so its descendants can use `useScrawlController`/`useScrawlSnapshot`/etc.
|
|
1879
|
+
* and render its default UI pieces (`DefaultBoardChrome`, `StyleShelf`, ...).
|
|
1880
|
+
* Prefer `Scrawl` unless you need to construct or own the controller's
|
|
1881
|
+
* lifecycle yourself (e.g. you create it outside React, or need it before
|
|
1882
|
+
* first render). Set `disposeOnUnmount` to have this provider call
|
|
1883
|
+
* `controller.dispose()` on unmount; otherwise disposal remains your own
|
|
1884
|
+
* responsibility.
|
|
1885
|
+
*/
|
|
1314
1886
|
declare function ScrawlProvider({ controller, children, preset, theme, portalContainer: customPortal, disposeOnUnmount, onThemeDiagnostic, className, style }: ScrawlProviderProps): react.JSX.Element;
|
|
1315
1887
|
interface ScrawlProps extends Omit<CreateBoardControllerOptions, "canvas"> {
|
|
1316
1888
|
children?: ReactNode;
|
|
@@ -1331,6 +1903,17 @@ interface ScrawlProps extends Omit<CreateBoardControllerOptions, "canvas"> {
|
|
|
1331
1903
|
*/
|
|
1332
1904
|
icons?: Partial<Record<BuiltInTool, ReactNode>>;
|
|
1333
1905
|
}
|
|
1906
|
+
/**
|
|
1907
|
+
* The fastest path to an embedded Board: creates and owns a
|
|
1908
|
+
* `BoardController` for you (constructed once, disposed on unmount) and
|
|
1909
|
+
* renders it into a canvas. Render with no `children` to get the SDK's
|
|
1910
|
+
* default toolbar/UI chrome, or supply your own `children` (using the
|
|
1911
|
+
* `useScrawlController`/`useScrawlSnapshot` hooks, or the exported
|
|
1912
|
+
* `DefaultBoardChrome`/`StyleShelf`/etc. pieces) to build a custom UI on
|
|
1913
|
+
* top of the same controller. Accepts every `CreateBoardControllerOptions`
|
|
1914
|
+
* field except `canvas` (pass `canvas={null}` for a headless board, or an
|
|
1915
|
+
* existing `<canvas>` element to control its identity yourself).
|
|
1916
|
+
*/
|
|
1334
1917
|
declare function Scrawl({ children, preset, theme, portalContainer, className, style, onReady, onError, onThemeDiagnostic, canvas: suppliedCanvas, icons, ...options }: ScrawlProps): react.JSX.Element;
|
|
1335
1918
|
interface ScrawlCanvasProps {
|
|
1336
1919
|
element?: HTMLCanvasElement;
|
|
@@ -1356,6 +1939,38 @@ declare function ScrawlPortal({ children }: {
|
|
|
1356
1939
|
declare function useScrawlController(): BoardController;
|
|
1357
1940
|
declare function useScrawlTheme(): ScrawlResolvedTheme;
|
|
1358
1941
|
declare function useScrawlSnapshot(): BoardSnapshot;
|
|
1942
|
+
/**
|
|
1943
|
+
* A thin, purely-derived convenience over
|
|
1944
|
+
* `useScrawlSnapshot().connection.persistence` (Phase 5) — for a Host
|
|
1945
|
+
* component that only cares about save status (e.g. a
|
|
1946
|
+
* "Saving…"/"Saved"/"Offline" indicator) and would otherwise re-derive
|
|
1947
|
+
* this same field access itself. Adds no state and no behavior of its
|
|
1948
|
+
* own: React still owns none of persistence, exactly as before — this
|
|
1949
|
+
* hook only re-reads what the controller already tracks.
|
|
1950
|
+
*
|
|
1951
|
+
* Deliberately does *not* go through `useScrawlSnapshot()` (Phase 9):
|
|
1952
|
+
* `changed()` rebuilds the whole `BoardSnapshot` — including a fresh
|
|
1953
|
+
* `connection.persistence` object — on every document mutation, even ones
|
|
1954
|
+
* that never touch persistence at all, so a component using only this
|
|
1955
|
+
* hook would otherwise re-render on every stroke draw. `useSyncStatus`
|
|
1956
|
+
* below memoizes on the value (`state`/`error`), not just the object
|
|
1957
|
+
* reference, and returns the same cached value across renders where
|
|
1958
|
+
* nothing relevant changed — the same shape `usePresence` already uses
|
|
1959
|
+
* for its own independently-scoped store.
|
|
1960
|
+
*/
|
|
1961
|
+
declare function usePersistenceStatus(): PersistenceSnapshot;
|
|
1962
|
+
/** The `CollaborationSnapshot` equivalent of `usePersistenceStatus` (Phase 6) — same value-memoized convenience over `useScrawlSnapshot().connection.collaboration` (Phase 9: see `usePersistenceStatus`'s doc comment for why it doesn't derive from the full snapshot). */
|
|
1963
|
+
declare function useCollaborationStatus(): CollaborationSnapshot;
|
|
1964
|
+
/**
|
|
1965
|
+
* The current presence roster (Phase 6), live-updating — wraps
|
|
1966
|
+
* `controller.presence.list()`/`subscribe` the same way `useScrawlSnapshot`
|
|
1967
|
+
* wraps the controller's main snapshot, via `useSyncExternalStore`. Does
|
|
1968
|
+
* not re-render on every remote pointer move by itself; it re-renders
|
|
1969
|
+
* whenever the roster the controller already tracks changes, at whatever
|
|
1970
|
+
* rate that arrives at (throttled adapter-side, per `presenceThrottleMs`).
|
|
1971
|
+
*/
|
|
1972
|
+
declare function usePresence(): readonly PresenceUser[];
|
|
1973
|
+
/** @deprecated Use `Scrawl` instead — this predates the full capability-grouped `BoardController` and only exposes the stroke-only `LocalBoard` surface. */
|
|
1359
1974
|
type ScrawlBoardProps = {
|
|
1360
1975
|
documentId: string;
|
|
1361
1976
|
initialDocument?: SerializedBoardDocument;
|
|
@@ -1363,7 +1978,8 @@ type ScrawlBoardProps = {
|
|
|
1363
1978
|
className?: string;
|
|
1364
1979
|
style?: CSSProperties;
|
|
1365
1980
|
};
|
|
1981
|
+
/** @deprecated Use `Scrawl` instead — this predates the full capability-grouped `BoardController` and only exposes the stroke-only `LocalBoard` surface. */
|
|
1366
1982
|
declare function ScrawlBoard({ documentId, initialDocument, onReady, className, style }: ScrawlBoardProps): react.JSX.Element;
|
|
1367
1983
|
|
|
1368
|
-
export { AssetResolutionError, DefaultBoardChrome, FocusedItemToolbar, InlineEditors, MultiplayerCursors, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, StyleShelf, assetRef, clampAssetCacheBytes, cloneCustomObject, isAssetRef, resolveScrawlTheme, scrawlThemePresets, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
|
|
1369
|
-
export type { AssetDiagnostic, AssetIngestRequest, AssetIngestResult, AssetIngestor, AssetKind, AssetPurpose, AssetRef, AssetResolutionErrorCode, AssetResolveRequest, AssetResolveResult, AssetResolver, BoardController, BoardKeyInput, BoardPointerInput, BoardScene, BoardSlotProps, BoardSnapshot, BoardStyle, BoardThemeOptions, CommentMarker, CreateBoardControllerOptions, CustomBoardObject, CustomObjectAddInput, CustomObjectDefinition, CustomTool, CustomToolDefinition, DefaultBoardChromeProps, DefaultUIRegion, DefaultUISlot, DefaultUISlots, DialogSlotProps, ExtensionCommand, ExtensionDiagnostic, ExtensionHitResult, ExtensionId, ExtensionRequirement, FocusedItem, FocusedItemToolbarProps, InlineEditorsProps, InputModifiers, JsonObject, JsonValue, LocalBoard, LocalBoardSnapshot, Mat2x3, MultiplayerCursorsProps, ObjectDescribeContext, ObjectIntent, ObjectType, PresenceCursor, PresenceUser, PresenceView, QueryableBoardObject, ReadonlyCustomObject, SceneEllipse, SceneGroup, SceneImage, ScenePath, SceneRect, SceneText, ScrawlBoardProps, ScrawlCanvasProps, ScrawlDefaultUIProps, ScrawlDensity, ScrawlExtension, ScrawlGridMode, ScrawlProps, ScrawlProviderProps, ScrawlResolvedTheme, ScrawlSurfaceTexture, ScrawlTheme, ScrawlThemeDiagnostic, ScrawlThemePreset, StyleShelfProps, SupportedAssetMediaType, ToolCancelReason, ToolCapabilities, ToolCursor, ToolId };
|
|
1984
|
+
export { AssetResolutionError, DefaultBoardChrome, FocusedItemToolbar, InlineEditors, MultiplayerCursors, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, StyleShelf, assetRef, clampAssetCacheBytes, cloneCustomObject, isAssetRef, resolveScrawlTheme, scrawlThemePresets, useCollaborationStatus, usePersistenceStatus, usePresence, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
|
|
1985
|
+
export type { AssetDiagnostic, AssetExportFailure, AssetIngestRequest, AssetIngestResult, AssetIngestor, AssetKind, AssetPurpose, AssetRef, AssetResolutionErrorCode, AssetResolveRequest, AssetResolveResult, AssetResolver, BoardController, BoardKeyInput, BoardPointerInput, BoardScene, BoardSlotProps, BoardSnapshot, BoardStyle, BoardThemeOptions, CollaborationSnapshot, CommentMarker, CreateBoardControllerOptions, CustomBoardObject, CustomObjectAddInput, CustomObjectDefinition, CustomTool, CustomToolDefinition, DefaultBoardChromeProps, DefaultUIRegion, DefaultUISlot, DefaultUISlots, DialogSlotProps, ExportDocumentSVGOptions, ExportDocumentSVGResult, ExtensionCommand, ExtensionDiagnostic, ExtensionHitResult, ExtensionId, ExtensionRequirement, FocusedItem, FocusedItemToolbarProps, InlineEditorsProps, InputModifiers, JsonObject, JsonValue, LocalBoard, LocalBoardSnapshot, LocalPresence, Mat2x3, MultiplayerCursorsProps, ObjectDescribeContext, ObjectIntent, ObjectType, PresenceCursor, PresenceUser, PresenceView, QueryableBoardObject, ReadonlyCustomObject, SceneEllipse, SceneGroup, SceneImage, ScenePath, SceneRect, SceneText, ScrawlBoardProps, ScrawlCanvasProps, ScrawlDefaultUIProps, ScrawlDensity, ScrawlExtension, ScrawlGridMode, ScrawlProps, ScrawlProviderProps, ScrawlResolvedTheme, ScrawlSurfaceTexture, ScrawlTheme, ScrawlThemeDiagnostic, ScrawlThemePreset, SearchHit, SearchHitKind, SearchableComment, StyleShelfProps, SupportedAssetMediaType, ToolCancelReason, ToolCapabilities, ToolCursor, ToolId };
|