@scrawl-board/board 0.1.0-beta.0

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.
@@ -0,0 +1,1797 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, ComponentType, CSSProperties } from 'react';
3
+
4
+ declare const documentIdBrand: unique symbol;
5
+ declare const strokeIdBrand: unique symbol;
6
+ type DocumentId = string & {
7
+ readonly [documentIdBrand]: "DocumentId";
8
+ };
9
+ type StrokeId = string & {
10
+ readonly [strokeIdBrand]: "StrokeId";
11
+ };
12
+ declare function documentId(value: string): DocumentId;
13
+ declare function strokeId(value: string): StrokeId;
14
+
15
+ /** Wire grammar: `asset:<namespace>:<opaque-id>`. Interpreted only by the Host. */
16
+ type AssetRef = string;
17
+ type AssetKind = "image";
18
+ type AssetPurpose = "render" | "thumbnail" | "export";
19
+ interface AssetResolveRequest {
20
+ ref: AssetRef;
21
+ kind: AssetKind;
22
+ purpose: AssetPurpose;
23
+ signal: AbortSignal;
24
+ }
25
+ interface AssetResolveResult {
26
+ bytes: Uint8Array;
27
+ mediaType: string;
28
+ }
29
+ /**
30
+ * Host-owned lookup. Scrawl passes only the reference, kind, purpose, and
31
+ * signal — never Document contents, an Extension instance, or credentials.
32
+ * The returned bytes are copied into SDK-owned storage and independently
33
+ * validated before use; a resolver's claimed `mediaType` is never trusted
34
+ * on its own (see `assetValidation.ts`).
35
+ */
36
+ interface AssetResolver {
37
+ resolve(request: AssetResolveRequest): Promise<AssetResolveResult>;
38
+ }
39
+ interface AssetIngestRequest {
40
+ bytes: Uint8Array;
41
+ mediaType: string;
42
+ name?: string;
43
+ signal: AbortSignal;
44
+ }
45
+ interface AssetIngestResult {
46
+ ref: AssetRef;
47
+ }
48
+ /**
49
+ * Host-owned upload/creation path. Scrawl validates the candidate against
50
+ * SDK limits before calling this, and validates the returned reference
51
+ * before it can enter any command. Cancellation or failure creates no
52
+ * Document object, Op, or history entry.
53
+ */
54
+ interface AssetIngestor {
55
+ ingest(request: AssetIngestRequest): Promise<AssetIngestResult>;
56
+ }
57
+ type AssetResolutionErrorCode = "resolver-unavailable" | "not-found" | "forbidden" | "offline" | "unsupported-type" | "too-large" | "invalid-content" | "decode-failed" | "budget-exceeded" | "aborted" | "unknown";
58
+ /** Runtime event for a resolution/ingestion failure — never carries credentials or a fetchable location. */
59
+ interface AssetDiagnostic {
60
+ code: AssetResolutionErrorCode;
61
+ ref?: AssetRef;
62
+ objectId: string;
63
+ objectKind: "image" | "custom";
64
+ retryable: boolean;
65
+ }
66
+
67
+ type Mat2x3 = [number, number, number, number, number, number];
68
+ declare const IDENTITY: Mat2x3;
69
+ declare function isIdentity(m: Mat2x3): boolean;
70
+ /** Compose: apply `first`, then `second`. */
71
+ declare function mul(second: Mat2x3, first: Mat2x3): Mat2x3;
72
+ declare function invert(m: Mat2x3): Mat2x3;
73
+ declare function apply(m: Mat2x3, p: BoardPoint): BoardPoint;
74
+ declare function translation(dx: number, dy: number): Mat2x3;
75
+ declare function rotationAbout(angle: number, cx: number, cy: number): Mat2x3;
76
+ declare function scalingAbout(sx: number, sy: number, cx: number, cy: number): Mat2x3;
77
+ /** Approximate uniform length scale (average of the axis scales). */
78
+ declare function avgScale(m: Mat2x3): number;
79
+
80
+ /** A stable namespaced string, e.g. `com.acme.kanban`. Never a display name. */
81
+ type ExtensionId = string;
82
+ /** A stable namespaced string, e.g. `com.acme.kanban/card-tool`. */
83
+ type ToolId = string;
84
+ /** A stable namespaced string, e.g. `com.acme.kanban/card`. */
85
+ type ObjectType = string;
86
+ interface ExtensionRequirement {
87
+ extensionId: ExtensionId;
88
+ extensionApiVersion: 1;
89
+ }
90
+ interface ScrawlExtension {
91
+ id: ExtensionId;
92
+ extensionApiVersion: 1;
93
+ requires?: readonly ExtensionRequirement[];
94
+ tools?: readonly CustomToolDefinition[];
95
+ objectTypes?: readonly CustomObjectDefinition[];
96
+ }
97
+ type JsonValue = null | boolean | number | string | readonly JsonValue[] | {
98
+ readonly [key: string]: JsonValue;
99
+ };
100
+ type JsonObject = {
101
+ readonly [key: string]: JsonValue;
102
+ };
103
+ interface CustomBoardObject {
104
+ id: string;
105
+ type: ObjectType;
106
+ schemaVersion: number;
107
+ transform: Mat2x3;
108
+ /** Safe placeholder geometry, refreshed by the SDK on every valid command. */
109
+ fallback: {
110
+ bounds: {
111
+ x: number;
112
+ y: number;
113
+ width: number;
114
+ height: number;
115
+ };
116
+ label?: string;
117
+ };
118
+ lock?: {
119
+ holderId: string;
120
+ acquiredAt: number;
121
+ };
122
+ props: JsonValue;
123
+ }
124
+ /**
125
+ * The read-only view handed to `describe`. Deep-readonly by construction
126
+ * (not derived via a shallow `Readonly<>`) because `describe` must treat its
127
+ * input as a pure snapshot — see the spec's "treat `describe` as a pure
128
+ * function" rule.
129
+ */
130
+ type ReadonlyCustomObject<Props extends JsonValue = JsonValue> = Readonly<{
131
+ id: string;
132
+ type: ObjectType;
133
+ schemaVersion: number;
134
+ transform: Mat2x3;
135
+ fallback: Readonly<{
136
+ bounds: Readonly<{
137
+ x: number;
138
+ y: number;
139
+ width: number;
140
+ height: number;
141
+ }>;
142
+ label?: string;
143
+ }>;
144
+ lock?: Readonly<{
145
+ holderId: string;
146
+ acquiredAt: number;
147
+ }>;
148
+ props: Props;
149
+ }>;
150
+ interface ObjectDescribeContext {
151
+ /** True while this object is part of the current selection. */
152
+ selected: boolean;
153
+ }
154
+ interface CustomObjectDefinition<Props extends JsonValue = JsonValue> {
155
+ type: ObjectType;
156
+ currentSchemaVersion: number;
157
+ /** Validates untrusted persisted/imported/pasted/remote data. Must be pure. */
158
+ parse(input: unknown, schemaVersion: number): Props;
159
+ /** One pure, synchronous step per consecutive schema version. */
160
+ migrate?: Readonly<Record<number, (oldProps: JsonValue) => JsonValue>>;
161
+ describe(object: ReadonlyCustomObject<Props>, context: ObjectDescribeContext): BoardScene$1;
162
+ }
163
+ interface SceneNodeBase {
164
+ key: string;
165
+ transform?: Mat2x3;
166
+ opacity?: number;
167
+ /** Semantic hit-region id (e.g. `resize-handle`, `cell:2:3`); never a renderer object. */
168
+ interactionRegion?: string;
169
+ }
170
+ interface SceneRect extends SceneNodeBase {
171
+ kind: "rect";
172
+ x: number;
173
+ y: number;
174
+ width: number;
175
+ height: number;
176
+ cornerRadius?: number;
177
+ fill?: string;
178
+ stroke?: string;
179
+ strokeWidth?: number;
180
+ }
181
+ interface SceneText extends SceneNodeBase {
182
+ kind: "text";
183
+ x: number;
184
+ y: number;
185
+ text: string;
186
+ fontSize: number;
187
+ color: string;
188
+ /** Horizontal alignment relative to (x, y); defaults to "start". */
189
+ align?: "start" | "center" | "end";
190
+ }
191
+ interface SceneGroup extends SceneNodeBase {
192
+ kind: "group";
193
+ children: readonly BoardScene$1[];
194
+ }
195
+ interface ScenePath$1 extends SceneNodeBase {
196
+ kind: "path";
197
+ /** SVG-style path data, board-local coordinates. */
198
+ d: string;
199
+ fill?: string;
200
+ stroke?: string;
201
+ strokeWidth?: number;
202
+ }
203
+ interface SceneImage extends SceneNodeBase {
204
+ kind: "image";
205
+ /** Resolved through the Host asset resolver (ticket #23); never a raw Blob/File/URL. */
206
+ ref: AssetRef;
207
+ x: number;
208
+ y: number;
209
+ width: number;
210
+ height: number;
211
+ /** How the image fills its declared (x, y, width, height) box; defaults to "fill". */
212
+ fit?: "fill" | "contain" | "cover";
213
+ /** Bounded plain-text accessible label, or an explicit decorative opt-out. */
214
+ alt: string | {
215
+ readonly decorative: true;
216
+ };
217
+ }
218
+ interface SceneEllipse extends SceneNodeBase {
219
+ kind: "ellipse";
220
+ cx: number;
221
+ cy: number;
222
+ rx: number;
223
+ ry: number;
224
+ fill?: string;
225
+ stroke?: string;
226
+ strokeWidth?: number;
227
+ }
228
+ type BoardScene$1 = SceneGroup | ScenePath$1 | SceneText | SceneImage | SceneRect | SceneEllipse;
229
+ type ToolCursor = "default" | "crosshair" | "pointer" | "grab" | "grabbing" | "text";
230
+ type ToolCancelReason = "escape-key" | "tool-switched" | "pointer-lost" | "error";
231
+ interface InputModifiers {
232
+ shift: boolean;
233
+ alt: boolean;
234
+ ctrl: boolean;
235
+ meta: boolean;
236
+ }
237
+ interface BoardPointerInput {
238
+ board: BoardPoint;
239
+ viewport: BoardPoint;
240
+ pointerId: number;
241
+ pointerType: "mouse" | "pen" | "touch";
242
+ pressure: number;
243
+ buttons: number;
244
+ modifiers: InputModifiers;
245
+ }
246
+ interface BoardKeyInput {
247
+ key: string;
248
+ modifiers: InputModifiers;
249
+ }
250
+ interface CustomToolDefinition {
251
+ id: ToolId;
252
+ label: string;
253
+ suggestedShortcut?: string;
254
+ cursor?: ToolCursor;
255
+ create(context: ToolCapabilities): CustomTool;
256
+ }
257
+ /** All handlers are synchronous — see the spec's "Lifecycle handlers are synchronous" rule. */
258
+ interface CustomTool {
259
+ activate?(): void;
260
+ pointerDown?(input: BoardPointerInput): void;
261
+ pointerMove?(input: BoardPointerInput): void;
262
+ pointerUp?(input: BoardPointerInput): void;
263
+ keyDown?(input: BoardKeyInput): void;
264
+ keyUp?(input: BoardKeyInput): void;
265
+ cancel?(reason: ToolCancelReason): void;
266
+ deactivate?(): void;
267
+ }
268
+ /**
269
+ * What an Extension supplies to add a Custom board object. `id` is optional
270
+ * (the controller assigns one when omitted); `schemaVersion` and `fallback`
271
+ * defaults are derived by the controller from the object's definition.
272
+ */
273
+ interface CustomObjectAddInput {
274
+ type: ObjectType;
275
+ id?: string;
276
+ transform?: Mat2x3;
277
+ fallback?: {
278
+ bounds: {
279
+ x: number;
280
+ y: number;
281
+ width: number;
282
+ height: number;
283
+ };
284
+ label?: string;
285
+ };
286
+ props: JsonValue;
287
+ }
288
+ type ObjectIntent = {
289
+ kind: "add";
290
+ object: CustomObjectAddInput;
291
+ } | {
292
+ kind: "update";
293
+ id: string;
294
+ patch: JsonObject;
295
+ } | {
296
+ kind: "remove";
297
+ id: string;
298
+ };
299
+ interface ExtensionCommand {
300
+ label?: string;
301
+ changes: readonly ObjectIntent[];
302
+ }
303
+ /** A structural, read-only view of any board object (built-in or Custom). */
304
+ type QueryableBoardObject = Readonly<{
305
+ id: string;
306
+ type: string;
307
+ }> & Readonly<Record<string, unknown>>;
308
+ interface ExtensionHitResult {
309
+ readonly objectId: string;
310
+ readonly interactionRegion?: string;
311
+ }
312
+ interface ToolCapabilities {
313
+ query: {
314
+ get(id: string): QueryableBoardObject | undefined;
315
+ selection(): readonly string[];
316
+ hitTest(point: BoardPoint): ExtensionHitResult | undefined;
317
+ };
318
+ coordinates: {
319
+ boardToViewport(point: BoardPoint): BoardPoint;
320
+ viewportToBoard(point: BoardPoint): BoardPoint;
321
+ };
322
+ /** Session-only geometry — never enters Document/history/persistence/collaboration. */
323
+ preview: {
324
+ set(scene: BoardScene$1): void;
325
+ clear(): void;
326
+ };
327
+ /** Constructs, validates, and commits one atomic command; controller derives Ops. */
328
+ submit(command: ExtensionCommand): void;
329
+ tools: {
330
+ select(toolId: ToolId | "select"): void;
331
+ cancel(): void;
332
+ };
333
+ }
334
+ interface ExtensionDiagnostic {
335
+ extensionId: ExtensionId;
336
+ phase: "parse" | "migrate" | "describe" | "tool-lifecycle" | "react-companion" | "asset-resolution";
337
+ toolId?: ToolId;
338
+ objectId?: string;
339
+ objectType?: ObjectType;
340
+ recoverable: boolean;
341
+ cause: unknown;
342
+ }
343
+
344
+ /** Who holds a lock. Stored on the item so only they can take it off. */
345
+ interface LockHolder {
346
+ userId: string;
347
+ name: string;
348
+ }
349
+ interface Lockable {
350
+ locked?: boolean;
351
+ lockedBy?: string;
352
+ lockedByName?: string;
353
+ }
354
+ /**
355
+ * Apply or clear a lock in place. Unlock always drops the holder so a stale
356
+ * name cannot linger on an unlocked item.
357
+ */
358
+ declare function applyItemLock(item: Lockable, locked: boolean, by?: LockHolder | null): void;
359
+ /**
360
+ * Only the person who locked it can unlock it. Items locked before ownership
361
+ * existed (no `lockedBy`) stay unlockable by anyone, so old boards don't brick.
362
+ */
363
+ declare function canUnlockItem(item: Lockable | undefined, userId: string | undefined): boolean;
364
+ /** Wire fields for a locked item; omitted entirely when unlocked. */
365
+ declare function serializeLock(item: Lockable): Lockable;
366
+
367
+ /** A kitchen timer sitting on the board. Remaining time is derived, not ticked. */
368
+ interface KitchenTimer extends Lockable {
369
+ id: string;
370
+ x: number;
371
+ y: number;
372
+ /** Face diameter in board units. */
373
+ size: number;
374
+ /** What you set it to — 1, 5, 10, 15 minutes. */
375
+ durationMs: number;
376
+ /** Remaining at the last start or pause. */
377
+ remainingMs: number;
378
+ /** Wall-clock ms when the current run started. Absent means paused. */
379
+ runningSince?: number;
380
+ }
381
+ declare const TIMER_DEFAULT_SIZE = 12;
382
+ declare const TIMER_DEFAULT_DURATION_MS: number;
383
+ declare const TIMER_PRESETS_MS: readonly [60000, number, number, number];
384
+ declare function cloneTimer(timer: KitchenTimer): KitchenTimer;
385
+ declare function timerRemaining(timer: KitchenTimer, now: number): number;
386
+ declare function timerExpired(timer: KitchenTimer, now: number): boolean;
387
+ declare function startTimer(timer: KitchenTimer, now: number): KitchenTimer;
388
+ declare function pauseTimer(timer: KitchenTimer, now: number): KitchenTimer;
389
+ declare function toggleTimer(timer: KitchenTimer, now: number): KitchenTimer;
390
+ declare function setTimerDuration(timer: KitchenTimer, durationMs: number): KitchenTimer;
391
+ declare function formatTimer(ms: number): string;
392
+
393
+ interface BoardPoint {
394
+ x: number;
395
+ y: number;
396
+ }
397
+ interface StrokePoint extends BoardPoint {
398
+ /** Resolved pressure in [0, 1] — real stylus pressure or velocity simulation. */
399
+ pressure: number;
400
+ /**
401
+ * Erasure channel (ADR 0003), 0..1. Undefined means 0. At or above
402
+ * ERASE_THRESHOLD the point is removed and the stroke may split.
403
+ */
404
+ erase?: number;
405
+ }
406
+ /** Which drawing tool made a stroke; undefined means marker (back-compat). */
407
+ type StrokeTool = "marker" | "highlighter";
408
+ interface Stroke extends Lockable {
409
+ id: string;
410
+ color: string;
411
+ baseWidth: number;
412
+ tool?: StrokeTool;
413
+ /** Points are stroke-local; `matrix` places them on the board. */
414
+ points: StrokePoint[];
415
+ /** 2D affine transform [a b c d tx ty]; undefined means identity. */
416
+ matrix?: [number, number, number, number, number, number];
417
+ /**
418
+ * Spatial group membership (ADR 0005). Assigned when the stroke is drawn;
419
+ * cluster records are derived from these ids, never stored themselves.
420
+ */
421
+ clusterId?: string;
422
+ }
423
+ /** Erasure level at which a point counts as fully erased. */
424
+ declare const ERASE_THRESHOLD = 0.95;
425
+ declare function cloneStroke(stroke: Stroke): Stroke;
426
+ type SerializedPoint = [number, number, number, number];
427
+ interface SerializedStroke extends Lockable {
428
+ id: string;
429
+ color: string;
430
+ baseWidth: number;
431
+ tool?: StrokeTool;
432
+ points: SerializedPoint[];
433
+ /** Omitted when identity. */
434
+ matrix?: [number, number, number, number, number, number];
435
+ clusterId?: string;
436
+ }
437
+ interface SerializedDocument {
438
+ /** Absent in every historical document; current saves always write 1. */
439
+ schemaVersion?: 1;
440
+ strokes: SerializedStroke[];
441
+ /** Absent in documents saved before notes existed. */
442
+ notes?: StickyNote[];
443
+ /** Absent in documents saved before the text tool existed. */
444
+ textBlocks?: TextBlock[];
445
+ /** Absent in documents saved before interactive tables existed. */
446
+ tables?: TableBlock[];
447
+ /** Absent in documents saved before images existed. */
448
+ images?: ImageBlock[];
449
+ /** Absent in documents saved before kitchen timers existed. */
450
+ timers?: KitchenTimer[];
451
+ /** Absent in documents saved before Custom board objects existed (ticket #22). */
452
+ customObjects?: CustomBoardObject[];
453
+ }
454
+ declare const INK_COLORS: {
455
+ readonly black: "#1C1C1E";
456
+ readonly red: "#E0333D";
457
+ readonly blue: "#2563EB";
458
+ readonly green: "#16A34A";
459
+ };
460
+ declare const HIGHLIGHT_COLORS: {
461
+ readonly yellow: "#FDE047";
462
+ readonly green: "#86EFAC";
463
+ readonly pink: "#F9A8D4";
464
+ readonly blue: "#93C5FD";
465
+ };
466
+ declare const NOTE_COLORS: {
467
+ readonly yellow: "#FDE68A";
468
+ readonly pink: "#FBCFE8";
469
+ readonly blue: "#BFDBFE";
470
+ readonly green: "#BBF7D0";
471
+ };
472
+ /**
473
+ * One collaborator's vote on a note. One per person; toggling removes it.
474
+ */
475
+ interface NoteVote {
476
+ userId: string;
477
+ name: string;
478
+ color: string;
479
+ }
480
+ /**
481
+ * A sticky note: content floating above the board at a z-offset (pillar 3 —
482
+ * depth as an organizational axis). Center position in board space.
483
+ */
484
+ interface StickyNote extends Lockable {
485
+ id: string;
486
+ x: number;
487
+ y: number;
488
+ /** Square side length in board units. */
489
+ size: number;
490
+ /** Height above the board surface; drives shadow offset, blur, and opacity. */
491
+ zOffset: number;
492
+ color: string;
493
+ text: string;
494
+ /** One vote per collaborator. Peel follows the count. */
495
+ votes?: NoteVote[];
496
+ }
497
+ /**
498
+ * Typed text on the board surface, rendered as SDF glyphs. Position is the
499
+ * top-left corner; lines flow downward (-y). Text joins the clustering
500
+ * system like handwriting (build prompt §6.4).
501
+ */
502
+ interface TextBlock extends Lockable {
503
+ id: string;
504
+ x: number;
505
+ y: number;
506
+ text: string;
507
+ /** Line height in board units. */
508
+ fontSize: number;
509
+ color: string;
510
+ clusterId?: string;
511
+ }
512
+ declare const TEXT_DEFAULT_SIZE = 1.8;
513
+ declare function cloneText(block: TextBlock): TextBlock;
514
+ /**
515
+ * Estimated bounds of a text block (average-glyph-width heuristic — no DOM,
516
+ * usable from clustering, export, and tests alike).
517
+ */
518
+ declare function measureTextBlock(text: string, fontSize: number): {
519
+ width: number;
520
+ height: number;
521
+ };
522
+ declare const NOTE_DEFAULT_SIZE = 10;
523
+ declare const NOTE_DEFAULT_Z = 1;
524
+ declare const NOTE_MIN_Z = 0.4;
525
+ declare const NOTE_MAX_Z = 6;
526
+ declare const NOTE_PEEL_STEP = 0.7;
527
+ declare function cloneNote(note: StickyNote): StickyNote;
528
+ /**
529
+ * Interactive structured table on the board. Position (x, y) is top-left in board units.
530
+ * Cells are indexed as `${row},${col}` keys mapping to cell text content.
531
+ */
532
+ interface TableBlock extends Lockable {
533
+ id: string;
534
+ x: number;
535
+ y: number;
536
+ rows: number;
537
+ cols: number;
538
+ colWidths: number[];
539
+ rowHeights: number[];
540
+ cells: Record<string, string>;
541
+ color?: string;
542
+ backgroundColor?: string;
543
+ clusterId?: string;
544
+ }
545
+ declare const TABLE_DEFAULT_CELL_WIDTH = 8;
546
+ declare const TABLE_DEFAULT_CELL_HEIGHT = 3.6;
547
+ declare const TABLE_DEFAULT_FONT_SIZE = 1.25;
548
+ declare function cloneTable(table: TableBlock): TableBlock;
549
+ declare function measureTable(table: TableBlock): {
550
+ width: number;
551
+ height: number;
552
+ };
553
+ declare const BOARD_COLOR = "#FFFFFF";
554
+ declare const FOG_COLOR = "#FFFFFF";
555
+ /**
556
+ * An imported image block on the board plane.
557
+ * Coordinates (x, y) represent the center of the image in board space.
558
+ */
559
+ interface ImageBlock extends Lockable {
560
+ id: string;
561
+ /**
562
+ * A legacy, read-only data URL (or, historically, an arbitrary string) —
563
+ * never written by new code once `ref` exists. Ticket #23's Host-managed
564
+ * Assets add `ref` as the durable path going forward; `src` and `ref` are
565
+ * mutually exclusive in practice, but both fields exist on every
566
+ * `ImageBlock` so old and new objects share one shape.
567
+ */
568
+ src: string;
569
+ /** Opaque Asset reference (ticket #23); when present, `src` is ignored. */
570
+ ref?: AssetRef;
571
+ x: number;
572
+ y: number;
573
+ width: number;
574
+ height: number;
575
+ aspectRatio: number;
576
+ name?: string;
577
+ createdAt?: string;
578
+ /** Present when this image is a stamp from the pad, not a photo. */
579
+ stamp?: string;
580
+ }
581
+ declare function cloneImage(img: ImageBlock): ImageBlock;
582
+
583
+ declare const CURRENT_DOCUMENT_SCHEMA_VERSION: 1;
584
+ type CurrentSerializedStroke = Omit<SerializedStroke, "id"> & {
585
+ id: StrokeId;
586
+ };
587
+ interface CurrentSerializedDocument extends Required<SerializedDocument> {
588
+ schemaVersion: typeof CURRENT_DOCUMENT_SCHEMA_VERSION;
589
+ strokes: CurrentSerializedStroke[];
590
+ }
591
+ type DocumentRecoveryCode = "DOCUMENT_JSON_INVALID" | "DOCUMENT_VALIDATION_FAILED" | "DOCUMENT_VERSION_UNSUPPORTED";
592
+ declare class DocumentRecoveryError extends Error {
593
+ readonly code: DocumentRecoveryCode;
594
+ readonly originalBytes?: string | undefined;
595
+ readonly path?: string | undefined;
596
+ constructor(code: DocumentRecoveryCode, message: string, originalBytes?: string | undefined, path?: string | undefined);
597
+ }
598
+ type DocumentLoadResult = {
599
+ ok: true;
600
+ document: CurrentSerializedDocument;
601
+ migratedFrom: 0 | 1;
602
+ } | {
603
+ ok: false;
604
+ error: DocumentRecoveryError;
605
+ };
606
+ declare function loadDocumentBytes(originalBytes: string): DocumentLoadResult;
607
+ declare function migrateDocument(raw: unknown): DocumentLoadResult;
608
+ declare function serializeDocument(document: unknown): string;
609
+
610
+ interface SearchableComment {
611
+ id: string;
612
+ text: string;
613
+ authorName: string;
614
+ x: number;
615
+ y: number;
616
+ replies: readonly {
617
+ text: string;
618
+ }[];
619
+ }
620
+ type SearchHitKind = "note" | "text" | "table" | "comment" | "stamp";
621
+ interface SearchHit {
622
+ kind: SearchHitKind;
623
+ id: string;
624
+ title: string;
625
+ snippet: string;
626
+ x: number;
627
+ y: number;
628
+ }
629
+ interface SearchableBoard {
630
+ notes: Iterable<StickyNote>;
631
+ texts: Iterable<TextBlock>;
632
+ tables: Iterable<TableBlock>;
633
+ images: Iterable<ImageBlock>;
634
+ timers?: Iterable<KitchenTimer>;
635
+ comments: Iterable<SearchableComment>;
636
+ }
637
+ /**
638
+ * Find notes, text, table cells, stamps, and comments whose text contains
639
+ * `query`. Empty / whitespace queries return nothing.
640
+ */
641
+ declare function searchBoard(query: string, board: SearchableBoard): SearchHit[];
642
+
643
+ /**
644
+ * The one place a live Stroke becomes its wire representation — in
645
+ * particular, points collapse from {x,y,pressure,erase?} objects into
646
+ * [x,y,pressure,erase] tuples. Anything that persists a stroke (toJSON, and
647
+ * the incremental ops opSync.ts sends) must go through this, or the two
648
+ * paths drift: a stroke saved with points-as-objects looks fine until the
649
+ * next load, where deserializeStrokes' array-destructuring throws "object is
650
+ * not iterable" — exactly what shipping the live object straight into an op
651
+ * used to do.
652
+ */
653
+ declare function serializeStroke(s: Stroke): SerializedStroke;
654
+ interface BBox {
655
+ minX: number;
656
+ minY: number;
657
+ maxX: number;
658
+ maxY: number;
659
+ }
660
+ interface DocumentChange {
661
+ added: Stroke[];
662
+ /** Ids of removed strokes. */
663
+ removed: string[];
664
+ /** Strokes whose points mutated in place (erasure decay); geometry must rebuild. */
665
+ updated: Stroke[];
666
+ /** Strokes whose matrix changed; geometry is untouched, placement moved. */
667
+ transformed: Stroke[];
668
+ notesAdded: StickyNote[];
669
+ /** Ids of removed notes. */
670
+ notesRemoved: string[];
671
+ notesUpdated: StickyNote[];
672
+ textAdded: TextBlock[];
673
+ /** Ids of removed text blocks. */
674
+ textRemoved: string[];
675
+ textUpdated: TextBlock[];
676
+ tablesAdded: TableBlock[];
677
+ /** Ids of removed tables. */
678
+ tablesRemoved: string[];
679
+ tablesUpdated: TableBlock[];
680
+ imagesAdded: ImageBlock[];
681
+ /** Ids of removed images. */
682
+ imagesRemoved: string[];
683
+ imagesUpdated: ImageBlock[];
684
+ timersAdded: KitchenTimer[];
685
+ /** Ids of removed timers. */
686
+ timersRemoved: string[];
687
+ timersUpdated: KitchenTimer[];
688
+ /** Custom board objects (ticket #22) — one map for every registered type, keyed by id. */
689
+ customObjectsAdded: CustomBoardObject[];
690
+ /** Ids of removed custom objects. */
691
+ customObjectsRemoved: string[];
692
+ customObjectsUpdated: CustomBoardObject[];
693
+ }
694
+ type Listener = (change: DocumentChange) => void;
695
+ declare class BoardDocument {
696
+ readonly id: DocumentId;
697
+ /** Last version acknowledged by the server; 0 = never saved. */
698
+ version: number;
699
+ private readonly strokes;
700
+ private readonly notes;
701
+ private readonly texts;
702
+ private readonly tables;
703
+ private readonly images;
704
+ private readonly timers;
705
+ /** All Custom board object types share one map, keyed by id — the envelope is already uniform. */
706
+ private readonly customObjects;
707
+ private readonly bboxes;
708
+ private readonly listeners;
709
+ constructor(id: DocumentId);
710
+ get(id: string): Stroke | undefined;
711
+ all(): IterableIterator<Stroke>;
712
+ bbox(id: string): BBox | undefined;
713
+ subscribe(listener: Listener): () => void;
714
+ addStrokes(strokes: Stroke[]): void;
715
+ removeStrokes(ids: string[]): void;
716
+ /** Announce in-place point mutations (erasure decay; bbox is unchanged). */
717
+ touchStrokes(strokes: Stroke[]): void;
718
+ /** Announce matrix changes; recomputes world bboxes. */
719
+ transformStrokes(strokes: Stroke[]): void;
720
+ getNote(id: string): StickyNote | undefined;
721
+ allNotes(): IterableIterator<StickyNote>;
722
+ addNotes(notes: StickyNote[]): void;
723
+ removeNotes(ids: string[]): void;
724
+ /** Replace a note's contents (move, peel, retext) under the same id. */
725
+ setNote(note: StickyNote): void;
726
+ getText(id: string): TextBlock | undefined;
727
+ allTexts(): IterableIterator<TextBlock>;
728
+ addTexts(blocks: TextBlock[]): void;
729
+ removeTexts(ids: string[]): void;
730
+ /** Replace a text block's contents (move, retext) under the same id. */
731
+ setText(block: TextBlock): void;
732
+ getTable(id: string): TableBlock | undefined;
733
+ allTables(): IterableIterator<TableBlock>;
734
+ addTables(tables: TableBlock[]): void;
735
+ removeTables(ids: string[]): void;
736
+ /** Replace a table's contents (move, resize, change cells) under the same id. */
737
+ setTable(table: TableBlock): void;
738
+ getImage(id: string): ImageBlock | undefined;
739
+ allImages(): IterableIterator<ImageBlock>;
740
+ addImages(images: ImageBlock[]): void;
741
+ removeImages(ids: string[]): void;
742
+ /** Replace an image block's contents (move, resize) under the same id. */
743
+ setImage(image: ImageBlock): void;
744
+ getTimer(id: string): KitchenTimer | undefined;
745
+ allTimers(): IterableIterator<KitchenTimer>;
746
+ addTimers(timers: KitchenTimer[]): void;
747
+ removeTimers(ids: string[]): void;
748
+ setTimer(timer: KitchenTimer): void;
749
+ getCustomObject(id: string): CustomBoardObject | undefined;
750
+ allCustomObjects(): IterableIterator<CustomBoardObject>;
751
+ addCustomObjects(objects: CustomBoardObject[]): void;
752
+ removeCustomObjects(ids: string[]): void;
753
+ /** Replace a custom object's contents under the same id. */
754
+ setCustomObject(object: CustomBoardObject): void;
755
+ setStrokeLocked(id: string, locked: boolean, by?: LockHolder | null): void;
756
+ setStrokesLocked(ids: string[], locked: boolean, by?: LockHolder | null): void;
757
+ setNoteLocked(id: string, locked: boolean, by?: LockHolder | null): void;
758
+ setTextLocked(id: string, locked: boolean, by?: LockHolder | null): void;
759
+ setTableLocked(id: string, locked: boolean, by?: LockHolder | null): void;
760
+ setImageLocked(id: string, locked: boolean, by?: LockHolder | null): void;
761
+ setTimerLocked(id: string, locked: boolean, by?: LockHolder | null): void;
762
+ /** Replace all content (initial load). Does not touch `version`. */
763
+ replaceAll(strokes: Stroke[], notes: StickyNote[], texts: TextBlock[], tables?: TableBlock[], images?: ImageBlock[], timers?: KitchenTimer[], customObjects?: CustomBoardObject[]): void;
764
+ /** Apply incremental real-time change received from a remote collaborator over WebSocket. */
765
+ applyRemoteChange(change: Partial<DocumentChange>): void;
766
+ toJSON(): SerializedDocument;
767
+ static deserializeCustomObjects(data: SerializedDocument): CustomBoardObject[];
768
+ static deserializeImages(data: SerializedDocument): ImageBlock[];
769
+ static deserializeTimers(data: SerializedDocument): KitchenTimer[];
770
+ static deserializeNotes(data: SerializedDocument): StickyNote[];
771
+ static deserializeTexts(data: SerializedDocument): TextBlock[];
772
+ static deserializeTables(data: SerializedDocument): TableBlock[];
773
+ static deserializeStrokes(data: SerializedDocument): Stroke[];
774
+ private emit;
775
+ }
776
+
777
+ type ClusterIdFactory = () => string;
778
+ declare class ClusterStore {
779
+ private readonly doc;
780
+ private readonly createId;
781
+ private readonly now;
782
+ private readonly clusters;
783
+ private readonly byStroke;
784
+ private readonly unsubscribe;
785
+ constructor(doc: BoardDocument, createId: ClusterIdFactory, now: () => number);
786
+ /**
787
+ * Pick (or create) the cluster for a freshly drawn stroke and stamp its
788
+ * clusterId. Gap threshold scales with stroke height, per the build prompt.
789
+ */
790
+ assign(stroke: Stroke, now?: number): void;
791
+ /** Same assignment for a typed text block, using its estimated bounds. */
792
+ assignText(block: TextBlock, now?: number): void;
793
+ private pick;
794
+ /** All strokes in the same cluster; a clusterless stroke is its own group. */
795
+ membersOf(strokeId: string): string[];
796
+ dispose(): void;
797
+ /** Strokes and text blocks index identically; bbox source differs. */
798
+ private memberBBox;
799
+ private addMember;
800
+ private removeMember;
801
+ private recompute;
802
+ }
803
+
804
+ interface Command {
805
+ readonly label: string;
806
+ apply(doc: BoardDocument): void;
807
+ revert(doc: BoardDocument): void;
808
+ }
809
+ /** How a command reached the document — undo/redo are audited distinctly. */
810
+ type CommandKind = "do" | "undo" | "redo";
811
+ declare class History {
812
+ private readonly doc;
813
+ private readonly undoStack;
814
+ private readonly redoStack;
815
+ /**
816
+ * Observer for every mutation, in one place: all board edits funnel
817
+ * through record/undo/redo. The audit trail listens here.
818
+ */
819
+ onCommand: ((command: Command, kind: CommandKind) => void) | null;
820
+ private undoing;
821
+ constructor(doc: BoardDocument);
822
+ /** Apply a command and make it undoable. */
823
+ execute(command: Command): void;
824
+ /** Make an already-applied change undoable (e.g. a live erase swipe). */
825
+ record(command: Command): void;
826
+ /**
827
+ * True while a revert is in flight. Persistence reads this: re-adding
828
+ * something the author deleted must be sent as a `restore`, the only op the
829
+ * server lets past a tombstone (ADR 0006).
830
+ */
831
+ get isUndoing(): boolean;
832
+ undo(): void;
833
+ redo(): void;
834
+ get canUndo(): boolean;
835
+ get canRedo(): boolean;
836
+ clear(): void;
837
+ }
838
+
839
+ /** One drawing action — a marker stroke, or a shape's strokes as one unit. */
840
+ declare class AddStrokesCommand implements Command {
841
+ readonly label = "draw";
842
+ private readonly strokes;
843
+ constructor(strokes: Stroke[]);
844
+ apply(doc: BoardDocument): void;
845
+ revert(doc: BoardDocument): void;
846
+ }
847
+ /**
848
+ * One eraser swipe (ADR 0003): `before` are the touched strokes as they were
849
+ * at swipe start; `after` is what survived — smudged, split, or gone.
850
+ */
851
+ declare class EraseCommand implements Command {
852
+ readonly label = "erase";
853
+ private readonly before;
854
+ private readonly after;
855
+ constructor(before: Stroke[], after: Stroke[]);
856
+ apply(doc: BoardDocument): void;
857
+ revert(doc: BoardDocument): void;
858
+ }
859
+ /**
860
+ * One transform gesture: `delta` composed onto each member's matrix
861
+ * (`child = delta × child`, per §6.3 — never baked into geometry).
862
+ */
863
+ declare class TransformCommand implements Command {
864
+ private readonly ids;
865
+ private readonly delta;
866
+ readonly label = "transform selection";
867
+ private readonly inverse;
868
+ constructor(ids: string[], delta: Mat2x3);
869
+ apply(doc: BoardDocument): void;
870
+ revert(doc: BoardDocument): void;
871
+ private compose;
872
+ }
873
+ declare class AddNoteCommand implements Command {
874
+ readonly label = "add note";
875
+ private readonly note;
876
+ constructor(note: StickyNote);
877
+ apply(doc: BoardDocument): void;
878
+ revert(doc: BoardDocument): void;
879
+ }
880
+ /** Any note mutation — move, peel, recolor, retext — as before/after. */
881
+ declare class UpdateNoteCommand implements Command {
882
+ readonly label = "update note";
883
+ private readonly before;
884
+ private readonly after;
885
+ constructor(before: StickyNote, after: StickyNote);
886
+ apply(doc: BoardDocument): void;
887
+ revert(doc: BoardDocument): void;
888
+ }
889
+ declare class DeleteNoteCommand implements Command {
890
+ readonly label = "delete note";
891
+ private readonly note;
892
+ constructor(note: StickyNote);
893
+ apply(doc: BoardDocument): void;
894
+ revert(doc: BoardDocument): void;
895
+ }
896
+ declare class AddTextCommand implements Command {
897
+ readonly label = "add text";
898
+ private readonly block;
899
+ constructor(block: TextBlock);
900
+ apply(doc: BoardDocument): void;
901
+ revert(doc: BoardDocument): void;
902
+ }
903
+ /** Any text-block mutation — move or retext — as before/after. */
904
+ declare class UpdateTextCommand implements Command {
905
+ readonly label = "update text";
906
+ private readonly before;
907
+ private readonly after;
908
+ constructor(before: TextBlock, after: TextBlock);
909
+ apply(doc: BoardDocument): void;
910
+ revert(doc: BoardDocument): void;
911
+ }
912
+ declare class DeleteTextCommand implements Command {
913
+ readonly label = "delete text";
914
+ private readonly block;
915
+ constructor(block: TextBlock);
916
+ apply(doc: BoardDocument): void;
917
+ revert(doc: BoardDocument): void;
918
+ }
919
+ declare class AddTableCommand implements Command {
920
+ readonly label = "add table";
921
+ private readonly table;
922
+ constructor(table: TableBlock);
923
+ apply(doc: BoardDocument): void;
924
+ revert(doc: BoardDocument): void;
925
+ }
926
+ /** Any table mutation — move, resize, cell text edit — as before/after. */
927
+ declare class UpdateTableCommand implements Command {
928
+ readonly label = "update table";
929
+ private readonly before;
930
+ private readonly after;
931
+ constructor(before: TableBlock, after: TableBlock);
932
+ apply(doc: BoardDocument): void;
933
+ revert(doc: BoardDocument): void;
934
+ }
935
+ declare class DeleteTableCommand implements Command {
936
+ readonly label = "delete table";
937
+ private readonly table;
938
+ constructor(table: TableBlock);
939
+ apply(doc: BoardDocument): void;
940
+ revert(doc: BoardDocument): void;
941
+ }
942
+ declare class DeleteStrokesCommand implements Command {
943
+ readonly label = "delete selection";
944
+ private readonly strokes;
945
+ constructor(strokes: Stroke[]);
946
+ apply(doc: BoardDocument): void;
947
+ revert(doc: BoardDocument): void;
948
+ }
949
+ declare class AddImageCommand implements Command {
950
+ readonly label = "add image";
951
+ private readonly image;
952
+ constructor(image: ImageBlock);
953
+ apply(doc: BoardDocument): void;
954
+ revert(doc: BoardDocument): void;
955
+ }
956
+ /** Any image mutation — move, resize — as before/after. */
957
+ declare class UpdateImageCommand implements Command {
958
+ readonly label = "update image";
959
+ private readonly before;
960
+ private readonly after;
961
+ constructor(before: ImageBlock, after: ImageBlock);
962
+ apply(doc: BoardDocument): void;
963
+ revert(doc: BoardDocument): void;
964
+ }
965
+ declare class DeleteImageCommand implements Command {
966
+ readonly label = "delete image";
967
+ private readonly image;
968
+ constructor(image: ImageBlock);
969
+ apply(doc: BoardDocument): void;
970
+ revert(doc: BoardDocument): void;
971
+ }
972
+ declare class AddTimerCommand implements Command {
973
+ readonly label = "add timer";
974
+ private readonly timer;
975
+ constructor(timer: KitchenTimer);
976
+ apply(doc: BoardDocument): void;
977
+ revert(doc: BoardDocument): void;
978
+ }
979
+ declare class UpdateTimerCommand implements Command {
980
+ readonly label = "update timer";
981
+ private readonly before;
982
+ private readonly after;
983
+ constructor(before: KitchenTimer, after: KitchenTimer);
984
+ apply(doc: BoardDocument): void;
985
+ revert(doc: BoardDocument): void;
986
+ }
987
+ declare class DeleteTimerCommand implements Command {
988
+ readonly label = "delete timer";
989
+ private readonly timer;
990
+ constructor(timer: KitchenTimer);
991
+ apply(doc: BoardDocument): void;
992
+ revert(doc: BoardDocument): void;
993
+ }
994
+ interface LockTarget {
995
+ type: "stroke" | "note" | "text" | "table" | "image" | "timer";
996
+ id: string;
997
+ locked: boolean;
998
+ lockedBy?: string;
999
+ lockedByName?: string;
1000
+ }
1001
+ declare class LockItemsCommand implements Command {
1002
+ readonly label = "toggle lock";
1003
+ private readonly targets;
1004
+ private readonly targetState;
1005
+ private readonly actor;
1006
+ constructor(targets: LockTarget[], targetState: boolean, actor?: LockHolder | null);
1007
+ apply(doc: BoardDocument): void;
1008
+ revert(doc: BoardDocument): void;
1009
+ private applyLock;
1010
+ }
1011
+
1012
+ type OpCollection = "strokes" | "notes" | "textBlocks" | "tables" | "images" | "timers" | "customObjects";
1013
+ type Op = {
1014
+ kind: "upsert";
1015
+ collection: OpCollection;
1016
+ object: {
1017
+ id: string;
1018
+ };
1019
+ } | {
1020
+ kind: "restore";
1021
+ collection: OpCollection;
1022
+ object: {
1023
+ id: string;
1024
+ };
1025
+ } | {
1026
+ kind: "remove";
1027
+ collection: OpCollection;
1028
+ id: string;
1029
+ };
1030
+ /** Convert a canonical Document change into durable, renderer-neutral Ops. */
1031
+ declare function changeToOps(change: DocumentChange, restoring?: boolean): Op[];
1032
+
1033
+ declare const MIN_WIDTH_FACTOR = 0.35;
1034
+ declare const END_TAPER = 0.55;
1035
+ interface RibbonEdgePoint {
1036
+ lx: number;
1037
+ ly: number;
1038
+ rx: number;
1039
+ ry: number;
1040
+ /** 1 = intact ink, 0 = fully erased (from the erasure channel). */
1041
+ alpha: number;
1042
+ }
1043
+ declare function ribbonEdges(points: StrokePoint[], baseWidth: number): RibbonEdgePoint[];
1044
+
1045
+ declare class SpatialIndex {
1046
+ private readonly doc;
1047
+ private readonly cells;
1048
+ private readonly strokeCells;
1049
+ private readonly unsubscribe;
1050
+ constructor(doc: BoardDocument);
1051
+ /** Ids of strokes whose bbox may overlap the query rect. */
1052
+ query(minX: number, minY: number, maxX: number, maxY: number): Set<string>;
1053
+ dispose(): void;
1054
+ private insert;
1055
+ private remove;
1056
+ }
1057
+
1058
+ /** A stamp is a small sticker dropped on the board — not ink, not a photo. */
1059
+ type StampKind = "star" | "check" | "ship" | "heart" | "plus" | "fire";
1060
+ declare const STAMP_SIZE = 6;
1061
+ declare const STAMPS: {
1062
+ kind: StampKind;
1063
+ label: string;
1064
+ glyph: string;
1065
+ }[];
1066
+ declare function stampDataUrl(kind: StampKind): string;
1067
+ declare function isStampKind(value: unknown): value is StampKind;
1068
+
1069
+ interface ViewportInset {
1070
+ top: number;
1071
+ right: number;
1072
+ bottom: number;
1073
+ left: number;
1074
+ }
1075
+ /** Keep beacons clear of typical chrome at the top and bottom of the Board. */
1076
+ declare const BEACON_INSET: ViewportInset;
1077
+ type PresencePlacement = {
1078
+ kind: "on-screen";
1079
+ x: number;
1080
+ y: number;
1081
+ } | {
1082
+ kind: "edge";
1083
+ x: number;
1084
+ y: number;
1085
+ angle: number;
1086
+ };
1087
+ /**
1088
+ * If a collaborator is in the usable viewport, return their screen position.
1089
+ * If they are outside it, clamp to the nearest edge and return the angle a
1090
+ * chevron should point (screen space, radians, 0 = right, clockwise).
1091
+ */
1092
+ declare function placePresenceBeacon(screen: {
1093
+ x: number;
1094
+ y: number;
1095
+ }, viewport: {
1096
+ width: number;
1097
+ height: number;
1098
+ }, inset?: ViewportInset): PresencePlacement;
1099
+
1100
+ interface ExtensionRegistry {
1101
+ readonly extensions: readonly ScrawlExtension[];
1102
+ tool(id: ToolId): CustomToolDefinition | undefined;
1103
+ objectType(type: ObjectType): CustomObjectDefinition | undefined;
1104
+ }
1105
+
1106
+ /**
1107
+ * `registry` is optional and only enables rendering Custom objects through
1108
+ * their own `describe()` — without it (or for an object whose extension
1109
+ * isn't in it), Custom objects still export via the standard fallback
1110
+ * placeholder (`fallback.bounds`/`label`), never silently dropped.
1111
+ *
1112
+ * `resolvedAssets` (ticket #23) maps an Asset reference to an already-
1113
+ * resolved `data:` URI — see `assetExport.ts`'s `exportDocumentSVGWithAssets`,
1114
+ * which is the only intended caller that ever passes one. Without it, every
1115
+ * `ref`-backed image (built-in or Custom `SceneImage`) renders its
1116
+ * placeholder instead of guessing at a URL; a legacy `src`-backed image is
1117
+ * unaffected either way.
1118
+ */
1119
+ declare function documentToSVG(doc: SerializedDocument, now?: number, registry?: ExtensionRegistry, resolvedAssets?: ReadonlyMap<AssetRef, string>): string;
1120
+
1121
+ declare const SDK_PACKAGE_NAME = "@scrawl-board/board";
1122
+ declare const SDK_DEVELOPMENT_VERSION = "0.0.0-development";
1123
+
1124
+ type BoardBounds = {
1125
+ minX: number;
1126
+ minY: number;
1127
+ maxX: number;
1128
+ maxY: number;
1129
+ };
1130
+ type ScenePath = {
1131
+ id: string;
1132
+ color: string;
1133
+ width: number;
1134
+ points: readonly BoardPoint[];
1135
+ };
1136
+ type BoardScene = {
1137
+ bounds: BoardBounds;
1138
+ paths: readonly ScenePath[];
1139
+ };
1140
+ type BoardStroke = Stroke;
1141
+ type SerializedBoardStroke = SerializedStroke;
1142
+ type SerializedBoardDocument = CurrentSerializedDocument;
1143
+
1144
+ interface AssetExportFailure {
1145
+ objectId: string;
1146
+ ref: AssetRef;
1147
+ code: AssetResolutionErrorCode;
1148
+ }
1149
+ interface ExportDocumentSVGOptions {
1150
+ missingAssets?: "placeholder";
1151
+ signal?: AbortSignal;
1152
+ }
1153
+ interface ExportDocumentSVGResult {
1154
+ svg: string;
1155
+ missingAssets: readonly AssetExportFailure[];
1156
+ }
1157
+
1158
+ type BuiltInTool = "select" | "pan" | "marker" | "highlighter" | "eraser" | "text" | "note" | "table" | "image" | "comment" | "stamp" | "timer" | `shape:${string}`;
1159
+ type PersistenceSnapshot = {
1160
+ state: "disabled";
1161
+ } | {
1162
+ state: "loading" | "idle" | "saving" | "saved";
1163
+ } | {
1164
+ state: "offline" | "error";
1165
+ error?: BoardControllerError;
1166
+ };
1167
+ type CollaborationSnapshot = {
1168
+ state: "disabled";
1169
+ } | {
1170
+ state: "connecting" | "online" | "reconnecting";
1171
+ } | {
1172
+ state: "offline" | "error";
1173
+ error?: BoardControllerError;
1174
+ };
1175
+ interface BoardStyle {
1176
+ readonly inkColor: string;
1177
+ readonly highlightColor: string;
1178
+ readonly eraserRadius: number;
1179
+ readonly noteColor: string;
1180
+ readonly tableRows: number;
1181
+ readonly tableCols: number;
1182
+ readonly stampKind: StampKind;
1183
+ readonly timerDurationMs: number;
1184
+ }
1185
+ interface BoardSnapshot {
1186
+ readonly status: "loading" | "ready" | "disposed";
1187
+ readonly documentId: string;
1188
+ readonly tool: BuiltInTool | (string & {});
1189
+ readonly zoom: number;
1190
+ readonly readOnly: boolean;
1191
+ readonly selection: readonly string[];
1192
+ readonly strokeCount: number;
1193
+ readonly objectCount: number;
1194
+ readonly canUndo: boolean;
1195
+ readonly canRedo: boolean;
1196
+ readonly style: BoardStyle;
1197
+ readonly connection: {
1198
+ readonly persistence: PersistenceSnapshot;
1199
+ readonly collaboration: CollaborationSnapshot;
1200
+ };
1201
+ }
1202
+ interface BoardControllerError {
1203
+ source: "controller" | "renderer" | "persistence" | "collaboration";
1204
+ code: string;
1205
+ retryable: boolean;
1206
+ cause?: unknown;
1207
+ }
1208
+ interface BoardEventMap {
1209
+ change: BoardSnapshot;
1210
+ "tool-change": {
1211
+ tool: string;
1212
+ };
1213
+ "view-change": {
1214
+ x: number;
1215
+ y: number;
1216
+ zoom: number;
1217
+ };
1218
+ "selection-change": {
1219
+ ids: readonly string[];
1220
+ };
1221
+ "style-change": BoardStyle;
1222
+ "edit-request": {
1223
+ kind: "note";
1224
+ id: string;
1225
+ text: string;
1226
+ color: string;
1227
+ screenRect: ScreenRect;
1228
+ } | {
1229
+ kind: "text";
1230
+ id: string | null;
1231
+ boardX: number;
1232
+ boardY: number;
1233
+ text: string;
1234
+ color: string;
1235
+ fontSizePx: number;
1236
+ screenRect: ScreenRect;
1237
+ } | {
1238
+ kind: "table-cell";
1239
+ id: string;
1240
+ row: number;
1241
+ col: number;
1242
+ text: string;
1243
+ fontSizePx: number;
1244
+ screenRect: ScreenRect;
1245
+ };
1246
+ "import-request": {
1247
+ accept: readonly string[];
1248
+ };
1249
+ "comment-open-request": {
1250
+ id: string;
1251
+ };
1252
+ "comment-draft-request": {
1253
+ board: BoardPoint;
1254
+ screen: ScreenPoint;
1255
+ };
1256
+ "pointer-move": {
1257
+ board: BoardPoint;
1258
+ velocity: BoardPoint;
1259
+ };
1260
+ "user-navigate": undefined;
1261
+ "live-stroke-preview": {
1262
+ stroke: Readonly<Stroke>;
1263
+ };
1264
+ "live-stroke-end": {
1265
+ id: string;
1266
+ };
1267
+ "timer-expired": {
1268
+ id: string;
1269
+ };
1270
+ /** A Custom object failed to parse/migrate/describe, or a Custom tool handler threw. */
1271
+ "extension-diagnostic": ExtensionDiagnostic;
1272
+ /** An Asset failed to resolve/ingest for a built-in image or a Custom `SceneImage` (ticket #23). */
1273
+ "asset-diagnostic": AssetDiagnostic;
1274
+ /** A batch of Ops was reconciled (not applied as-sent) by the persistence adapter (ticket #24). */
1275
+ "persistence-diagnostic": PersistenceDiagnostic;
1276
+ audit: unknown;
1277
+ error: BoardControllerError;
1278
+ disposed: undefined;
1279
+ }
1280
+ interface ScreenPoint {
1281
+ x: number;
1282
+ y: number;
1283
+ }
1284
+ interface ScreenRect {
1285
+ left: number;
1286
+ top: number;
1287
+ width: number;
1288
+ height: number;
1289
+ }
1290
+ interface BoardView {
1291
+ x: number;
1292
+ y: number;
1293
+ zoom: number;
1294
+ }
1295
+ /**
1296
+ * A Host-owned comment, summarized for Board-side search and marker
1297
+ * rendering. Comments are not Document content — they carry no undo
1298
+ * history and never enter the Ops/collaboration pipeline — so this is a
1299
+ * read/query capability, not a `content` object type.
1300
+ */
1301
+ interface CommentMarker extends SearchableComment {
1302
+ readonly authorColor: string;
1303
+ readonly resolved: boolean;
1304
+ }
1305
+ interface PresenceCursor {
1306
+ readonly boardX: number;
1307
+ readonly boardY: number;
1308
+ readonly vx?: number;
1309
+ readonly vy?: number;
1310
+ }
1311
+ /** The engine's native camera shape — not BoardView's zoom, the raw height a peer's camera broadcasts. */
1312
+ interface PresenceView {
1313
+ readonly x: number;
1314
+ readonly y: number;
1315
+ readonly height: number;
1316
+ }
1317
+ /**
1318
+ * A Host-owned collaborator, synced in for cursor/roster rendering only.
1319
+ * Presence is ephemeral — it never touches the Document, Ops, undo/redo,
1320
+ * or persistence — so this is a read/query capability, not an adapter.
1321
+ */
1322
+ interface PresenceUser {
1323
+ readonly id: string;
1324
+ readonly name: string;
1325
+ readonly color: string;
1326
+ readonly tool?: string;
1327
+ readonly cursor?: PresenceCursor;
1328
+ readonly view?: PresenceView;
1329
+ }
1330
+ /**
1331
+ * The Custom arm wraps `CustomBoardObject` under the same `type` discriminant
1332
+ * convention the six built-ins use — `type: "custom"` plus a `customType`
1333
+ * field carrying the registered Extension object type (e.g.
1334
+ * `"com.scrawl.examples/badge"`). This is deliberate, not cosmetic: giving
1335
+ * the arm a literal `"custom"` discriminant (instead of exposing
1336
+ * `CustomBoardObject`'s own `type: string` directly) keeps every existing
1337
+ * `object.type === "table"`-style check elsewhere in this codebase safely
1338
+ * exhaustive — a non-literal `string` arm mixed into this union would make
1339
+ * TypeScript unable to prove any built-in case excludes it. `toBoardObject`/
1340
+ * `toCustomBoardObject` convert to/from the unwrapped `CustomBoardObject`
1341
+ * envelope at the boundaries (document storage, wire Ops) that use it directly.
1342
+ */
1343
+ type BoardObject = ({
1344
+ type: "stroke";
1345
+ } & Stroke) | ({
1346
+ type: "note";
1347
+ } & StickyNote) | ({
1348
+ type: "text";
1349
+ } & TextBlock) | ({
1350
+ type: "table";
1351
+ } & TableBlock) | ({
1352
+ type: "image";
1353
+ } & ImageBlock) | ({
1354
+ type: "timer";
1355
+ } & KitchenTimer) | ({
1356
+ type: "custom";
1357
+ customType: ObjectType;
1358
+ } & Omit<CustomBoardObject, "type">);
1359
+ type BoardObjectInput = {
1360
+ type: "stroke";
1361
+ id?: string;
1362
+ color: string;
1363
+ baseWidth: number;
1364
+ tool?: Stroke["tool"];
1365
+ points: StrokePoint[];
1366
+ matrix?: Stroke["matrix"];
1367
+ clusterId?: string;
1368
+ } | ({
1369
+ type: "note";
1370
+ id?: string;
1371
+ } & Omit<StickyNote, "id">) | ({
1372
+ type: "text";
1373
+ id?: string;
1374
+ } & Omit<TextBlock, "id">) | ({
1375
+ type: "table";
1376
+ id?: string;
1377
+ } & Omit<TableBlock, "id">) | ({
1378
+ type: "image";
1379
+ id?: string;
1380
+ } & Omit<ImageBlock, "id">) | ({
1381
+ type: "timer";
1382
+ id?: string;
1383
+ } & Omit<KitchenTimer, "id">) | ({
1384
+ type: "custom";
1385
+ id?: string;
1386
+ customType: ObjectType;
1387
+ } & Omit<CustomObjectAddInput, "type" | "id">);
1388
+ type BoardObjectPatch = Record<string, unknown>;
1389
+ type DeepReadonly<T> = T extends (...args: never[]) => unknown ? T : T extends readonly (infer Item)[] ? readonly DeepReadonly<Item>[] : T extends object ? {
1390
+ readonly [Key in keyof T]: DeepReadonly<T[Key]>;
1391
+ } : T;
1392
+ interface ReadonlyDocumentChange {
1393
+ readonly added: readonly DeepReadonly<BoardObject>[];
1394
+ readonly updated: readonly DeepReadonly<BoardObject>[];
1395
+ readonly removed: readonly string[];
1396
+ }
1397
+ interface ReadonlyBoardDocument {
1398
+ readonly id: string;
1399
+ get(id: string): DeepReadonly<BoardObject> | undefined;
1400
+ all(): IterableIterator<DeepReadonly<BoardObject>>;
1401
+ subscribe(listener: (change: ReadonlyDocumentChange) => void): () => void;
1402
+ }
1403
+ interface DocumentContext {
1404
+ documentId: string;
1405
+ signal: AbortSignal;
1406
+ }
1407
+ type LoadResult = {
1408
+ state: "found";
1409
+ document: CurrentSerializedDocument;
1410
+ revision: string;
1411
+ } | {
1412
+ state: "missing";
1413
+ };
1414
+ interface PersistenceAdapter {
1415
+ load(context: DocumentContext): Promise<LoadResult>;
1416
+ applyOps(context: DocumentContext, ops: readonly ControllerOp[]): Promise<ApplyOpsResult>;
1417
+ replace(context: DocumentContext, document: CurrentSerializedDocument, baseRevision: string): Promise<unknown>;
1418
+ }
1419
+ /**
1420
+ * `"reconcile"` (ticket #24) means the server authoritatively resolved the
1421
+ * whole batch — some Ops it accepted, `rejectedOpIds` it didn't (a stale
1422
+ * tombstoned id, a permission change, an unrecognized schema, or a
1423
+ * conflicting concurrent edit). The batch is never re-queued in this case;
1424
+ * the controller instead reloads authoritative state via `load()`.
1425
+ */
1426
+ type ApplyOpsResult = {
1427
+ state: "applied";
1428
+ revision: string;
1429
+ } | {
1430
+ state: "reconcile";
1431
+ revision: string;
1432
+ rejectedOpIds: readonly string[];
1433
+ reason: "tombstone" | "permission" | "schema" | "conflict";
1434
+ };
1435
+ /** A batch of Ops was reconciled (not applied as-sent) by the persistence adapter (ticket #24). */
1436
+ interface PersistenceDiagnostic {
1437
+ code: "tombstone" | "permission" | "schema" | "conflict";
1438
+ rejectedOpIds: readonly string[];
1439
+ revision: string;
1440
+ }
1441
+ interface ControllerOp {
1442
+ id: string;
1443
+ schemaVersion: 1;
1444
+ kind: "upsert" | "restore" | "remove";
1445
+ objectType: "stroke" | "note" | "text" | "table" | "image" | "timer" | "custom";
1446
+ objectId: string;
1447
+ payload?: unknown;
1448
+ }
1449
+ interface CollaborationAdapter {
1450
+ connect(options: DocumentContext & {
1451
+ identity: CollaboratorIdentity;
1452
+ receive: CollaborationReceiver;
1453
+ }): Promise<CollaborationSession>;
1454
+ }
1455
+ interface CollaboratorIdentity {
1456
+ id: string;
1457
+ name: string;
1458
+ color?: string;
1459
+ }
1460
+ interface CollaborationReceiver {
1461
+ ops(ops: readonly ControllerOp[]): void;
1462
+ status(state: "online" | "reconnecting" | "offline"): void;
1463
+ error(cause: unknown): void;
1464
+ }
1465
+ interface CollaborationSession {
1466
+ sendOps(ops: readonly ControllerOp[]): void;
1467
+ close(): Promise<void>;
1468
+ }
1469
+ interface CreateBoardControllerOptions {
1470
+ document: {
1471
+ id: string;
1472
+ initial?: SerializedBoardDocument;
1473
+ };
1474
+ /** Canvas-backed controllers delegate rendering and normalized input to the private engine. */
1475
+ canvas?: HTMLCanvasElement;
1476
+ adapters?: {
1477
+ persistence?: PersistenceAdapter;
1478
+ collaboration?: CollaborationAdapter;
1479
+ };
1480
+ identity?: CollaboratorIdentity;
1481
+ createId?: () => string;
1482
+ /**
1483
+ * Trusted Custom tool/object registrations (ticket #22, design:
1484
+ * docs/research/extension-contracts.md). Validated atomically at
1485
+ * construction; registration failure throws before any controller is
1486
+ * returned. Not yet re-exported from a public package entry point —
1487
+ * internal-only until the reference Extension proves the seam.
1488
+ */
1489
+ extensions?: readonly ScrawlExtension[];
1490
+ /**
1491
+ * Optional Host-managed Asset capabilities (ticket #23, design:
1492
+ * docs/research/asset-resolution-resource-policy.md). Without a
1493
+ * resolver, referenced Assets preserve their Document geometry and
1494
+ * render an accessible placeholder. Not yet re-exported from a public
1495
+ * package entry point — internal-only until the reference resolver
1496
+ * proves the seam, matching how `extensions` is scoped.
1497
+ */
1498
+ assetResolver?: AssetResolver;
1499
+ assetIngestor?: AssetIngestor;
1500
+ /** Clamped to 64–512MiB; defaults to 256MiB. */
1501
+ assetCacheBytes?: number;
1502
+ }
1503
+ interface BoardController {
1504
+ readonly document: ReadonlyBoardDocument;
1505
+ readonly tools: {
1506
+ select(tool: BuiltInTool | (string & {})): void;
1507
+ current(): string;
1508
+ };
1509
+ readonly style: {
1510
+ setInkColor(color: string): void;
1511
+ set(patch: Partial<{
1512
+ inkColor: string;
1513
+ highlightColor: string;
1514
+ eraserRadius: number;
1515
+ noteColor: string;
1516
+ tableRows: number;
1517
+ tableCols: number;
1518
+ stampKind: StampKind;
1519
+ timerDurationMs: number;
1520
+ }>): void;
1521
+ current(): BoardStyle;
1522
+ };
1523
+ readonly history: {
1524
+ undo(): void;
1525
+ redo(): void;
1526
+ };
1527
+ readonly view: {
1528
+ fit(): void;
1529
+ zoomTo(value: number): void;
1530
+ centerOn(point: BoardPoint): void;
1531
+ get(): BoardView;
1532
+ boardToScreen(point: BoardPoint): ScreenPoint;
1533
+ screenToBoard(point: ScreenPoint): BoardPoint;
1534
+ };
1535
+ readonly content: {
1536
+ add(input: BoardObjectInput): string;
1537
+ update(id: string, patch: BoardObjectPatch): void;
1538
+ remove(ids: readonly string[]): void;
1539
+ table: {
1540
+ addRow(tableId: string): void;
1541
+ addCol(tableId: string): void;
1542
+ deleteRow(tableId: string, rowIndex?: number): void;
1543
+ deleteCol(tableId: string, colIndex?: number): void;
1544
+ /** Commit one cell's text, auto-growing its row height for wrapped content. */
1545
+ commitCell(tableId: string, row: number, col: number, text: string): void;
1546
+ };
1547
+ select(ids: readonly string[]): void;
1548
+ import(document: SerializedBoardDocument): readonly string[];
1549
+ };
1550
+ readonly query: {
1551
+ get(id: string): DeepReadonly<BoardObject> | undefined;
1552
+ all(): readonly DeepReadonly<BoardObject>[];
1553
+ search(query: string): readonly SearchHit[];
1554
+ };
1555
+ /** Host-owned comments, synced in for search and marker rendering only — see CommentMarker. */
1556
+ readonly comments: {
1557
+ sync(comments: readonly CommentMarker[]): void;
1558
+ open(id: string): void;
1559
+ setActive(id: string | null): void;
1560
+ };
1561
+ /**
1562
+ * Host-owned collaborator roster, synced in for cursor rendering and
1563
+ * view-following only — see PresenceUser. `subscribe` is intentionally
1564
+ * separate from the controller's own `subscribe`/`getSnapshot`: cursor
1565
+ * updates arrive at pointer-move frequency per remote user, and routing
1566
+ * that through the main snapshot cycle would re-render the whole Board
1567
+ * chrome on every remote mouse move.
1568
+ */
1569
+ readonly presence: {
1570
+ sync(users: readonly PresenceUser[]): void;
1571
+ list(): readonly PresenceUser[];
1572
+ subscribe(listener: () => void): () => void;
1573
+ /** Snap the camera to a peer's view; a no-op while the local user is mid-stroke. */
1574
+ follow(view: PresenceView): void;
1575
+ /** Ease the camera to a peer's view; returns false (no-op) while mid-stroke. */
1576
+ gather(view: PresenceView): boolean;
1577
+ };
1578
+ readonly export: {
1579
+ svg(): string;
1580
+ /** `awaitAssets` (ticket #23) is a best-effort, not-strict wait — see ScrawlEngine.exportPNG. */
1581
+ png(options?: {
1582
+ maxSide?: number;
1583
+ awaitAssets?: boolean;
1584
+ }): Promise<Blob | null>;
1585
+ json(): CurrentSerializedDocument;
1586
+ /** Async, self-contained SVG export (ticket #23) — see ExportDocumentSVGOptions. */
1587
+ svgAsync(options?: ExportDocumentSVGOptions): Promise<ExportDocumentSVGResult>;
1588
+ };
1589
+ /** Host-managed Asset ingestion (ticket #23); throws if no `assetIngestor` is configured. */
1590
+ readonly assets: {
1591
+ ingest(bytes: Uint8Array, mediaType: string, name?: string, signal?: AbortSignal): Promise<AssetRef>;
1592
+ };
1593
+ getSnapshot(): BoardSnapshot;
1594
+ subscribe(listener: () => void): () => void;
1595
+ on<K extends keyof BoardEventMap>(event: K, listener: (payload: BoardEventMap[K]) => void): () => void;
1596
+ setReadOnly(value: boolean): void;
1597
+ flush(): Promise<{
1598
+ state: "idle" | "flushed" | "disabled";
1599
+ }>;
1600
+ dispose(): Promise<void>;
1601
+ }
1602
+ declare function createBoardController(options: CreateBoardControllerOptions): BoardController;
1603
+ type LocalBoardSnapshot = {
1604
+ documentId: string;
1605
+ selectedStrokeId: string | null;
1606
+ strokeCount: number;
1607
+ canUndo: boolean;
1608
+ canRedo: boolean;
1609
+ disposed: boolean;
1610
+ };
1611
+ type LocalBoardOptions = {
1612
+ documentId: string;
1613
+ initialDocument?: SerializedBoardDocument;
1614
+ };
1615
+ type LocalBoard = {
1616
+ drawStroke(stroke: Stroke): void;
1617
+ selectAt(point: BoardPoint): string | null;
1618
+ undo(): void;
1619
+ redo(): void;
1620
+ serialize(): SerializedBoardDocument;
1621
+ getSnapshot(): LocalBoardSnapshot;
1622
+ subscribe(listener: () => void): () => void;
1623
+ dispose(): Promise<void>;
1624
+ };
1625
+ declare function createLocalBoard(options: LocalBoardOptions): LocalBoard;
1626
+
1627
+ type ScrawlThemePreset = "light" | "dark";
1628
+ type ScrawlDensity = "comfortable" | "compact";
1629
+ interface ScrawlTheme {
1630
+ surface?: string;
1631
+ surfaceRaised?: string;
1632
+ surfaceMuted?: string;
1633
+ text?: string;
1634
+ textMuted?: string;
1635
+ edge?: string;
1636
+ focus?: string;
1637
+ selection?: string;
1638
+ danger?: string;
1639
+ warning?: string;
1640
+ success?: string;
1641
+ uiFontFamily?: string;
1642
+ dataFontFamily?: string;
1643
+ baseFontSize?: number;
1644
+ regularWeight?: number;
1645
+ strongWeight?: number;
1646
+ controlRadius?: number;
1647
+ panelRadius?: number;
1648
+ elevationLow?: string;
1649
+ elevationHigh?: string;
1650
+ motionDuration?: number;
1651
+ motionEasing?: string;
1652
+ density?: ScrawlDensity;
1653
+ }
1654
+ type ResolvedScrawlTheme = Required<ScrawlTheme>;
1655
+ interface ScrawlThemeDiagnostic {
1656
+ token: string;
1657
+ message: string;
1658
+ }
1659
+ interface ScrawlResolvedTheme {
1660
+ preset: ScrawlThemePreset;
1661
+ values: ResolvedScrawlTheme;
1662
+ variables: Record<string, string>;
1663
+ diagnostics: readonly ScrawlThemeDiagnostic[];
1664
+ }
1665
+ declare const scrawlThemePresets: Readonly<Record<ScrawlThemePreset, Readonly<ResolvedScrawlTheme>>>;
1666
+ declare function validateScrawlTheme(theme: ScrawlTheme | Record<string, unknown>): ScrawlThemeDiagnostic[];
1667
+ declare function resolveScrawlTheme(preset?: ScrawlThemePreset, theme?: ScrawlTheme | Record<string, unknown>): ScrawlResolvedTheme;
1668
+
1669
+ interface DefaultBoardChromeProps {
1670
+ controller: BoardController;
1671
+ snapshot: BoardSnapshot;
1672
+ renderPortal(children: ReactNode): ReactNode;
1673
+ className?: string;
1674
+ style?: React.CSSProperties;
1675
+ regions?: Partial<Record<DefaultUIRegion, boolean>>;
1676
+ slots?: DefaultUISlots;
1677
+ }
1678
+ type DefaultUIRegion = "tools" | "history" | "view" | "style" | "search" | "import" | "export" | "inlineEditing" | "styleShelf";
1679
+ /** Props for the toolbar/topBar/stylePanel/contextMenu slots. */
1680
+ interface BoardSlotProps {
1681
+ controller: BoardController;
1682
+ snapshot: BoardSnapshot;
1683
+ /** The SDK's own default content for this slot — render it to wrap rather than fully replace. */
1684
+ children?: ReactNode;
1685
+ }
1686
+ /** Props for the dialogs slot — parameterized by whichever panel is currently open. */
1687
+ interface DialogSlotProps {
1688
+ label: string;
1689
+ onClose(): void;
1690
+ children?: ReactNode;
1691
+ }
1692
+ type DefaultUISlot = "toolbar" | "topBar" | "stylePanel" | "dialogs" | "contextMenu";
1693
+ interface DefaultUISlots {
1694
+ toolbar?: ComponentType<BoardSlotProps> | null;
1695
+ topBar?: ComponentType<BoardSlotProps> | null;
1696
+ stylePanel?: ComponentType<BoardSlotProps> | null;
1697
+ dialogs?: ComponentType<DialogSlotProps> | null;
1698
+ /**
1699
+ * No built-in trigger exists yet — nothing in the SDK opens a context
1700
+ * menu today (no right-click or selection-anchored affordance). Included
1701
+ * for API completeness; a Host-supplied component here currently has
1702
+ * nothing to attach to.
1703
+ */
1704
+ contextMenu?: ComponentType<BoardSlotProps> | null;
1705
+ }
1706
+ declare function DefaultBoardChrome({ controller, snapshot, renderPortal, className, style, regions, slots }: DefaultBoardChromeProps): react.JSX.Element;
1707
+
1708
+ interface InlineEditorsProps {
1709
+ controller: BoardController;
1710
+ renderPortal(children: ReactNode): ReactNode;
1711
+ }
1712
+ /** Inline note, text, and table-cell editors — driven entirely by public controller events and commands. */
1713
+ declare function InlineEditors({ controller, renderPortal }: InlineEditorsProps): react.JSX.Element;
1714
+
1715
+ interface MultiplayerCursorsProps {
1716
+ controller: BoardController;
1717
+ onSelectUser?(user: PresenceUser, screenPos: {
1718
+ x: number;
1719
+ y: number;
1720
+ }): void;
1721
+ onJumpToUser?(user: PresenceUser): void;
1722
+ }
1723
+ /**
1724
+ * Remote collaborator cursors and off-screen "jump to" beacons, driven by
1725
+ * controller.presence. Styled entirely inline (not through styles.css) —
1726
+ * unlike toolbar/style-shelf chrome, a Host may reasonably mount this
1727
+ * standalone outside a `[data-scrawl-root]` wrapper.
1728
+ */
1729
+ declare function MultiplayerCursors({ controller, onSelectUser, onJumpToUser }: MultiplayerCursorsProps): react.JSX.Element | null;
1730
+
1731
+ interface StyleShelfProps {
1732
+ controller: BoardController;
1733
+ snapshot: BoardSnapshot;
1734
+ }
1735
+ /** Contextual per-tool style controls — visible while a styleable tool is active. */
1736
+ declare function StyleShelf({ controller, snapshot }: StyleShelfProps): react.JSX.Element | null;
1737
+
1738
+ type ThemeStyle = CSSProperties & Record<`--scrawl-${string}`, string | number | undefined>;
1739
+ interface ScrawlProviderProps {
1740
+ controller: BoardController;
1741
+ children?: ReactNode;
1742
+ preset?: ScrawlThemePreset;
1743
+ theme?: ScrawlTheme;
1744
+ portalContainer?: HTMLElement | null;
1745
+ disposeOnUnmount?: boolean;
1746
+ onThemeDiagnostic?: (diagnostic: ScrawlThemeDiagnostic) => void;
1747
+ className?: string;
1748
+ style?: ThemeStyle;
1749
+ }
1750
+ declare function ScrawlProvider({ controller, children, preset, theme, portalContainer: customPortal, disposeOnUnmount, onThemeDiagnostic, className, style }: ScrawlProviderProps): react.JSX.Element;
1751
+ interface ScrawlProps extends Omit<CreateBoardControllerOptions, "canvas"> {
1752
+ children?: ReactNode;
1753
+ preset?: ScrawlThemePreset;
1754
+ theme?: ScrawlTheme;
1755
+ portalContainer?: HTMLElement | null;
1756
+ className?: string;
1757
+ style?: ThemeStyle;
1758
+ onReady?: (controller: BoardController) => void;
1759
+ onError?: (error: unknown) => void;
1760
+ onThemeDiagnostic?: (diagnostic: ScrawlThemeDiagnostic) => void;
1761
+ /** Provide null for a headless Board, or an existing canvas to control its identity. */
1762
+ canvas?: HTMLCanvasElement | null;
1763
+ }
1764
+ declare function Scrawl({ children, preset, theme, portalContainer, className, style, onReady, onError, onThemeDiagnostic, canvas: suppliedCanvas, ...options }: ScrawlProps): react.JSX.Element;
1765
+ interface ScrawlCanvasProps {
1766
+ element?: HTMLCanvasElement;
1767
+ className?: string;
1768
+ style?: CSSProperties;
1769
+ "aria-label"?: string;
1770
+ }
1771
+ declare function ScrawlCanvas({ element, className, style, "aria-label": ariaLabel }: ScrawlCanvasProps): react.JSX.Element;
1772
+ interface ScrawlDefaultUIProps {
1773
+ className?: string;
1774
+ style?: CSSProperties;
1775
+ /** Disable any migrated region independently while a Host supplies its replacement. */
1776
+ regions?: Partial<Record<DefaultUIRegion, boolean>>;
1777
+ /** Replace (component) or hide (null) a coarse region; omit for the SDK default. */
1778
+ slots?: DefaultUISlots;
1779
+ }
1780
+ declare function ScrawlDefaultUI({ className, style, regions, slots }: ScrawlDefaultUIProps): react.JSX.Element;
1781
+ declare function ScrawlPortal({ children }: {
1782
+ children: ReactNode;
1783
+ }): react.ReactPortal | null;
1784
+ declare function useScrawlController(): BoardController;
1785
+ declare function useScrawlTheme(): ScrawlResolvedTheme;
1786
+ declare function useScrawlSnapshot(): BoardSnapshot;
1787
+ type ScrawlBoardProps = {
1788
+ documentId: string;
1789
+ initialDocument?: SerializedBoardDocument;
1790
+ onReady?: (board: LocalBoard) => void;
1791
+ className?: string;
1792
+ style?: CSSProperties;
1793
+ };
1794
+ declare function ScrawlBoard({ documentId, initialDocument, onReady, className, style }: ScrawlBoardProps): react.JSX.Element;
1795
+
1796
+ export { AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, BEACON_INSET, BOARD_COLOR, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, DefaultBoardChrome, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, 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, 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, avgScale, canUnlockItem, changeToOps, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, createBoardController, createLocalBoard, documentId, documentToSVG, formatTimer, invert, 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 };
1797
+ export type { ApplyOpsResult, BBox, BoardBounds, BoardController, BoardControllerError, BoardEventMap, BoardObject, BoardObjectInput, BoardObjectPatch, BoardPoint, BoardScene, BoardSlotProps, BoardSnapshot, BoardStroke, BoardStyle, BoardView, BuiltInTool, ClusterIdFactory, CollaborationAdapter, CollaborationReceiver, CollaborationSession, CollaborationSnapshot, CollaboratorIdentity, Command, CommandKind, CommentMarker, ControllerOp, CreateBoardControllerOptions, CurrentSerializedDocument, CurrentSerializedStroke, DeepReadonly, DefaultBoardChromeProps, DefaultUIRegion, DefaultUISlot, DefaultUISlots, DialogSlotProps, DocumentChange, DocumentContext, DocumentId, DocumentLoadResult, DocumentRecoveryCode, ImageBlock, InlineEditorsProps, KitchenTimer, LoadResult, LocalBoard, LocalBoardOptions, LocalBoardSnapshot, LockHolder, LockTarget, Lockable, Mat2x3, MultiplayerCursorsProps, NoteVote, Op, OpCollection, PersistenceAdapter, PersistenceDiagnostic, PersistenceSnapshot, PresenceCursor, PresencePlacement, PresenceUser, PresenceView, ReadonlyBoardDocument, ReadonlyDocumentChange, RibbonEdgePoint, ScenePath, ScrawlBoardProps, ScrawlCanvasProps, ScrawlDefaultUIProps, ScrawlProps, ScrawlProviderProps, ScrawlResolvedTheme, ScrawlTheme, ScrawlThemeDiagnostic, ScrawlThemePreset, ScreenPoint, ScreenRect, SearchHit, SearchHitKind, SearchableBoard, SearchableComment, SerializedBoardDocument, SerializedBoardStroke, SerializedPoint, SerializedStroke, StampKind, StickyNote, Stroke, StrokeId, StrokePoint, StrokeTool, StyleShelfProps, TableBlock, TextBlock, ViewportInset };