@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/dist/index.d.ts CHANGED
@@ -184,6 +184,21 @@ interface CustomObjectDefinition<Props extends JsonValue = JsonValue> {
184
184
  /** One pure, synchronous step per consecutive schema version. */
185
185
  migrate?: Readonly<Record<number, (oldProps: JsonValue) => JsonValue>>;
186
186
  describe(object: ReadonlyCustomObject<Props>, context: ObjectDescribeContext): BoardScene;
187
+ /**
188
+ * Optional point-level hit-test precision (Phase 8). Every custom object
189
+ * hit-tests against its bounding box (`fallback.bounds`) by default — this
190
+ * lets a non-rectangular shape (e.g. a circular card, an L-shaped region)
191
+ * reject a point that's inside that box but outside its actual visible
192
+ * silhouette, tightening a click/marquee/raycast hit to the shape's real
193
+ * outline. `point` is in this object's own local space — the same
194
+ * untransformed space `describe`'s returned geometry already lives in
195
+ * (the caller inverse-transforms the pointer's board point through
196
+ * `object.transform` before calling this). Absent means every point
197
+ * inside the bounding box hits, matching pre-Phase-8 behavior exactly.
198
+ * Rejecting a point here does not fall through to whatever's underneath —
199
+ * the gesture simply misses this object, same as clicking empty space.
200
+ */
201
+ hitTest?(object: ReadonlyCustomObject<Props>, point: BoardPoint): boolean;
187
202
  }
188
203
  interface SceneNodeBase {
189
204
  key: string;
@@ -217,6 +232,15 @@ interface SceneGroup extends SceneNodeBase {
217
232
  kind: "group";
218
233
  children: readonly BoardScene[];
219
234
  }
235
+ /**
236
+ * **No renderer or SVG-export interpreter exists for this node kind yet**
237
+ * (tracked as deferred work — see `renderer/shapes/customObjects.ts`'s
238
+ * `"path"` case). Returning a `ScenePath` from `describe()` renders nothing,
239
+ * exports nothing, and contributes no hit-test bounds — it neither errors
240
+ * nor emits a diagnostic. Until an interpreter ships, build custom shapes
241
+ * from `SceneRect`/`SceneEllipse`/`SceneGroup`/`SceneText`/`SceneImage`
242
+ * instead.
243
+ */
220
244
  interface ScenePath extends SceneNodeBase {
221
245
  kind: "path";
222
246
  /** SVG-style path data, board-local coordinates. */
@@ -389,8 +413,23 @@ declare function canUnlockItem(item: Lockable | undefined, userId: string | unde
389
413
  /** Wire fields for a locked item; omitted entirely when unlocked. */
390
414
  declare function serializeLock(item: Lockable): Lockable;
391
415
 
416
+ /**
417
+ * Per-object visibility (Phase 8) — mirrors `itemLock.ts`'s `Lockable`
418
+ * pattern exactly, but simpler: unlike a lock, hidden state carries no
419
+ * holder/ownership concept, so there's no analogue to `LockHolder`/
420
+ * `canUnlockItem`. A hidden object stays fully present in the Document
421
+ * (still serializes, persists, syncs, undoes/redoes) — it just skips
422
+ * rendering and hit-testing/selection candidacy. `hidden` absent or
423
+ * `false` means visible; this keeps every pre-Phase-8 document (which has
424
+ * no `hidden` field on any object at all) implicitly fully visible with
425
+ * zero migration needed.
426
+ */
427
+ interface Hideable {
428
+ hidden?: boolean;
429
+ }
430
+
392
431
  /** A kitchen timer sitting on the board. Remaining time is derived, not ticked. */
393
- interface KitchenTimer extends Lockable {
432
+ interface KitchenTimer extends Lockable, Hideable {
394
433
  id: string;
395
434
  x: number;
396
435
  y: number;
@@ -415,6 +454,160 @@ declare function toggleTimer(timer: KitchenTimer, now: number): KitchenTimer;
415
454
  declare function setTimerDuration(timer: KitchenTimer, durationMs: number): KitchenTimer;
416
455
  declare function formatTimer(ms: number): string;
417
456
 
457
+ declare const SHAPE_MIN_SIZE = 0.5;
458
+ /**
459
+ * Centralized shape style defaults (Phase 8) — one object a Host can read
460
+ * to know (or, by not relying on the standalone constants below, override
461
+ * via its own UI state) what a newly drawn Rectangle/Ellipse/Line/Arrow/
462
+ * Polygon/Star/Heart starts with when the user hasn't picked a stroke/width
463
+ * yet. `shapeTool.ts` (commit-time), `renderer/shapes/lines.ts` (render-time
464
+ * fallback for an object missing these fields), and
465
+ * `persistence/serialization/svg.ts` (export-time fallback) all read from
466
+ * here — the two standalone constants below are kept for source
467
+ * compatibility and simply mirror this object's values, not a second
468
+ * source of truth.
469
+ */
470
+ declare const SHAPE_STYLE_DEFAULTS: {
471
+ readonly stroke: "#1C1C1E";
472
+ readonly strokeWidth: 0.12;
473
+ };
474
+ declare const SHAPE_DEFAULT_STROKE: "#1C1C1E";
475
+ declare const SHAPE_DEFAULT_STROKE_WIDTH: 0.12;
476
+ interface RectangleObject extends Lockable, Hideable {
477
+ id: string;
478
+ x: number;
479
+ y: number;
480
+ width: number;
481
+ height: number;
482
+ fill?: string;
483
+ stroke?: string;
484
+ strokeWidth?: number;
485
+ /** Corner radius in board units; clamped to at most half the shorter side at render time. */
486
+ cornerRadius?: number;
487
+ /** `[0, 1]`; undefined means fully opaque (Phase 4). */
488
+ opacity?: number;
489
+ /**
490
+ * Radians, about the shape's own center `(x + width/2, y - height/2)`.
491
+ * Undefined means 0 (Phase 3). `x`/`y`/`width`/`height` stay in the
492
+ * shape's own unrotated local frame — rotation is a separate, applied-last
493
+ * transform, not baked into them, matching how Stroke/CustomBoardObject
494
+ * keep geometry and placement independent via their own `matrix`.
495
+ */
496
+ rotation?: number;
497
+ }
498
+ interface EllipseObject extends Lockable, Hideable {
499
+ id: string;
500
+ x: number;
501
+ y: number;
502
+ width: number;
503
+ height: number;
504
+ fill?: string;
505
+ stroke?: string;
506
+ strokeWidth?: number;
507
+ /** `[0, 1]`; undefined means fully opaque (Phase 4). */
508
+ opacity?: number;
509
+ /** Radians, about the shape's own center — see RectangleObject's `rotation` doc. */
510
+ rotation?: number;
511
+ }
512
+ declare function cloneRectangle(rect: RectangleObject): RectangleObject;
513
+ declare function cloneEllipse(ellipse: EllipseObject): EllipseObject;
514
+ /** `"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. */
515
+ type ArrowHeadStyle = "triangle" | "none";
516
+ interface LineObject extends Lockable, Hideable {
517
+ id: string;
518
+ start: BoardPoint;
519
+ end: BoardPoint;
520
+ stroke?: string;
521
+ strokeWidth?: number;
522
+ opacity?: number;
523
+ }
524
+ interface ArrowObject extends Lockable, Hideable {
525
+ id: string;
526
+ start: BoardPoint;
527
+ end: BoardPoint;
528
+ head?: ArrowHeadStyle;
529
+ stroke?: string;
530
+ strokeWidth?: number;
531
+ opacity?: number;
532
+ }
533
+ /**
534
+ * Triangle(3)/Diamond(4)/Pentagon(5)/Hexagon(6)/Octagon(8) as one shared
535
+ * type instead of five near-duplicate interfaces — a regular N-gon
536
+ * inscribed in the same `x`/`y`/`width`/`height`/`rotation` bounding box
537
+ * Rectangle already uses, parameterized by `sides`. Diamond is exactly a
538
+ * 4-sided regular polygon with vertex 0 pointing right (not up, like
539
+ * Triangle/Pentagon/Hexagon) — see `polygonGeometry.ts`'s
540
+ * `polygonStartAngle`, which encodes each side count's own vertex
541
+ * orientation so the outline always matches the legacy drag-preview shape.
542
+ */
543
+ interface PolygonObject extends Lockable, Hideable {
544
+ id: string;
545
+ x: number;
546
+ y: number;
547
+ width: number;
548
+ height: number;
549
+ sides: 3 | 4 | 5 | 6 | 8;
550
+ fill?: string;
551
+ stroke?: string;
552
+ strokeWidth?: number;
553
+ opacity?: number;
554
+ /** Radians, about the shape's own center — see RectangleObject's `rotation` doc. */
555
+ rotation?: number;
556
+ }
557
+ declare function clonePolygon(polygon: PolygonObject): PolygonObject;
558
+ /** 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`). */
559
+ interface StarObject extends Lockable, Hideable {
560
+ id: string;
561
+ x: number;
562
+ y: number;
563
+ width: number;
564
+ height: number;
565
+ /** Vertex count; today's only shipped preset is 5, matching the legacy tool. */
566
+ points: number;
567
+ /** `(0, 1)` — inner vertex radius as a fraction of the outer radius. */
568
+ innerRadiusRatio: number;
569
+ fill?: string;
570
+ stroke?: string;
571
+ strokeWidth?: number;
572
+ opacity?: number;
573
+ rotation?: number;
574
+ }
575
+ declare function cloneStar(star: StarObject): StarObject;
576
+ /** 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. */
577
+ interface HeartObject extends Lockable, Hideable {
578
+ id: string;
579
+ x: number;
580
+ y: number;
581
+ width: number;
582
+ height: number;
583
+ fill?: string;
584
+ stroke?: string;
585
+ strokeWidth?: number;
586
+ opacity?: number;
587
+ rotation?: number;
588
+ }
589
+ declare function cloneHeart(heart: HeartObject): HeartObject;
590
+ declare function cloneLine(line: LineObject): LineObject;
591
+ declare function cloneArrow(arrow: ArrowObject): ArrowObject;
592
+ /**
593
+ * A logical grouping of other board objects (Phase 3 — Selection,
594
+ * Transformation & Grouping). Deliberately has no `x`/`y`/`transform` of its
595
+ * own — a group's bounds are always derived on demand from its (recursively
596
+ * resolved) children, and "moving/rotating/scaling the group" is exactly a
597
+ * multi-object transform applied to those children, nothing more. A group
598
+ * has no renderer/mesh of its own; its only visual presence is the
599
+ * selection gizmo's bounding box while it's the current selection.
600
+ *
601
+ * `children` may itself contain other group ids (nested groups) — expanding
602
+ * a group into its leaf members is always done by the caller (recursively,
603
+ * with cycle protection), never assumed here.
604
+ */
605
+ interface GroupObject extends Lockable, Hideable {
606
+ id: string;
607
+ children: string[];
608
+ }
609
+ declare function cloneGroup(group: GroupObject): GroupObject;
610
+
418
611
  interface BoardPoint {
419
612
  x: number;
420
613
  y: number;
@@ -435,7 +628,7 @@ interface StrokePoint extends BoardPoint {
435
628
  * geometric outline, not an expressive ink mark.
436
629
  */
437
630
  type StrokeTool = "marker" | "highlighter" | "shape";
438
- interface Stroke extends Lockable {
631
+ interface Stroke extends Lockable, Hideable {
439
632
  id: string;
440
633
  color: string;
441
634
  baseWidth: number;
@@ -454,7 +647,7 @@ interface Stroke extends Lockable {
454
647
  declare const ERASE_THRESHOLD = 0.95;
455
648
  declare function cloneStroke(stroke: Stroke): Stroke;
456
649
  type SerializedPoint = [number, number, number, number];
457
- interface SerializedStroke extends Lockable {
650
+ interface SerializedStroke extends Lockable, Hideable {
458
651
  id: string;
459
652
  color: string;
460
653
  baseWidth: number;
@@ -480,6 +673,31 @@ interface SerializedDocument {
480
673
  timers?: KitchenTimer[];
481
674
  /** Absent in documents saved before Custom board objects existed (ticket #22). */
482
675
  customObjects?: CustomBoardObject[];
676
+ /** Absent in documents saved before semantic Rectangle objects existed (Phase 2). */
677
+ rectangles?: RectangleObject[];
678
+ /** Absent in documents saved before semantic Ellipse objects existed (Phase 2). */
679
+ ellipses?: EllipseObject[];
680
+ /** Absent in documents saved before Groups existed (Phase 3). */
681
+ groups?: GroupObject[];
682
+ /** Absent in documents saved before semantic Line objects existed (Phase 4). */
683
+ lines?: LineObject[];
684
+ /** Absent in documents saved before semantic Arrow objects existed (Phase 4). */
685
+ arrows?: ArrowObject[];
686
+ /** Absent in documents saved before semantic Polygon objects existed (Phase 4). */
687
+ polygons?: PolygonObject[];
688
+ /** Absent in documents saved before semantic Star objects existed (Phase 4). */
689
+ stars?: StarObject[];
690
+ /** Absent in documents saved before semantic Heart objects existed (Phase 4). */
691
+ hearts?: HeartObject[];
692
+ /**
693
+ * Every content-object id (every type above except comments, which are
694
+ * host-synced and never enter this schema) in paint order, back to front.
695
+ * Absent in documents saved before per-object z-order existed (Phase 3) —
696
+ * migration synthesizes a default order preserving the old fixed-Z-band
697
+ * visual stacking exactly, so an existing document never visibly changes
698
+ * on load; only an explicit reorder action touches this from then on.
699
+ */
700
+ objectOrder?: string[];
483
701
  }
484
702
  declare const INK_COLORS: {
485
703
  readonly black: "#1C1C1E";
@@ -523,7 +741,7 @@ interface NoteVote {
523
741
  * A sticky note: content floating above the board at a z-offset (pillar 3 —
524
742
  * depth as an organizational axis). Center position in board space.
525
743
  */
526
- interface StickyNote extends Lockable {
744
+ interface StickyNote extends Lockable, Hideable {
527
745
  id: string;
528
746
  x: number;
529
747
  y: number;
@@ -541,7 +759,7 @@ interface StickyNote extends Lockable {
541
759
  * top-left corner; lines flow downward (-y). Text joins the clustering
542
760
  * system like handwriting (build prompt §6.4).
543
761
  */
544
- interface TextBlock extends Lockable {
762
+ interface TextBlock extends Lockable, Hideable {
545
763
  id: string;
546
764
  x: number;
547
765
  y: number;
@@ -571,7 +789,7 @@ declare function cloneNote(note: StickyNote): StickyNote;
571
789
  * Interactive structured table on the board. Position (x, y) is top-left in board units.
572
790
  * Cells are indexed as `${row},${col}` keys mapping to cell text content.
573
791
  */
574
- interface TableBlock extends Lockable {
792
+ interface TableBlock extends Lockable, Hideable {
575
793
  id: string;
576
794
  x: number;
577
795
  y: number;
@@ -597,7 +815,7 @@ declare const FOG_COLOR = "#FFFFFF";
597
815
  * An imported image block on the board plane.
598
816
  * Coordinates (x, y) represent the center of the image in board space.
599
817
  */
600
- interface ImageBlock extends Lockable {
818
+ interface ImageBlock extends Lockable, Hideable {
601
819
  id: string;
602
820
  /**
603
821
  * A legacy, read-only data URL (or, historically, an arbitrary string) —
@@ -647,6 +865,15 @@ type DocumentLoadResult = {
647
865
  declare function loadDocumentBytes(originalBytes: string): DocumentLoadResult;
648
866
  declare function migrateDocument(raw: unknown): DocumentLoadResult;
649
867
  declare function serializeDocument(document: unknown): string;
868
+ /**
869
+ * A Document's serialized size in bytes (Phase 5) — UTF-8, not UTF-16
870
+ * `string.length`, since a Document with non-ASCII note/text content (most
871
+ * of them, eventually) would otherwise under-report. Useful for a Host
872
+ * deciding when to warn about an unusually large board, or for logging/
873
+ * telemetry around save size — not consulted by anything inside this
874
+ * package itself, which has no size limit of its own.
875
+ */
876
+ declare function documentSize(document: CurrentSerializedDocument): number;
650
877
 
651
878
  interface SearchableComment {
652
879
  id: string;
@@ -731,6 +958,60 @@ interface DocumentChange {
731
958
  /** Ids of removed custom objects. */
732
959
  customObjectsRemoved: string[];
733
960
  customObjectsUpdated: CustomBoardObject[];
961
+ /** Semantic Rectangle objects (Phase 2). */
962
+ rectanglesAdded: RectangleObject[];
963
+ /** Ids of removed rectangles. */
964
+ rectanglesRemoved: string[];
965
+ rectanglesUpdated: RectangleObject[];
966
+ /** Semantic Ellipse objects (Phase 2). */
967
+ ellipsesAdded: EllipseObject[];
968
+ /** Ids of removed ellipses. */
969
+ ellipsesRemoved: string[];
970
+ ellipsesUpdated: EllipseObject[];
971
+ /** Groups (Phase 3). */
972
+ groupsAdded: GroupObject[];
973
+ /** Ids of removed groups (ungrouping, or deleting a group). */
974
+ groupsRemoved: string[];
975
+ groupsUpdated: GroupObject[];
976
+ /** Semantic Line objects (Phase 4). */
977
+ linesAdded: LineObject[];
978
+ /** Ids of removed lines. */
979
+ linesRemoved: string[];
980
+ linesUpdated: LineObject[];
981
+ /** Semantic Arrow objects (Phase 4). */
982
+ arrowsAdded: ArrowObject[];
983
+ /** Ids of removed arrows. */
984
+ arrowsRemoved: string[];
985
+ arrowsUpdated: ArrowObject[];
986
+ /** Semantic Polygon objects (Phase 4) — Triangle/Diamond/Pentagon/Hexagon/Octagon. */
987
+ polygonsAdded: PolygonObject[];
988
+ /** Ids of removed polygons. */
989
+ polygonsRemoved: string[];
990
+ polygonsUpdated: PolygonObject[];
991
+ /** Semantic Star objects (Phase 4). */
992
+ starsAdded: StarObject[];
993
+ /** Ids of removed stars. */
994
+ starsRemoved: string[];
995
+ starsUpdated: StarObject[];
996
+ /** Semantic Heart objects (Phase 4). */
997
+ heartsAdded: HeartObject[];
998
+ /** Ids of removed hearts. */
999
+ heartsRemoved: string[];
1000
+ heartsUpdated: HeartObject[];
1001
+ /**
1002
+ * The full current paint order (back to front) of every flat content
1003
+ * object — strokes, texts, tables, images, rectangles, ellipses, lines,
1004
+ * arrows, custom objects, and groups. Populated whenever `objectOrder`
1005
+ * actually changed:
1006
+ * an explicit reorder (`bringForward` etc.), or any add/remove that
1007
+ * touches it — a removal shifts every id after it down one rank, not
1008
+ * just the removed one, so renderers need this to resync everyone, not
1009
+ * only the ids the same change's own `*Added`/`*Removed`/`*Updated`
1010
+ * fields name. Notes (their own `zOffset` peel depth) and Kitchen Timers
1011
+ * (genuine 3D objects, not a flat layer) are intentionally not part of
1012
+ * this order at all.
1013
+ */
1014
+ orderChanged: readonly string[];
734
1015
  }
735
1016
  type Listener = (change: DocumentChange) => void;
736
1017
  declare class BoardDocument {
@@ -745,12 +1026,73 @@ declare class BoardDocument {
745
1026
  private readonly timers;
746
1027
  /** All Custom board object types share one map, keyed by id — the envelope is already uniform. */
747
1028
  private readonly customObjects;
1029
+ private readonly rectangles;
1030
+ private readonly ellipses;
1031
+ private readonly groups;
1032
+ private readonly lines;
1033
+ private readonly arrows;
1034
+ private readonly polygons;
1035
+ private readonly stars;
1036
+ private readonly hearts;
748
1037
  private readonly bboxes;
749
1038
  private readonly listeners;
1039
+ /** Paint order (back to front) of every flat content object — see `DocumentChange.orderChanged`'s doc comment. */
1040
+ private objectOrder;
1041
+ private orderIndex;
750
1042
  constructor(id: DocumentId);
1043
+ private reindexOrder;
1044
+ /** The full current paint order, back to front. */
1045
+ order(): readonly string[];
1046
+ /** This object's rank in the paint order, or -1 if it doesn't participate (unknown id, a note, or a timer). */
1047
+ orderRank(id: string): number;
1048
+ private bringForward;
1049
+ private sendBackward;
1050
+ private bringToFront;
1051
+ private sendToBack;
1052
+ /** Reorders `id` relative to its current neighbors. A no-op for an id that doesn't participate in paint order (see `orderRank`). */
1053
+ reorder(id: string, direction: "forward" | "backward" | "front" | "back"): void;
1054
+ /**
1055
+ * Overwrites the paint order directly — used only when loading a document
1056
+ * that already carries a persisted `objectOrder`; every other order
1057
+ * mutation goes through `reorder`/the automatic append-on-add tracking in
1058
+ * `emit`. Ids not present in the document are dropped; ids present in the
1059
+ * document but missing from `order` are appended at the back, so a
1060
+ * partially-stale order (e.g. from a schema migration) never silently
1061
+ * drops an object from paint order entirely.
1062
+ */
1063
+ private setOrder;
751
1064
  get(id: string): Stroke | undefined;
752
1065
  all(): IterableIterator<Stroke>;
753
- bbox(id: string): BBox | undefined;
1066
+ /**
1067
+ * World-space bounds for any content object, of any type. Strokes hit
1068
+ * their cached-on-mutation fast path (`bboxes`, populated by
1069
+ * `addStrokes`/`transformStrokes` — many points, worth caching); every
1070
+ * other type computes on demand via `objectBounds.ts` (cheap arithmetic,
1071
+ * no caching needed). A group's bounds are the union of its (recursively
1072
+ * resolved) children — `seen` guards against a cycle in nested groups.
1073
+ */
1074
+ bbox(id: string, seen?: Set<string>): BBox | undefined;
1075
+ /**
1076
+ * True if `id` exists and is locked, for any type — the same per-type
1077
+ * probe pattern as `bbox`, for interactive gestures (drag/transform) that
1078
+ * need to gate on lock state regardless of what's selected. Custom
1079
+ * objects are deliberately excluded: their `lock` field is a different
1080
+ * shape (`{holderId, acquiredAt}`, no display name) with no interactive
1081
+ * lock UI yet, matching the existing, deliberate "always unlockable,
1082
+ * never gates a drag" treatment already established elsewhere (e.g.
1083
+ * `getSelectedItemInfo`'s custom branch hardcodes `isLocked: false`).
1084
+ */
1085
+ isLocked(id: string): boolean;
1086
+ /**
1087
+ * True if `id` exists and is hidden, for any type — the same per-type
1088
+ * probe pattern as {@link isLocked} (Phase 8). Custom objects are
1089
+ * excluded for the same reason `isLocked` excludes them: they have no
1090
+ * `Hideable` field at all, so "hidden" isn't a concept that applies to
1091
+ * them yet. Used by marquee selection (`selectTool.ts`) to keep a hidden
1092
+ * object out of a rubber-band selection even for object types whose own
1093
+ * renderer doesn't yet suppress click-based hit-testing.
1094
+ */
1095
+ isHidden(id: string): boolean;
754
1096
  subscribe(listener: Listener): () => void;
755
1097
  addStrokes(strokes: Stroke[]): void;
756
1098
  removeStrokes(ids: string[]): void;
@@ -793,6 +1135,54 @@ declare class BoardDocument {
793
1135
  removeCustomObjects(ids: string[]): void;
794
1136
  /** Replace a custom object's contents under the same id. */
795
1137
  setCustomObject(object: CustomBoardObject): void;
1138
+ getRectangle(id: string): RectangleObject | undefined;
1139
+ allRectangles(): IterableIterator<RectangleObject>;
1140
+ addRectangles(rectangles: RectangleObject[]): void;
1141
+ removeRectangles(ids: string[]): void;
1142
+ /** Replace a rectangle's contents (move, resize, restyle) under the same id. */
1143
+ setRectangle(rect: RectangleObject): void;
1144
+ getEllipse(id: string): EllipseObject | undefined;
1145
+ allEllipses(): IterableIterator<EllipseObject>;
1146
+ addEllipses(ellipses: EllipseObject[]): void;
1147
+ removeEllipses(ids: string[]): void;
1148
+ /** Replace an ellipse's contents (move, resize, restyle) under the same id. */
1149
+ setEllipse(ellipse: EllipseObject): void;
1150
+ getGroup(id: string): GroupObject | undefined;
1151
+ allGroups(): IterableIterator<GroupObject>;
1152
+ addGroups(groups: GroupObject[]): void;
1153
+ removeGroups(ids: string[]): void;
1154
+ /** Replace a group's contents (its children list) under the same id. */
1155
+ setGroup(group: GroupObject): void;
1156
+ getLine(id: string): LineObject | undefined;
1157
+ allLines(): IterableIterator<LineObject>;
1158
+ addLines(lines: LineObject[]): void;
1159
+ removeLines(ids: string[]): void;
1160
+ /** Replace a line's contents (move, restyle) under the same id. */
1161
+ setLine(line: LineObject): void;
1162
+ getArrow(id: string): ArrowObject | undefined;
1163
+ allArrows(): IterableIterator<ArrowObject>;
1164
+ addArrows(arrows: ArrowObject[]): void;
1165
+ removeArrows(ids: string[]): void;
1166
+ /** Replace an arrow's contents (move, restyle, change head) under the same id. */
1167
+ setArrow(arrow: ArrowObject): void;
1168
+ getPolygon(id: string): PolygonObject | undefined;
1169
+ allPolygons(): IterableIterator<PolygonObject>;
1170
+ addPolygons(polygons: PolygonObject[]): void;
1171
+ removePolygons(ids: string[]): void;
1172
+ /** Replace a polygon's contents (move, resize, rotate, restyle) under the same id. */
1173
+ setPolygon(polygon: PolygonObject): void;
1174
+ getStar(id: string): StarObject | undefined;
1175
+ allStars(): IterableIterator<StarObject>;
1176
+ addStars(stars: StarObject[]): void;
1177
+ removeStars(ids: string[]): void;
1178
+ /** Replace a star's contents (move, resize, rotate, restyle) under the same id. */
1179
+ setStar(star: StarObject): void;
1180
+ getHeart(id: string): HeartObject | undefined;
1181
+ allHearts(): IterableIterator<HeartObject>;
1182
+ addHearts(hearts: HeartObject[]): void;
1183
+ removeHearts(ids: string[]): void;
1184
+ /** Replace a heart's contents (move, resize, rotate, restyle) under the same id. */
1185
+ setHeart(heart: HeartObject): void;
796
1186
  setStrokeLocked(id: string, locked: boolean, by?: LockHolder | null): void;
797
1187
  setStrokesLocked(ids: string[], locked: boolean, by?: LockHolder | null): void;
798
1188
  setNoteLocked(id: string, locked: boolean, by?: LockHolder | null): void;
@@ -800,18 +1190,69 @@ declare class BoardDocument {
800
1190
  setTableLocked(id: string, locked: boolean, by?: LockHolder | null): void;
801
1191
  setImageLocked(id: string, locked: boolean, by?: LockHolder | null): void;
802
1192
  setTimerLocked(id: string, locked: boolean, by?: LockHolder | null): void;
1193
+ setRectangleLocked(id: string, locked: boolean, by?: LockHolder | null): void;
1194
+ setEllipseLocked(id: string, locked: boolean, by?: LockHolder | null): void;
1195
+ setGroupLocked(id: string, locked: boolean, by?: LockHolder | null): void;
1196
+ setLineLocked(id: string, locked: boolean, by?: LockHolder | null): void;
1197
+ setArrowLocked(id: string, locked: boolean, by?: LockHolder | null): void;
1198
+ setPolygonLocked(id: string, locked: boolean, by?: LockHolder | null): void;
1199
+ setStarLocked(id: string, locked: boolean, by?: LockHolder | null): void;
1200
+ setHeartLocked(id: string, locked: boolean, by?: LockHolder | null): void;
803
1201
  /** Replace all content (initial load). Does not touch `version`. */
804
- replaceAll(strokes: Stroke[], notes: StickyNote[], texts: TextBlock[], tables?: TableBlock[], images?: ImageBlock[], timers?: KitchenTimer[], customObjects?: CustomBoardObject[]): void;
1202
+ replaceAll(strokes: Stroke[], notes: StickyNote[], texts: TextBlock[], tables?: TableBlock[], images?: ImageBlock[], timers?: KitchenTimer[], customObjects?: CustomBoardObject[], rectangles?: RectangleObject[], ellipses?: EllipseObject[], groups?: GroupObject[], lines?: LineObject[], arrows?: ArrowObject[], polygons?: PolygonObject[], stars?: StarObject[], hearts?: HeartObject[],
1203
+ /** Persisted paint order; absent for a document saved before Phase 3, in which case one is synthesized (see `ORDERED_ADDED_FIELDS`'s doc comment). */
1204
+ objectOrder?: readonly string[]): void;
805
1205
  /** Apply incremental real-time change received from a remote collaborator over WebSocket. */
806
1206
  applyRemoteChange(change: Partial<DocumentChange>): void;
807
1207
  toJSON(): SerializedDocument;
1208
+ static deserializeGroups(data: SerializedDocument): GroupObject[];
1209
+ static deserializeLines(data: SerializedDocument): LineObject[];
1210
+ static deserializeArrows(data: SerializedDocument): ArrowObject[];
1211
+ static deserializePolygons(data: SerializedDocument): PolygonObject[];
1212
+ static deserializeStars(data: SerializedDocument): StarObject[];
1213
+ static deserializeHearts(data: SerializedDocument): HeartObject[];
808
1214
  static deserializeCustomObjects(data: SerializedDocument): CustomBoardObject[];
1215
+ static deserializeRectangles(data: SerializedDocument): RectangleObject[];
1216
+ static deserializeEllipses(data: SerializedDocument): EllipseObject[];
809
1217
  static deserializeImages(data: SerializedDocument): ImageBlock[];
810
1218
  static deserializeTimers(data: SerializedDocument): KitchenTimer[];
811
1219
  static deserializeNotes(data: SerializedDocument): StickyNote[];
812
1220
  static deserializeTexts(data: SerializedDocument): TextBlock[];
813
1221
  static deserializeTables(data: SerializedDocument): TableBlock[];
814
- static deserializeStrokes(data: SerializedDocument): Stroke[];
1222
+ /**
1223
+ * `onSkip` (Phase 9) replaces an unconditional `console.error` — `core`
1224
+ * must never do raw console I/O (no dev-gate, no way for a Host to
1225
+ * suppress or redirect it), so a skipped stroke is now reported only if
1226
+ * the caller asks for it, via whatever diagnostic channel it already
1227
+ * has (e.g. `controller-internal.ts` routes this into the same typed
1228
+ * `"error"` event every other diagnostic already uses). Silent by
1229
+ * default, matching how every other `deserialize*` method here already
1230
+ * behaves (no diagnostics at all).
1231
+ */
1232
+ static deserializeStrokes(data: SerializedDocument, onSkip?: (id: string, cause: unknown) => void): Stroke[];
1233
+ /**
1234
+ * Every mutation funnels through here, so paint-order tracking lives in
1235
+ * exactly one place rather than at every individual add/remove call site
1236
+ * (18 of them, times `replaceAll`/`applyRemoteChange`) — new ids are
1237
+ * appended to the back (front-most) of `objectOrder`, removed ids are
1238
+ * spliced out. An explicit reorder (`reorder`/`setOrder`) updates
1239
+ * `objectOrder` itself before calling this, so this step is a no-op for
1240
+ * ids already tracked (idempotent by construction: `orderIndex.has` gates
1241
+ * every append).
1242
+ *
1243
+ * `orderChanged` (Phase 9) reports exactly the ids whose rank actually
1244
+ * changed, using `orderIndex` throughout instead of `indexOf` — a pure
1245
+ * append never shifts any existing id's rank (new ids land at the tail,
1246
+ * already covered by this same change's own `Added` field, so
1247
+ * `orderChanged` stays unset), while a removal shifts every id at-or-
1248
+ * after the lowest removed rank down by one, computed in a single O(n)
1249
+ * filter pass (not one `indexOf`+`splice` per removed id) regardless of
1250
+ * how many ids this one change removes. Every renderer's `onChange` now
1251
+ * looks up only the ids actually in `orderChanged` instead of walking
1252
+ * its entire mesh map on any order-touching change — a broad, unfiltered
1253
+ * `orderChanged` here would silently defeat that fix, not just waste
1254
+ * cycles here.
1255
+ */
815
1256
  private emit;
816
1257
  }
817
1258
 
@@ -847,6 +1288,21 @@ interface Command {
847
1288
  apply(doc: BoardDocument): void;
848
1289
  revert(doc: BoardDocument): void;
849
1290
  }
1291
+ /**
1292
+ * Several commands applied/reverted together as one undo step (Phase 3
1293
+ * consolidation — this exact class used to be hand-duplicated as a private
1294
+ * `CommandBatch` in `controller-internal.ts` and an exported
1295
+ * `ExtensionCommandBatch` in `interaction/tools/customTool.ts`; both now
1296
+ * import this one instead). Revert runs in reverse order, so a batch that
1297
+ * depends on ordering (e.g. add-then-reference) undoes cleanly.
1298
+ */
1299
+ declare class CommandBatch implements Command {
1300
+ readonly label: string;
1301
+ private readonly commands;
1302
+ constructor(label: string, commands: readonly Command[]);
1303
+ apply(doc: BoardDocument): void;
1304
+ revert(doc: BoardDocument): void;
1305
+ }
850
1306
  /** How a command reached the document — undo/redo are audited distinctly. */
851
1307
  type CommandKind = "do" | "undo" | "redo";
852
1308
  declare class History {
@@ -911,6 +1367,40 @@ declare class TransformCommand implements Command {
911
1367
  revert(doc: BoardDocument): void;
912
1368
  private compose;
913
1369
  }
1370
+ /**
1371
+ * One move gesture over a mixed-type selection (Phase 3) — the general
1372
+ * successor to `TransformCommand` for translation. `TransformCommand`
1373
+ * itself stays as-is (still used for scale/rotate, which remain stroke-only
1374
+ * this phase — see `interaction/tools/selectTool.ts`'s `TransformState`):
1375
+ * its own per-id `doc.get(id)` check already no-ops safely for any id that
1376
+ * isn't a stroke, so it doesn't need touching for that narrower case.
1377
+ *
1378
+ * `delta` here is always a pure translation (never scale/rotate), so
1379
+ * `apply(delta, point)` is a safe, uniform way to move every position-only
1380
+ * type's `x`/`y` — translating commutes trivially regardless of a shape's
1381
+ * own rotation. Matrix-carrying types (stroke, Custom) instead compose
1382
+ * `delta` onto their existing matrix/transform, matching `TransformCommand`.
1383
+ *
1384
+ * A group id in `ids` is expanded into its (recursively resolved, cycle-
1385
+ * safe) children every time `compose` runs — deterministic, since group
1386
+ * membership never changes mid-command — so moving a selected group means
1387
+ * moving every one of its members by the same delta. This expansion is
1388
+ * `TransformObjectsCommand`'s own responsibility precisely so a caller that
1389
+ * doesn't itself expand groups (e.g. `ScrawlEngine.nudgeSelection`) still
1390
+ * gets correct behavior; `interaction/tools/selectTool.ts`'s `TransformState`
1391
+ * separately expands for its own reason (live per-child drag preview), which
1392
+ * makes this a no-op re-expansion for that caller, not a conflict.
1393
+ */
1394
+ declare class TransformObjectsCommand implements Command {
1395
+ private readonly ids;
1396
+ private readonly delta;
1397
+ readonly label = "move selection";
1398
+ private readonly inverse;
1399
+ constructor(ids: readonly string[], delta: Mat2x3);
1400
+ apply(doc: BoardDocument): void;
1401
+ revert(doc: BoardDocument): void;
1402
+ private compose;
1403
+ }
914
1404
  declare class AddNoteCommand implements Command {
915
1405
  readonly label = "add note";
916
1406
  private readonly note;
@@ -1032,8 +1522,213 @@ declare class DeleteTimerCommand implements Command {
1032
1522
  apply(doc: BoardDocument): void;
1033
1523
  revert(doc: BoardDocument): void;
1034
1524
  }
1525
+ /** Add/update/delete for semantic Rectangle objects (Phase 2). */
1526
+ declare class AddRectangleCommand implements Command {
1527
+ readonly label = "add rectangle";
1528
+ private readonly rect;
1529
+ constructor(rect: RectangleObject);
1530
+ apply(doc: BoardDocument): void;
1531
+ revert(doc: BoardDocument): void;
1532
+ }
1533
+ declare class UpdateRectangleCommand implements Command {
1534
+ readonly label = "update rectangle";
1535
+ private readonly before;
1536
+ private readonly after;
1537
+ constructor(before: RectangleObject, after: RectangleObject);
1538
+ apply(doc: BoardDocument): void;
1539
+ revert(doc: BoardDocument): void;
1540
+ }
1541
+ declare class DeleteRectangleCommand implements Command {
1542
+ readonly label = "delete rectangle";
1543
+ private readonly rect;
1544
+ constructor(rect: RectangleObject);
1545
+ apply(doc: BoardDocument): void;
1546
+ revert(doc: BoardDocument): void;
1547
+ }
1548
+ /** Add/update/delete for semantic Ellipse objects (Phase 2). */
1549
+ declare class AddEllipseCommand implements Command {
1550
+ readonly label = "add ellipse";
1551
+ private readonly ellipse;
1552
+ constructor(ellipse: EllipseObject);
1553
+ apply(doc: BoardDocument): void;
1554
+ revert(doc: BoardDocument): void;
1555
+ }
1556
+ declare class UpdateEllipseCommand implements Command {
1557
+ readonly label = "update ellipse";
1558
+ private readonly before;
1559
+ private readonly after;
1560
+ constructor(before: EllipseObject, after: EllipseObject);
1561
+ apply(doc: BoardDocument): void;
1562
+ revert(doc: BoardDocument): void;
1563
+ }
1564
+ declare class DeleteEllipseCommand implements Command {
1565
+ readonly label = "delete ellipse";
1566
+ private readonly ellipse;
1567
+ constructor(ellipse: EllipseObject);
1568
+ apply(doc: BoardDocument): void;
1569
+ revert(doc: BoardDocument): void;
1570
+ }
1571
+ /** Add/update/delete for semantic Line objects (Phase 4). */
1572
+ declare class AddLineCommand implements Command {
1573
+ readonly label = "add line";
1574
+ private readonly line;
1575
+ constructor(line: LineObject);
1576
+ apply(doc: BoardDocument): void;
1577
+ revert(doc: BoardDocument): void;
1578
+ }
1579
+ declare class UpdateLineCommand implements Command {
1580
+ readonly label = "update line";
1581
+ private readonly before;
1582
+ private readonly after;
1583
+ constructor(before: LineObject, after: LineObject);
1584
+ apply(doc: BoardDocument): void;
1585
+ revert(doc: BoardDocument): void;
1586
+ }
1587
+ declare class DeleteLineCommand implements Command {
1588
+ readonly label = "delete line";
1589
+ private readonly line;
1590
+ constructor(line: LineObject);
1591
+ apply(doc: BoardDocument): void;
1592
+ revert(doc: BoardDocument): void;
1593
+ }
1594
+ /** Add/update/delete for semantic Arrow objects (Phase 4). */
1595
+ declare class AddArrowCommand implements Command {
1596
+ readonly label = "add arrow";
1597
+ private readonly arrow;
1598
+ constructor(arrow: ArrowObject);
1599
+ apply(doc: BoardDocument): void;
1600
+ revert(doc: BoardDocument): void;
1601
+ }
1602
+ declare class UpdateArrowCommand implements Command {
1603
+ readonly label = "update arrow";
1604
+ private readonly before;
1605
+ private readonly after;
1606
+ constructor(before: ArrowObject, after: ArrowObject);
1607
+ apply(doc: BoardDocument): void;
1608
+ revert(doc: BoardDocument): void;
1609
+ }
1610
+ declare class DeleteArrowCommand implements Command {
1611
+ readonly label = "delete arrow";
1612
+ private readonly arrow;
1613
+ constructor(arrow: ArrowObject);
1614
+ apply(doc: BoardDocument): void;
1615
+ revert(doc: BoardDocument): void;
1616
+ }
1617
+ /** Add/update/delete for semantic Polygon objects (Phase 4) — Triangle/Diamond/Pentagon/Hexagon/Octagon. */
1618
+ declare class AddPolygonCommand implements Command {
1619
+ readonly label = "add polygon";
1620
+ private readonly polygon;
1621
+ constructor(polygon: PolygonObject);
1622
+ apply(doc: BoardDocument): void;
1623
+ revert(doc: BoardDocument): void;
1624
+ }
1625
+ declare class UpdatePolygonCommand implements Command {
1626
+ readonly label = "update polygon";
1627
+ private readonly before;
1628
+ private readonly after;
1629
+ constructor(before: PolygonObject, after: PolygonObject);
1630
+ apply(doc: BoardDocument): void;
1631
+ revert(doc: BoardDocument): void;
1632
+ }
1633
+ declare class DeletePolygonCommand implements Command {
1634
+ readonly label = "delete polygon";
1635
+ private readonly polygon;
1636
+ constructor(polygon: PolygonObject);
1637
+ apply(doc: BoardDocument): void;
1638
+ revert(doc: BoardDocument): void;
1639
+ }
1640
+ /** Add/update/delete for semantic Star objects (Phase 4). */
1641
+ declare class AddStarCommand implements Command {
1642
+ readonly label = "add star";
1643
+ private readonly star;
1644
+ constructor(star: StarObject);
1645
+ apply(doc: BoardDocument): void;
1646
+ revert(doc: BoardDocument): void;
1647
+ }
1648
+ declare class UpdateStarCommand implements Command {
1649
+ readonly label = "update star";
1650
+ private readonly before;
1651
+ private readonly after;
1652
+ constructor(before: StarObject, after: StarObject);
1653
+ apply(doc: BoardDocument): void;
1654
+ revert(doc: BoardDocument): void;
1655
+ }
1656
+ declare class DeleteStarCommand implements Command {
1657
+ readonly label = "delete star";
1658
+ private readonly star;
1659
+ constructor(star: StarObject);
1660
+ apply(doc: BoardDocument): void;
1661
+ revert(doc: BoardDocument): void;
1662
+ }
1663
+ /** Add/update/delete for semantic Heart objects (Phase 4). */
1664
+ declare class AddHeartCommand implements Command {
1665
+ readonly label = "add heart";
1666
+ private readonly heart;
1667
+ constructor(heart: HeartObject);
1668
+ apply(doc: BoardDocument): void;
1669
+ revert(doc: BoardDocument): void;
1670
+ }
1671
+ declare class UpdateHeartCommand implements Command {
1672
+ readonly label = "update heart";
1673
+ private readonly before;
1674
+ private readonly after;
1675
+ constructor(before: HeartObject, after: HeartObject);
1676
+ apply(doc: BoardDocument): void;
1677
+ revert(doc: BoardDocument): void;
1678
+ }
1679
+ declare class DeleteHeartCommand implements Command {
1680
+ readonly label = "delete heart";
1681
+ private readonly heart;
1682
+ constructor(heart: HeartObject);
1683
+ apply(doc: BoardDocument): void;
1684
+ revert(doc: BoardDocument): void;
1685
+ }
1686
+ /** Add/update/delete for Groups (Phase 3). */
1687
+ declare class AddGroupCommand implements Command {
1688
+ readonly label = "group";
1689
+ private readonly group;
1690
+ constructor(group: GroupObject);
1691
+ apply(doc: BoardDocument): void;
1692
+ revert(doc: BoardDocument): void;
1693
+ }
1694
+ declare class UpdateGroupCommand implements Command {
1695
+ readonly label = "update group";
1696
+ private readonly before;
1697
+ private readonly after;
1698
+ constructor(before: GroupObject, after: GroupObject);
1699
+ apply(doc: BoardDocument): void;
1700
+ revert(doc: BoardDocument): void;
1701
+ }
1702
+ declare class DeleteGroupCommand implements Command {
1703
+ readonly label = "ungroup";
1704
+ private readonly group;
1705
+ constructor(group: GroupObject);
1706
+ apply(doc: BoardDocument): void;
1707
+ revert(doc: BoardDocument): void;
1708
+ }
1709
+ type ReorderDirection = "forward" | "backward" | "front" | "back";
1710
+ /**
1711
+ * Bring-forward / send-backward / bring-to-front / send-to-back (Phase 3) —
1712
+ * one command family covering all four directions rather than four
1713
+ * near-identical classes, since the only difference between them is which
1714
+ * `BoardDocument.reorder` direction to replay. Captures the full paint order
1715
+ * on first `apply` rather than in the constructor — `BoardDocument.order()`
1716
+ * needs the doc, which a `Command` only ever receives via `apply`/`revert` —
1717
+ * so `revert` can restore it exactly; redo re-runs the same `reorder` call,
1718
+ * which is deterministic because `revert` always restores the identical
1719
+ * starting order first.
1720
+ */
1721
+ declare class ReorderObjectCommand implements Command {
1722
+ private readonly id;
1723
+ private readonly direction;
1724
+ readonly label: string;
1725
+ private before;
1726
+ constructor(id: string, direction: ReorderDirection);
1727
+ apply(doc: BoardDocument): void;
1728
+ revert(doc: BoardDocument): void;
1729
+ }
1035
1730
  interface LockTarget {
1036
- type: "stroke" | "note" | "text" | "table" | "image" | "timer";
1731
+ type: "stroke" | "note" | "text" | "table" | "image" | "timer" | "rectangle" | "ellipse" | "group" | "line" | "arrow" | "polygon" | "star" | "heart";
1037
1732
  id: string;
1038
1733
  locked: boolean;
1039
1734
  lockedBy?: string;
@@ -1050,7 +1745,7 @@ declare class LockItemsCommand implements Command {
1050
1745
  private applyLock;
1051
1746
  }
1052
1747
 
1053
- type OpCollection = "strokes" | "notes" | "textBlocks" | "tables" | "images" | "timers" | "customObjects";
1748
+ type OpCollection = "strokes" | "notes" | "textBlocks" | "tables" | "images" | "timers" | "rectangles" | "ellipses" | "groups" | "lines" | "arrows" | "polygons" | "stars" | "hearts" | "customObjects";
1054
1749
  type Op = {
1055
1750
  kind: "upsert";
1056
1751
  collection: OpCollection;
@@ -1094,10 +1789,10 @@ declare function ribbonEdges(points: StrokePoint[], baseWidth: number, handDrawn
1094
1789
  declare class SpatialIndex {
1095
1790
  private readonly doc;
1096
1791
  private readonly cells;
1097
- private readonly strokeCells;
1792
+ private readonly objectCells;
1098
1793
  private readonly unsubscribe;
1099
1794
  constructor(doc: BoardDocument);
1100
- /** Ids of strokes whose bbox may overlap the query rect. */
1795
+ /** Ids of content objects (any type except groups) whose bbox may overlap the query rect. */
1101
1796
  query(minX: number, minY: number, maxX: number, maxY: number): Set<string>;
1102
1797
  dispose(): void;
1103
1798
  private insert;
@@ -1252,7 +1947,7 @@ interface BoardSnapshot {
1252
1947
  * here, matching the engine's own internal selection-badge behavior.
1253
1948
  */
1254
1949
  interface FocusedItem {
1255
- readonly type: "stroke" | "note" | "text" | "table" | "image" | "timer" | "custom";
1950
+ readonly type: "stroke" | "note" | "text" | "table" | "image" | "timer" | "rectangle" | "ellipse" | "group" | "line" | "arrow" | "polygon" | "star" | "heart" | "custom";
1256
1951
  readonly id: string;
1257
1952
  readonly locked: boolean;
1258
1953
  readonly lockedBy?: string;
@@ -1333,6 +2028,8 @@ interface BoardEventMap {
1333
2028
  "asset-diagnostic": AssetDiagnostic;
1334
2029
  /** A batch of Ops was reconciled (not applied as-sent) by the persistence adapter (ticket #24). */
1335
2030
  "persistence-diagnostic": PersistenceDiagnostic;
2031
+ /** 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. */
2032
+ "collaboration-ops-acknowledged": CollaborationAckDiagnostic;
1336
2033
  audit: unknown;
1337
2034
  error: BoardControllerError;
1338
2035
  disposed: undefined;
@@ -1396,9 +2093,12 @@ interface PresenceView {
1396
2093
  readonly height: number;
1397
2094
  }
1398
2095
  /**
1399
- * A Host-owned collaborator, synced in for cursor/roster rendering only.
1400
- * Presence is ephemeral — it never touches the Document, Ops, undo/redo,
1401
- * or persistence — so this is a read/query capability, not an adapter.
2096
+ * A collaborator, synced in for cursor/roster rendering only. Presence is
2097
+ * ephemeral — it never touches the Document, Ops, undo/redo, or persistence
2098
+ * (ADR 0006/0007). Two ways a roster gets populated (`presence.sync`
2099
+ * directly, or a `CollaborationAdapter`'s optional presence channel —
2100
+ * Phase 6, ADR 0015) both feed the exact same read/query capability below;
2101
+ * a Host picks one, not both, for a given controller.
1402
2102
  */
1403
2103
  interface PresenceUser {
1404
2104
  readonly id: string;
@@ -1407,6 +2107,14 @@ interface PresenceUser {
1407
2107
  readonly tool?: string;
1408
2108
  readonly cursor?: PresenceCursor;
1409
2109
  readonly view?: PresenceView;
2110
+ /** Host-supplied extras (avatar URL, role, etc.) — opaque to Scrawl, never interpreted. */
2111
+ readonly metadata?: Record<string, unknown>;
2112
+ }
2113
+ /** This client's own local presence, published via `presence.broadcast()` (Phase 6). */
2114
+ interface LocalPresence {
2115
+ readonly cursor?: PresenceCursor | null;
2116
+ readonly view?: PresenceView | null;
2117
+ readonly tool?: string;
1410
2118
  }
1411
2119
  /**
1412
2120
  * The Custom arm wraps `CustomBoardObject` under the same `type` discriminant
@@ -1434,6 +2142,22 @@ type BoardObject = ({
1434
2142
  } & ImageBlock) | ({
1435
2143
  type: "timer";
1436
2144
  } & KitchenTimer) | ({
2145
+ type: "rectangle";
2146
+ } & RectangleObject) | ({
2147
+ type: "ellipse";
2148
+ } & EllipseObject) | ({
2149
+ type: "group";
2150
+ } & GroupObject) | ({
2151
+ type: "line";
2152
+ } & LineObject) | ({
2153
+ type: "arrow";
2154
+ } & ArrowObject) | ({
2155
+ type: "polygon";
2156
+ } & PolygonObject) | ({
2157
+ type: "star";
2158
+ } & StarObject) | ({
2159
+ type: "heart";
2160
+ } & HeartObject) | ({
1437
2161
  type: "custom";
1438
2162
  customType: ObjectType;
1439
2163
  } & Omit<CustomBoardObject, "type">);
@@ -1462,6 +2186,30 @@ type BoardObjectInput = {
1462
2186
  type: "timer";
1463
2187
  id?: string;
1464
2188
  } & Omit<KitchenTimer, "id">) | ({
2189
+ type: "rectangle";
2190
+ id?: string;
2191
+ } & Omit<RectangleObject, "id">) | ({
2192
+ type: "ellipse";
2193
+ id?: string;
2194
+ } & Omit<EllipseObject, "id">) | ({
2195
+ type: "group";
2196
+ id?: string;
2197
+ } & Omit<GroupObject, "id">) | ({
2198
+ type: "line";
2199
+ id?: string;
2200
+ } & Omit<LineObject, "id">) | ({
2201
+ type: "arrow";
2202
+ id?: string;
2203
+ } & Omit<ArrowObject, "id">) | ({
2204
+ type: "polygon";
2205
+ id?: string;
2206
+ } & Omit<PolygonObject, "id">) | ({
2207
+ type: "star";
2208
+ id?: string;
2209
+ } & Omit<StarObject, "id">) | ({
2210
+ type: "heart";
2211
+ id?: string;
2212
+ } & Omit<HeartObject, "id">) | ({
1465
2213
  type: "custom";
1466
2214
  id?: string;
1467
2215
  customType: ObjectType;
@@ -1492,10 +2240,34 @@ type LoadResult = {
1492
2240
  } | {
1493
2241
  state: "missing";
1494
2242
  };
2243
+ /**
2244
+ * Result of a whole-document `PersistenceAdapter.replace()` call (ADR 0006:
2245
+ * "Whole-document writes survive only for create, clear-board and import,
2246
+ * where replacing everything is the actual intent"). Revision-gated, unlike
2247
+ * `applyOps` — `conflict` means `baseRevision` was stale (someone else's
2248
+ * write landed first); the caller must reload and never overwrites blind.
2249
+ */
2250
+ type ReplaceResult = {
2251
+ state: "applied";
2252
+ revision: string;
2253
+ } | {
2254
+ state: "conflict";
2255
+ currentRevision: string;
2256
+ };
2257
+ /**
2258
+ * The one sanctioned seam for persisting a Board's Document to a Host's own
2259
+ * storage — implement this against a database, an HTTP API, IndexedDB
2260
+ * (see `@scrawl-board/board/local`'s `createIndexedDBPersistence`), or
2261
+ * anything else. `load()` fetches the current state on connect; `applyOps()`
2262
+ * streams incremental Ops as edits happen; `replace()` is only for
2263
+ * whole-document writes (create, clear-board, import — see ADR 0006) and is
2264
+ * revision-gated so a stale write never silently clobbers a newer one.
2265
+ * Passed via `createBoardController({ adapters: { persistence } })`.
2266
+ */
1495
2267
  interface PersistenceAdapter {
1496
2268
  load(context: DocumentContext): Promise<LoadResult>;
1497
2269
  applyOps(context: DocumentContext, ops: readonly ControllerOp[]): Promise<ApplyOpsResult>;
1498
- replace(context: DocumentContext, document: CurrentSerializedDocument, baseRevision: string): Promise<unknown>;
2270
+ replace(context: DocumentContext, document: CurrentSerializedDocument, baseRevision: string): Promise<ReplaceResult>;
1499
2271
  }
1500
2272
  /**
1501
2273
  * `"reconcile"` (ticket #24) means the server authoritatively resolved the
@@ -1519,14 +2291,59 @@ interface PersistenceDiagnostic {
1519
2291
  rejectedOpIds: readonly string[];
1520
2292
  revision: string;
1521
2293
  }
2294
+ /** Emitted as `"collaboration-ops-acknowledged"` (Phase 7) — the server has confirmed receipt of these op ids on the live pipe. */
2295
+ interface CollaborationAckDiagnostic {
2296
+ opIds: readonly string[];
2297
+ }
1522
2298
  interface ControllerOp {
1523
2299
  id: string;
1524
2300
  schemaVersion: 1;
1525
2301
  kind: "upsert" | "restore" | "remove";
1526
- objectType: "stroke" | "note" | "text" | "table" | "image" | "timer" | "custom";
2302
+ objectType: "stroke" | "note" | "text" | "table" | "image" | "timer" | "rectangle" | "ellipse" | "group" | "line" | "arrow" | "polygon" | "star" | "heart" | "custom"
2303
+ /**
2304
+ * A whole-document paint-order sync (Phase 3), not a per-object type —
2305
+ * `objectId` is always the fixed sentinel `"order"` and `payload` is
2306
+ * `{ order: string[] }`. The only `objectType` with no matching
2307
+ * `BoardObject`/document collection; kept in this same union (rather
2308
+ * than a separate wire message) so it flows through the existing
2309
+ * `PersistenceAdapter`/`CollaborationAdapter` opaquely, unchanged.
2310
+ */
2311
+ | "order";
1527
2312
  objectId: string;
1528
2313
  payload?: unknown;
2314
+ /**
2315
+ * This op's position in its own originating client's local sequence
2316
+ * (Phase 7) — 1, 2, 3, ... per controller instance, distinct from `id`
2317
+ * (an opaque, globally-unique identifier used for dedup/ack, not
2318
+ * ordering) and from a server's own authoritative ordering (e.g.
2319
+ * `referenceCollaborationServer.ts`'s per-room `version` counter).
2320
+ * Present on every op this SDK originates locally; a remote peer's op
2321
+ * carries whatever its own origin set, unchanged — never renumbered in
2322
+ * transit. Absent on an op minted by decoding the legacy wire envelope
2323
+ * (`scrawlOpEnvelope.ts`), which predates this field and has no
2324
+ * per-client sequence concept of its own.
2325
+ */
2326
+ clientSequence?: number;
2327
+ /**
2328
+ * The `CollaboratorIdentity.id` of this op's originating client (Phase
2329
+ * 7) — set for every op this SDK originates locally when `identity` is
2330
+ * configured, omitted entirely otherwise (never sent as `undefined`).
2331
+ * The explicit foundation for a future per-author undo filter (a local
2332
+ * user's own undo should only ever touch their own ops) — no undo-stack
2333
+ * behavior itself changes this phase.
2334
+ */
2335
+ clientId?: string;
1529
2336
  }
2337
+ /**
2338
+ * The one sanctioned seam for real-time multiplayer — implement this against
2339
+ * a Host's own collaboration backend (WebSocket relay, CRDT server, etc.).
2340
+ * `connect()` is called once per controller with the local user's
2341
+ * `identity` and a `receive` callback the adapter invokes with incoming
2342
+ * Ops, presence updates, acks, and connection status; it resolves with a
2343
+ * `CollaborationSession` the controller uses to send local Ops and presence
2344
+ * back out. Passed via `createBoardController({ adapters: { collaboration } })`;
2345
+ * omit it entirely to run single-player.
2346
+ */
1530
2347
  interface CollaborationAdapter {
1531
2348
  connect(options: DocumentContext & {
1532
2349
  identity: CollaboratorIdentity;
@@ -1537,14 +2354,72 @@ interface CollaboratorIdentity {
1537
2354
  id: string;
1538
2355
  name: string;
1539
2356
  color?: string;
2357
+ /** Host-supplied extras (avatar URL, role, etc.) — opaque to Scrawl, forwarded into any resulting `PresenceUser` unread and never interpreted. */
2358
+ metadata?: Record<string, unknown>;
1540
2359
  }
1541
2360
  interface CollaborationReceiver {
1542
2361
  ops(ops: readonly ControllerOp[]): void;
2362
+ /**
2363
+ * The current presence roster (Phase 6, ADR 0015) — always a full
2364
+ * replacement, never a delta, matching `presence.sync`'s existing
2365
+ * semantics exactly (an adapter that aggregates wire deltas into a full
2366
+ * roster before calling this is the adapter's own job, not the
2367
+ * controller's). Required on this interface (not optional) because a
2368
+ * Host only ever *consumes* `CollaborationReceiver` — never implements
2369
+ * it — so adding a required method here cannot break an existing custom
2370
+ * `CollaborationAdapter`. An adapter with no presence support simply
2371
+ * never calls it.
2372
+ */
2373
+ presence(users: readonly PresenceUser[]): void;
2374
+ /**
2375
+ * The server has confirmed receipt of these op ids (Phase 7) —
2376
+ * distinguishes "sent" from "server accepted," which `sendOps` alone
2377
+ * (fire-and-forget) cannot. Required for the same reason `presence` is:
2378
+ * Hosts only ever consume this interface, never implement it, so this
2379
+ * cannot break an existing custom `CollaborationAdapter`. An adapter with
2380
+ * no ack support simply never calls it — the collaboration pipe still
2381
+ * works exactly as it did before this existed, just without the
2382
+ * bookkeeping/observability this enables.
2383
+ */
2384
+ acknowledged(opIds: readonly string[]): void;
1543
2385
  status(state: "online" | "reconnecting" | "offline"): void;
1544
2386
  error(cause: unknown): void;
1545
2387
  }
2388
+ /**
2389
+ * Result of `CollaborationSession.requestSync()` (Phase 7). `"ops"` means
2390
+ * the adapter's own live-pipe cache fully covered the gap since the
2391
+ * caller's last known revision — apply `ops` and the client is caught up,
2392
+ * no persistence reload needed. `"unavailable"` means it couldn't (gap too
2393
+ * large, server restarted, or the adapter has no retained history at all)
2394
+ * — the caller must fall back to a persistence-backed reload. This is a
2395
+ * best-effort *liveness* cache, deliberately never a durable source of
2396
+ * truth (ADR 0006's "collaboration is never a second source of document
2397
+ * truth" — see ADR 0015's own extension of that principle to presence,
2398
+ * now extended once more, the same way, to this).
2399
+ */
2400
+ type CollaborationSyncResult = {
2401
+ state: "ops";
2402
+ ops: readonly ControllerOp[];
2403
+ serverRevision: string;
2404
+ } | {
2405
+ state: "unavailable";
2406
+ };
1546
2407
  interface CollaborationSession {
1547
2408
  sendOps(ops: readonly ControllerOp[]): void;
2409
+ /**
2410
+ * Publishes this client's own local presence (Phase 6, ADR 0015) —
2411
+ * best-effort, unordered, never persisted, never an Op. Optional: an
2412
+ * adapter that doesn't support presence simply omits this method, and
2413
+ * `presence.broadcast()` becomes a silent no-op.
2414
+ */
2415
+ updatePresence?(presence: LocalPresence): void;
2416
+ /**
2417
+ * Requests an incremental catch-up after a reconnect (Phase 7) — optional;
2418
+ * an adapter that doesn't support this simply omits the method, and the
2419
+ * caller (`resyncAfterReconnect`) goes straight to its existing
2420
+ * persistence-backed full reload, unchanged from Phase 6.
2421
+ */
2422
+ requestSync?(): Promise<CollaborationSyncResult>;
1548
2423
  close(): Promise<void>;
1549
2424
  }
1550
2425
  interface CreateBoardControllerOptions {
@@ -1584,6 +2459,87 @@ interface CreateBoardControllerOptions {
1584
2459
  * doesn't use the React theme system can set this directly instead.
1585
2460
  */
1586
2461
  boardTheme?: BoardThemeOptions;
2462
+ /**
2463
+ * Debounced auto-flush of pending persistence Ops after document changes
2464
+ * settle (Phase 5). Enabled by default (1000ms debounce) whenever
2465
+ * `adapters.persistence` is configured — today, without this, a Host must
2466
+ * call `flush()` manually after every edit for anything to persist. Pass
2467
+ * `false` to opt out entirely and drive `flush()` yourself, preserving
2468
+ * prior behavior exactly. Never fires on a per-change basis — rapid edits
2469
+ * coalesce into one flush of their final state (ADR 0006).
2470
+ */
2471
+ autosave?: boolean | {
2472
+ debounceMs?: number;
2473
+ };
2474
+ /**
2475
+ * Throttle for `presence.broadcast()` (Phase 6, ADR 0015) — the minimum
2476
+ * interval between outgoing presence updates sent via the configured
2477
+ * `CollaborationAdapter`. Defaults to 50ms. A trailing throttle: the
2478
+ * latest value passed to `broadcast()` always eventually sends, even if
2479
+ * calls arrive faster than this interval.
2480
+ */
2481
+ presenceThrottleMs?: number;
2482
+ /**
2483
+ * Caps how many `ControllerOp`s can sit queued, unsent, for the
2484
+ * persistence pipe (`pendingOps`) or the collaboration pipe
2485
+ * (`pendingCollaborationOps`) at once (Phase 7) — each pipe is capped
2486
+ * independently. Prevents unbounded memory growth from a long-lived
2487
+ * offline session or a stuck adapter. Exceeding it never fails or drops
2488
+ * the local edit itself (the Document already applied it optimistically)
2489
+ * — only queueing for that one pipe is skipped, and a
2490
+ * `{code:"queue-overflow", retryable:false}` error is emitted so a Host
2491
+ * can react. Defaults to 1000 — the Phase 9 collaboration coalescing
2492
+ * above (`collaborationCoalesceMs`) already keeps a busy drag from
2493
+ * approaching this on its own, so hitting it in practice means a pipe
2494
+ * has been offline/stuck for a genuinely long editing session.
2495
+ *
2496
+ * **Recovery** (Phase 9): the dropped op itself is gone from that one
2497
+ * pipe's queue — there is no automatic backfill, and the live
2498
+ * controller keeps running with that pipe now silently missing one
2499
+ * edit. Two things stay true regardless: (1) the in-memory Document is
2500
+ * never affected — a queue-overflow can never corrupt or roll back a
2501
+ * local edit, only skip sending it; (2) staleness is per-object, not
2502
+ * permanent — any *later* edit to that same object produces a brand
2503
+ * new, undropped Op carrying its full current state, which naturally
2504
+ * supersedes the gap (the Op model is already last-write-wins/
2505
+ * idempotent, so a superseding Op doesn't need the earlier one to have
2506
+ * arrived). The real risk is an object that's dropped and never edited
2507
+ * again before the controller is disposed or the page reloads — a Host
2508
+ * that needs strict durability should treat `queue-overflow` as a
2509
+ * signal to check `persistence.state`/`pendingOps` pressure (via
2510
+ * `usePersistenceStatus`/`getSnapshot().connection.persistence`)
2511
+ * before disposing, not assume disposing and reconnecting alone
2512
+ * repairs the gap (a fresh `load()` only returns what the backend
2513
+ * already has, which is exactly what's missing the dropped edit).
2514
+ */
2515
+ maxPendingOps?: number;
2516
+ /**
2517
+ * Coalescing window (ms) for outgoing collaboration Ops (Phase 9) — same
2518
+ * trailing-throttle shape as `presenceThrottleMs`: the first Op after an
2519
+ * idle period sends immediately, and subsequent Ops for the *same*
2520
+ * object within this window replace each other (latest value wins,
2521
+ * matching the already-idempotent Op model) rather than each triggering
2522
+ * its own send. A multi-second drag that previously sent one full Op per
2523
+ * pointer-move now sends at most one per window per touched object.
2524
+ * Persistence (`pendingOps`) is unaffected — it already debounces via
2525
+ * `autosave`, so this option only changes live collaboration traffic.
2526
+ * Defaults to 50ms.
2527
+ */
2528
+ collaborationCoalesceMs?: number;
2529
+ /**
2530
+ * Caps how many resolved objects a single `content.copy`/`content.cut`
2531
+ * (or their Cmd/Ctrl+C/X keyboard equivalents) will hold in the
2532
+ * in-memory clipboard at once (Phase 9) — `expandSelection` recursively
2533
+ * expands groups, so an unbounded selection (a huge group, or thousands
2534
+ * of individually selected strokes) could otherwise clone and retain an
2535
+ * arbitrarily large snapshot indefinitely, until the next copy/cut
2536
+ * replaces it. Exceeding it rejects the whole copy/cut (nothing is
2537
+ * cloned, and — for cut — nothing is removed from the Document either,
2538
+ * never a partial copy of an arbitrary subset) and emits a
2539
+ * `{code:"clipboard-overflow", retryable:false}` error. Defaults to
2540
+ * 5000.
2541
+ */
2542
+ maxClipboardItems?: number;
1587
2543
  }
1588
2544
  interface BoardController {
1589
2545
  readonly document: ReadonlyBoardDocument;
@@ -1615,6 +2571,15 @@ interface BoardController {
1615
2571
  };
1616
2572
  readonly view: {
1617
2573
  fit(): void;
2574
+ /**
2575
+ * Frame the current selection (Phase 8), the same way `fit()` frames the
2576
+ * whole board. A no-op with nothing selected — deliberately doesn't fall
2577
+ * back to `fit()`'s "frame everything," which would be a surprising
2578
+ * result for an empty selection. On a headless board this can only
2579
+ * re-center the view (no viewport to compute a real zoom-to-fit from),
2580
+ * matching `fit()`'s own headless limitation exactly.
2581
+ */
2582
+ zoomToSelection(): void;
1618
2583
  zoomTo(value: number): void;
1619
2584
  centerOn(point: BoardPoint): void;
1620
2585
  get(): BoardView;
@@ -1636,6 +2601,64 @@ interface BoardController {
1636
2601
  * Unknown ids are silently skipped, matching `remove`'s convention.
1637
2602
  */
1638
2603
  duplicate(ids: readonly string[]): readonly string[];
2604
+ /**
2605
+ * Creates a new Group referencing `ids` as its children and returns its
2606
+ * id, as one undoable step. Unknown ids are silently skipped, matching
2607
+ * `duplicate`/`remove`'s convention. A child id that's itself a group
2608
+ * makes a nested group — expanding nested groups into their leaf
2609
+ * members is always the caller's job, never assumed here (matches the
2610
+ * document-model `GroupObject` itself).
2611
+ */
2612
+ group(ids: readonly string[]): string;
2613
+ /**
2614
+ * Dissolves one group, returning its immediate children's ids (a nested
2615
+ * subgroup among them stays intact, itself still a group) — the group
2616
+ * record itself is removed, the children are untouched. A no-op
2617
+ * (returns `[]`) if `groupId` isn't a group.
2618
+ */
2619
+ ungroup(groupId: string): readonly string[];
2620
+ /**
2621
+ * Aligns every given object's matching edge/center to the corresponding
2622
+ * edge/center of their combined bounding box, as one undoable step.
2623
+ * `"top"`/`"bottom"` follow board space's Y-up convention (`"top"` is
2624
+ * the larger Y). Ids that don't resolve, or resolve to a Group (which
2625
+ * has no position of its own), are skipped. A no-op under 2 resolvable
2626
+ * ids — there's nothing to align relative to.
2627
+ */
2628
+ align(ids: readonly string[], edge: "left" | "right" | "top" | "bottom" | "centerX" | "centerY"): void;
2629
+ /**
2630
+ * Spaces the middle objects' centers evenly between the first and last
2631
+ * (sorted along `axis`), as one undoable step — the two endpoints don't
2632
+ * move. Ids that don't resolve, or resolve to a Group, are skipped. A
2633
+ * no-op under 3 resolvable ids — there's no "middle" to distribute.
2634
+ */
2635
+ distribute(ids: readonly string[], axis: "x" | "y"): void;
2636
+ /**
2637
+ * Snapshots `ids` (recursively expanded through any group, same as
2638
+ * `duplicate`) into an internal in-memory clipboard — never
2639
+ * `navigator.clipboard`, scoped to this one controller instance and
2640
+ * replaced wholesale by the next `copy`/`cut`. Read-only; works even
2641
+ * on a read-only board.
2642
+ */
2643
+ copy(ids: readonly string[]): void;
2644
+ /** `copy`, then removes every resolved object (recursively through any group) as one undoable step. */
2645
+ cut(ids: readonly string[]): void;
2646
+ /**
2647
+ * Clones the current clipboard contents onto the board as one undoable
2648
+ * step, offset the same small cascade `duplicate` uses (no cursor
2649
+ * position to paste relative to yet). Returns the new top-level ids —
2650
+ * a pasted group's own id stands for its (also-pasted) children, which
2651
+ * aren't listed separately. `[]` when the clipboard is empty.
2652
+ */
2653
+ paste(): readonly string[];
2654
+ /**
2655
+ * Select every top-level object (Phase 8) — a group's own id stands for
2656
+ * its children, which aren't selected separately, matching `paste`'s own
2657
+ * "what the user sees" id list. Hidden objects are excluded, consistent
2658
+ * with them already being excluded from marquee selection. Works with
2659
+ * no canvas/engine, same as {@link toggleSelectionVisibility}.
2660
+ */
2661
+ selectAll(): void;
1639
2662
  table: {
1640
2663
  addRow(tableId: string): void;
1641
2664
  addCol(tableId: string): void;
@@ -1646,6 +2669,29 @@ interface BoardController {
1646
2669
  };
1647
2670
  select(ids: readonly string[]): void;
1648
2671
  import(document: SerializedBoardDocument): readonly string[];
2672
+ /**
2673
+ * Toggle lock state for the current selection (or focused note/text/
2674
+ * table/image/timer), matching whatever a single Host lock/unlock
2675
+ * control already does per object type. A no-op with nothing selected,
2676
+ * on a headless board, or when every actionable target is locked by
2677
+ * another collaborator who isn't the current lock holder.
2678
+ */
2679
+ toggleSelectionLock(): void;
2680
+ /**
2681
+ * Toggle hidden state for the current selection, as one undo entry
2682
+ * (Phase 8). If any selected object is hidden, shows every selected
2683
+ * object; otherwise hides them all — same "any wins" semantics as
2684
+ * {@link toggleSelectionLock}. Hidden objects stay fully present in the
2685
+ * document (they still serialize, persist, sync, undo/redo) — they just
2686
+ * stop rendering and stop being hit-testable/selectable via pointer
2687
+ * interaction. Unlike `toggleSelectionLock`, this works on a headless
2688
+ * board too: it only touches `selection`/the document, no canvas or
2689
+ * engine involved. Custom objects have no visibility concept (no
2690
+ * `Hideable` field) and are silently skipped, matching how
2691
+ * `toggleSelectionLock` already excludes them. A no-op with nothing
2692
+ * selected or when the selection is only custom objects.
2693
+ */
2694
+ toggleSelectionVisibility(): void;
1649
2695
  };
1650
2696
  readonly query: {
1651
2697
  get(id: string): DeepReadonly<BoardObject> | undefined;
@@ -1674,6 +2720,15 @@ interface BoardController {
1674
2720
  follow(view: PresenceView): void;
1675
2721
  /** Ease the camera to a peer's view; returns false (no-op) while mid-stroke. */
1676
2722
  gather(view: PresenceView): boolean;
2723
+ /**
2724
+ * Publishes this client's own cursor/tool/view for other collaborators
2725
+ * (Phase 6, ADR 0015), via the configured `CollaborationAdapter` —
2726
+ * throttled internally (`presenceThrottleMs` option, default 50ms) so
2727
+ * a raw pointermove stream never becomes a message-per-event flood. A
2728
+ * no-op if no collaboration adapter is configured, or if the
2729
+ * configured one doesn't implement `updatePresence`.
2730
+ */
2731
+ broadcast(local: LocalPresence): void;
1677
2732
  };
1678
2733
  readonly export: {
1679
2734
  svg(): string;
@@ -1699,7 +2754,19 @@ interface BoardController {
1699
2754
  }>;
1700
2755
  dispose(): Promise<void>;
1701
2756
  }
2757
+ /**
2758
+ * Creates a {@link BoardController} — the SDK's canonical, capability-grouped
2759
+ * entry point (`content`, `tools`, `style`, `history`, `view`, `query`,
2760
+ * `comments`, `presence`, `export`, `assets`, plus top-level `getSnapshot`/
2761
+ * `subscribe`/`on`/`setReadOnly`/`flush`/`dispose`) for driving a Board
2762
+ * imperatively from any JS/TS runtime. Supply a `canvas` to render, or omit
2763
+ * it to run headless (SSR, tests, or a document/history-only integration).
2764
+ * Persistence and Collaboration are opt-in via `options.adapters` — without
2765
+ * them the controller runs entirely in memory. Call `dispose()` when done to
2766
+ * release the renderer, adapters, and any pending timers.
2767
+ */
1702
2768
  declare function createBoardController(options: CreateBoardControllerOptions): BoardController;
2769
+ /** @deprecated Use `BoardController.getSnapshot()`'s return type instead. */
1703
2770
  type LocalBoardSnapshot = {
1704
2771
  documentId: string;
1705
2772
  selectedStrokeId: string | null;
@@ -1708,10 +2775,12 @@ type LocalBoardSnapshot = {
1708
2775
  canRedo: boolean;
1709
2776
  disposed: boolean;
1710
2777
  };
2778
+ /** @deprecated Use `CreateBoardControllerOptions` with `createBoardController` instead. */
1711
2779
  type LocalBoardOptions = {
1712
2780
  documentId: string;
1713
2781
  initialDocument?: SerializedBoardDocument;
1714
2782
  };
2783
+ /** @deprecated Use `BoardController` (from `createBoardController`) instead — this stroke-only, single-tool surface predates the full capability-grouped controller. */
1715
2784
  type LocalBoard = {
1716
2785
  drawStroke(stroke: Stroke): void;
1717
2786
  selectAt(point: BoardPoint): string | null;
@@ -1722,8 +2791,21 @@ type LocalBoard = {
1722
2791
  subscribe(listener: () => void): () => void;
1723
2792
  dispose(): Promise<void>;
1724
2793
  };
2794
+ /** @deprecated Use `createBoardController` instead — this is a thin, stroke-only wrapper kept for the original Phase 1 tracer's compatibility. */
1725
2795
  declare function createLocalBoard(options: LocalBoardOptions): LocalBoard;
1726
2796
 
2797
+ interface CreateMemoryPersistenceOptions {
2798
+ /** Pre-seeded documents, keyed by document id — as if a prior session had already saved them. Seeded documents start at revision "1". */
2799
+ seed?: Record<string, CurrentSerializedDocument>;
2800
+ }
2801
+ /**
2802
+ * Creates a real, in-memory `PersistenceAdapter`. One instance can back
2803
+ * multiple documents (keyed by `DocumentContext.documentId`, like every
2804
+ * other adapter in this package). State lives only in this instance —
2805
+ * discarded on garbage collection, never written to disk.
2806
+ */
2807
+ declare function createMemoryPersistence(options?: CreateMemoryPersistenceOptions): PersistenceAdapter;
2808
+
1727
2809
  type ScrawlThemePreset = "light" | "dark";
1728
2810
  type ScrawlDensity = "comfortable" | "compact";
1729
2811
  type ScrawlGridMode = "none" | "line" | "dot";
@@ -1899,10 +2981,19 @@ interface FocusedItemToolbarProps {
1899
2981
  snapshot: BoardSnapshot;
1900
2982
  }
1901
2983
  /**
1902
- * Floating toolbar above the focused note, shape, or text block — Colour,
1903
- * Size (note/text) or Width (shape), Lock/Unlock, Duplicate, Delete.
1904
- * Table/image/timer/custom objects and plain (non-shape) ink strokes never
1905
- * get a toolbar here — out of scope for this destination.
2984
+ * Floating toolbar above the focused note, shape, text block, or semantic
2985
+ * Rectangle/Ellipse — Colour, Size (note/text) or Width (shape), Fill/Stroke
2986
+ * (Rectangle/Ellipse), Lock/Unlock, Duplicate, Delete. Table/image/timer/
2987
+ * custom objects and plain (non-shape) ink strokes never get a toolbar here
2988
+ * — out of scope for this destination.
2989
+ *
2990
+ * Rectangle/Ellipse (Phase 2) are a second, parallel "shape" concept from
2991
+ * the legacy ink-stroke shape below: both draw from the same toolbar
2992
+ * buttons and look the same to a user, but a Rectangle/Ellipse is a real
2993
+ * BoardObject with its own resize handles (independent width/height,
2994
+ * `ShapeResizeHandle`), while a legacy shape-stroke keeps going through the
2995
+ * native selection gizmo. This duality is deliberate and temporary — see
2996
+ * docs/reports/phase-2-document-object-model.md.
1906
2997
  *
1907
2998
  * Lock/Unlock carries no per-user ownership gating: nothing else in this
1908
2999
  * SDK enforces lock ownership either (`content.update` never checks
@@ -1957,6 +3048,16 @@ interface ScrawlProviderProps {
1957
3048
  className?: string;
1958
3049
  style?: ThemeStyle;
1959
3050
  }
3051
+ /**
3052
+ * Wraps a Board `controller` you created yourself (via `createBoardController`)
3053
+ * so its descendants can use `useScrawlController`/`useScrawlSnapshot`/etc.
3054
+ * and render its default UI pieces (`DefaultBoardChrome`, `StyleShelf`, ...).
3055
+ * Prefer `Scrawl` unless you need to construct or own the controller's
3056
+ * lifecycle yourself (e.g. you create it outside React, or need it before
3057
+ * first render). Set `disposeOnUnmount` to have this provider call
3058
+ * `controller.dispose()` on unmount; otherwise disposal remains your own
3059
+ * responsibility.
3060
+ */
1960
3061
  declare function ScrawlProvider({ controller, children, preset, theme, portalContainer: customPortal, disposeOnUnmount, onThemeDiagnostic, className, style }: ScrawlProviderProps): react.JSX.Element;
1961
3062
  interface ScrawlProps extends Omit<CreateBoardControllerOptions, "canvas"> {
1962
3063
  children?: ReactNode;
@@ -1977,6 +3078,17 @@ interface ScrawlProps extends Omit<CreateBoardControllerOptions, "canvas"> {
1977
3078
  */
1978
3079
  icons?: Partial<Record<BuiltInTool, ReactNode>>;
1979
3080
  }
3081
+ /**
3082
+ * The fastest path to an embedded Board: creates and owns a
3083
+ * `BoardController` for you (constructed once, disposed on unmount) and
3084
+ * renders it into a canvas. Render with no `children` to get the SDK's
3085
+ * default toolbar/UI chrome, or supply your own `children` (using the
3086
+ * `useScrawlController`/`useScrawlSnapshot` hooks, or the exported
3087
+ * `DefaultBoardChrome`/`StyleShelf`/etc. pieces) to build a custom UI on
3088
+ * top of the same controller. Accepts every `CreateBoardControllerOptions`
3089
+ * field except `canvas` (pass `canvas={null}` for a headless board, or an
3090
+ * existing `<canvas>` element to control its identity yourself).
3091
+ */
1980
3092
  declare function Scrawl({ children, preset, theme, portalContainer, className, style, onReady, onError, onThemeDiagnostic, canvas: suppliedCanvas, icons, ...options }: ScrawlProps): react.JSX.Element;
1981
3093
  interface ScrawlCanvasProps {
1982
3094
  element?: HTMLCanvasElement;
@@ -2002,6 +3114,38 @@ declare function ScrawlPortal({ children }: {
2002
3114
  declare function useScrawlController(): BoardController;
2003
3115
  declare function useScrawlTheme(): ScrawlResolvedTheme;
2004
3116
  declare function useScrawlSnapshot(): BoardSnapshot;
3117
+ /**
3118
+ * A thin, purely-derived convenience over
3119
+ * `useScrawlSnapshot().connection.persistence` (Phase 5) — for a Host
3120
+ * component that only cares about save status (e.g. a
3121
+ * "Saving…"/"Saved"/"Offline" indicator) and would otherwise re-derive
3122
+ * this same field access itself. Adds no state and no behavior of its
3123
+ * own: React still owns none of persistence, exactly as before — this
3124
+ * hook only re-reads what the controller already tracks.
3125
+ *
3126
+ * Deliberately does *not* go through `useScrawlSnapshot()` (Phase 9):
3127
+ * `changed()` rebuilds the whole `BoardSnapshot` — including a fresh
3128
+ * `connection.persistence` object — on every document mutation, even ones
3129
+ * that never touch persistence at all, so a component using only this
3130
+ * hook would otherwise re-render on every stroke draw. `useSyncStatus`
3131
+ * below memoizes on the value (`state`/`error`), not just the object
3132
+ * reference, and returns the same cached value across renders where
3133
+ * nothing relevant changed — the same shape `usePresence` already uses
3134
+ * for its own independently-scoped store.
3135
+ */
3136
+ declare function usePersistenceStatus(): PersistenceSnapshot;
3137
+ /** 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). */
3138
+ declare function useCollaborationStatus(): CollaborationSnapshot;
3139
+ /**
3140
+ * The current presence roster (Phase 6), live-updating — wraps
3141
+ * `controller.presence.list()`/`subscribe` the same way `useScrawlSnapshot`
3142
+ * wraps the controller's main snapshot, via `useSyncExternalStore`. Does
3143
+ * not re-render on every remote pointer move by itself; it re-renders
3144
+ * whenever the roster the controller already tracks changes, at whatever
3145
+ * rate that arrives at (throttled adapter-side, per `presenceThrottleMs`).
3146
+ */
3147
+ declare function usePresence(): readonly PresenceUser[];
3148
+ /** @deprecated Use `Scrawl` instead — this predates the full capability-grouped `BoardController` and only exposes the stroke-only `LocalBoard` surface. */
2005
3149
  type ScrawlBoardProps = {
2006
3150
  documentId: string;
2007
3151
  initialDocument?: SerializedBoardDocument;
@@ -2009,7 +3153,8 @@ type ScrawlBoardProps = {
2009
3153
  className?: string;
2010
3154
  style?: CSSProperties;
2011
3155
  };
3156
+ /** @deprecated Use `Scrawl` instead — this predates the full capability-grouped `BoardController` and only exposes the stroke-only `LocalBoard` surface. */
2012
3157
  declare function ScrawlBoard({ documentId, initialDocument, onReady, className, style }: ScrawlBoardProps): react.JSX.Element;
2013
3158
 
2014
- export { ASSET_CACHE_BYTES_DEFAULT, ASSET_CACHE_BYTES_MAX, ASSET_CACHE_BYTES_MIN, ASSET_EXPORT_MAX_DECODED_MEGAPIXELS, ASSET_EXPORT_MAX_ENCODED_BYTES, ASSET_MAX_CONCURRENT_RESOLUTIONS, ASSET_MAX_DECODED_MEGAPIXELS, ASSET_MAX_DIMENSION_PX, ASSET_MAX_ENCODED_BYTES, ASSET_REF_MAX_BYTES, ASSET_REF_PATTERN, AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, AssetResolutionError, BEACON_INSET, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, DefaultBoardChrome, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, FocusedItemToolbar, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, InlineEditors, LockItemsCommand, MIN_WIDTH_FACTOR, MultiplayerCursors, NOTE_COLORS, NOTE_DEFAULT_SIZE, NOTE_DEFAULT_Z, NOTE_MAX_Z, NOTE_MIN_Z, NOTE_PEEL_STEP, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, STAMPS, STAMP_SIZE, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, SpatialIndex, StyleShelf, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, UpdateImageCommand, UpdateNoteCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, assetRef, avgScale, canUnlockItem, changeToOps, clampAssetCacheBytes, cloneCustomObject, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, createBoardController, createLocalBoard, documentId, documentToSVG, formatTimer, invert, isAssetRef, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, resolveScrawlTheme, ribbonEdges, rotationAbout, scalingAbout, scrawlThemePresets, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
2015
- export type { ApplyOpsResult, AssetDiagnostic, AssetIngestRequest, AssetIngestResult, AssetIngestor, AssetKind, AssetPurpose, AssetRef, AssetResolutionErrorCode, AssetResolveRequest, AssetResolveResult, AssetResolver, BBox, BoardController, BoardControllerError, BoardEventMap, BoardKeyInput, BoardObject, BoardObjectInput, BoardObjectPatch, BoardPoint, BoardPointerInput, BoardScene, BoardSlotProps, BoardSnapshot, BoardStroke, BoardStyle, BoardThemeOptions, BoardView, BuiltInTool, ClusterIdFactory, CollaborationAdapter, CollaborationReceiver, CollaborationSession, CollaborationSnapshot, CollaboratorIdentity, Command, CommandKind, CommentMarker, ControllerOp, CreateBoardControllerOptions, CurrentSerializedDocument, CurrentSerializedStroke, CustomBoardObject, CustomObjectAddInput, CustomObjectDefinition, CustomTool, CustomToolDefinition, DeepReadonly, DefaultBoardChromeProps, DefaultUIRegion, DefaultUISlot, DefaultUISlots, DialogSlotProps, DocumentChange, DocumentContext, DocumentId, DocumentLoadResult, DocumentRecoveryCode, ExtensionCommand, ExtensionDiagnostic, ExtensionHitResult, ExtensionId, ExtensionRequirement, FocusedItem, FocusedItemToolbarProps, ImageBlock, InlineEditorsProps, InputModifiers, JsonObject, JsonValue, KitchenTimer, LoadResult, LocalBoard, LocalBoardOptions, LocalBoardSnapshot, LockHolder, LockTarget, Lockable, Mat2x3, MultiplayerCursorsProps, NoteVote, ObjectDescribeContext, ObjectIntent, ObjectType, Op, OpCollection, PersistenceAdapter, PersistenceDiagnostic, PersistenceSnapshot, PresenceCursor, PresencePlacement, PresenceUser, PresenceView, QueryableBoardObject, ReadonlyBoardDocument, ReadonlyCustomObject, ReadonlyDocumentChange, RibbonEdgePoint, SceneEllipse, SceneGroup, SceneImage, ScenePath, SceneRect, SceneText, ScrawlBoardProps, ScrawlCanvasProps, ScrawlDefaultUIProps, ScrawlDensity, ScrawlExtension, ScrawlGridMode, ScrawlProps, ScrawlProviderProps, ScrawlResolvedTheme, ScrawlSurfaceTexture, ScrawlTheme, ScrawlThemeDiagnostic, ScrawlThemePreset, ScreenPoint, ScreenRect, SearchHit, SearchHitKind, SearchableBoard, SearchableComment, SerializedBoardDocument, SerializedBoardStroke, SerializedPoint, SerializedStroke, StampKind, StickyNote, Stroke, StrokeId, StrokePoint, StrokeTool, StyleShelfProps, SupportedAssetMediaType, TableBlock, TextBlock, ToolCancelReason, ToolCapabilities, ToolCursor, ToolId, ViewportInset };
3159
+ export { ASSET_CACHE_BYTES_DEFAULT, ASSET_CACHE_BYTES_MAX, ASSET_CACHE_BYTES_MIN, ASSET_EXPORT_MAX_DECODED_MEGAPIXELS, ASSET_EXPORT_MAX_ENCODED_BYTES, ASSET_MAX_CONCURRENT_RESOLUTIONS, ASSET_MAX_DECODED_MEGAPIXELS, ASSET_MAX_DIMENSION_PX, ASSET_MAX_ENCODED_BYTES, ASSET_REF_MAX_BYTES, ASSET_REF_PATTERN, AddArrowCommand, AddEllipseCommand, AddGroupCommand, AddHeartCommand, AddImageCommand, AddLineCommand, AddNoteCommand, AddPolygonCommand, AddRectangleCommand, AddStarCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, AssetResolutionError, BEACON_INSET, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, CommandBatch, DefaultBoardChrome, DeleteArrowCommand, DeleteEllipseCommand, DeleteGroupCommand, DeleteHeartCommand, DeleteImageCommand, DeleteLineCommand, DeleteNoteCommand, DeletePolygonCommand, DeleteRectangleCommand, DeleteStarCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, FocusedItemToolbar, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, InlineEditors, LockItemsCommand, MIN_WIDTH_FACTOR, MultiplayerCursors, NOTE_COLORS, NOTE_DEFAULT_SIZE, NOTE_DEFAULT_Z, NOTE_MAX_Z, NOTE_MIN_Z, NOTE_PEEL_STEP, ReorderObjectCommand, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, SHAPE_DEFAULT_STROKE, SHAPE_DEFAULT_STROKE_WIDTH, SHAPE_MIN_SIZE, SHAPE_STYLE_DEFAULTS, STAMPS, STAMP_SIZE, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, SpatialIndex, StyleShelf, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, TransformObjectsCommand, UpdateArrowCommand, UpdateEllipseCommand, UpdateGroupCommand, UpdateHeartCommand, UpdateImageCommand, UpdateLineCommand, UpdateNoteCommand, UpdatePolygonCommand, UpdateRectangleCommand, UpdateStarCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, assetRef, avgScale, canUnlockItem, changeToOps, clampAssetCacheBytes, cloneArrow, cloneCustomObject, cloneEllipse, cloneGroup, cloneHeart, cloneImage, cloneLine, cloneNote, clonePolygon, cloneRectangle, cloneStar, cloneStroke, cloneTable, cloneText, cloneTimer, createBoardController, createLocalBoard, createMemoryPersistence, documentId, documentSize, documentToSVG, formatTimer, invert, isAssetRef, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, resolveScrawlTheme, ribbonEdges, rotationAbout, scalingAbout, scrawlThemePresets, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation, useCollaborationStatus, usePersistenceStatus, usePresence, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
3160
+ export type { ApplyOpsResult, ArrowHeadStyle, ArrowObject, AssetDiagnostic, AssetExportFailure, AssetIngestRequest, AssetIngestResult, AssetIngestor, AssetKind, AssetPurpose, AssetRef, AssetResolutionErrorCode, AssetResolveRequest, AssetResolveResult, AssetResolver, BBox, BoardController, BoardControllerError, BoardEventMap, BoardKeyInput, BoardObject, BoardObjectInput, BoardObjectPatch, BoardPoint, BoardPointerInput, BoardScene, BoardSlotProps, BoardSnapshot, BoardStroke, BoardStyle, BoardThemeOptions, BoardView, BuiltInTool, ClusterIdFactory, CollaborationAckDiagnostic, CollaborationAdapter, CollaborationReceiver, CollaborationSession, CollaborationSnapshot, CollaborationSyncResult, CollaboratorIdentity, Command, CommandKind, CommentMarker, ControllerOp, CreateBoardControllerOptions, CreateMemoryPersistenceOptions, CurrentSerializedDocument, CurrentSerializedStroke, CustomBoardObject, CustomObjectAddInput, CustomObjectDefinition, CustomTool, CustomToolDefinition, DeepReadonly, DefaultBoardChromeProps, DefaultUIRegion, DefaultUISlot, DefaultUISlots, DialogSlotProps, DocumentChange, DocumentContext, DocumentId, DocumentLoadResult, DocumentRecoveryCode, EllipseObject, ExportDocumentSVGOptions, ExportDocumentSVGResult, ExtensionCommand, ExtensionDiagnostic, ExtensionHitResult, ExtensionId, ExtensionRequirement, FocusedItem, FocusedItemToolbarProps, GroupObject, HeartObject, ImageBlock, InlineEditorsProps, InputModifiers, JsonObject, JsonValue, KitchenTimer, LineObject, LoadResult, LocalBoard, LocalBoardOptions, LocalBoardSnapshot, LocalPresence, LockHolder, LockTarget, Lockable, Mat2x3, MultiplayerCursorsProps, NoteVote, ObjectDescribeContext, ObjectIntent, ObjectType, Op, OpCollection, PersistenceAdapter, PersistenceDiagnostic, PersistenceSnapshot, PolygonObject, PresenceCursor, PresencePlacement, PresenceUser, PresenceView, QueryableBoardObject, ReadonlyBoardDocument, ReadonlyCustomObject, ReadonlyDocumentChange, RectangleObject, ReorderDirection, ReplaceResult, RibbonEdgePoint, SceneEllipse, SceneGroup, SceneImage, ScenePath, SceneRect, SceneText, ScrawlBoardProps, ScrawlCanvasProps, ScrawlDefaultUIProps, ScrawlDensity, ScrawlExtension, ScrawlGridMode, ScrawlProps, ScrawlProviderProps, ScrawlResolvedTheme, ScrawlSurfaceTexture, ScrawlTheme, ScrawlThemeDiagnostic, ScrawlThemePreset, ScreenPoint, ScreenRect, SearchHit, SearchHitKind, SearchableBoard, SearchableComment, SerializedBoardDocument, SerializedBoardStroke, SerializedPoint, SerializedStroke, StampKind, StarObject, StickyNote, Stroke, StrokeId, StrokePoint, StrokeTool, StyleShelfProps, SupportedAssetMediaType, TableBlock, TextBlock, ToolCancelReason, ToolCapabilities, ToolCursor, ToolId, ViewportInset };