@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.
- package/LICENSE +201 -0
- package/NOTICE +28 -0
- package/README.md +106 -0
- package/dist/browser.d.ts +1012 -0
- package/dist/browser.js +49190 -0
- package/dist/core.d.ts +1084 -0
- package/dist/core.js +2253 -0
- package/dist/index.d.ts +1797 -0
- package/dist/index.js +50827 -0
- package/dist/react.d.ts +1171 -0
- package/dist/react.js +50797 -0
- package/dist/styles.css +297 -0
- package/package.json +72 -0
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,1171 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, ComponentType, CSSProperties } from 'react';
|
|
3
|
+
|
|
4
|
+
declare const strokeIdBrand: unique symbol;
|
|
5
|
+
type StrokeId = string & {
|
|
6
|
+
readonly [strokeIdBrand]: "StrokeId";
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** Wire grammar: `asset:<namespace>:<opaque-id>`. Interpreted only by the Host. */
|
|
10
|
+
type AssetRef = string;
|
|
11
|
+
type AssetKind = "image";
|
|
12
|
+
type AssetPurpose = "render" | "thumbnail" | "export";
|
|
13
|
+
interface AssetResolveRequest {
|
|
14
|
+
ref: AssetRef;
|
|
15
|
+
kind: AssetKind;
|
|
16
|
+
purpose: AssetPurpose;
|
|
17
|
+
signal: AbortSignal;
|
|
18
|
+
}
|
|
19
|
+
interface AssetResolveResult {
|
|
20
|
+
bytes: Uint8Array;
|
|
21
|
+
mediaType: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Host-owned lookup. Scrawl passes only the reference, kind, purpose, and
|
|
25
|
+
* signal — never Document contents, an Extension instance, or credentials.
|
|
26
|
+
* The returned bytes are copied into SDK-owned storage and independently
|
|
27
|
+
* validated before use; a resolver's claimed `mediaType` is never trusted
|
|
28
|
+
* on its own (see `assetValidation.ts`).
|
|
29
|
+
*/
|
|
30
|
+
interface AssetResolver {
|
|
31
|
+
resolve(request: AssetResolveRequest): Promise<AssetResolveResult>;
|
|
32
|
+
}
|
|
33
|
+
interface AssetIngestRequest {
|
|
34
|
+
bytes: Uint8Array;
|
|
35
|
+
mediaType: string;
|
|
36
|
+
name?: string;
|
|
37
|
+
signal: AbortSignal;
|
|
38
|
+
}
|
|
39
|
+
interface AssetIngestResult {
|
|
40
|
+
ref: AssetRef;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Host-owned upload/creation path. Scrawl validates the candidate against
|
|
44
|
+
* SDK limits before calling this, and validates the returned reference
|
|
45
|
+
* before it can enter any command. Cancellation or failure creates no
|
|
46
|
+
* Document object, Op, or history entry.
|
|
47
|
+
*/
|
|
48
|
+
interface AssetIngestor {
|
|
49
|
+
ingest(request: AssetIngestRequest): Promise<AssetIngestResult>;
|
|
50
|
+
}
|
|
51
|
+
type AssetResolutionErrorCode = "resolver-unavailable" | "not-found" | "forbidden" | "offline" | "unsupported-type" | "too-large" | "invalid-content" | "decode-failed" | "budget-exceeded" | "aborted" | "unknown";
|
|
52
|
+
/** Runtime event for a resolution/ingestion failure — never carries credentials or a fetchable location. */
|
|
53
|
+
interface AssetDiagnostic {
|
|
54
|
+
code: AssetResolutionErrorCode;
|
|
55
|
+
ref?: AssetRef;
|
|
56
|
+
objectId: string;
|
|
57
|
+
objectKind: "image" | "custom";
|
|
58
|
+
retryable: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type Mat2x3 = [number, number, number, number, number, number];
|
|
62
|
+
|
|
63
|
+
/** A stable namespaced string, e.g. `com.acme.kanban`. Never a display name. */
|
|
64
|
+
type ExtensionId = string;
|
|
65
|
+
/** A stable namespaced string, e.g. `com.acme.kanban/card-tool`. */
|
|
66
|
+
type ToolId = string;
|
|
67
|
+
/** A stable namespaced string, e.g. `com.acme.kanban/card`. */
|
|
68
|
+
type ObjectType = string;
|
|
69
|
+
interface ExtensionRequirement {
|
|
70
|
+
extensionId: ExtensionId;
|
|
71
|
+
extensionApiVersion: 1;
|
|
72
|
+
}
|
|
73
|
+
interface ScrawlExtension {
|
|
74
|
+
id: ExtensionId;
|
|
75
|
+
extensionApiVersion: 1;
|
|
76
|
+
requires?: readonly ExtensionRequirement[];
|
|
77
|
+
tools?: readonly CustomToolDefinition[];
|
|
78
|
+
objectTypes?: readonly CustomObjectDefinition[];
|
|
79
|
+
}
|
|
80
|
+
type JsonValue = null | boolean | number | string | readonly JsonValue[] | {
|
|
81
|
+
readonly [key: string]: JsonValue;
|
|
82
|
+
};
|
|
83
|
+
type JsonObject = {
|
|
84
|
+
readonly [key: string]: JsonValue;
|
|
85
|
+
};
|
|
86
|
+
interface CustomBoardObject {
|
|
87
|
+
id: string;
|
|
88
|
+
type: ObjectType;
|
|
89
|
+
schemaVersion: number;
|
|
90
|
+
transform: Mat2x3;
|
|
91
|
+
/** Safe placeholder geometry, refreshed by the SDK on every valid command. */
|
|
92
|
+
fallback: {
|
|
93
|
+
bounds: {
|
|
94
|
+
x: number;
|
|
95
|
+
y: number;
|
|
96
|
+
width: number;
|
|
97
|
+
height: number;
|
|
98
|
+
};
|
|
99
|
+
label?: string;
|
|
100
|
+
};
|
|
101
|
+
lock?: {
|
|
102
|
+
holderId: string;
|
|
103
|
+
acquiredAt: number;
|
|
104
|
+
};
|
|
105
|
+
props: JsonValue;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The read-only view handed to `describe`. Deep-readonly by construction
|
|
109
|
+
* (not derived via a shallow `Readonly<>`) because `describe` must treat its
|
|
110
|
+
* input as a pure snapshot — see the spec's "treat `describe` as a pure
|
|
111
|
+
* function" rule.
|
|
112
|
+
*/
|
|
113
|
+
type ReadonlyCustomObject<Props extends JsonValue = JsonValue> = Readonly<{
|
|
114
|
+
id: string;
|
|
115
|
+
type: ObjectType;
|
|
116
|
+
schemaVersion: number;
|
|
117
|
+
transform: Mat2x3;
|
|
118
|
+
fallback: Readonly<{
|
|
119
|
+
bounds: Readonly<{
|
|
120
|
+
x: number;
|
|
121
|
+
y: number;
|
|
122
|
+
width: number;
|
|
123
|
+
height: number;
|
|
124
|
+
}>;
|
|
125
|
+
label?: string;
|
|
126
|
+
}>;
|
|
127
|
+
lock?: Readonly<{
|
|
128
|
+
holderId: string;
|
|
129
|
+
acquiredAt: number;
|
|
130
|
+
}>;
|
|
131
|
+
props: Props;
|
|
132
|
+
}>;
|
|
133
|
+
interface ObjectDescribeContext {
|
|
134
|
+
/** True while this object is part of the current selection. */
|
|
135
|
+
selected: boolean;
|
|
136
|
+
}
|
|
137
|
+
interface CustomObjectDefinition<Props extends JsonValue = JsonValue> {
|
|
138
|
+
type: ObjectType;
|
|
139
|
+
currentSchemaVersion: number;
|
|
140
|
+
/** Validates untrusted persisted/imported/pasted/remote data. Must be pure. */
|
|
141
|
+
parse(input: unknown, schemaVersion: number): Props;
|
|
142
|
+
/** One pure, synchronous step per consecutive schema version. */
|
|
143
|
+
migrate?: Readonly<Record<number, (oldProps: JsonValue) => JsonValue>>;
|
|
144
|
+
describe(object: ReadonlyCustomObject<Props>, context: ObjectDescribeContext): BoardScene;
|
|
145
|
+
}
|
|
146
|
+
interface SceneNodeBase {
|
|
147
|
+
key: string;
|
|
148
|
+
transform?: Mat2x3;
|
|
149
|
+
opacity?: number;
|
|
150
|
+
/** Semantic hit-region id (e.g. `resize-handle`, `cell:2:3`); never a renderer object. */
|
|
151
|
+
interactionRegion?: string;
|
|
152
|
+
}
|
|
153
|
+
interface SceneRect extends SceneNodeBase {
|
|
154
|
+
kind: "rect";
|
|
155
|
+
x: number;
|
|
156
|
+
y: number;
|
|
157
|
+
width: number;
|
|
158
|
+
height: number;
|
|
159
|
+
cornerRadius?: number;
|
|
160
|
+
fill?: string;
|
|
161
|
+
stroke?: string;
|
|
162
|
+
strokeWidth?: number;
|
|
163
|
+
}
|
|
164
|
+
interface SceneText extends SceneNodeBase {
|
|
165
|
+
kind: "text";
|
|
166
|
+
x: number;
|
|
167
|
+
y: number;
|
|
168
|
+
text: string;
|
|
169
|
+
fontSize: number;
|
|
170
|
+
color: string;
|
|
171
|
+
/** Horizontal alignment relative to (x, y); defaults to "start". */
|
|
172
|
+
align?: "start" | "center" | "end";
|
|
173
|
+
}
|
|
174
|
+
interface SceneGroup extends SceneNodeBase {
|
|
175
|
+
kind: "group";
|
|
176
|
+
children: readonly BoardScene[];
|
|
177
|
+
}
|
|
178
|
+
interface ScenePath extends SceneNodeBase {
|
|
179
|
+
kind: "path";
|
|
180
|
+
/** SVG-style path data, board-local coordinates. */
|
|
181
|
+
d: string;
|
|
182
|
+
fill?: string;
|
|
183
|
+
stroke?: string;
|
|
184
|
+
strokeWidth?: number;
|
|
185
|
+
}
|
|
186
|
+
interface SceneImage extends SceneNodeBase {
|
|
187
|
+
kind: "image";
|
|
188
|
+
/** Resolved through the Host asset resolver (ticket #23); never a raw Blob/File/URL. */
|
|
189
|
+
ref: AssetRef;
|
|
190
|
+
x: number;
|
|
191
|
+
y: number;
|
|
192
|
+
width: number;
|
|
193
|
+
height: number;
|
|
194
|
+
/** How the image fills its declared (x, y, width, height) box; defaults to "fill". */
|
|
195
|
+
fit?: "fill" | "contain" | "cover";
|
|
196
|
+
/** Bounded plain-text accessible label, or an explicit decorative opt-out. */
|
|
197
|
+
alt: string | {
|
|
198
|
+
readonly decorative: true;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
interface SceneEllipse extends SceneNodeBase {
|
|
202
|
+
kind: "ellipse";
|
|
203
|
+
cx: number;
|
|
204
|
+
cy: number;
|
|
205
|
+
rx: number;
|
|
206
|
+
ry: number;
|
|
207
|
+
fill?: string;
|
|
208
|
+
stroke?: string;
|
|
209
|
+
strokeWidth?: number;
|
|
210
|
+
}
|
|
211
|
+
type BoardScene = SceneGroup | ScenePath | SceneText | SceneImage | SceneRect | SceneEllipse;
|
|
212
|
+
type ToolCursor = "default" | "crosshair" | "pointer" | "grab" | "grabbing" | "text";
|
|
213
|
+
type ToolCancelReason = "escape-key" | "tool-switched" | "pointer-lost" | "error";
|
|
214
|
+
interface InputModifiers {
|
|
215
|
+
shift: boolean;
|
|
216
|
+
alt: boolean;
|
|
217
|
+
ctrl: boolean;
|
|
218
|
+
meta: boolean;
|
|
219
|
+
}
|
|
220
|
+
interface BoardPointerInput {
|
|
221
|
+
board: BoardPoint;
|
|
222
|
+
viewport: BoardPoint;
|
|
223
|
+
pointerId: number;
|
|
224
|
+
pointerType: "mouse" | "pen" | "touch";
|
|
225
|
+
pressure: number;
|
|
226
|
+
buttons: number;
|
|
227
|
+
modifiers: InputModifiers;
|
|
228
|
+
}
|
|
229
|
+
interface BoardKeyInput {
|
|
230
|
+
key: string;
|
|
231
|
+
modifiers: InputModifiers;
|
|
232
|
+
}
|
|
233
|
+
interface CustomToolDefinition {
|
|
234
|
+
id: ToolId;
|
|
235
|
+
label: string;
|
|
236
|
+
suggestedShortcut?: string;
|
|
237
|
+
cursor?: ToolCursor;
|
|
238
|
+
create(context: ToolCapabilities): CustomTool;
|
|
239
|
+
}
|
|
240
|
+
/** All handlers are synchronous — see the spec's "Lifecycle handlers are synchronous" rule. */
|
|
241
|
+
interface CustomTool {
|
|
242
|
+
activate?(): void;
|
|
243
|
+
pointerDown?(input: BoardPointerInput): void;
|
|
244
|
+
pointerMove?(input: BoardPointerInput): void;
|
|
245
|
+
pointerUp?(input: BoardPointerInput): void;
|
|
246
|
+
keyDown?(input: BoardKeyInput): void;
|
|
247
|
+
keyUp?(input: BoardKeyInput): void;
|
|
248
|
+
cancel?(reason: ToolCancelReason): void;
|
|
249
|
+
deactivate?(): void;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* What an Extension supplies to add a Custom board object. `id` is optional
|
|
253
|
+
* (the controller assigns one when omitted); `schemaVersion` and `fallback`
|
|
254
|
+
* defaults are derived by the controller from the object's definition.
|
|
255
|
+
*/
|
|
256
|
+
interface CustomObjectAddInput {
|
|
257
|
+
type: ObjectType;
|
|
258
|
+
id?: string;
|
|
259
|
+
transform?: Mat2x3;
|
|
260
|
+
fallback?: {
|
|
261
|
+
bounds: {
|
|
262
|
+
x: number;
|
|
263
|
+
y: number;
|
|
264
|
+
width: number;
|
|
265
|
+
height: number;
|
|
266
|
+
};
|
|
267
|
+
label?: string;
|
|
268
|
+
};
|
|
269
|
+
props: JsonValue;
|
|
270
|
+
}
|
|
271
|
+
type ObjectIntent = {
|
|
272
|
+
kind: "add";
|
|
273
|
+
object: CustomObjectAddInput;
|
|
274
|
+
} | {
|
|
275
|
+
kind: "update";
|
|
276
|
+
id: string;
|
|
277
|
+
patch: JsonObject;
|
|
278
|
+
} | {
|
|
279
|
+
kind: "remove";
|
|
280
|
+
id: string;
|
|
281
|
+
};
|
|
282
|
+
interface ExtensionCommand {
|
|
283
|
+
label?: string;
|
|
284
|
+
changes: readonly ObjectIntent[];
|
|
285
|
+
}
|
|
286
|
+
/** A structural, read-only view of any board object (built-in or Custom). */
|
|
287
|
+
type QueryableBoardObject = Readonly<{
|
|
288
|
+
id: string;
|
|
289
|
+
type: string;
|
|
290
|
+
}> & Readonly<Record<string, unknown>>;
|
|
291
|
+
interface ExtensionHitResult {
|
|
292
|
+
readonly objectId: string;
|
|
293
|
+
readonly interactionRegion?: string;
|
|
294
|
+
}
|
|
295
|
+
interface ToolCapabilities {
|
|
296
|
+
query: {
|
|
297
|
+
get(id: string): QueryableBoardObject | undefined;
|
|
298
|
+
selection(): readonly string[];
|
|
299
|
+
hitTest(point: BoardPoint): ExtensionHitResult | undefined;
|
|
300
|
+
};
|
|
301
|
+
coordinates: {
|
|
302
|
+
boardToViewport(point: BoardPoint): BoardPoint;
|
|
303
|
+
viewportToBoard(point: BoardPoint): BoardPoint;
|
|
304
|
+
};
|
|
305
|
+
/** Session-only geometry — never enters Document/history/persistence/collaboration. */
|
|
306
|
+
preview: {
|
|
307
|
+
set(scene: BoardScene): void;
|
|
308
|
+
clear(): void;
|
|
309
|
+
};
|
|
310
|
+
/** Constructs, validates, and commits one atomic command; controller derives Ops. */
|
|
311
|
+
submit(command: ExtensionCommand): void;
|
|
312
|
+
tools: {
|
|
313
|
+
select(toolId: ToolId | "select"): void;
|
|
314
|
+
cancel(): void;
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
interface ExtensionDiagnostic {
|
|
318
|
+
extensionId: ExtensionId;
|
|
319
|
+
phase: "parse" | "migrate" | "describe" | "tool-lifecycle" | "react-companion" | "asset-resolution";
|
|
320
|
+
toolId?: ToolId;
|
|
321
|
+
objectId?: string;
|
|
322
|
+
objectType?: ObjectType;
|
|
323
|
+
recoverable: boolean;
|
|
324
|
+
cause: unknown;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
interface Lockable {
|
|
328
|
+
locked?: boolean;
|
|
329
|
+
lockedBy?: string;
|
|
330
|
+
lockedByName?: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** A kitchen timer sitting on the board. Remaining time is derived, not ticked. */
|
|
334
|
+
interface KitchenTimer extends Lockable {
|
|
335
|
+
id: string;
|
|
336
|
+
x: number;
|
|
337
|
+
y: number;
|
|
338
|
+
/** Face diameter in board units. */
|
|
339
|
+
size: number;
|
|
340
|
+
/** What you set it to — 1, 5, 10, 15 minutes. */
|
|
341
|
+
durationMs: number;
|
|
342
|
+
/** Remaining at the last start or pause. */
|
|
343
|
+
remainingMs: number;
|
|
344
|
+
/** Wall-clock ms when the current run started. Absent means paused. */
|
|
345
|
+
runningSince?: number;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
interface BoardPoint {
|
|
349
|
+
x: number;
|
|
350
|
+
y: number;
|
|
351
|
+
}
|
|
352
|
+
interface StrokePoint extends BoardPoint {
|
|
353
|
+
/** Resolved pressure in [0, 1] — real stylus pressure or velocity simulation. */
|
|
354
|
+
pressure: number;
|
|
355
|
+
/**
|
|
356
|
+
* Erasure channel (ADR 0003), 0..1. Undefined means 0. At or above
|
|
357
|
+
* ERASE_THRESHOLD the point is removed and the stroke may split.
|
|
358
|
+
*/
|
|
359
|
+
erase?: number;
|
|
360
|
+
}
|
|
361
|
+
/** Which drawing tool made a stroke; undefined means marker (back-compat). */
|
|
362
|
+
type StrokeTool = "marker" | "highlighter";
|
|
363
|
+
interface Stroke extends Lockable {
|
|
364
|
+
id: string;
|
|
365
|
+
color: string;
|
|
366
|
+
baseWidth: number;
|
|
367
|
+
tool?: StrokeTool;
|
|
368
|
+
/** Points are stroke-local; `matrix` places them on the board. */
|
|
369
|
+
points: StrokePoint[];
|
|
370
|
+
/** 2D affine transform [a b c d tx ty]; undefined means identity. */
|
|
371
|
+
matrix?: [number, number, number, number, number, number];
|
|
372
|
+
/**
|
|
373
|
+
* Spatial group membership (ADR 0005). Assigned when the stroke is drawn;
|
|
374
|
+
* cluster records are derived from these ids, never stored themselves.
|
|
375
|
+
*/
|
|
376
|
+
clusterId?: string;
|
|
377
|
+
}
|
|
378
|
+
type SerializedPoint = [number, number, number, number];
|
|
379
|
+
interface SerializedStroke extends Lockable {
|
|
380
|
+
id: string;
|
|
381
|
+
color: string;
|
|
382
|
+
baseWidth: number;
|
|
383
|
+
tool?: StrokeTool;
|
|
384
|
+
points: SerializedPoint[];
|
|
385
|
+
/** Omitted when identity. */
|
|
386
|
+
matrix?: [number, number, number, number, number, number];
|
|
387
|
+
clusterId?: string;
|
|
388
|
+
}
|
|
389
|
+
interface SerializedDocument {
|
|
390
|
+
/** Absent in every historical document; current saves always write 1. */
|
|
391
|
+
schemaVersion?: 1;
|
|
392
|
+
strokes: SerializedStroke[];
|
|
393
|
+
/** Absent in documents saved before notes existed. */
|
|
394
|
+
notes?: StickyNote[];
|
|
395
|
+
/** Absent in documents saved before the text tool existed. */
|
|
396
|
+
textBlocks?: TextBlock[];
|
|
397
|
+
/** Absent in documents saved before interactive tables existed. */
|
|
398
|
+
tables?: TableBlock[];
|
|
399
|
+
/** Absent in documents saved before images existed. */
|
|
400
|
+
images?: ImageBlock[];
|
|
401
|
+
/** Absent in documents saved before kitchen timers existed. */
|
|
402
|
+
timers?: KitchenTimer[];
|
|
403
|
+
/** Absent in documents saved before Custom board objects existed (ticket #22). */
|
|
404
|
+
customObjects?: CustomBoardObject[];
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* One collaborator's vote on a note. One per person; toggling removes it.
|
|
408
|
+
*/
|
|
409
|
+
interface NoteVote {
|
|
410
|
+
userId: string;
|
|
411
|
+
name: string;
|
|
412
|
+
color: string;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* A sticky note: content floating above the board at a z-offset (pillar 3 —
|
|
416
|
+
* depth as an organizational axis). Center position in board space.
|
|
417
|
+
*/
|
|
418
|
+
interface StickyNote extends Lockable {
|
|
419
|
+
id: string;
|
|
420
|
+
x: number;
|
|
421
|
+
y: number;
|
|
422
|
+
/** Square side length in board units. */
|
|
423
|
+
size: number;
|
|
424
|
+
/** Height above the board surface; drives shadow offset, blur, and opacity. */
|
|
425
|
+
zOffset: number;
|
|
426
|
+
color: string;
|
|
427
|
+
text: string;
|
|
428
|
+
/** One vote per collaborator. Peel follows the count. */
|
|
429
|
+
votes?: NoteVote[];
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Typed text on the board surface, rendered as SDF glyphs. Position is the
|
|
433
|
+
* top-left corner; lines flow downward (-y). Text joins the clustering
|
|
434
|
+
* system like handwriting (build prompt §6.4).
|
|
435
|
+
*/
|
|
436
|
+
interface TextBlock extends Lockable {
|
|
437
|
+
id: string;
|
|
438
|
+
x: number;
|
|
439
|
+
y: number;
|
|
440
|
+
text: string;
|
|
441
|
+
/** Line height in board units. */
|
|
442
|
+
fontSize: number;
|
|
443
|
+
color: string;
|
|
444
|
+
clusterId?: string;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Interactive structured table on the board. Position (x, y) is top-left in board units.
|
|
448
|
+
* Cells are indexed as `${row},${col}` keys mapping to cell text content.
|
|
449
|
+
*/
|
|
450
|
+
interface TableBlock extends Lockable {
|
|
451
|
+
id: string;
|
|
452
|
+
x: number;
|
|
453
|
+
y: number;
|
|
454
|
+
rows: number;
|
|
455
|
+
cols: number;
|
|
456
|
+
colWidths: number[];
|
|
457
|
+
rowHeights: number[];
|
|
458
|
+
cells: Record<string, string>;
|
|
459
|
+
color?: string;
|
|
460
|
+
backgroundColor?: string;
|
|
461
|
+
clusterId?: string;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* An imported image block on the board plane.
|
|
465
|
+
* Coordinates (x, y) represent the center of the image in board space.
|
|
466
|
+
*/
|
|
467
|
+
interface ImageBlock extends Lockable {
|
|
468
|
+
id: string;
|
|
469
|
+
/**
|
|
470
|
+
* A legacy, read-only data URL (or, historically, an arbitrary string) —
|
|
471
|
+
* never written by new code once `ref` exists. Ticket #23's Host-managed
|
|
472
|
+
* Assets add `ref` as the durable path going forward; `src` and `ref` are
|
|
473
|
+
* mutually exclusive in practice, but both fields exist on every
|
|
474
|
+
* `ImageBlock` so old and new objects share one shape.
|
|
475
|
+
*/
|
|
476
|
+
src: string;
|
|
477
|
+
/** Opaque Asset reference (ticket #23); when present, `src` is ignored. */
|
|
478
|
+
ref?: AssetRef;
|
|
479
|
+
x: number;
|
|
480
|
+
y: number;
|
|
481
|
+
width: number;
|
|
482
|
+
height: number;
|
|
483
|
+
aspectRatio: number;
|
|
484
|
+
name?: string;
|
|
485
|
+
createdAt?: string;
|
|
486
|
+
/** Present when this image is a stamp from the pad, not a photo. */
|
|
487
|
+
stamp?: string;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
declare const CURRENT_DOCUMENT_SCHEMA_VERSION: 1;
|
|
491
|
+
type CurrentSerializedStroke = Omit<SerializedStroke, "id"> & {
|
|
492
|
+
id: StrokeId;
|
|
493
|
+
};
|
|
494
|
+
interface CurrentSerializedDocument extends Required<SerializedDocument> {
|
|
495
|
+
schemaVersion: typeof CURRENT_DOCUMENT_SCHEMA_VERSION;
|
|
496
|
+
strokes: CurrentSerializedStroke[];
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
interface SearchableComment {
|
|
500
|
+
id: string;
|
|
501
|
+
text: string;
|
|
502
|
+
authorName: string;
|
|
503
|
+
x: number;
|
|
504
|
+
y: number;
|
|
505
|
+
replies: readonly {
|
|
506
|
+
text: string;
|
|
507
|
+
}[];
|
|
508
|
+
}
|
|
509
|
+
type SearchHitKind = "note" | "text" | "table" | "comment" | "stamp";
|
|
510
|
+
interface SearchHit {
|
|
511
|
+
kind: SearchHitKind;
|
|
512
|
+
id: string;
|
|
513
|
+
title: string;
|
|
514
|
+
snippet: string;
|
|
515
|
+
x: number;
|
|
516
|
+
y: number;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** A stamp is a small sticker dropped on the board — not ink, not a photo. */
|
|
520
|
+
type StampKind = "star" | "check" | "ship" | "heart" | "plus" | "fire";
|
|
521
|
+
|
|
522
|
+
type SerializedBoardDocument = CurrentSerializedDocument;
|
|
523
|
+
|
|
524
|
+
interface AssetExportFailure {
|
|
525
|
+
objectId: string;
|
|
526
|
+
ref: AssetRef;
|
|
527
|
+
code: AssetResolutionErrorCode;
|
|
528
|
+
}
|
|
529
|
+
interface ExportDocumentSVGOptions {
|
|
530
|
+
missingAssets?: "placeholder";
|
|
531
|
+
signal?: AbortSignal;
|
|
532
|
+
}
|
|
533
|
+
interface ExportDocumentSVGResult {
|
|
534
|
+
svg: string;
|
|
535
|
+
missingAssets: readonly AssetExportFailure[];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
type BuiltInTool = "select" | "pan" | "marker" | "highlighter" | "eraser" | "text" | "note" | "table" | "image" | "comment" | "stamp" | "timer" | `shape:${string}`;
|
|
539
|
+
type PersistenceSnapshot = {
|
|
540
|
+
state: "disabled";
|
|
541
|
+
} | {
|
|
542
|
+
state: "loading" | "idle" | "saving" | "saved";
|
|
543
|
+
} | {
|
|
544
|
+
state: "offline" | "error";
|
|
545
|
+
error?: BoardControllerError;
|
|
546
|
+
};
|
|
547
|
+
type CollaborationSnapshot = {
|
|
548
|
+
state: "disabled";
|
|
549
|
+
} | {
|
|
550
|
+
state: "connecting" | "online" | "reconnecting";
|
|
551
|
+
} | {
|
|
552
|
+
state: "offline" | "error";
|
|
553
|
+
error?: BoardControllerError;
|
|
554
|
+
};
|
|
555
|
+
interface BoardStyle {
|
|
556
|
+
readonly inkColor: string;
|
|
557
|
+
readonly highlightColor: string;
|
|
558
|
+
readonly eraserRadius: number;
|
|
559
|
+
readonly noteColor: string;
|
|
560
|
+
readonly tableRows: number;
|
|
561
|
+
readonly tableCols: number;
|
|
562
|
+
readonly stampKind: StampKind;
|
|
563
|
+
readonly timerDurationMs: number;
|
|
564
|
+
}
|
|
565
|
+
interface BoardSnapshot {
|
|
566
|
+
readonly status: "loading" | "ready" | "disposed";
|
|
567
|
+
readonly documentId: string;
|
|
568
|
+
readonly tool: BuiltInTool | (string & {});
|
|
569
|
+
readonly zoom: number;
|
|
570
|
+
readonly readOnly: boolean;
|
|
571
|
+
readonly selection: readonly string[];
|
|
572
|
+
readonly strokeCount: number;
|
|
573
|
+
readonly objectCount: number;
|
|
574
|
+
readonly canUndo: boolean;
|
|
575
|
+
readonly canRedo: boolean;
|
|
576
|
+
readonly style: BoardStyle;
|
|
577
|
+
readonly connection: {
|
|
578
|
+
readonly persistence: PersistenceSnapshot;
|
|
579
|
+
readonly collaboration: CollaborationSnapshot;
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
interface BoardControllerError {
|
|
583
|
+
source: "controller" | "renderer" | "persistence" | "collaboration";
|
|
584
|
+
code: string;
|
|
585
|
+
retryable: boolean;
|
|
586
|
+
cause?: unknown;
|
|
587
|
+
}
|
|
588
|
+
interface BoardEventMap {
|
|
589
|
+
change: BoardSnapshot;
|
|
590
|
+
"tool-change": {
|
|
591
|
+
tool: string;
|
|
592
|
+
};
|
|
593
|
+
"view-change": {
|
|
594
|
+
x: number;
|
|
595
|
+
y: number;
|
|
596
|
+
zoom: number;
|
|
597
|
+
};
|
|
598
|
+
"selection-change": {
|
|
599
|
+
ids: readonly string[];
|
|
600
|
+
};
|
|
601
|
+
"style-change": BoardStyle;
|
|
602
|
+
"edit-request": {
|
|
603
|
+
kind: "note";
|
|
604
|
+
id: string;
|
|
605
|
+
text: string;
|
|
606
|
+
color: string;
|
|
607
|
+
screenRect: ScreenRect;
|
|
608
|
+
} | {
|
|
609
|
+
kind: "text";
|
|
610
|
+
id: string | null;
|
|
611
|
+
boardX: number;
|
|
612
|
+
boardY: number;
|
|
613
|
+
text: string;
|
|
614
|
+
color: string;
|
|
615
|
+
fontSizePx: number;
|
|
616
|
+
screenRect: ScreenRect;
|
|
617
|
+
} | {
|
|
618
|
+
kind: "table-cell";
|
|
619
|
+
id: string;
|
|
620
|
+
row: number;
|
|
621
|
+
col: number;
|
|
622
|
+
text: string;
|
|
623
|
+
fontSizePx: number;
|
|
624
|
+
screenRect: ScreenRect;
|
|
625
|
+
};
|
|
626
|
+
"import-request": {
|
|
627
|
+
accept: readonly string[];
|
|
628
|
+
};
|
|
629
|
+
"comment-open-request": {
|
|
630
|
+
id: string;
|
|
631
|
+
};
|
|
632
|
+
"comment-draft-request": {
|
|
633
|
+
board: BoardPoint;
|
|
634
|
+
screen: ScreenPoint;
|
|
635
|
+
};
|
|
636
|
+
"pointer-move": {
|
|
637
|
+
board: BoardPoint;
|
|
638
|
+
velocity: BoardPoint;
|
|
639
|
+
};
|
|
640
|
+
"user-navigate": undefined;
|
|
641
|
+
"live-stroke-preview": {
|
|
642
|
+
stroke: Readonly<Stroke>;
|
|
643
|
+
};
|
|
644
|
+
"live-stroke-end": {
|
|
645
|
+
id: string;
|
|
646
|
+
};
|
|
647
|
+
"timer-expired": {
|
|
648
|
+
id: string;
|
|
649
|
+
};
|
|
650
|
+
/** A Custom object failed to parse/migrate/describe, or a Custom tool handler threw. */
|
|
651
|
+
"extension-diagnostic": ExtensionDiagnostic;
|
|
652
|
+
/** An Asset failed to resolve/ingest for a built-in image or a Custom `SceneImage` (ticket #23). */
|
|
653
|
+
"asset-diagnostic": AssetDiagnostic;
|
|
654
|
+
/** A batch of Ops was reconciled (not applied as-sent) by the persistence adapter (ticket #24). */
|
|
655
|
+
"persistence-diagnostic": PersistenceDiagnostic;
|
|
656
|
+
audit: unknown;
|
|
657
|
+
error: BoardControllerError;
|
|
658
|
+
disposed: undefined;
|
|
659
|
+
}
|
|
660
|
+
interface ScreenPoint {
|
|
661
|
+
x: number;
|
|
662
|
+
y: number;
|
|
663
|
+
}
|
|
664
|
+
interface ScreenRect {
|
|
665
|
+
left: number;
|
|
666
|
+
top: number;
|
|
667
|
+
width: number;
|
|
668
|
+
height: number;
|
|
669
|
+
}
|
|
670
|
+
interface BoardView {
|
|
671
|
+
x: number;
|
|
672
|
+
y: number;
|
|
673
|
+
zoom: number;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* A Host-owned comment, summarized for Board-side search and marker
|
|
677
|
+
* rendering. Comments are not Document content — they carry no undo
|
|
678
|
+
* history and never enter the Ops/collaboration pipeline — so this is a
|
|
679
|
+
* read/query capability, not a `content` object type.
|
|
680
|
+
*/
|
|
681
|
+
interface CommentMarker extends SearchableComment {
|
|
682
|
+
readonly authorColor: string;
|
|
683
|
+
readonly resolved: boolean;
|
|
684
|
+
}
|
|
685
|
+
interface PresenceCursor {
|
|
686
|
+
readonly boardX: number;
|
|
687
|
+
readonly boardY: number;
|
|
688
|
+
readonly vx?: number;
|
|
689
|
+
readonly vy?: number;
|
|
690
|
+
}
|
|
691
|
+
/** The engine's native camera shape — not BoardView's zoom, the raw height a peer's camera broadcasts. */
|
|
692
|
+
interface PresenceView {
|
|
693
|
+
readonly x: number;
|
|
694
|
+
readonly y: number;
|
|
695
|
+
readonly height: number;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* A Host-owned collaborator, synced in for cursor/roster rendering only.
|
|
699
|
+
* Presence is ephemeral — it never touches the Document, Ops, undo/redo,
|
|
700
|
+
* or persistence — so this is a read/query capability, not an adapter.
|
|
701
|
+
*/
|
|
702
|
+
interface PresenceUser {
|
|
703
|
+
readonly id: string;
|
|
704
|
+
readonly name: string;
|
|
705
|
+
readonly color: string;
|
|
706
|
+
readonly tool?: string;
|
|
707
|
+
readonly cursor?: PresenceCursor;
|
|
708
|
+
readonly view?: PresenceView;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* The Custom arm wraps `CustomBoardObject` under the same `type` discriminant
|
|
712
|
+
* convention the six built-ins use — `type: "custom"` plus a `customType`
|
|
713
|
+
* field carrying the registered Extension object type (e.g.
|
|
714
|
+
* `"com.scrawl.examples/badge"`). This is deliberate, not cosmetic: giving
|
|
715
|
+
* the arm a literal `"custom"` discriminant (instead of exposing
|
|
716
|
+
* `CustomBoardObject`'s own `type: string` directly) keeps every existing
|
|
717
|
+
* `object.type === "table"`-style check elsewhere in this codebase safely
|
|
718
|
+
* exhaustive — a non-literal `string` arm mixed into this union would make
|
|
719
|
+
* TypeScript unable to prove any built-in case excludes it. `toBoardObject`/
|
|
720
|
+
* `toCustomBoardObject` convert to/from the unwrapped `CustomBoardObject`
|
|
721
|
+
* envelope at the boundaries (document storage, wire Ops) that use it directly.
|
|
722
|
+
*/
|
|
723
|
+
type BoardObject = ({
|
|
724
|
+
type: "stroke";
|
|
725
|
+
} & Stroke) | ({
|
|
726
|
+
type: "note";
|
|
727
|
+
} & StickyNote) | ({
|
|
728
|
+
type: "text";
|
|
729
|
+
} & TextBlock) | ({
|
|
730
|
+
type: "table";
|
|
731
|
+
} & TableBlock) | ({
|
|
732
|
+
type: "image";
|
|
733
|
+
} & ImageBlock) | ({
|
|
734
|
+
type: "timer";
|
|
735
|
+
} & KitchenTimer) | ({
|
|
736
|
+
type: "custom";
|
|
737
|
+
customType: ObjectType;
|
|
738
|
+
} & Omit<CustomBoardObject, "type">);
|
|
739
|
+
type BoardObjectInput = {
|
|
740
|
+
type: "stroke";
|
|
741
|
+
id?: string;
|
|
742
|
+
color: string;
|
|
743
|
+
baseWidth: number;
|
|
744
|
+
tool?: Stroke["tool"];
|
|
745
|
+
points: StrokePoint[];
|
|
746
|
+
matrix?: Stroke["matrix"];
|
|
747
|
+
clusterId?: string;
|
|
748
|
+
} | ({
|
|
749
|
+
type: "note";
|
|
750
|
+
id?: string;
|
|
751
|
+
} & Omit<StickyNote, "id">) | ({
|
|
752
|
+
type: "text";
|
|
753
|
+
id?: string;
|
|
754
|
+
} & Omit<TextBlock, "id">) | ({
|
|
755
|
+
type: "table";
|
|
756
|
+
id?: string;
|
|
757
|
+
} & Omit<TableBlock, "id">) | ({
|
|
758
|
+
type: "image";
|
|
759
|
+
id?: string;
|
|
760
|
+
} & Omit<ImageBlock, "id">) | ({
|
|
761
|
+
type: "timer";
|
|
762
|
+
id?: string;
|
|
763
|
+
} & Omit<KitchenTimer, "id">) | ({
|
|
764
|
+
type: "custom";
|
|
765
|
+
id?: string;
|
|
766
|
+
customType: ObjectType;
|
|
767
|
+
} & Omit<CustomObjectAddInput, "type" | "id">);
|
|
768
|
+
type BoardObjectPatch = Record<string, unknown>;
|
|
769
|
+
type DeepReadonly<T> = T extends (...args: never[]) => unknown ? T : T extends readonly (infer Item)[] ? readonly DeepReadonly<Item>[] : T extends object ? {
|
|
770
|
+
readonly [Key in keyof T]: DeepReadonly<T[Key]>;
|
|
771
|
+
} : T;
|
|
772
|
+
interface ReadonlyDocumentChange {
|
|
773
|
+
readonly added: readonly DeepReadonly<BoardObject>[];
|
|
774
|
+
readonly updated: readonly DeepReadonly<BoardObject>[];
|
|
775
|
+
readonly removed: readonly string[];
|
|
776
|
+
}
|
|
777
|
+
interface ReadonlyBoardDocument {
|
|
778
|
+
readonly id: string;
|
|
779
|
+
get(id: string): DeepReadonly<BoardObject> | undefined;
|
|
780
|
+
all(): IterableIterator<DeepReadonly<BoardObject>>;
|
|
781
|
+
subscribe(listener: (change: ReadonlyDocumentChange) => void): () => void;
|
|
782
|
+
}
|
|
783
|
+
interface DocumentContext {
|
|
784
|
+
documentId: string;
|
|
785
|
+
signal: AbortSignal;
|
|
786
|
+
}
|
|
787
|
+
type LoadResult = {
|
|
788
|
+
state: "found";
|
|
789
|
+
document: CurrentSerializedDocument;
|
|
790
|
+
revision: string;
|
|
791
|
+
} | {
|
|
792
|
+
state: "missing";
|
|
793
|
+
};
|
|
794
|
+
interface PersistenceAdapter {
|
|
795
|
+
load(context: DocumentContext): Promise<LoadResult>;
|
|
796
|
+
applyOps(context: DocumentContext, ops: readonly ControllerOp[]): Promise<ApplyOpsResult>;
|
|
797
|
+
replace(context: DocumentContext, document: CurrentSerializedDocument, baseRevision: string): Promise<unknown>;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* `"reconcile"` (ticket #24) means the server authoritatively resolved the
|
|
801
|
+
* whole batch — some Ops it accepted, `rejectedOpIds` it didn't (a stale
|
|
802
|
+
* tombstoned id, a permission change, an unrecognized schema, or a
|
|
803
|
+
* conflicting concurrent edit). The batch is never re-queued in this case;
|
|
804
|
+
* the controller instead reloads authoritative state via `load()`.
|
|
805
|
+
*/
|
|
806
|
+
type ApplyOpsResult = {
|
|
807
|
+
state: "applied";
|
|
808
|
+
revision: string;
|
|
809
|
+
} | {
|
|
810
|
+
state: "reconcile";
|
|
811
|
+
revision: string;
|
|
812
|
+
rejectedOpIds: readonly string[];
|
|
813
|
+
reason: "tombstone" | "permission" | "schema" | "conflict";
|
|
814
|
+
};
|
|
815
|
+
/** A batch of Ops was reconciled (not applied as-sent) by the persistence adapter (ticket #24). */
|
|
816
|
+
interface PersistenceDiagnostic {
|
|
817
|
+
code: "tombstone" | "permission" | "schema" | "conflict";
|
|
818
|
+
rejectedOpIds: readonly string[];
|
|
819
|
+
revision: string;
|
|
820
|
+
}
|
|
821
|
+
interface ControllerOp {
|
|
822
|
+
id: string;
|
|
823
|
+
schemaVersion: 1;
|
|
824
|
+
kind: "upsert" | "restore" | "remove";
|
|
825
|
+
objectType: "stroke" | "note" | "text" | "table" | "image" | "timer" | "custom";
|
|
826
|
+
objectId: string;
|
|
827
|
+
payload?: unknown;
|
|
828
|
+
}
|
|
829
|
+
interface CollaborationAdapter {
|
|
830
|
+
connect(options: DocumentContext & {
|
|
831
|
+
identity: CollaboratorIdentity;
|
|
832
|
+
receive: CollaborationReceiver;
|
|
833
|
+
}): Promise<CollaborationSession>;
|
|
834
|
+
}
|
|
835
|
+
interface CollaboratorIdentity {
|
|
836
|
+
id: string;
|
|
837
|
+
name: string;
|
|
838
|
+
color?: string;
|
|
839
|
+
}
|
|
840
|
+
interface CollaborationReceiver {
|
|
841
|
+
ops(ops: readonly ControllerOp[]): void;
|
|
842
|
+
status(state: "online" | "reconnecting" | "offline"): void;
|
|
843
|
+
error(cause: unknown): void;
|
|
844
|
+
}
|
|
845
|
+
interface CollaborationSession {
|
|
846
|
+
sendOps(ops: readonly ControllerOp[]): void;
|
|
847
|
+
close(): Promise<void>;
|
|
848
|
+
}
|
|
849
|
+
interface CreateBoardControllerOptions {
|
|
850
|
+
document: {
|
|
851
|
+
id: string;
|
|
852
|
+
initial?: SerializedBoardDocument;
|
|
853
|
+
};
|
|
854
|
+
/** Canvas-backed controllers delegate rendering and normalized input to the private engine. */
|
|
855
|
+
canvas?: HTMLCanvasElement;
|
|
856
|
+
adapters?: {
|
|
857
|
+
persistence?: PersistenceAdapter;
|
|
858
|
+
collaboration?: CollaborationAdapter;
|
|
859
|
+
};
|
|
860
|
+
identity?: CollaboratorIdentity;
|
|
861
|
+
createId?: () => string;
|
|
862
|
+
/**
|
|
863
|
+
* Trusted Custom tool/object registrations (ticket #22, design:
|
|
864
|
+
* docs/research/extension-contracts.md). Validated atomically at
|
|
865
|
+
* construction; registration failure throws before any controller is
|
|
866
|
+
* returned. Not yet re-exported from a public package entry point —
|
|
867
|
+
* internal-only until the reference Extension proves the seam.
|
|
868
|
+
*/
|
|
869
|
+
extensions?: readonly ScrawlExtension[];
|
|
870
|
+
/**
|
|
871
|
+
* Optional Host-managed Asset capabilities (ticket #23, design:
|
|
872
|
+
* docs/research/asset-resolution-resource-policy.md). Without a
|
|
873
|
+
* resolver, referenced Assets preserve their Document geometry and
|
|
874
|
+
* render an accessible placeholder. Not yet re-exported from a public
|
|
875
|
+
* package entry point — internal-only until the reference resolver
|
|
876
|
+
* proves the seam, matching how `extensions` is scoped.
|
|
877
|
+
*/
|
|
878
|
+
assetResolver?: AssetResolver;
|
|
879
|
+
assetIngestor?: AssetIngestor;
|
|
880
|
+
/** Clamped to 64–512MiB; defaults to 256MiB. */
|
|
881
|
+
assetCacheBytes?: number;
|
|
882
|
+
}
|
|
883
|
+
interface BoardController {
|
|
884
|
+
readonly document: ReadonlyBoardDocument;
|
|
885
|
+
readonly tools: {
|
|
886
|
+
select(tool: BuiltInTool | (string & {})): void;
|
|
887
|
+
current(): string;
|
|
888
|
+
};
|
|
889
|
+
readonly style: {
|
|
890
|
+
setInkColor(color: string): void;
|
|
891
|
+
set(patch: Partial<{
|
|
892
|
+
inkColor: string;
|
|
893
|
+
highlightColor: string;
|
|
894
|
+
eraserRadius: number;
|
|
895
|
+
noteColor: string;
|
|
896
|
+
tableRows: number;
|
|
897
|
+
tableCols: number;
|
|
898
|
+
stampKind: StampKind;
|
|
899
|
+
timerDurationMs: number;
|
|
900
|
+
}>): void;
|
|
901
|
+
current(): BoardStyle;
|
|
902
|
+
};
|
|
903
|
+
readonly history: {
|
|
904
|
+
undo(): void;
|
|
905
|
+
redo(): void;
|
|
906
|
+
};
|
|
907
|
+
readonly view: {
|
|
908
|
+
fit(): void;
|
|
909
|
+
zoomTo(value: number): void;
|
|
910
|
+
centerOn(point: BoardPoint): void;
|
|
911
|
+
get(): BoardView;
|
|
912
|
+
boardToScreen(point: BoardPoint): ScreenPoint;
|
|
913
|
+
screenToBoard(point: ScreenPoint): BoardPoint;
|
|
914
|
+
};
|
|
915
|
+
readonly content: {
|
|
916
|
+
add(input: BoardObjectInput): string;
|
|
917
|
+
update(id: string, patch: BoardObjectPatch): void;
|
|
918
|
+
remove(ids: readonly string[]): void;
|
|
919
|
+
table: {
|
|
920
|
+
addRow(tableId: string): void;
|
|
921
|
+
addCol(tableId: string): void;
|
|
922
|
+
deleteRow(tableId: string, rowIndex?: number): void;
|
|
923
|
+
deleteCol(tableId: string, colIndex?: number): void;
|
|
924
|
+
/** Commit one cell's text, auto-growing its row height for wrapped content. */
|
|
925
|
+
commitCell(tableId: string, row: number, col: number, text: string): void;
|
|
926
|
+
};
|
|
927
|
+
select(ids: readonly string[]): void;
|
|
928
|
+
import(document: SerializedBoardDocument): readonly string[];
|
|
929
|
+
};
|
|
930
|
+
readonly query: {
|
|
931
|
+
get(id: string): DeepReadonly<BoardObject> | undefined;
|
|
932
|
+
all(): readonly DeepReadonly<BoardObject>[];
|
|
933
|
+
search(query: string): readonly SearchHit[];
|
|
934
|
+
};
|
|
935
|
+
/** Host-owned comments, synced in for search and marker rendering only — see CommentMarker. */
|
|
936
|
+
readonly comments: {
|
|
937
|
+
sync(comments: readonly CommentMarker[]): void;
|
|
938
|
+
open(id: string): void;
|
|
939
|
+
setActive(id: string | null): void;
|
|
940
|
+
};
|
|
941
|
+
/**
|
|
942
|
+
* Host-owned collaborator roster, synced in for cursor rendering and
|
|
943
|
+
* view-following only — see PresenceUser. `subscribe` is intentionally
|
|
944
|
+
* separate from the controller's own `subscribe`/`getSnapshot`: cursor
|
|
945
|
+
* updates arrive at pointer-move frequency per remote user, and routing
|
|
946
|
+
* that through the main snapshot cycle would re-render the whole Board
|
|
947
|
+
* chrome on every remote mouse move.
|
|
948
|
+
*/
|
|
949
|
+
readonly presence: {
|
|
950
|
+
sync(users: readonly PresenceUser[]): void;
|
|
951
|
+
list(): readonly PresenceUser[];
|
|
952
|
+
subscribe(listener: () => void): () => void;
|
|
953
|
+
/** Snap the camera to a peer's view; a no-op while the local user is mid-stroke. */
|
|
954
|
+
follow(view: PresenceView): void;
|
|
955
|
+
/** Ease the camera to a peer's view; returns false (no-op) while mid-stroke. */
|
|
956
|
+
gather(view: PresenceView): boolean;
|
|
957
|
+
};
|
|
958
|
+
readonly export: {
|
|
959
|
+
svg(): string;
|
|
960
|
+
/** `awaitAssets` (ticket #23) is a best-effort, not-strict wait — see ScrawlEngine.exportPNG. */
|
|
961
|
+
png(options?: {
|
|
962
|
+
maxSide?: number;
|
|
963
|
+
awaitAssets?: boolean;
|
|
964
|
+
}): Promise<Blob | null>;
|
|
965
|
+
json(): CurrentSerializedDocument;
|
|
966
|
+
/** Async, self-contained SVG export (ticket #23) — see ExportDocumentSVGOptions. */
|
|
967
|
+
svgAsync(options?: ExportDocumentSVGOptions): Promise<ExportDocumentSVGResult>;
|
|
968
|
+
};
|
|
969
|
+
/** Host-managed Asset ingestion (ticket #23); throws if no `assetIngestor` is configured. */
|
|
970
|
+
readonly assets: {
|
|
971
|
+
ingest(bytes: Uint8Array, mediaType: string, name?: string, signal?: AbortSignal): Promise<AssetRef>;
|
|
972
|
+
};
|
|
973
|
+
getSnapshot(): BoardSnapshot;
|
|
974
|
+
subscribe(listener: () => void): () => void;
|
|
975
|
+
on<K extends keyof BoardEventMap>(event: K, listener: (payload: BoardEventMap[K]) => void): () => void;
|
|
976
|
+
setReadOnly(value: boolean): void;
|
|
977
|
+
flush(): Promise<{
|
|
978
|
+
state: "idle" | "flushed" | "disabled";
|
|
979
|
+
}>;
|
|
980
|
+
dispose(): Promise<void>;
|
|
981
|
+
}
|
|
982
|
+
type LocalBoardSnapshot = {
|
|
983
|
+
documentId: string;
|
|
984
|
+
selectedStrokeId: string | null;
|
|
985
|
+
strokeCount: number;
|
|
986
|
+
canUndo: boolean;
|
|
987
|
+
canRedo: boolean;
|
|
988
|
+
disposed: boolean;
|
|
989
|
+
};
|
|
990
|
+
type LocalBoard = {
|
|
991
|
+
drawStroke(stroke: Stroke): void;
|
|
992
|
+
selectAt(point: BoardPoint): string | null;
|
|
993
|
+
undo(): void;
|
|
994
|
+
redo(): void;
|
|
995
|
+
serialize(): SerializedBoardDocument;
|
|
996
|
+
getSnapshot(): LocalBoardSnapshot;
|
|
997
|
+
subscribe(listener: () => void): () => void;
|
|
998
|
+
dispose(): Promise<void>;
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
type ScrawlThemePreset = "light" | "dark";
|
|
1002
|
+
type ScrawlDensity = "comfortable" | "compact";
|
|
1003
|
+
interface ScrawlTheme {
|
|
1004
|
+
surface?: string;
|
|
1005
|
+
surfaceRaised?: string;
|
|
1006
|
+
surfaceMuted?: string;
|
|
1007
|
+
text?: string;
|
|
1008
|
+
textMuted?: string;
|
|
1009
|
+
edge?: string;
|
|
1010
|
+
focus?: string;
|
|
1011
|
+
selection?: string;
|
|
1012
|
+
danger?: string;
|
|
1013
|
+
warning?: string;
|
|
1014
|
+
success?: string;
|
|
1015
|
+
uiFontFamily?: string;
|
|
1016
|
+
dataFontFamily?: string;
|
|
1017
|
+
baseFontSize?: number;
|
|
1018
|
+
regularWeight?: number;
|
|
1019
|
+
strongWeight?: number;
|
|
1020
|
+
controlRadius?: number;
|
|
1021
|
+
panelRadius?: number;
|
|
1022
|
+
elevationLow?: string;
|
|
1023
|
+
elevationHigh?: string;
|
|
1024
|
+
motionDuration?: number;
|
|
1025
|
+
motionEasing?: string;
|
|
1026
|
+
density?: ScrawlDensity;
|
|
1027
|
+
}
|
|
1028
|
+
type ResolvedScrawlTheme = Required<ScrawlTheme>;
|
|
1029
|
+
interface ScrawlThemeDiagnostic {
|
|
1030
|
+
token: string;
|
|
1031
|
+
message: string;
|
|
1032
|
+
}
|
|
1033
|
+
interface ScrawlResolvedTheme {
|
|
1034
|
+
preset: ScrawlThemePreset;
|
|
1035
|
+
values: ResolvedScrawlTheme;
|
|
1036
|
+
variables: Record<string, string>;
|
|
1037
|
+
diagnostics: readonly ScrawlThemeDiagnostic[];
|
|
1038
|
+
}
|
|
1039
|
+
declare const scrawlThemePresets: Readonly<Record<ScrawlThemePreset, Readonly<ResolvedScrawlTheme>>>;
|
|
1040
|
+
declare function validateScrawlTheme(theme: ScrawlTheme | Record<string, unknown>): ScrawlThemeDiagnostic[];
|
|
1041
|
+
declare function resolveScrawlTheme(preset?: ScrawlThemePreset, theme?: ScrawlTheme | Record<string, unknown>): ScrawlResolvedTheme;
|
|
1042
|
+
|
|
1043
|
+
interface DefaultBoardChromeProps {
|
|
1044
|
+
controller: BoardController;
|
|
1045
|
+
snapshot: BoardSnapshot;
|
|
1046
|
+
renderPortal(children: ReactNode): ReactNode;
|
|
1047
|
+
className?: string;
|
|
1048
|
+
style?: React.CSSProperties;
|
|
1049
|
+
regions?: Partial<Record<DefaultUIRegion, boolean>>;
|
|
1050
|
+
slots?: DefaultUISlots;
|
|
1051
|
+
}
|
|
1052
|
+
type DefaultUIRegion = "tools" | "history" | "view" | "style" | "search" | "import" | "export" | "inlineEditing" | "styleShelf";
|
|
1053
|
+
/** Props for the toolbar/topBar/stylePanel/contextMenu slots. */
|
|
1054
|
+
interface BoardSlotProps {
|
|
1055
|
+
controller: BoardController;
|
|
1056
|
+
snapshot: BoardSnapshot;
|
|
1057
|
+
/** The SDK's own default content for this slot — render it to wrap rather than fully replace. */
|
|
1058
|
+
children?: ReactNode;
|
|
1059
|
+
}
|
|
1060
|
+
/** Props for the dialogs slot — parameterized by whichever panel is currently open. */
|
|
1061
|
+
interface DialogSlotProps {
|
|
1062
|
+
label: string;
|
|
1063
|
+
onClose(): void;
|
|
1064
|
+
children?: ReactNode;
|
|
1065
|
+
}
|
|
1066
|
+
type DefaultUISlot = "toolbar" | "topBar" | "stylePanel" | "dialogs" | "contextMenu";
|
|
1067
|
+
interface DefaultUISlots {
|
|
1068
|
+
toolbar?: ComponentType<BoardSlotProps> | null;
|
|
1069
|
+
topBar?: ComponentType<BoardSlotProps> | null;
|
|
1070
|
+
stylePanel?: ComponentType<BoardSlotProps> | null;
|
|
1071
|
+
dialogs?: ComponentType<DialogSlotProps> | null;
|
|
1072
|
+
/**
|
|
1073
|
+
* No built-in trigger exists yet — nothing in the SDK opens a context
|
|
1074
|
+
* menu today (no right-click or selection-anchored affordance). Included
|
|
1075
|
+
* for API completeness; a Host-supplied component here currently has
|
|
1076
|
+
* nothing to attach to.
|
|
1077
|
+
*/
|
|
1078
|
+
contextMenu?: ComponentType<BoardSlotProps> | null;
|
|
1079
|
+
}
|
|
1080
|
+
declare function DefaultBoardChrome({ controller, snapshot, renderPortal, className, style, regions, slots }: DefaultBoardChromeProps): react.JSX.Element;
|
|
1081
|
+
|
|
1082
|
+
interface InlineEditorsProps {
|
|
1083
|
+
controller: BoardController;
|
|
1084
|
+
renderPortal(children: ReactNode): ReactNode;
|
|
1085
|
+
}
|
|
1086
|
+
/** Inline note, text, and table-cell editors — driven entirely by public controller events and commands. */
|
|
1087
|
+
declare function InlineEditors({ controller, renderPortal }: InlineEditorsProps): react.JSX.Element;
|
|
1088
|
+
|
|
1089
|
+
interface MultiplayerCursorsProps {
|
|
1090
|
+
controller: BoardController;
|
|
1091
|
+
onSelectUser?(user: PresenceUser, screenPos: {
|
|
1092
|
+
x: number;
|
|
1093
|
+
y: number;
|
|
1094
|
+
}): void;
|
|
1095
|
+
onJumpToUser?(user: PresenceUser): void;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Remote collaborator cursors and off-screen "jump to" beacons, driven by
|
|
1099
|
+
* controller.presence. Styled entirely inline (not through styles.css) —
|
|
1100
|
+
* unlike toolbar/style-shelf chrome, a Host may reasonably mount this
|
|
1101
|
+
* standalone outside a `[data-scrawl-root]` wrapper.
|
|
1102
|
+
*/
|
|
1103
|
+
declare function MultiplayerCursors({ controller, onSelectUser, onJumpToUser }: MultiplayerCursorsProps): react.JSX.Element | null;
|
|
1104
|
+
|
|
1105
|
+
interface StyleShelfProps {
|
|
1106
|
+
controller: BoardController;
|
|
1107
|
+
snapshot: BoardSnapshot;
|
|
1108
|
+
}
|
|
1109
|
+
/** Contextual per-tool style controls — visible while a styleable tool is active. */
|
|
1110
|
+
declare function StyleShelf({ controller, snapshot }: StyleShelfProps): react.JSX.Element | null;
|
|
1111
|
+
|
|
1112
|
+
type ThemeStyle = CSSProperties & Record<`--scrawl-${string}`, string | number | undefined>;
|
|
1113
|
+
interface ScrawlProviderProps {
|
|
1114
|
+
controller: BoardController;
|
|
1115
|
+
children?: ReactNode;
|
|
1116
|
+
preset?: ScrawlThemePreset;
|
|
1117
|
+
theme?: ScrawlTheme;
|
|
1118
|
+
portalContainer?: HTMLElement | null;
|
|
1119
|
+
disposeOnUnmount?: boolean;
|
|
1120
|
+
onThemeDiagnostic?: (diagnostic: ScrawlThemeDiagnostic) => void;
|
|
1121
|
+
className?: string;
|
|
1122
|
+
style?: ThemeStyle;
|
|
1123
|
+
}
|
|
1124
|
+
declare function ScrawlProvider({ controller, children, preset, theme, portalContainer: customPortal, disposeOnUnmount, onThemeDiagnostic, className, style }: ScrawlProviderProps): react.JSX.Element;
|
|
1125
|
+
interface ScrawlProps extends Omit<CreateBoardControllerOptions, "canvas"> {
|
|
1126
|
+
children?: ReactNode;
|
|
1127
|
+
preset?: ScrawlThemePreset;
|
|
1128
|
+
theme?: ScrawlTheme;
|
|
1129
|
+
portalContainer?: HTMLElement | null;
|
|
1130
|
+
className?: string;
|
|
1131
|
+
style?: ThemeStyle;
|
|
1132
|
+
onReady?: (controller: BoardController) => void;
|
|
1133
|
+
onError?: (error: unknown) => void;
|
|
1134
|
+
onThemeDiagnostic?: (diagnostic: ScrawlThemeDiagnostic) => void;
|
|
1135
|
+
/** Provide null for a headless Board, or an existing canvas to control its identity. */
|
|
1136
|
+
canvas?: HTMLCanvasElement | null;
|
|
1137
|
+
}
|
|
1138
|
+
declare function Scrawl({ children, preset, theme, portalContainer, className, style, onReady, onError, onThemeDiagnostic, canvas: suppliedCanvas, ...options }: ScrawlProps): react.JSX.Element;
|
|
1139
|
+
interface ScrawlCanvasProps {
|
|
1140
|
+
element?: HTMLCanvasElement;
|
|
1141
|
+
className?: string;
|
|
1142
|
+
style?: CSSProperties;
|
|
1143
|
+
"aria-label"?: string;
|
|
1144
|
+
}
|
|
1145
|
+
declare function ScrawlCanvas({ element, className, style, "aria-label": ariaLabel }: ScrawlCanvasProps): react.JSX.Element;
|
|
1146
|
+
interface ScrawlDefaultUIProps {
|
|
1147
|
+
className?: string;
|
|
1148
|
+
style?: CSSProperties;
|
|
1149
|
+
/** Disable any migrated region independently while a Host supplies its replacement. */
|
|
1150
|
+
regions?: Partial<Record<DefaultUIRegion, boolean>>;
|
|
1151
|
+
/** Replace (component) or hide (null) a coarse region; omit for the SDK default. */
|
|
1152
|
+
slots?: DefaultUISlots;
|
|
1153
|
+
}
|
|
1154
|
+
declare function ScrawlDefaultUI({ className, style, regions, slots }: ScrawlDefaultUIProps): react.JSX.Element;
|
|
1155
|
+
declare function ScrawlPortal({ children }: {
|
|
1156
|
+
children: ReactNode;
|
|
1157
|
+
}): react.ReactPortal | null;
|
|
1158
|
+
declare function useScrawlController(): BoardController;
|
|
1159
|
+
declare function useScrawlTheme(): ScrawlResolvedTheme;
|
|
1160
|
+
declare function useScrawlSnapshot(): BoardSnapshot;
|
|
1161
|
+
type ScrawlBoardProps = {
|
|
1162
|
+
documentId: string;
|
|
1163
|
+
initialDocument?: SerializedBoardDocument;
|
|
1164
|
+
onReady?: (board: LocalBoard) => void;
|
|
1165
|
+
className?: string;
|
|
1166
|
+
style?: CSSProperties;
|
|
1167
|
+
};
|
|
1168
|
+
declare function ScrawlBoard({ documentId, initialDocument, onReady, className, style }: ScrawlBoardProps): react.JSX.Element;
|
|
1169
|
+
|
|
1170
|
+
export { DefaultBoardChrome, InlineEditors, MultiplayerCursors, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, StyleShelf, resolveScrawlTheme, scrawlThemePresets, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
|
|
1171
|
+
export type { BoardController, BoardSlotProps, BoardSnapshot, BoardStyle, CommentMarker, CreateBoardControllerOptions, DefaultBoardChromeProps, DefaultUIRegion, DefaultUISlot, DefaultUISlots, DialogSlotProps, InlineEditorsProps, LocalBoard, LocalBoardSnapshot, MultiplayerCursorsProps, PresenceCursor, PresenceUser, PresenceView, ScrawlBoardProps, ScrawlCanvasProps, ScrawlDefaultUIProps, ScrawlProps, ScrawlProviderProps, ScrawlResolvedTheme, ScrawlTheme, ScrawlThemeDiagnostic, ScrawlThemePreset, StyleShelfProps };
|