@scrawl-board/board 0.1.0-beta.0 → 0.1.0-beta.10

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/browser.d.ts CHANGED
@@ -1,10 +1,8 @@
1
- declare const strokeIdBrand: unique symbol;
2
- type StrokeId = string & {
3
- readonly [strokeIdBrand]: "StrokeId";
4
- };
5
-
6
1
  /** Wire grammar: `asset:<namespace>:<opaque-id>`. Interpreted only by the Host. */
7
2
  type AssetRef = string;
3
+ declare function isAssetRef(value: unknown): value is AssetRef;
4
+ /** Throws on malformed input; use `isAssetRef` where a boolean is wanted instead. */
5
+ declare function assetRef(value: string): AssetRef;
8
6
  type AssetKind = "image";
9
7
  type AssetPurpose = "render" | "thumbnail" | "export";
10
8
  interface AssetResolveRequest {
@@ -46,6 +44,12 @@ interface AssetIngestor {
46
44
  ingest(request: AssetIngestRequest): Promise<AssetIngestResult>;
47
45
  }
48
46
  type AssetResolutionErrorCode = "resolver-unavailable" | "not-found" | "forbidden" | "offline" | "unsupported-type" | "too-large" | "invalid-content" | "decode-failed" | "budget-exceeded" | "aborted" | "unknown";
47
+ declare class AssetResolutionError extends Error {
48
+ readonly code: AssetResolutionErrorCode;
49
+ readonly retryable: boolean;
50
+ readonly ref?: AssetRef | undefined;
51
+ constructor(code: AssetResolutionErrorCode, retryable: boolean, message: string, ref?: AssetRef | undefined);
52
+ }
49
53
  /** Runtime event for a resolution/ingestion failure — never carries credentials or a fetchable location. */
50
54
  interface AssetDiagnostic {
51
55
  code: AssetResolutionErrorCode;
@@ -54,6 +58,9 @@ interface AssetDiagnostic {
54
58
  objectKind: "image" | "custom";
55
59
  retryable: boolean;
56
60
  }
61
+ declare const SUPPORTED_ASSET_MEDIA_TYPES: readonly ["image/png", "image/jpeg", "image/webp"];
62
+ type SupportedAssetMediaType = (typeof SUPPORTED_ASSET_MEDIA_TYPES)[number];
63
+ declare function clampAssetCacheBytes(value: number | undefined): number;
57
64
 
58
65
  type Mat2x3 = [number, number, number, number, number, number];
59
66
 
@@ -101,6 +108,7 @@ interface CustomBoardObject {
101
108
  };
102
109
  props: JsonValue;
103
110
  }
111
+ declare function cloneCustomObject(object: CustomBoardObject): CustomBoardObject;
104
112
  /**
105
113
  * The read-only view handed to `describe`. Deep-readonly by construction
106
114
  * (not derived via a shallow `Readonly<>`) because `describe` must treat its
@@ -139,6 +147,21 @@ interface CustomObjectDefinition<Props extends JsonValue = JsonValue> {
139
147
  /** One pure, synchronous step per consecutive schema version. */
140
148
  migrate?: Readonly<Record<number, (oldProps: JsonValue) => JsonValue>>;
141
149
  describe(object: ReadonlyCustomObject<Props>, context: ObjectDescribeContext): BoardScene;
150
+ /**
151
+ * Optional point-level hit-test precision (Phase 8). Every custom object
152
+ * hit-tests against its bounding box (`fallback.bounds`) by default — this
153
+ * lets a non-rectangular shape (e.g. a circular card, an L-shaped region)
154
+ * reject a point that's inside that box but outside its actual visible
155
+ * silhouette, tightening a click/marquee/raycast hit to the shape's real
156
+ * outline. `point` is in this object's own local space — the same
157
+ * untransformed space `describe`'s returned geometry already lives in
158
+ * (the caller inverse-transforms the pointer's board point through
159
+ * `object.transform` before calling this). Absent means every point
160
+ * inside the bounding box hits, matching pre-Phase-8 behavior exactly.
161
+ * Rejecting a point here does not fall through to whatever's underneath —
162
+ * the gesture simply misses this object, same as clicking empty space.
163
+ */
164
+ hitTest?(object: ReadonlyCustomObject<Props>, point: BoardPoint): boolean;
142
165
  }
143
166
  interface SceneNodeBase {
144
167
  key: string;
@@ -172,6 +195,15 @@ interface SceneGroup extends SceneNodeBase {
172
195
  kind: "group";
173
196
  children: readonly BoardScene[];
174
197
  }
198
+ /**
199
+ * **No renderer or SVG-export interpreter exists for this node kind yet**
200
+ * (tracked as deferred work — see `renderer/shapes/customObjects.ts`'s
201
+ * `"path"` case). Returning a `ScenePath` from `describe()` renders nothing,
202
+ * exports nothing, and contributes no hit-test bounds — it neither errors
203
+ * nor emits a diagnostic. Until an interpreter ships, build custom shapes
204
+ * from `SceneRect`/`SceneEllipse`/`SceneGroup`/`SceneText`/`SceneImage`
205
+ * instead.
206
+ */
175
207
  interface ScenePath extends SceneNodeBase {
176
208
  kind: "path";
177
209
  /** SVG-style path data, board-local coordinates. */
@@ -327,8 +359,23 @@ interface Lockable {
327
359
  lockedByName?: string;
328
360
  }
329
361
 
362
+ /**
363
+ * Per-object visibility (Phase 8) — mirrors `itemLock.ts`'s `Lockable`
364
+ * pattern exactly, but simpler: unlike a lock, hidden state carries no
365
+ * holder/ownership concept, so there's no analogue to `LockHolder`/
366
+ * `canUnlockItem`. A hidden object stays fully present in the Document
367
+ * (still serializes, persists, syncs, undoes/redoes) — it just skips
368
+ * rendering and hit-testing/selection candidacy. `hidden` absent or
369
+ * `false` means visible; this keeps every pre-Phase-8 document (which has
370
+ * no `hidden` field on any object at all) implicitly fully visible with
371
+ * zero migration needed.
372
+ */
373
+ interface Hideable {
374
+ hidden?: boolean;
375
+ }
376
+
330
377
  /** A kitchen timer sitting on the board. Remaining time is derived, not ticked. */
331
- interface KitchenTimer extends Lockable {
378
+ interface KitchenTimer extends Lockable, Hideable {
332
379
  id: string;
333
380
  x: number;
334
381
  y: number;
@@ -342,6 +389,146 @@ interface KitchenTimer extends Lockable {
342
389
  runningSince?: number;
343
390
  }
344
391
 
392
+ interface RectangleObject extends Lockable, Hideable {
393
+ id: string;
394
+ x: number;
395
+ y: number;
396
+ width: number;
397
+ height: number;
398
+ fill?: string;
399
+ stroke?: string;
400
+ strokeWidth?: number;
401
+ /** Corner radius in board units; clamped to at most half the shorter side at render time. */
402
+ cornerRadius?: number;
403
+ /** `[0, 1]`; undefined means fully opaque (Phase 4). */
404
+ opacity?: number;
405
+ /**
406
+ * Radians, about the shape's own center `(x + width/2, y - height/2)`.
407
+ * Undefined means 0 (Phase 3). `x`/`y`/`width`/`height` stay in the
408
+ * shape's own unrotated local frame — rotation is a separate, applied-last
409
+ * transform, not baked into them, matching how Stroke/CustomBoardObject
410
+ * keep geometry and placement independent via their own `matrix`.
411
+ */
412
+ rotation?: number;
413
+ }
414
+ interface EllipseObject extends Lockable, Hideable {
415
+ id: string;
416
+ x: number;
417
+ y: number;
418
+ width: number;
419
+ height: number;
420
+ fill?: string;
421
+ stroke?: string;
422
+ strokeWidth?: number;
423
+ /** `[0, 1]`; undefined means fully opaque (Phase 4). */
424
+ opacity?: number;
425
+ /** Radians, about the shape's own center — see RectangleObject's `rotation` doc. */
426
+ rotation?: number;
427
+ }
428
+ /** `"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. */
429
+ type ArrowHeadStyle = "triangle" | "none";
430
+ interface LineObject extends Lockable, Hideable {
431
+ id: string;
432
+ start: BoardPoint;
433
+ end: BoardPoint;
434
+ stroke?: string;
435
+ strokeWidth?: number;
436
+ opacity?: number;
437
+ }
438
+ /** An endpoint anchored to a normalized position on another object's bounds. */
439
+ interface ConnectorBinding {
440
+ objectId: string;
441
+ /** Horizontal and vertical fractions of the target's axis-aligned bounds. */
442
+ x: number;
443
+ y: number;
444
+ }
445
+ interface ArrowObject extends Lockable, Hideable {
446
+ /** Omitted for legacy arrows. Connectors share Arrow's persistence/history contract. */
447
+ routing?: "straight" | "curved" | "polyline";
448
+ /** Intermediate board-space vertices for a multi-point connector. */
449
+ waypoints?: BoardPoint[];
450
+ startBinding?: ConnectorBinding;
451
+ endBinding?: ConnectorBinding;
452
+ id: string;
453
+ start: BoardPoint;
454
+ end: BoardPoint;
455
+ head?: ArrowHeadStyle;
456
+ stroke?: string;
457
+ strokeWidth?: number;
458
+ opacity?: number;
459
+ }
460
+ /**
461
+ * Triangle(3)/Diamond(4)/Pentagon(5)/Hexagon(6)/Octagon(8) as one shared
462
+ * type instead of five near-duplicate interfaces — a regular N-gon
463
+ * inscribed in the same `x`/`y`/`width`/`height`/`rotation` bounding box
464
+ * Rectangle already uses, parameterized by `sides`. Diamond is exactly a
465
+ * 4-sided regular polygon with vertex 0 pointing right (not up, like
466
+ * Triangle/Pentagon/Hexagon) — see `polygonGeometry.ts`'s
467
+ * `polygonStartAngle`, which encodes each side count's own vertex
468
+ * orientation so the outline always matches the legacy drag-preview shape.
469
+ */
470
+ interface PolygonObject extends Lockable, Hideable {
471
+ id: string;
472
+ x: number;
473
+ y: number;
474
+ width: number;
475
+ height: number;
476
+ sides: 3 | 4 | 5 | 6 | 8;
477
+ fill?: string;
478
+ stroke?: string;
479
+ strokeWidth?: number;
480
+ opacity?: number;
481
+ /** Radians, about the shape's own center — see RectangleObject's `rotation` doc. */
482
+ rotation?: number;
483
+ }
484
+ /** 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`). */
485
+ interface StarObject extends Lockable, Hideable {
486
+ id: string;
487
+ x: number;
488
+ y: number;
489
+ width: number;
490
+ height: number;
491
+ /** Vertex count; today's only shipped preset is 5, matching the legacy tool. */
492
+ points: number;
493
+ /** `(0, 1)` — inner vertex radius as a fraction of the outer radius. */
494
+ innerRadiusRatio: number;
495
+ fill?: string;
496
+ stroke?: string;
497
+ strokeWidth?: number;
498
+ opacity?: number;
499
+ rotation?: number;
500
+ }
501
+ /** 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. */
502
+ interface HeartObject extends Lockable, Hideable {
503
+ id: string;
504
+ x: number;
505
+ y: number;
506
+ width: number;
507
+ height: number;
508
+ fill?: string;
509
+ stroke?: string;
510
+ strokeWidth?: number;
511
+ opacity?: number;
512
+ rotation?: number;
513
+ }
514
+ /**
515
+ * A logical grouping of other board objects (Phase 3 — Selection,
516
+ * Transformation & Grouping). Deliberately has no `x`/`y`/`transform` of its
517
+ * own — a group's bounds are always derived on demand from its (recursively
518
+ * resolved) children, and "moving/rotating/scaling the group" is exactly a
519
+ * multi-object transform applied to those children, nothing more. A group
520
+ * has no renderer/mesh of its own; its only visual presence is the
521
+ * selection gizmo's bounding box while it's the current selection.
522
+ *
523
+ * `children` may itself contain other group ids (nested groups) — expanding
524
+ * a group into its leaf members is always done by the caller (recursively,
525
+ * with cycle protection), never assumed here.
526
+ */
527
+ interface GroupObject extends Lockable, Hideable {
528
+ id: string;
529
+ children: string[];
530
+ }
531
+
345
532
  interface BoardPoint {
346
533
  x: number;
347
534
  y: number;
@@ -355,9 +542,14 @@ interface StrokePoint extends BoardPoint {
355
542
  */
356
543
  erase?: number;
357
544
  }
358
- /** Which drawing tool made a stroke; undefined means marker (back-compat). */
359
- type StrokeTool = "marker" | "highlighter";
360
- interface Stroke extends Lockable {
545
+ /**
546
+ * Which drawing tool made a stroke; undefined means marker (back-compat).
547
+ * `"shape"` (rect/ellipse/line/arrow/polygon/star/heart) renders at its
548
+ * exact configured width with no pressure variance or end taper — a
549
+ * geometric outline, not an expressive ink mark.
550
+ */
551
+ type StrokeTool = "marker" | "highlighter" | "shape";
552
+ interface Stroke extends Lockable, Hideable {
361
553
  id: string;
362
554
  color: string;
363
555
  baseWidth: number;
@@ -373,7 +565,7 @@ interface Stroke extends Lockable {
373
565
  clusterId?: string;
374
566
  }
375
567
  type SerializedPoint = [number, number, number, number];
376
- interface SerializedStroke extends Lockable {
568
+ interface SerializedStroke extends Lockable, Hideable {
377
569
  id: string;
378
570
  color: string;
379
571
  baseWidth: number;
@@ -383,7 +575,36 @@ interface SerializedStroke extends Lockable {
383
575
  matrix?: [number, number, number, number, number, number];
384
576
  clusterId?: string;
385
577
  }
578
+ /** A named notebook containing sections. */
579
+ interface BoardNotebook {
580
+ readonly id: string;
581
+ readonly name: string;
582
+ readonly color: string;
583
+ }
584
+ /** A named section containing pages. */
585
+ interface BoardSection {
586
+ readonly id: string;
587
+ readonly notebookId: string;
588
+ readonly name: string;
589
+ readonly color: string;
590
+ }
591
+ /** An additional page. The first page remains in the document's top-level collections. */
592
+ interface SerializedBoardPage {
593
+ id: string;
594
+ sectionId?: string;
595
+ name: string;
596
+ content: SerializedDocument;
597
+ }
386
598
  interface SerializedDocument {
599
+ /** Notebook and section metadata. Older boards use an implicit default of each. */
600
+ notebooks?: BoardNotebook[];
601
+ sections?: BoardSection[];
602
+ /** Section of the original first page; defaults to `default`. */
603
+ pageSectionId?: string;
604
+ /** Display name of the first page (id `default`). */
605
+ pageName?: string;
606
+ /** Additional pages, in sidebar order. Nested pages are not allowed. */
607
+ pages?: SerializedBoardPage[];
387
608
  /** Absent in every historical document; current saves always write 1. */
388
609
  schemaVersion?: 1;
389
610
  strokes: SerializedStroke[];
@@ -399,6 +620,31 @@ interface SerializedDocument {
399
620
  timers?: KitchenTimer[];
400
621
  /** Absent in documents saved before Custom board objects existed (ticket #22). */
401
622
  customObjects?: CustomBoardObject[];
623
+ /** Absent in documents saved before semantic Rectangle objects existed (Phase 2). */
624
+ rectangles?: RectangleObject[];
625
+ /** Absent in documents saved before semantic Ellipse objects existed (Phase 2). */
626
+ ellipses?: EllipseObject[];
627
+ /** Absent in documents saved before Groups existed (Phase 3). */
628
+ groups?: GroupObject[];
629
+ /** Absent in documents saved before semantic Line objects existed (Phase 4). */
630
+ lines?: LineObject[];
631
+ /** Absent in documents saved before semantic Arrow objects existed (Phase 4). */
632
+ arrows?: ArrowObject[];
633
+ /** Absent in documents saved before semantic Polygon objects existed (Phase 4). */
634
+ polygons?: PolygonObject[];
635
+ /** Absent in documents saved before semantic Star objects existed (Phase 4). */
636
+ stars?: StarObject[];
637
+ /** Absent in documents saved before semantic Heart objects existed (Phase 4). */
638
+ hearts?: HeartObject[];
639
+ /**
640
+ * Every content-object id (every type above except comments, which are
641
+ * host-synced and never enter this schema) in paint order, back to front.
642
+ * Absent in documents saved before per-object z-order existed (Phase 3) —
643
+ * migration synthesizes a default order preserving the old fixed-Z-band
644
+ * visual stacking exactly, so an existing document never visibly changes
645
+ * on load; only an explicit reorder action touches this from then on.
646
+ */
647
+ objectOrder?: string[];
402
648
  }
403
649
  /**
404
650
  * One collaborator's vote on a note. One per person; toggling removes it.
@@ -412,7 +658,7 @@ interface NoteVote {
412
658
  * A sticky note: content floating above the board at a z-offset (pillar 3 —
413
659
  * depth as an organizational axis). Center position in board space.
414
660
  */
415
- interface StickyNote extends Lockable {
661
+ interface StickyNote extends Lockable, Hideable {
416
662
  id: string;
417
663
  x: number;
418
664
  y: number;
@@ -430,7 +676,7 @@ interface StickyNote extends Lockable {
430
676
  * top-left corner; lines flow downward (-y). Text joins the clustering
431
677
  * system like handwriting (build prompt §6.4).
432
678
  */
433
- interface TextBlock extends Lockable {
679
+ interface TextBlock extends Lockable, Hideable {
434
680
  id: string;
435
681
  x: number;
436
682
  y: number;
@@ -444,7 +690,7 @@ interface TextBlock extends Lockable {
444
690
  * Interactive structured table on the board. Position (x, y) is top-left in board units.
445
691
  * Cells are indexed as `${row},${col}` keys mapping to cell text content.
446
692
  */
447
- interface TableBlock extends Lockable {
693
+ interface TableBlock extends Lockable, Hideable {
448
694
  id: string;
449
695
  x: number;
450
696
  y: number;
@@ -461,7 +707,7 @@ interface TableBlock extends Lockable {
461
707
  * An imported image block on the board plane.
462
708
  * Coordinates (x, y) represent the center of the image in board space.
463
709
  */
464
- interface ImageBlock extends Lockable {
710
+ interface ImageBlock extends Lockable, Hideable {
465
711
  id: string;
466
712
  /**
467
713
  * A legacy, read-only data URL (or, historically, an arbitrary string) —
@@ -484,13 +730,23 @@ interface ImageBlock extends Lockable {
484
730
  stamp?: string;
485
731
  }
486
732
 
733
+ declare const strokeIdBrand: unique symbol;
734
+ type StrokeId = string & {
735
+ readonly [strokeIdBrand]: "StrokeId";
736
+ };
737
+
487
738
  declare const CURRENT_DOCUMENT_SCHEMA_VERSION: 1;
488
739
  type CurrentSerializedStroke = Omit<SerializedStroke, "id"> & {
489
740
  id: StrokeId;
490
741
  };
491
- interface CurrentSerializedDocument extends Required<SerializedDocument> {
742
+ interface CurrentSerializedDocument extends Required<Omit<SerializedDocument, "pages" | "pageName" | "notebooks" | "sections" | "pageSectionId">> {
492
743
  schemaVersion: typeof CURRENT_DOCUMENT_SCHEMA_VERSION;
493
744
  strokes: CurrentSerializedStroke[];
745
+ pageName?: string;
746
+ notebooks?: BoardNotebook[];
747
+ sections?: BoardSection[];
748
+ pageSectionId?: string;
749
+ pages?: SerializedBoardPage[];
494
750
  }
495
751
 
496
752
  interface SearchableComment {
@@ -523,6 +779,532 @@ declare const STAMPS: {
523
779
  declare function stampDataUrl(kind: StampKind): string;
524
780
  declare function isStampKind(value: unknown): value is StampKind;
525
781
 
782
+ interface IconDefinition {
783
+ /** Stable, set-prefixed id (`lucide:rocket`), also what an ImageBlock stores in its `stamp` field. */
784
+ readonly name: string;
785
+ readonly label: string;
786
+ readonly category: string;
787
+ /** Inner SVG markup for a 24x24 viewBox; strokes use `currentColor`. */
788
+ readonly body: string;
789
+ }
790
+ declare const ICON_LIBRARY: readonly [{
791
+ readonly name: "lucide:check";
792
+ readonly label: "Check";
793
+ readonly category: "Status";
794
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M20 6L9 17l-5-5\"/>";
795
+ }, {
796
+ readonly name: "lucide:x";
797
+ readonly label: "Cross";
798
+ readonly category: "Status";
799
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/>";
800
+ }, {
801
+ readonly name: "lucide:star";
802
+ readonly label: "Star";
803
+ readonly category: "Status";
804
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.12 2.12 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.12 2.12 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.12 2.12 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.12 2.12 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.12 2.12 0 0 0 1.597-1.16z\"/>";
805
+ }, {
806
+ readonly name: "lucide:heart";
807
+ readonly label: "Heart";
808
+ readonly category: "Status";
809
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M2 9.5a5.5 5.5 0 0 1 9.591-3.676a.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5\"/>";
810
+ }, {
811
+ readonly name: "lucide:flag";
812
+ readonly label: "Flag";
813
+ readonly category: "Status";
814
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528\"/>";
815
+ }, {
816
+ readonly name: "lucide:bookmark";
817
+ readonly label: "Bookmark";
818
+ readonly category: "Status";
819
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z\"/>";
820
+ }, {
821
+ readonly name: "lucide:bell";
822
+ readonly label: "Bell";
823
+ readonly category: "Status";
824
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10.268 21a2 2 0 0 0 3.464 0m-10.47-5.674A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326\"/>";
825
+ }, {
826
+ readonly name: "lucide:info";
827
+ readonly label: "Info";
828
+ readonly category: "Status";
829
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4m0-4h.01\"/></g>";
830
+ }, {
831
+ readonly name: "lucide:circle-alert";
832
+ readonly label: "Alert";
833
+ readonly category: "Status";
834
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 8v4m0 4h.01\"/></g>";
835
+ }, {
836
+ readonly name: "lucide:circle-question-mark";
837
+ readonly label: "Question";
838
+ readonly category: "Status";
839
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3m.08 4h.01\"/></g>";
840
+ }, {
841
+ readonly name: "lucide:thumbs-up";
842
+ readonly label: "Thumbs up";
843
+ readonly category: "Status";
844
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 5.88L14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88M7 10v12\"/>";
845
+ }, {
846
+ readonly name: "lucide:thumbs-down";
847
+ readonly label: "Thumbs down";
848
+ readonly category: "Status";
849
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 18.12L10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88M17 14V2\"/>";
850
+ }, {
851
+ readonly name: "lucide:arrow-up";
852
+ readonly label: "Arrow up";
853
+ readonly category: "Arrows";
854
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m5 12l7-7l7 7m-7 7V5\"/>";
855
+ }, {
856
+ readonly name: "lucide:arrow-down";
857
+ readonly label: "Arrow down";
858
+ readonly category: "Arrows";
859
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 5v14m7-7l-7 7l-7-7\"/>";
860
+ }, {
861
+ readonly name: "lucide:arrow-left";
862
+ readonly label: "Arrow left";
863
+ readonly category: "Arrows";
864
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m12 19l-7-7l7-7m7 7H5\"/>";
865
+ }, {
866
+ readonly name: "lucide:arrow-right";
867
+ readonly label: "Arrow right";
868
+ readonly category: "Arrows";
869
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14m-7-7l7 7l-7 7\"/>";
870
+ }, {
871
+ readonly name: "lucide:arrow-up-right";
872
+ readonly label: "Arrow up-right";
873
+ readonly category: "Arrows";
874
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M7 7h10v10M7 17L17 7\"/>";
875
+ }, {
876
+ readonly name: "lucide:refresh-cw";
877
+ readonly label: "Refresh";
878
+ readonly category: "Arrows";
879
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M3 12a9 9 0 0 1 9-9a9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5m5 4a9 9 0 0 1-9 9a9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/></g>";
880
+ }, {
881
+ readonly name: "lucide:move";
882
+ readonly label: "Move";
883
+ readonly category: "Arrows";
884
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 2v20m3-3l-3 3l-3-3M19 9l3 3l-3 3M2 12h20M5 9l-3 3l3 3M9 5l3-3l3 3\"/>";
885
+ }, {
886
+ readonly name: "lucide:shuffle";
887
+ readonly label: "Shuffle";
888
+ readonly category: "Arrows";
889
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"m18 14l4 4l-4 4m0-20l4 4l-4 4\"/><path d=\"M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22M2 6h1.972a4 4 0 0 1 3.6 2.2M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45\"/></g>";
890
+ }, {
891
+ readonly name: "lucide:user";
892
+ readonly label: "User";
893
+ readonly category: "People";
894
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/></g>";
895
+ }, {
896
+ readonly name: "lucide:users";
897
+ readonly label: "Users";
898
+ readonly category: "People";
899
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M16 3.128a4 4 0 0 1 0 7.744M22 21v-2a4 4 0 0 0-3-3.87\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/></g>";
900
+ }, {
901
+ readonly name: "lucide:briefcase";
902
+ readonly label: "Briefcase";
903
+ readonly category: "People";
904
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16\"/><rect width=\"20\" height=\"14\" x=\"2\" y=\"6\" rx=\"2\"/></g>";
905
+ }, {
906
+ readonly name: "lucide:building";
907
+ readonly label: "Building";
908
+ readonly category: "People";
909
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M12 10h.01M12 14h.01M12 6h.01M16 10h.01M16 14h.01M16 6h.01M8 10h.01M8 14h.01M8 6h.01M9 22v-3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3\"/><rect width=\"16\" height=\"20\" x=\"4\" y=\"2\" rx=\"2\"/></g>";
910
+ }, {
911
+ readonly name: "lucide:calendar";
912
+ readonly label: "Calendar";
913
+ readonly category: "People";
914
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M8 2v3m8-3v3\"/><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M3 9h18\"/></g>";
915
+ }, {
916
+ readonly name: "lucide:clock";
917
+ readonly label: "Clock";
918
+ readonly category: "People";
919
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 6v6l4 2\"/></g>";
920
+ }, {
921
+ readonly name: "lucide:mail";
922
+ readonly label: "Mail";
923
+ readonly category: "People";
924
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"m22 7l-8.991 5.727a2 2 0 0 1-2.009 0L2 7\"/><rect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"/></g>";
925
+ }, {
926
+ readonly name: "lucide:message-circle";
927
+ readonly label: "Message";
928
+ readonly category: "People";
929
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092a10 10 0 1 0-4.777-4.719\"/>";
930
+ }, {
931
+ readonly name: "lucide:file";
932
+ readonly label: "File";
933
+ readonly category: "Objects";
934
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\"/><path d=\"M14 2v5a1 1 0 0 0 1 1h5\"/></g>";
935
+ }, {
936
+ readonly name: "lucide:folder";
937
+ readonly label: "Folder";
938
+ readonly category: "Objects";
939
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"/>";
940
+ }, {
941
+ readonly name: "lucide:image";
942
+ readonly label: "Image";
943
+ readonly category: "Objects";
944
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\"/><circle cx=\"9\" cy=\"9\" r=\"2\"/><path d=\"m21 15l-3.086-3.086a2 2 0 0 0-2.828 0L6 21\"/></g>";
945
+ }, {
946
+ readonly name: "lucide:link";
947
+ readonly label: "Link";
948
+ readonly category: "Objects";
949
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></g>";
950
+ }, {
951
+ readonly name: "lucide:paperclip";
952
+ readonly label: "Paperclip";
953
+ readonly category: "Objects";
954
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m16 6l-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551\"/>";
955
+ }, {
956
+ readonly name: "lucide:pin";
957
+ readonly label: "Pin";
958
+ readonly category: "Objects";
959
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 17v5M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4a1 1 0 0 1 1 1z\"/>";
960
+ }, {
961
+ readonly name: "lucide:map-pin";
962
+ readonly label: "Map pin";
963
+ readonly category: "Objects";
964
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0\"/><circle cx=\"12\" cy=\"10\" r=\"3\"/></g>";
965
+ }, {
966
+ readonly name: "lucide:search";
967
+ readonly label: "Search";
968
+ readonly category: "Objects";
969
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"m21 21l-4.34-4.34\"/><circle cx=\"11\" cy=\"11\" r=\"8\"/></g>";
970
+ }, {
971
+ readonly name: "lucide:settings";
972
+ readonly label: "Settings";
973
+ readonly category: "Objects";
974
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M9.671 4.136a2.34 2.34 0 0 1 4.659 0a2.34 2.34 0 0 0 3.319 1.915a2.34 2.34 0 0 1 2.33 4.033a2.34 2.34 0 0 0 0 3.831a2.34 2.34 0 0 1-2.33 4.033a2.34 2.34 0 0 0-3.319 1.915a2.34 2.34 0 0 1-4.659 0a2.34 2.34 0 0 0-3.32-1.915a2.34 2.34 0 0 1-2.33-4.033a2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></g>";
975
+ }, {
976
+ readonly name: "lucide:trash-2";
977
+ readonly label: "Trash";
978
+ readonly category: "Objects";
979
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 11v6m4-6v6m5-11v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"/>";
980
+ }, {
981
+ readonly name: "lucide:lock";
982
+ readonly label: "Lock";
983
+ readonly category: "Objects";
984
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><rect width=\"18\" height=\"11\" x=\"3\" y=\"11\" rx=\"2\" ry=\"2\"/><path d=\"M7 11V7a5 5 0 0 1 10 0v4\"/></g>";
985
+ }, {
986
+ readonly name: "lucide:key";
987
+ readonly label: "Key";
988
+ readonly category: "Objects";
989
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"m15.5 7.5l2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4m2-2l-9.6 9.6\"/><circle cx=\"7.5\" cy=\"15.5\" r=\"5.5\"/></g>";
990
+ }, {
991
+ readonly name: "lucide:code";
992
+ readonly label: "Code";
993
+ readonly category: "Tech";
994
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m16 18l6-6l-6-6M8 6l-6 6l6 6\"/>";
995
+ }, {
996
+ readonly name: "lucide:database";
997
+ readonly label: "Database";
998
+ readonly category: "Tech";
999
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><ellipse cx=\"12\" cy=\"5\" rx=\"9\" ry=\"3\"/><path d=\"M3 5v14a9 3 0 0 0 18 0V5\"/><path d=\"M3 12a9 3 0 0 0 18 0\"/></g>";
1000
+ }, {
1001
+ readonly name: "lucide:server";
1002
+ readonly label: "Server";
1003
+ readonly category: "Tech";
1004
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><rect width=\"20\" height=\"8\" x=\"2\" y=\"2\" rx=\"2\" ry=\"2\"/><rect width=\"20\" height=\"8\" x=\"2\" y=\"14\" rx=\"2\" ry=\"2\"/><path d=\"M6 6h.01M6 18h.01\"/></g>";
1005
+ }, {
1006
+ readonly name: "lucide:cloud";
1007
+ readonly label: "Cloud";
1008
+ readonly category: "Tech";
1009
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9\"/>";
1010
+ }, {
1011
+ readonly name: "lucide:git-branch";
1012
+ readonly label: "Branch";
1013
+ readonly category: "Tech";
1014
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M15 6a9 9 0 0 0-9 9V3\"/><circle cx=\"18\" cy=\"6\" r=\"3\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/></g>";
1015
+ }, {
1016
+ readonly name: "lucide:terminal";
1017
+ readonly label: "Terminal";
1018
+ readonly category: "Tech";
1019
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 19h8M4 17l6-6l-6-6\"/>";
1020
+ }, {
1021
+ readonly name: "lucide:cpu";
1022
+ readonly label: "CPU";
1023
+ readonly category: "Tech";
1024
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M12 20v2m0-20v2m5 16v2m0-20v2M2 12h2m-2 5h2M2 7h2m16 5h2m-2 5h2M20 7h2M7 20v2M7 2v2\"/><rect width=\"16\" height=\"16\" x=\"4\" y=\"4\" rx=\"2\"/><rect width=\"8\" height=\"8\" x=\"8\" y=\"8\" rx=\"1\"/></g>";
1025
+ }, {
1026
+ readonly name: "lucide:bug";
1027
+ readonly label: "Bug";
1028
+ readonly category: "Tech";
1029
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M12 20v-9m2-4a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4zm.12-3.12L16 2\"/><path d=\"M21 21a4 4 0 0 0-3.81-4M21 5a4 4 0 0 1-3.55 3.97M22 13h-4M3 21a4 4 0 0 1 3.81-4M3 5a4 4 0 0 0 3.55 3.97M6 13H2M8 2l1.88 1.88M9 7.13V6a3 3 0 1 1 6 0v1.13\"/></g>";
1030
+ }, {
1031
+ readonly name: "lucide:lightbulb";
1032
+ readonly label: "Idea";
1033
+ readonly category: "Ideas";
1034
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 14c.2-1 .7-1.7 1.5-2.5c1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5c.7.7 1.3 1.5 1.5 2.5m0 4h6m-5 4h4\"/>";
1035
+ }, {
1036
+ readonly name: "lucide:zap";
1037
+ readonly label: "Zap";
1038
+ readonly category: "Ideas";
1039
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15.914 4a1.5 1.5 0 0 0-2.474-1.561l-9 9A1.5 1.5 0 0 0 5.5 14h4.002a.5.5 0 0 1 .471.666L8.086 20a1.5 1.5 0 0 0 2.475 1.56l9-9A1.5 1.5 0 0 0 18.5 10h-3.997a.5.5 0 0 1-.472-.667z\"/>";
1040
+ }, {
1041
+ readonly name: "lucide:target";
1042
+ readonly label: "Target";
1043
+ readonly category: "Ideas";
1044
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></g>";
1045
+ }, {
1046
+ readonly name: "lucide:rocket";
1047
+ readonly label: "Rocket";
1048
+ readonly category: "Ideas";
1049
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09\"/><path d=\"M9 12a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.4 22.4 0 0 1-4 2z\"/><path d=\"M9 12H4s.55-3.03 2-4c1.62-1.08 5 .05 5 .05\"/></g>";
1050
+ }, {
1051
+ readonly name: "lucide:trophy";
1052
+ readonly label: "Trophy";
1053
+ readonly category: "Ideas";
1054
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M10 14.66V17a1 1 0 0 1-1 1a2 2 0 0 0-2 2v2m7-7.34V17a1 1 0 0 0 1 1a2 2 0 0 1 2 2v2m.916-12H19.5A2.5 2.5 0 0 0 22 7.5V5a1 1 0 0 0-1-1h-3M4 22h16\"/><path d=\"M6 9a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1z\"/><path d=\"M6.084 10H4.5A2.5 2.5 0 0 1 2 7.5V5a1 1 0 0 1 1-1h3\"/></g>";
1055
+ }, {
1056
+ readonly name: "lucide:gift";
1057
+ readonly label: "Gift";
1058
+ readonly category: "Ideas";
1059
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M12 7v14m8-10v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8m3.5-4a1 1 0 0 1 0-5A4.8 8 0 0 1 12 7a4.8 8 0 0 1 4.5-5a1 1 0 0 1 0 5\"/><rect width=\"18\" height=\"4\" x=\"3\" y=\"7\" rx=\"1\"/></g>";
1060
+ }, {
1061
+ readonly name: "lucide:coffee";
1062
+ readonly label: "Coffee";
1063
+ readonly category: "Ideas";
1064
+ readonly body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 2v2m4-2v2m2 4a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1M6 2v2\"/>";
1065
+ }, {
1066
+ readonly name: "lucide:smile";
1067
+ readonly label: "Smile";
1068
+ readonly category: "Ideas";
1069
+ readonly body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M8 14s1.5 2 4 2s4-2 4-2M9 9h.01M15 9h.01\"/></g>";
1070
+ }];
1071
+ /** Every vendored icon's id, as a literal union. */
1072
+ type IconName = (typeof ICON_LIBRARY)[number]["name"];
1073
+
1074
+ /**
1075
+ * Emoji stickers. Unlike the icon library there's no artwork to vendor: each
1076
+ * is a Unicode character, drawn by the platform's own colour-emoji font when
1077
+ * its SVG is rasterised — so an emoji looks native to each viewer's OS
1078
+ * (Apple, Segoe, Noto), the same way it would in a chat app.
1079
+ */
1080
+ interface EmojiDefinition {
1081
+ /** Stable, set-prefixed id (`emoji:fire`), also what an ImageBlock stores in its `stamp` field. */
1082
+ readonly name: string;
1083
+ readonly label: string;
1084
+ readonly category: string;
1085
+ readonly char: string;
1086
+ }
1087
+ declare const EMOJI_LIBRARY: readonly [{
1088
+ readonly name: "emoji:grinning";
1089
+ readonly label: "Grinning face";
1090
+ readonly category: "Smileys";
1091
+ readonly char: "😀";
1092
+ }, {
1093
+ readonly name: "emoji:joy";
1094
+ readonly label: "Tears of joy";
1095
+ readonly category: "Smileys";
1096
+ readonly char: "😂";
1097
+ }, {
1098
+ readonly name: "emoji:smiling";
1099
+ readonly label: "Smiling face";
1100
+ readonly category: "Smileys";
1101
+ readonly char: "😊";
1102
+ }, {
1103
+ readonly name: "emoji:heart-eyes";
1104
+ readonly label: "Heart eyes";
1105
+ readonly category: "Smileys";
1106
+ readonly char: "😍";
1107
+ }, {
1108
+ readonly name: "emoji:thinking";
1109
+ readonly label: "Thinking face";
1110
+ readonly category: "Smileys";
1111
+ readonly char: "🤔";
1112
+ }, {
1113
+ readonly name: "emoji:sunglasses";
1114
+ readonly label: "Sunglasses";
1115
+ readonly category: "Smileys";
1116
+ readonly char: "😎";
1117
+ }, {
1118
+ readonly name: "emoji:crying";
1119
+ readonly label: "Crying face";
1120
+ readonly category: "Smileys";
1121
+ readonly char: "😢";
1122
+ }, {
1123
+ readonly name: "emoji:angry";
1124
+ readonly label: "Angry face";
1125
+ readonly category: "Smileys";
1126
+ readonly char: "😡";
1127
+ }, {
1128
+ readonly name: "emoji:sleeping";
1129
+ readonly label: "Sleeping face";
1130
+ readonly category: "Smileys";
1131
+ readonly char: "😴";
1132
+ }, {
1133
+ readonly name: "emoji:mind-blown";
1134
+ readonly label: "Mind blown";
1135
+ readonly category: "Smileys";
1136
+ readonly char: "🤯";
1137
+ }, {
1138
+ readonly name: "emoji:grimacing";
1139
+ readonly label: "Grimacing face";
1140
+ readonly category: "Smileys";
1141
+ readonly char: "😬";
1142
+ }, {
1143
+ readonly name: "emoji:partying";
1144
+ readonly label: "Partying face";
1145
+ readonly category: "Smileys";
1146
+ readonly char: "🥳";
1147
+ }, {
1148
+ readonly name: "emoji:thumbs-up";
1149
+ readonly label: "Thumbs up emoji";
1150
+ readonly category: "Gestures";
1151
+ readonly char: "👍";
1152
+ }, {
1153
+ readonly name: "emoji:thumbs-down";
1154
+ readonly label: "Thumbs down emoji";
1155
+ readonly category: "Gestures";
1156
+ readonly char: "👎";
1157
+ }, {
1158
+ readonly name: "emoji:clap";
1159
+ readonly label: "Clapping hands";
1160
+ readonly category: "Gestures";
1161
+ readonly char: "👏";
1162
+ }, {
1163
+ readonly name: "emoji:raised-hands";
1164
+ readonly label: "Raised hands";
1165
+ readonly category: "Gestures";
1166
+ readonly char: "🙌";
1167
+ }, {
1168
+ readonly name: "emoji:folded-hands";
1169
+ readonly label: "Folded hands";
1170
+ readonly category: "Gestures";
1171
+ readonly char: "🙏";
1172
+ }, {
1173
+ readonly name: "emoji:eyes";
1174
+ readonly label: "Eyes";
1175
+ readonly category: "Gestures";
1176
+ readonly char: "👀";
1177
+ }, {
1178
+ readonly name: "emoji:muscle";
1179
+ readonly label: "Flexed biceps";
1180
+ readonly category: "Gestures";
1181
+ readonly char: "💪";
1182
+ }, {
1183
+ readonly name: "emoji:wave";
1184
+ readonly label: "Waving hand";
1185
+ readonly category: "Gestures";
1186
+ readonly char: "👋";
1187
+ }, {
1188
+ readonly name: "emoji:red-heart";
1189
+ readonly label: "Red heart";
1190
+ readonly category: "Symbols";
1191
+ readonly char: "❤️";
1192
+ }, {
1193
+ readonly name: "emoji:fire";
1194
+ readonly label: "Flame";
1195
+ readonly category: "Symbols";
1196
+ readonly char: "🔥";
1197
+ }, {
1198
+ readonly name: "emoji:star";
1199
+ readonly label: "Medium star";
1200
+ readonly category: "Symbols";
1201
+ readonly char: "⭐";
1202
+ }, {
1203
+ readonly name: "emoji:check";
1204
+ readonly label: "Check mark button";
1205
+ readonly category: "Symbols";
1206
+ readonly char: "✅";
1207
+ }, {
1208
+ readonly name: "emoji:cross";
1209
+ readonly label: "Cross mark";
1210
+ readonly category: "Symbols";
1211
+ readonly char: "❌";
1212
+ }, {
1213
+ readonly name: "emoji:warning";
1214
+ readonly label: "Warning sign";
1215
+ readonly category: "Symbols";
1216
+ readonly char: "⚠️";
1217
+ }, {
1218
+ readonly name: "emoji:question";
1219
+ readonly label: "Red question mark";
1220
+ readonly category: "Symbols";
1221
+ readonly char: "❓";
1222
+ }, {
1223
+ readonly name: "emoji:hundred";
1224
+ readonly label: "Hundred points";
1225
+ readonly category: "Symbols";
1226
+ readonly char: "💯";
1227
+ }, {
1228
+ readonly name: "emoji:light-bulb";
1229
+ readonly label: "Light bulb";
1230
+ readonly category: "Symbols";
1231
+ readonly char: "💡";
1232
+ }, {
1233
+ readonly name: "emoji:bullseye";
1234
+ readonly label: "Bullseye";
1235
+ readonly category: "Symbols";
1236
+ readonly char: "🎯";
1237
+ }, {
1238
+ readonly name: "emoji:party-popper";
1239
+ readonly label: "Party popper";
1240
+ readonly category: "Objects";
1241
+ readonly char: "🎉";
1242
+ }, {
1243
+ readonly name: "emoji:rocket";
1244
+ readonly label: "Rocket emoji";
1245
+ readonly category: "Objects";
1246
+ readonly char: "🚀";
1247
+ }, {
1248
+ readonly name: "emoji:trophy";
1249
+ readonly label: "Trophy emoji";
1250
+ readonly category: "Objects";
1251
+ readonly char: "🏆";
1252
+ }, {
1253
+ readonly name: "emoji:pushpin";
1254
+ readonly label: "Pushpin";
1255
+ readonly category: "Objects";
1256
+ readonly char: "📌";
1257
+ }, {
1258
+ readonly name: "emoji:memo";
1259
+ readonly label: "Memo";
1260
+ readonly category: "Objects";
1261
+ readonly char: "📝";
1262
+ }, {
1263
+ readonly name: "emoji:calendar";
1264
+ readonly label: "Tear-off calendar";
1265
+ readonly category: "Objects";
1266
+ readonly char: "📅";
1267
+ }, {
1268
+ readonly name: "emoji:alarm-clock";
1269
+ readonly label: "Alarm clock";
1270
+ readonly category: "Objects";
1271
+ readonly char: "⏰";
1272
+ }, {
1273
+ readonly name: "emoji:speech-balloon";
1274
+ readonly label: "Speech balloon";
1275
+ readonly category: "Objects";
1276
+ readonly char: "💬";
1277
+ }];
1278
+ type EmojiName = (typeof EMOJI_LIBRARY)[number]["name"];
1279
+ declare function isEmojiName(value: unknown): value is EmojiName;
1280
+ /** An emoji as a standalone SVG data URL, sized to rasterise sharply (see icons.ts). */
1281
+ declare function emojiDataUrl(name: string): string;
1282
+
1283
+ /**
1284
+ * What the stamp tool will drop next: one of the six built-in stamp-pad
1285
+ * stickers, an emoji, or any vendored icon. All land on the board the same way
1286
+ * — as an image whose `stamp` field records which one it was (see `placeStamp`).
1287
+ */
1288
+ type StickerKind = StampKind | EmojiName | IconName;
1289
+ declare function isIconName(value: unknown): value is IconName;
1290
+ /** The vendored icons grouped for a picker, in the order they were vendored. */
1291
+ declare function iconCategories(): {
1292
+ category: string;
1293
+ icons: readonly IconDefinition[];
1294
+ }[];
1295
+ /**
1296
+ * An icon as a standalone SVG data URL, drawn in `color`.
1297
+ *
1298
+ * The library's art strokes in `currentColor`, which has no meaning once the
1299
+ * markup is detached into a data URL — an `<img>`/texture has no inherited
1300
+ * colour to resolve it against — so the colour is substituted in rather than
1301
+ * inherited.
1302
+ */
1303
+ declare function iconDataUrl(name: string, color: string): string;
1304
+ declare function isStickerKind(value: unknown): value is StickerKind;
1305
+ /** Human-readable name for any sticker kind — what a placed sticker is called and searched by. */
1306
+ declare function stickerLabel(kind: StickerKind): string;
1307
+
526
1308
  type SerializedBoardDocument = CurrentSerializedDocument;
527
1309
 
528
1310
  interface AssetExportFailure {
@@ -563,16 +1345,30 @@ interface BoardStyle {
563
1345
  readonly noteColor: string;
564
1346
  readonly tableRows: number;
565
1347
  readonly tableCols: number;
566
- readonly stampKind: StampKind;
1348
+ /** A stamp-pad kind or any vendored icon name (see `ICON_LIBRARY`). */
1349
+ readonly stampKind: StickerKind;
567
1350
  readonly timerDurationMs: number;
568
1351
  }
1352
+ /** A page listed in the Board navigator. `default` is the original first page. */
1353
+ interface BoardPage {
1354
+ readonly id: string;
1355
+ readonly name: string;
1356
+ readonly sectionId: string;
1357
+ }
569
1358
  interface BoardSnapshot {
1359
+ readonly notebooks: readonly BoardNotebook[];
1360
+ readonly sections: readonly BoardSection[];
1361
+ readonly activeNotebookId: string;
1362
+ readonly activeSectionId: string;
1363
+ readonly pages: readonly BoardPage[];
1364
+ readonly activePageId: string;
570
1365
  readonly status: "loading" | "ready" | "disposed";
571
1366
  readonly documentId: string;
572
1367
  readonly tool: BuiltInTool | (string & {});
573
1368
  readonly zoom: number;
574
1369
  readonly readOnly: boolean;
575
1370
  readonly selection: readonly string[];
1371
+ readonly focusedItem: FocusedItem | null;
576
1372
  readonly strokeCount: number;
577
1373
  readonly objectCount: number;
578
1374
  readonly canUndo: boolean;
@@ -583,6 +1379,27 @@ interface BoardSnapshot {
583
1379
  readonly collaboration: CollaborationSnapshot;
584
1380
  };
585
1381
  }
1382
+ /**
1383
+ * `selection` collapsed into the one thing a selected-object toolbar needs:
1384
+ * what's selected, its lock state, and where to anchor above it. `null`
1385
+ * when nothing is selected, or when the selection mixes types/objects that
1386
+ * don't resolve to a single focus (anything but one object, or several
1387
+ * strokes sharing a `clusterId` — a multi-stroke shape).
1388
+ *
1389
+ * Raw lock fields, not a derived `canUnlock` — this API has no notion of
1390
+ * "the local user" to judge that against (see `canUnlockItem` in `../core`,
1391
+ * which takes a `userId` the Host already owns). A Custom object's lock
1392
+ * shape doesn't carry a holder name, so it always reports `locked: false`
1393
+ * here, matching the engine's own internal selection-badge behavior.
1394
+ */
1395
+ interface FocusedItem {
1396
+ readonly type: "stroke" | "note" | "text" | "table" | "image" | "timer" | "rectangle" | "ellipse" | "group" | "line" | "arrow" | "polygon" | "star" | "heart" | "custom";
1397
+ readonly id: string;
1398
+ readonly locked: boolean;
1399
+ readonly lockedBy?: string;
1400
+ readonly lockedByName?: string;
1401
+ readonly screenPosition?: ScreenPoint;
1402
+ }
586
1403
  interface BoardControllerError {
587
1404
  source: "controller" | "renderer" | "persistence" | "collaboration";
588
1405
  code: string;
@@ -591,6 +1408,9 @@ interface BoardControllerError {
591
1408
  }
592
1409
  interface BoardEventMap {
593
1410
  change: BoardSnapshot;
1411
+ "page-changing": {
1412
+ pageId: string;
1413
+ };
594
1414
  "tool-change": {
595
1415
  tool: string;
596
1416
  };
@@ -657,6 +1477,8 @@ interface BoardEventMap {
657
1477
  "asset-diagnostic": AssetDiagnostic;
658
1478
  /** A batch of Ops was reconciled (not applied as-sent) by the persistence adapter (ticket #24). */
659
1479
  "persistence-diagnostic": PersistenceDiagnostic;
1480
+ /** 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. */
1481
+ "collaboration-ops-acknowledged": CollaborationAckDiagnostic;
660
1482
  audit: unknown;
661
1483
  error: BoardControllerError;
662
1484
  disposed: undefined;
@@ -676,6 +1498,27 @@ interface BoardView {
676
1498
  y: number;
677
1499
  zoom: number;
678
1500
  }
1501
+ /**
1502
+ * The rendered board surface's color, reference grid, and shape stroke
1503
+ * width — the subset of {@link ScrawlTheme} that reaches the rendering
1504
+ * engine directly (everything else is UI-chrome-only, applied as CSS). Any
1505
+ * field left unset keeps its current value.
1506
+ */
1507
+ interface BoardThemeOptions {
1508
+ /** The board/canvas background color — distinct from UI chrome panels. */
1509
+ surface?: string;
1510
+ /** `"flat"` (default): a plain, uniform board surface. `"textured"`: a subtle "melamine" micro-noise + satin sheen, like a physical whiteboard. */
1511
+ surfaceTexture?: "flat" | "textured";
1512
+ /** `"none"` (default) keeps the board a plain surface; `"line"`/`"dot"` draw a zoom-adaptive reference grid. */
1513
+ gridMode?: "none" | "line" | "dot";
1514
+ gridColor?: string;
1515
+ /** Grid spacing in board units at 100% zoom. Ignored when `gridMode` is `"none"`. */
1516
+ gridSpacing?: number;
1517
+ /** On-screen grid line/dot width in CSS pixels — stays this width at any zoom, since the grid is a reference aid, not content. */
1518
+ gridLineWidth?: number;
1519
+ /** Shape (rectangle, ellipse, arrow, ...) border width, in board units — scales with zoom like ink, since it's part of the drawn content. */
1520
+ shapeStrokeWidth?: number;
1521
+ }
679
1522
  /**
680
1523
  * A Host-owned comment, summarized for Board-side search and marker
681
1524
  * rendering. Comments are not Document content — they carry no undo
@@ -699,9 +1542,12 @@ interface PresenceView {
699
1542
  readonly height: number;
700
1543
  }
701
1544
  /**
702
- * A Host-owned collaborator, synced in for cursor/roster rendering only.
703
- * Presence is ephemeral — it never touches the Document, Ops, undo/redo,
704
- * or persistence — so this is a read/query capability, not an adapter.
1545
+ * A collaborator, synced in for cursor/roster rendering only. Presence is
1546
+ * ephemeral — it never touches the Document, Ops, undo/redo, or persistence
1547
+ * (ADR 0006/0007). Two ways a roster gets populated (`presence.sync`
1548
+ * directly, or a `CollaborationAdapter`'s optional presence channel —
1549
+ * Phase 6, ADR 0015) both feed the exact same read/query capability below;
1550
+ * a Host picks one, not both, for a given controller.
705
1551
  */
706
1552
  interface PresenceUser {
707
1553
  readonly id: string;
@@ -710,6 +1556,14 @@ interface PresenceUser {
710
1556
  readonly tool?: string;
711
1557
  readonly cursor?: PresenceCursor;
712
1558
  readonly view?: PresenceView;
1559
+ /** Host-supplied extras (avatar URL, role, etc.) — opaque to Scrawl, never interpreted. */
1560
+ readonly metadata?: Record<string, unknown>;
1561
+ }
1562
+ /** This client's own local presence, published via `presence.broadcast()` (Phase 6). */
1563
+ interface LocalPresence {
1564
+ readonly cursor?: PresenceCursor | null;
1565
+ readonly view?: PresenceView | null;
1566
+ readonly tool?: string;
713
1567
  }
714
1568
  /**
715
1569
  * The Custom arm wraps `CustomBoardObject` under the same `type` discriminant
@@ -737,6 +1591,22 @@ type BoardObject = ({
737
1591
  } & ImageBlock) | ({
738
1592
  type: "timer";
739
1593
  } & KitchenTimer) | ({
1594
+ type: "rectangle";
1595
+ } & RectangleObject) | ({
1596
+ type: "ellipse";
1597
+ } & EllipseObject) | ({
1598
+ type: "group";
1599
+ } & GroupObject) | ({
1600
+ type: "line";
1601
+ } & LineObject) | ({
1602
+ type: "arrow";
1603
+ } & ArrowObject) | ({
1604
+ type: "polygon";
1605
+ } & PolygonObject) | ({
1606
+ type: "star";
1607
+ } & StarObject) | ({
1608
+ type: "heart";
1609
+ } & HeartObject) | ({
740
1610
  type: "custom";
741
1611
  customType: ObjectType;
742
1612
  } & Omit<CustomBoardObject, "type">);
@@ -765,6 +1635,30 @@ type BoardObjectInput = {
765
1635
  type: "timer";
766
1636
  id?: string;
767
1637
  } & Omit<KitchenTimer, "id">) | ({
1638
+ type: "rectangle";
1639
+ id?: string;
1640
+ } & Omit<RectangleObject, "id">) | ({
1641
+ type: "ellipse";
1642
+ id?: string;
1643
+ } & Omit<EllipseObject, "id">) | ({
1644
+ type: "group";
1645
+ id?: string;
1646
+ } & Omit<GroupObject, "id">) | ({
1647
+ type: "line";
1648
+ id?: string;
1649
+ } & Omit<LineObject, "id">) | ({
1650
+ type: "arrow";
1651
+ id?: string;
1652
+ } & Omit<ArrowObject, "id">) | ({
1653
+ type: "polygon";
1654
+ id?: string;
1655
+ } & Omit<PolygonObject, "id">) | ({
1656
+ type: "star";
1657
+ id?: string;
1658
+ } & Omit<StarObject, "id">) | ({
1659
+ type: "heart";
1660
+ id?: string;
1661
+ } & Omit<HeartObject, "id">) | ({
768
1662
  type: "custom";
769
1663
  id?: string;
770
1664
  customType: ObjectType;
@@ -795,10 +1689,34 @@ type LoadResult = {
795
1689
  } | {
796
1690
  state: "missing";
797
1691
  };
1692
+ /**
1693
+ * Result of a whole-document `PersistenceAdapter.replace()` call (ADR 0006:
1694
+ * "Whole-document writes survive only for create, clear-board and import,
1695
+ * where replacing everything is the actual intent"). Revision-gated, unlike
1696
+ * `applyOps` — `conflict` means `baseRevision` was stale (someone else's
1697
+ * write landed first); the caller must reload and never overwrites blind.
1698
+ */
1699
+ type ReplaceResult = {
1700
+ state: "applied";
1701
+ revision: string;
1702
+ } | {
1703
+ state: "conflict";
1704
+ currentRevision: string;
1705
+ };
1706
+ /**
1707
+ * The one sanctioned seam for persisting a Board's Document to a Host's own
1708
+ * storage — implement this against a database, an HTTP API, IndexedDB
1709
+ * (see `@scrawl-board/board/local`'s `createIndexedDBPersistence`), or
1710
+ * anything else. `load()` fetches the current state on connect; `applyOps()`
1711
+ * streams incremental Ops as edits happen; `replace()` is only for
1712
+ * whole-document writes (create, clear-board, import — see ADR 0006) and is
1713
+ * revision-gated so a stale write never silently clobbers a newer one.
1714
+ * Passed via `createBoardController({ adapters: { persistence } })`.
1715
+ */
798
1716
  interface PersistenceAdapter {
799
1717
  load(context: DocumentContext): Promise<LoadResult>;
800
1718
  applyOps(context: DocumentContext, ops: readonly ControllerOp[]): Promise<ApplyOpsResult>;
801
- replace(context: DocumentContext, document: CurrentSerializedDocument, baseRevision: string): Promise<unknown>;
1719
+ replace(context: DocumentContext, document: CurrentSerializedDocument, baseRevision: string): Promise<ReplaceResult>;
802
1720
  }
803
1721
  /**
804
1722
  * `"reconcile"` (ticket #24) means the server authoritatively resolved the
@@ -822,14 +1740,61 @@ interface PersistenceDiagnostic {
822
1740
  rejectedOpIds: readonly string[];
823
1741
  revision: string;
824
1742
  }
1743
+ /** Emitted as `"collaboration-ops-acknowledged"` (Phase 7) — the server has confirmed receipt of these op ids on the live pipe. */
1744
+ interface CollaborationAckDiagnostic {
1745
+ opIds: readonly string[];
1746
+ }
825
1747
  interface ControllerOp {
826
1748
  id: string;
827
1749
  schemaVersion: 1;
828
1750
  kind: "upsert" | "restore" | "remove";
829
- objectType: "stroke" | "note" | "text" | "table" | "image" | "timer" | "custom";
1751
+ objectType: "stroke" | "note" | "text" | "table" | "image" | "timer" | "rectangle" | "ellipse" | "group" | "line" | "arrow" | "polygon" | "star" | "heart" | "custom"
1752
+ /**
1753
+ * A whole-document paint-order sync (Phase 3), not a per-object type —
1754
+ * `objectId` is always the fixed sentinel `"order"` and `payload` is
1755
+ * `{ order: string[] }`. The only `objectType` with no matching
1756
+ * `BoardObject`/document collection; kept in this same union (rather
1757
+ * than a separate wire message) so it flows through the existing
1758
+ * `PersistenceAdapter`/`CollaborationAdapter` opaquely, unchanged.
1759
+ */
1760
+ | "order" | "page" | "notebook" | "section";
1761
+ /** Absent for the original page; scopes content and order Ops to an additional page. */
1762
+ pageId?: string;
830
1763
  objectId: string;
831
1764
  payload?: unknown;
1765
+ /**
1766
+ * This op's position in its own originating client's local sequence
1767
+ * (Phase 7) — 1, 2, 3, ... per controller instance, distinct from `id`
1768
+ * (an opaque, globally-unique identifier used for dedup/ack, not
1769
+ * ordering) and from a server's own authoritative ordering (e.g.
1770
+ * `referenceCollaborationServer.ts`'s per-room `version` counter).
1771
+ * Present on every op this SDK originates locally; a remote peer's op
1772
+ * carries whatever its own origin set, unchanged — never renumbered in
1773
+ * transit. Absent on an op minted by decoding the legacy wire envelope
1774
+ * (`scrawlOpEnvelope.ts`), which predates this field and has no
1775
+ * per-client sequence concept of its own.
1776
+ */
1777
+ clientSequence?: number;
1778
+ /**
1779
+ * The `CollaboratorIdentity.id` of this op's originating client (Phase
1780
+ * 7) — set for every op this SDK originates locally when `identity` is
1781
+ * configured, omitted entirely otherwise (never sent as `undefined`).
1782
+ * The explicit foundation for a future per-author undo filter (a local
1783
+ * user's own undo should only ever touch their own ops) — no undo-stack
1784
+ * behavior itself changes this phase.
1785
+ */
1786
+ clientId?: string;
832
1787
  }
1788
+ /**
1789
+ * The one sanctioned seam for real-time multiplayer — implement this against
1790
+ * a Host's own collaboration backend (WebSocket relay, CRDT server, etc.).
1791
+ * `connect()` is called once per controller with the local user's
1792
+ * `identity` and a `receive` callback the adapter invokes with incoming
1793
+ * Ops, presence updates, acks, and connection status; it resolves with a
1794
+ * `CollaborationSession` the controller uses to send local Ops and presence
1795
+ * back out. Passed via `createBoardController({ adapters: { collaboration } })`;
1796
+ * omit it entirely to run single-player.
1797
+ */
833
1798
  interface CollaborationAdapter {
834
1799
  connect(options: DocumentContext & {
835
1800
  identity: CollaboratorIdentity;
@@ -840,14 +1805,72 @@ interface CollaboratorIdentity {
840
1805
  id: string;
841
1806
  name: string;
842
1807
  color?: string;
1808
+ /** Host-supplied extras (avatar URL, role, etc.) — opaque to Scrawl, forwarded into any resulting `PresenceUser` unread and never interpreted. */
1809
+ metadata?: Record<string, unknown>;
843
1810
  }
844
1811
  interface CollaborationReceiver {
845
1812
  ops(ops: readonly ControllerOp[]): void;
1813
+ /**
1814
+ * The current presence roster (Phase 6, ADR 0015) — always a full
1815
+ * replacement, never a delta, matching `presence.sync`'s existing
1816
+ * semantics exactly (an adapter that aggregates wire deltas into a full
1817
+ * roster before calling this is the adapter's own job, not the
1818
+ * controller's). Required on this interface (not optional) because a
1819
+ * Host only ever *consumes* `CollaborationReceiver` — never implements
1820
+ * it — so adding a required method here cannot break an existing custom
1821
+ * `CollaborationAdapter`. An adapter with no presence support simply
1822
+ * never calls it.
1823
+ */
1824
+ presence(users: readonly PresenceUser[]): void;
1825
+ /**
1826
+ * The server has confirmed receipt of these op ids (Phase 7) —
1827
+ * distinguishes "sent" from "server accepted," which `sendOps` alone
1828
+ * (fire-and-forget) cannot. Required for the same reason `presence` is:
1829
+ * Hosts only ever consume this interface, never implement it, so this
1830
+ * cannot break an existing custom `CollaborationAdapter`. An adapter with
1831
+ * no ack support simply never calls it — the collaboration pipe still
1832
+ * works exactly as it did before this existed, just without the
1833
+ * bookkeeping/observability this enables.
1834
+ */
1835
+ acknowledged(opIds: readonly string[]): void;
846
1836
  status(state: "online" | "reconnecting" | "offline"): void;
847
1837
  error(cause: unknown): void;
848
1838
  }
1839
+ /**
1840
+ * Result of `CollaborationSession.requestSync()` (Phase 7). `"ops"` means
1841
+ * the adapter's own live-pipe cache fully covered the gap since the
1842
+ * caller's last known revision — apply `ops` and the client is caught up,
1843
+ * no persistence reload needed. `"unavailable"` means it couldn't (gap too
1844
+ * large, server restarted, or the adapter has no retained history at all)
1845
+ * — the caller must fall back to a persistence-backed reload. This is a
1846
+ * best-effort *liveness* cache, deliberately never a durable source of
1847
+ * truth (ADR 0006's "collaboration is never a second source of document
1848
+ * truth" — see ADR 0015's own extension of that principle to presence,
1849
+ * now extended once more, the same way, to this).
1850
+ */
1851
+ type CollaborationSyncResult = {
1852
+ state: "ops";
1853
+ ops: readonly ControllerOp[];
1854
+ serverRevision: string;
1855
+ } | {
1856
+ state: "unavailable";
1857
+ };
849
1858
  interface CollaborationSession {
850
1859
  sendOps(ops: readonly ControllerOp[]): void;
1860
+ /**
1861
+ * Publishes this client's own local presence (Phase 6, ADR 0015) —
1862
+ * best-effort, unordered, never persisted, never an Op. Optional: an
1863
+ * adapter that doesn't support presence simply omits this method, and
1864
+ * `presence.broadcast()` becomes a silent no-op.
1865
+ */
1866
+ updatePresence?(presence: LocalPresence): void;
1867
+ /**
1868
+ * Requests an incremental catch-up after a reconnect (Phase 7) — optional;
1869
+ * an adapter that doesn't support this simply omits the method, and the
1870
+ * caller (`resyncAfterReconnect`) goes straight to its existing
1871
+ * persistence-backed full reload, unchanged from Phase 6.
1872
+ */
1873
+ requestSync?(): Promise<CollaborationSyncResult>;
851
1874
  close(): Promise<void>;
852
1875
  }
853
1876
  interface CreateBoardControllerOptions {
@@ -867,24 +1890,133 @@ interface CreateBoardControllerOptions {
867
1890
  * Trusted Custom tool/object registrations (ticket #22, design:
868
1891
  * docs/research/extension-contracts.md). Validated atomically at
869
1892
  * construction; registration failure throws before any controller is
870
- * returned. Not yet re-exported from a public package entry point —
871
- * internal-only until the reference Extension proves the seam.
1893
+ * returned.
872
1894
  */
873
1895
  extensions?: readonly ScrawlExtension[];
874
1896
  /**
875
1897
  * Optional Host-managed Asset capabilities (ticket #23, design:
876
1898
  * docs/research/asset-resolution-resource-policy.md). Without a
877
1899
  * resolver, referenced Assets preserve their Document geometry and
878
- * render an accessible placeholder. Not yet re-exported from a public
879
- * package entry point — internal-only until the reference resolver
880
- * proves the seam, matching how `extensions` is scoped.
1900
+ * render an accessible placeholder.
881
1901
  */
882
1902
  assetResolver?: AssetResolver;
883
1903
  assetIngestor?: AssetIngestor;
884
1904
  /** Clamped to 64–512MiB; defaults to 256MiB. */
885
1905
  assetCacheBytes?: number;
1906
+ /**
1907
+ * The rendered board surface's color and reference grid. Defaults to the
1908
+ * light theme preset's values; `<Scrawl>` keeps this current across theme
1909
+ * changes via `boardTheme.set` below — a headless/browser-tier Host that
1910
+ * doesn't use the React theme system can set this directly instead.
1911
+ */
1912
+ boardTheme?: BoardThemeOptions;
1913
+ /**
1914
+ * Debounced auto-flush of pending persistence Ops after document changes
1915
+ * settle (Phase 5). Enabled by default (1000ms debounce) whenever
1916
+ * `adapters.persistence` is configured — today, without this, a Host must
1917
+ * call `flush()` manually after every edit for anything to persist. Pass
1918
+ * `false` to opt out entirely and drive `flush()` yourself, preserving
1919
+ * prior behavior exactly. Never fires on a per-change basis — rapid edits
1920
+ * coalesce into one flush of their final state (ADR 0006).
1921
+ */
1922
+ autosave?: boolean | {
1923
+ debounceMs?: number;
1924
+ };
1925
+ /**
1926
+ * Throttle for `presence.broadcast()` (Phase 6, ADR 0015) — the minimum
1927
+ * interval between outgoing presence updates sent via the configured
1928
+ * `CollaborationAdapter`. Defaults to 50ms. A trailing throttle: the
1929
+ * latest value passed to `broadcast()` always eventually sends, even if
1930
+ * calls arrive faster than this interval.
1931
+ */
1932
+ presenceThrottleMs?: number;
1933
+ /**
1934
+ * Caps how many `ControllerOp`s can sit queued, unsent, for the
1935
+ * persistence pipe (`pendingOps`) or the collaboration pipe
1936
+ * (`pendingCollaborationOps`) at once (Phase 7) — each pipe is capped
1937
+ * independently. Prevents unbounded memory growth from a long-lived
1938
+ * offline session or a stuck adapter. Exceeding it never fails or drops
1939
+ * the local edit itself (the Document already applied it optimistically)
1940
+ * — only queueing for that one pipe is skipped, and a
1941
+ * `{code:"queue-overflow", retryable:false}` error is emitted so a Host
1942
+ * can react. Defaults to 1000 — the Phase 9 collaboration coalescing
1943
+ * above (`collaborationCoalesceMs`) already keeps a busy drag from
1944
+ * approaching this on its own, so hitting it in practice means a pipe
1945
+ * has been offline/stuck for a genuinely long editing session.
1946
+ *
1947
+ * **Recovery** (Phase 9): the dropped op itself is gone from that one
1948
+ * pipe's queue — there is no automatic backfill, and the live
1949
+ * controller keeps running with that pipe now silently missing one
1950
+ * edit. Two things stay true regardless: (1) the in-memory Document is
1951
+ * never affected — a queue-overflow can never corrupt or roll back a
1952
+ * local edit, only skip sending it; (2) staleness is per-object, not
1953
+ * permanent — any *later* edit to that same object produces a brand
1954
+ * new, undropped Op carrying its full current state, which naturally
1955
+ * supersedes the gap (the Op model is already last-write-wins/
1956
+ * idempotent, so a superseding Op doesn't need the earlier one to have
1957
+ * arrived). The real risk is an object that's dropped and never edited
1958
+ * again before the controller is disposed or the page reloads — a Host
1959
+ * that needs strict durability should treat `queue-overflow` as a
1960
+ * signal to check `persistence.state`/`pendingOps` pressure (via
1961
+ * `usePersistenceStatus`/`getSnapshot().connection.persistence`)
1962
+ * before disposing, not assume disposing and reconnecting alone
1963
+ * repairs the gap (a fresh `load()` only returns what the backend
1964
+ * already has, which is exactly what's missing the dropped edit).
1965
+ */
1966
+ maxPendingOps?: number;
1967
+ /**
1968
+ * Coalescing window (ms) for outgoing collaboration Ops (Phase 9) — same
1969
+ * trailing-throttle shape as `presenceThrottleMs`: the first Op after an
1970
+ * idle period sends immediately, and subsequent Ops for the *same*
1971
+ * object within this window replace each other (latest value wins,
1972
+ * matching the already-idempotent Op model) rather than each triggering
1973
+ * its own send. A multi-second drag that previously sent one full Op per
1974
+ * pointer-move now sends at most one per window per touched object.
1975
+ * Persistence (`pendingOps`) is unaffected — it already debounces via
1976
+ * `autosave`, so this option only changes live collaboration traffic.
1977
+ * Defaults to 50ms.
1978
+ */
1979
+ collaborationCoalesceMs?: number;
1980
+ /**
1981
+ * Caps how many resolved objects a single `content.copy`/`content.cut`
1982
+ * (or their Cmd/Ctrl+C/X keyboard equivalents) will hold in the
1983
+ * in-memory clipboard at once (Phase 9) — `expandSelection` recursively
1984
+ * expands groups, so an unbounded selection (a huge group, or thousands
1985
+ * of individually selected strokes) could otherwise clone and retain an
1986
+ * arbitrarily large snapshot indefinitely, until the next copy/cut
1987
+ * replaces it. Exceeding it rejects the whole copy/cut (nothing is
1988
+ * cloned, and — for cut — nothing is removed from the Document either,
1989
+ * never a partial copy of an arbitrary subset) and emits a
1990
+ * `{code:"clipboard-overflow", retryable:false}` error. Defaults to
1991
+ * 5000.
1992
+ */
1993
+ maxClipboardItems?: number;
886
1994
  }
887
1995
  interface BoardController {
1996
+ readonly notebooks: {
1997
+ /** Create a notebook containing one section and an empty page; select it. */
1998
+ add(name?: string): string;
1999
+ select(id: string): void;
2000
+ rename(id: string, name: string): void;
2001
+ list(): readonly BoardNotebook[];
2002
+ current(): string;
2003
+ };
2004
+ readonly sections: {
2005
+ /** Create a section in the current or specified notebook, with one empty page. */
2006
+ add(name?: string, notebookId?: string): string;
2007
+ select(id: string): void;
2008
+ rename(id: string, name: string): void;
2009
+ list(notebookId?: string): readonly BoardSection[];
2010
+ current(): string;
2011
+ };
2012
+ readonly pages: {
2013
+ /** Add an empty page and switch to it. */
2014
+ add(name?: string, sectionId?: string): string;
2015
+ select(id: string): void;
2016
+ rename(id: string, name: string): void;
2017
+ list(sectionId?: string): readonly BoardPage[];
2018
+ current(): string;
2019
+ };
888
2020
  readonly document: ReadonlyBoardDocument;
889
2021
  readonly tools: {
890
2022
  select(tool: BuiltInTool | (string & {})): void;
@@ -899,7 +2031,7 @@ interface BoardController {
899
2031
  noteColor: string;
900
2032
  tableRows: number;
901
2033
  tableCols: number;
902
- stampKind: StampKind;
2034
+ stampKind: StickerKind;
903
2035
  timerDurationMs: number;
904
2036
  }>): void;
905
2037
  current(): BoardStyle;
@@ -908,10 +2040,35 @@ interface BoardController {
908
2040
  undo(): void;
909
2041
  redo(): void;
910
2042
  };
2043
+ readonly boardTheme: {
2044
+ /** Live update of the board surface color/grid/shape-stroke-width — the controller's identity stays fixed across theme changes. */
2045
+ set(theme: BoardThemeOptions): void;
2046
+ };
911
2047
  readonly view: {
912
2048
  fit(): void;
2049
+ /**
2050
+ * Frame the current selection (Phase 8), the same way `fit()` frames the
2051
+ * whole board. A no-op with nothing selected — deliberately doesn't fall
2052
+ * back to `fit()`'s "frame everything," which would be a surprising
2053
+ * result for an empty selection. On a headless board this can only
2054
+ * re-center the view (no viewport to compute a real zoom-to-fit from),
2055
+ * matching `fit()`'s own headless limitation exactly.
2056
+ */
2057
+ zoomToSelection(): void;
913
2058
  zoomTo(value: number): void;
914
2059
  centerOn(point: BoardPoint): void;
2060
+ /**
2061
+ * Tell the canvas an inline text editor is open over a text block (`{ id }`)
2062
+ * or a table cell (`{ id, row, col }`), so it stops drawing that text until
2063
+ * the editor closes (`null`) and the two copies don't overlap. The default
2064
+ * UI's inline editors call this; a Host with its own editors should too.
2065
+ * Presentation only — never touches the Document. A no-op when headless.
2066
+ */
2067
+ setInlineEditing(target: {
2068
+ id: string;
2069
+ row?: number;
2070
+ col?: number;
2071
+ } | null): void;
915
2072
  get(): BoardView;
916
2073
  boardToScreen(point: BoardPoint): ScreenPoint;
917
2074
  screenToBoard(point: ScreenPoint): BoardPoint;
@@ -920,6 +2077,75 @@ interface BoardController {
920
2077
  add(input: BoardObjectInput): string;
921
2078
  update(id: string, patch: BoardObjectPatch): void;
922
2079
  remove(ids: readonly string[]): void;
2080
+ /**
2081
+ * Clone each given object as a new, unlocked copy offset by a small
2082
+ * fixed cascade (matching the Duplicate affordance's established Host
2083
+ * convention), as one undoable step. Order-preserving: `result[i]` is
2084
+ * the clone of `ids[i]`. Strokes that share a `clusterId` among the
2085
+ * given ids get a single fresh shared `clusterId` in the result, so
2086
+ * duplicating a whole multi-stroke shape (e.g. an arrow's shaft + head)
2087
+ * keeps it one shape — pass every member's id together, not just one.
2088
+ * Unknown ids are silently skipped, matching `remove`'s convention.
2089
+ */
2090
+ duplicate(ids: readonly string[]): readonly string[];
2091
+ /**
2092
+ * Creates a new Group referencing `ids` as its children and returns its
2093
+ * id, as one undoable step. Unknown ids are silently skipped, matching
2094
+ * `duplicate`/`remove`'s convention. A child id that's itself a group
2095
+ * makes a nested group — expanding nested groups into their leaf
2096
+ * members is always the caller's job, never assumed here (matches the
2097
+ * document-model `GroupObject` itself).
2098
+ */
2099
+ group(ids: readonly string[]): string;
2100
+ /**
2101
+ * Dissolves one group, returning its immediate children's ids (a nested
2102
+ * subgroup among them stays intact, itself still a group) — the group
2103
+ * record itself is removed, the children are untouched. A no-op
2104
+ * (returns `[]`) if `groupId` isn't a group.
2105
+ */
2106
+ ungroup(groupId: string): readonly string[];
2107
+ /**
2108
+ * Aligns every given object's matching edge/center to the corresponding
2109
+ * edge/center of their combined bounding box, as one undoable step.
2110
+ * `"top"`/`"bottom"` follow board space's Y-up convention (`"top"` is
2111
+ * the larger Y). Ids that don't resolve, or resolve to a Group (which
2112
+ * has no position of its own), are skipped. A no-op under 2 resolvable
2113
+ * ids — there's nothing to align relative to.
2114
+ */
2115
+ align(ids: readonly string[], edge: "left" | "right" | "top" | "bottom" | "centerX" | "centerY"): void;
2116
+ /**
2117
+ * Spaces the middle objects' centers evenly between the first and last
2118
+ * (sorted along `axis`), as one undoable step — the two endpoints don't
2119
+ * move. Ids that don't resolve, or resolve to a Group, are skipped. A
2120
+ * no-op under 3 resolvable ids — there's no "middle" to distribute.
2121
+ */
2122
+ distribute(ids: readonly string[], axis: "x" | "y"): void;
2123
+ /**
2124
+ * Snapshots `ids` (recursively expanded through any group, same as
2125
+ * `duplicate`) into an internal in-memory clipboard — never
2126
+ * `navigator.clipboard`, scoped to this one controller instance and
2127
+ * replaced wholesale by the next `copy`/`cut`. Read-only; works even
2128
+ * on a read-only board.
2129
+ */
2130
+ copy(ids: readonly string[]): void;
2131
+ /** `copy`, then removes every resolved object (recursively through any group) as one undoable step. */
2132
+ cut(ids: readonly string[]): void;
2133
+ /**
2134
+ * Clones the current clipboard contents onto the board as one undoable
2135
+ * step, offset the same small cascade `duplicate` uses (no cursor
2136
+ * position to paste relative to yet). Returns the new top-level ids —
2137
+ * a pasted group's own id stands for its (also-pasted) children, which
2138
+ * aren't listed separately. `[]` when the clipboard is empty.
2139
+ */
2140
+ paste(): readonly string[];
2141
+ /**
2142
+ * Select every top-level object (Phase 8) — a group's own id stands for
2143
+ * its children, which aren't selected separately, matching `paste`'s own
2144
+ * "what the user sees" id list. Hidden objects are excluded, consistent
2145
+ * with them already being excluded from marquee selection. Works with
2146
+ * no canvas/engine, same as {@link toggleSelectionVisibility}.
2147
+ */
2148
+ selectAll(): void;
923
2149
  table: {
924
2150
  addRow(tableId: string): void;
925
2151
  addCol(tableId: string): void;
@@ -930,6 +2156,29 @@ interface BoardController {
930
2156
  };
931
2157
  select(ids: readonly string[]): void;
932
2158
  import(document: SerializedBoardDocument): readonly string[];
2159
+ /**
2160
+ * Toggle lock state for the current selection (or focused note/text/
2161
+ * table/image/timer), matching whatever a single Host lock/unlock
2162
+ * control already does per object type. A no-op with nothing selected,
2163
+ * on a headless board, or when every actionable target is locked by
2164
+ * another collaborator who isn't the current lock holder.
2165
+ */
2166
+ toggleSelectionLock(): void;
2167
+ /**
2168
+ * Toggle hidden state for the current selection, as one undo entry
2169
+ * (Phase 8). If any selected object is hidden, shows every selected
2170
+ * object; otherwise hides them all — same "any wins" semantics as
2171
+ * {@link toggleSelectionLock}. Hidden objects stay fully present in the
2172
+ * document (they still serialize, persist, sync, undo/redo) — they just
2173
+ * stop rendering and stop being hit-testable/selectable via pointer
2174
+ * interaction. Unlike `toggleSelectionLock`, this works on a headless
2175
+ * board too: it only touches `selection`/the document, no canvas or
2176
+ * engine involved. Custom objects have no visibility concept (no
2177
+ * `Hideable` field) and are silently skipped, matching how
2178
+ * `toggleSelectionLock` already excludes them. A no-op with nothing
2179
+ * selected or when the selection is only custom objects.
2180
+ */
2181
+ toggleSelectionVisibility(): void;
933
2182
  };
934
2183
  readonly query: {
935
2184
  get(id: string): DeepReadonly<BoardObject> | undefined;
@@ -958,6 +2207,15 @@ interface BoardController {
958
2207
  follow(view: PresenceView): void;
959
2208
  /** Ease the camera to a peer's view; returns false (no-op) while mid-stroke. */
960
2209
  gather(view: PresenceView): boolean;
2210
+ /**
2211
+ * Publishes this client's own cursor/tool/view for other collaborators
2212
+ * (Phase 6, ADR 0015), via the configured `CollaborationAdapter` —
2213
+ * throttled internally (`presenceThrottleMs` option, default 50ms) so
2214
+ * a raw pointermove stream never becomes a message-per-event flood. A
2215
+ * no-op if no collaboration adapter is configured, or if the
2216
+ * configured one doesn't implement `updatePresence`.
2217
+ */
2218
+ broadcast(local: LocalPresence): void;
961
2219
  };
962
2220
  readonly export: {
963
2221
  svg(): string;
@@ -983,7 +2241,19 @@ interface BoardController {
983
2241
  }>;
984
2242
  dispose(): Promise<void>;
985
2243
  }
2244
+ /**
2245
+ * Creates a {@link BoardController} — the SDK's canonical, capability-grouped
2246
+ * entry point (`content`, `tools`, `style`, `history`, `view`, `query`,
2247
+ * `comments`, `presence`, `export`, `assets`, plus top-level `getSnapshot`/
2248
+ * `subscribe`/`on`/`setReadOnly`/`flush`/`dispose`) for driving a Board
2249
+ * imperatively from any JS/TS runtime. Supply a `canvas` to render, or omit
2250
+ * it to run headless (SSR, tests, or a document/history-only integration).
2251
+ * Persistence and Collaboration are opt-in via `options.adapters` — without
2252
+ * them the controller runs entirely in memory. Call `dispose()` when done to
2253
+ * release the renderer, adapters, and any pending timers.
2254
+ */
986
2255
  declare function createBoardController(options: CreateBoardControllerOptions): BoardController;
2256
+ /** @deprecated Use `BoardController.getSnapshot()`'s return type instead. */
987
2257
  type LocalBoardSnapshot = {
988
2258
  documentId: string;
989
2259
  selectedStrokeId: string | null;
@@ -992,10 +2262,12 @@ type LocalBoardSnapshot = {
992
2262
  canRedo: boolean;
993
2263
  disposed: boolean;
994
2264
  };
2265
+ /** @deprecated Use `CreateBoardControllerOptions` with `createBoardController` instead. */
995
2266
  type LocalBoardOptions = {
996
2267
  documentId: string;
997
2268
  initialDocument?: SerializedBoardDocument;
998
2269
  };
2270
+ /** @deprecated Use `BoardController` (from `createBoardController`) instead — this stroke-only, single-tool surface predates the full capability-grouped controller. */
999
2271
  type LocalBoard = {
1000
2272
  drawStroke(stroke: Stroke): void;
1001
2273
  selectAt(point: BoardPoint): string | null;
@@ -1006,7 +2278,20 @@ type LocalBoard = {
1006
2278
  subscribe(listener: () => void): () => void;
1007
2279
  dispose(): Promise<void>;
1008
2280
  };
2281
+ /** @deprecated Use `createBoardController` instead — this is a thin, stroke-only wrapper kept for the original Phase 1 tracer's compatibility. */
1009
2282
  declare function createLocalBoard(options: LocalBoardOptions): LocalBoard;
1010
2283
 
1011
- export { STAMPS, createBoardController, createLocalBoard, isStampKind, stampDataUrl };
1012
- export type { ApplyOpsResult, BoardController, BoardControllerError, BoardEventMap, BoardObject, BoardObjectInput, BoardObjectPatch, BoardSnapshot, BoardStyle, BoardView, BuiltInTool, CollaborationAdapter, CollaborationReceiver, CollaborationSession, CollaborationSnapshot, CollaboratorIdentity, CommentMarker, ControllerOp, CreateBoardControllerOptions, DeepReadonly, DocumentContext, LoadResult, LocalBoard, LocalBoardOptions, LocalBoardSnapshot, PersistenceAdapter, PersistenceDiagnostic, PersistenceSnapshot, PresenceCursor, PresenceUser, PresenceView, ReadonlyBoardDocument, ReadonlyDocumentChange, ScreenPoint, ScreenRect, StampKind };
2284
+ interface CreateMemoryPersistenceOptions {
2285
+ /** Pre-seeded documents, keyed by document id — as if a prior session had already saved them. Seeded documents start at revision "1". */
2286
+ seed?: Record<string, CurrentSerializedDocument>;
2287
+ }
2288
+ /**
2289
+ * Creates a real, in-memory `PersistenceAdapter`. One instance can back
2290
+ * multiple documents (keyed by `DocumentContext.documentId`, like every
2291
+ * other adapter in this package). State lives only in this instance —
2292
+ * discarded on garbage collection, never written to disk.
2293
+ */
2294
+ declare function createMemoryPersistence(options?: CreateMemoryPersistenceOptions): PersistenceAdapter;
2295
+
2296
+ export { AssetResolutionError, EMOJI_LIBRARY, ICON_LIBRARY, STAMPS, SUPPORTED_ASSET_MEDIA_TYPES, assetRef, clampAssetCacheBytes, cloneCustomObject, createBoardController, createLocalBoard, createMemoryPersistence, emojiDataUrl, iconCategories, iconDataUrl, isAssetRef, isEmojiName, isIconName, isStampKind, isStickerKind, stampDataUrl, stickerLabel };
2297
+ export type { ApplyOpsResult, AssetDiagnostic, AssetExportFailure, AssetIngestRequest, AssetIngestResult, AssetIngestor, AssetKind, AssetPurpose, AssetRef, AssetResolutionErrorCode, AssetResolveRequest, AssetResolveResult, AssetResolver, BoardController, BoardControllerError, BoardEventMap, BoardKeyInput, BoardNotebook, BoardObject, BoardObjectInput, BoardObjectPatch, BoardPage, BoardPointerInput, BoardScene, BoardSection, BoardSnapshot, BoardStyle, BoardThemeOptions, BoardView, BuiltInTool, CollaborationAckDiagnostic, CollaborationAdapter, CollaborationReceiver, CollaborationSession, CollaborationSnapshot, CollaborationSyncResult, CollaboratorIdentity, CommentMarker, ControllerOp, CreateBoardControllerOptions, CreateMemoryPersistenceOptions, CustomBoardObject, CustomObjectAddInput, CustomObjectDefinition, CustomTool, CustomToolDefinition, DeepReadonly, DocumentContext, EmojiDefinition, EmojiName, ExportDocumentSVGOptions, ExportDocumentSVGResult, ExtensionCommand, ExtensionDiagnostic, ExtensionHitResult, ExtensionId, ExtensionRequirement, FocusedItem, IconDefinition, IconName, InputModifiers, JsonObject, JsonValue, LoadResult, LocalBoard, LocalBoardOptions, LocalBoardSnapshot, LocalPresence, Mat2x3, ObjectDescribeContext, ObjectIntent, ObjectType, PersistenceAdapter, PersistenceDiagnostic, PersistenceSnapshot, PresenceCursor, PresenceUser, PresenceView, QueryableBoardObject, ReadonlyBoardDocument, ReadonlyCustomObject, ReadonlyDocumentChange, ReplaceResult, SceneEllipse, SceneGroup, SceneImage, ScenePath, SceneRect, SceneText, ScrawlExtension, ScreenPoint, ScreenRect, SearchHit, SearchHitKind, SearchableComment, StampKind, StickerKind, SupportedAssetMediaType, ToolCancelReason, ToolCapabilities, ToolCursor, ToolId };