@scrawl-board/board 0.1.0-beta.5 → 0.1.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,8 +27,10 @@ undo and redo, zoom and fit, search, JSON import, and PNG, SVG, or JSON export.
27
27
  Every control calls a public grouped controller capability. The default keyboard
28
28
  surface is scoped to the focused Board: `P` selects marker, `H` selects
29
29
  highlighter, `V` selects selection, `E` selects eraser, `N` selects note, `T`
30
- selects text, `F` fits content, `Mod+Z` undoes, `Mod+Shift+Z` redoes, and `Mod+F`
31
- opens Board search. Dialogs trap focus, close with Escape, and restore focus to
30
+ selects text, `F` fits content, `Mod+Z` undoes, `Mod+Shift+Z` redoes, `Mod+F`
31
+ opens Board search, and the arrow keys nudge the current selection by a small
32
+ step (`Shift` for a bigger one) across any mix of selected object types, as one
33
+ undo step. Dialogs trap focus, close with Escape, and restore focus to
32
34
  their opener. Hosts that replace the default UI can remove it independently while
33
35
  retaining `ScrawlProvider` and `ScrawlCanvas`, or disable individual `tools`,
34
36
  `history`, `view`, `style`, `search`, `import`, and `export` regions with the
package/dist/browser.d.ts CHANGED
@@ -355,6 +355,133 @@ interface KitchenTimer extends Lockable {
355
355
  runningSince?: number;
356
356
  }
357
357
 
358
+ interface RectangleObject extends Lockable {
359
+ id: string;
360
+ x: number;
361
+ y: number;
362
+ width: number;
363
+ height: number;
364
+ fill?: string;
365
+ stroke?: string;
366
+ strokeWidth?: number;
367
+ /** Corner radius in board units; clamped to at most half the shorter side at render time. */
368
+ cornerRadius?: number;
369
+ /** `[0, 1]`; undefined means fully opaque (Phase 4). */
370
+ opacity?: number;
371
+ /**
372
+ * Radians, about the shape's own center `(x + width/2, y - height/2)`.
373
+ * Undefined means 0 (Phase 3). `x`/`y`/`width`/`height` stay in the
374
+ * shape's own unrotated local frame — rotation is a separate, applied-last
375
+ * transform, not baked into them, matching how Stroke/CustomBoardObject
376
+ * keep geometry and placement independent via their own `matrix`.
377
+ */
378
+ rotation?: number;
379
+ }
380
+ interface EllipseObject extends Lockable {
381
+ id: string;
382
+ x: number;
383
+ y: number;
384
+ width: number;
385
+ height: number;
386
+ fill?: string;
387
+ stroke?: string;
388
+ strokeWidth?: number;
389
+ /** `[0, 1]`; undefined means fully opaque (Phase 4). */
390
+ opacity?: number;
391
+ /** Radians, about the shape's own center — see RectangleObject's `rotation` doc. */
392
+ rotation?: number;
393
+ }
394
+ /** `"none"` is a plain line with no arrowhead; today's only real head shape is `"triangle"`. New head shapes extend this union without touching `ArrowObject`'s own fields. */
395
+ type ArrowHeadStyle = "triangle" | "none";
396
+ interface LineObject extends Lockable {
397
+ id: string;
398
+ start: BoardPoint;
399
+ end: BoardPoint;
400
+ stroke?: string;
401
+ strokeWidth?: number;
402
+ opacity?: number;
403
+ }
404
+ interface ArrowObject extends Lockable {
405
+ id: string;
406
+ start: BoardPoint;
407
+ end: BoardPoint;
408
+ head?: ArrowHeadStyle;
409
+ stroke?: string;
410
+ strokeWidth?: number;
411
+ opacity?: number;
412
+ }
413
+ /**
414
+ * Triangle(3)/Diamond(4)/Pentagon(5)/Hexagon(6)/Octagon(8) as one shared
415
+ * type instead of five near-duplicate interfaces — a regular N-gon
416
+ * inscribed in the same `x`/`y`/`width`/`height`/`rotation` bounding box
417
+ * Rectangle already uses, parameterized by `sides`. Diamond is exactly a
418
+ * 4-sided regular polygon with vertex 0 pointing right (not up, like
419
+ * Triangle/Pentagon/Hexagon) — see `polygonGeometry.ts`'s
420
+ * `polygonStartAngle`, which encodes each side count's own vertex
421
+ * orientation so the outline always matches the legacy drag-preview shape.
422
+ */
423
+ interface PolygonObject extends Lockable {
424
+ id: string;
425
+ x: number;
426
+ y: number;
427
+ width: number;
428
+ height: number;
429
+ sides: 3 | 4 | 5 | 6 | 8;
430
+ fill?: string;
431
+ stroke?: string;
432
+ strokeWidth?: number;
433
+ opacity?: number;
434
+ /** Radians, about the shape's own center — see RectangleObject's `rotation` doc. */
435
+ rotation?: number;
436
+ }
437
+ /** Same bounding-box/rotation convention as Rectangle; a 5-pointed star with a tuned inner-radius ratio, matching the legacy tool's own default (see `polygonGeometry.ts`'s `starPoints`). */
438
+ interface StarObject extends Lockable {
439
+ id: string;
440
+ x: number;
441
+ y: number;
442
+ width: number;
443
+ height: number;
444
+ /** Vertex count; today's only shipped preset is 5, matching the legacy tool. */
445
+ points: number;
446
+ /** `(0, 1)` — inner vertex radius as a fraction of the outer radius. */
447
+ innerRadiusRatio: number;
448
+ fill?: string;
449
+ stroke?: string;
450
+ strokeWidth?: number;
451
+ opacity?: number;
452
+ rotation?: number;
453
+ }
454
+ /** Same bounding-box/rotation convention as Rectangle; the standard parametric heart curve (see `polygonGeometry.ts`'s `heartPoints`), no extra parameters beyond the shared shape fields. */
455
+ interface HeartObject extends Lockable {
456
+ id: string;
457
+ x: number;
458
+ y: number;
459
+ width: number;
460
+ height: number;
461
+ fill?: string;
462
+ stroke?: string;
463
+ strokeWidth?: number;
464
+ opacity?: number;
465
+ rotation?: number;
466
+ }
467
+ /**
468
+ * A logical grouping of other board objects (Phase 3 — Selection,
469
+ * Transformation & Grouping). Deliberately has no `x`/`y`/`transform` of its
470
+ * own — a group's bounds are always derived on demand from its (recursively
471
+ * resolved) children, and "moving/rotating/scaling the group" is exactly a
472
+ * multi-object transform applied to those children, nothing more. A group
473
+ * has no renderer/mesh of its own; its only visual presence is the
474
+ * selection gizmo's bounding box while it's the current selection.
475
+ *
476
+ * `children` may itself contain other group ids (nested groups) — expanding
477
+ * a group into its leaf members is always done by the caller (recursively,
478
+ * with cycle protection), never assumed here.
479
+ */
480
+ interface GroupObject extends Lockable {
481
+ id: string;
482
+ children: string[];
483
+ }
484
+
358
485
  interface BoardPoint {
359
486
  x: number;
360
487
  y: number;
@@ -417,6 +544,31 @@ interface SerializedDocument {
417
544
  timers?: KitchenTimer[];
418
545
  /** Absent in documents saved before Custom board objects existed (ticket #22). */
419
546
  customObjects?: CustomBoardObject[];
547
+ /** Absent in documents saved before semantic Rectangle objects existed (Phase 2). */
548
+ rectangles?: RectangleObject[];
549
+ /** Absent in documents saved before semantic Ellipse objects existed (Phase 2). */
550
+ ellipses?: EllipseObject[];
551
+ /** Absent in documents saved before Groups existed (Phase 3). */
552
+ groups?: GroupObject[];
553
+ /** Absent in documents saved before semantic Line objects existed (Phase 4). */
554
+ lines?: LineObject[];
555
+ /** Absent in documents saved before semantic Arrow objects existed (Phase 4). */
556
+ arrows?: ArrowObject[];
557
+ /** Absent in documents saved before semantic Polygon objects existed (Phase 4). */
558
+ polygons?: PolygonObject[];
559
+ /** Absent in documents saved before semantic Star objects existed (Phase 4). */
560
+ stars?: StarObject[];
561
+ /** Absent in documents saved before semantic Heart objects existed (Phase 4). */
562
+ hearts?: HeartObject[];
563
+ /**
564
+ * Every content-object id (every type above except comments, which are
565
+ * host-synced and never enter this schema) in paint order, back to front.
566
+ * Absent in documents saved before per-object z-order existed (Phase 3) —
567
+ * migration synthesizes a default order preserving the old fixed-Z-band
568
+ * visual stacking exactly, so an existing document never visibly changes
569
+ * on load; only an explicit reorder action touches this from then on.
570
+ */
571
+ objectOrder?: string[];
420
572
  }
421
573
  /**
422
574
  * One collaborator's vote on a note. One per person; toggling removes it.
@@ -616,7 +768,7 @@ interface BoardSnapshot {
616
768
  * here, matching the engine's own internal selection-badge behavior.
617
769
  */
618
770
  interface FocusedItem {
619
- readonly type: "stroke" | "note" | "text" | "table" | "image" | "timer" | "custom";
771
+ readonly type: "stroke" | "note" | "text" | "table" | "image" | "timer" | "rectangle" | "ellipse" | "group" | "line" | "arrow" | "polygon" | "star" | "heart" | "custom";
620
772
  readonly id: string;
621
773
  readonly locked: boolean;
622
774
  readonly lockedBy?: string;
@@ -798,6 +950,22 @@ type BoardObject = ({
798
950
  } & ImageBlock) | ({
799
951
  type: "timer";
800
952
  } & KitchenTimer) | ({
953
+ type: "rectangle";
954
+ } & RectangleObject) | ({
955
+ type: "ellipse";
956
+ } & EllipseObject) | ({
957
+ type: "group";
958
+ } & GroupObject) | ({
959
+ type: "line";
960
+ } & LineObject) | ({
961
+ type: "arrow";
962
+ } & ArrowObject) | ({
963
+ type: "polygon";
964
+ } & PolygonObject) | ({
965
+ type: "star";
966
+ } & StarObject) | ({
967
+ type: "heart";
968
+ } & HeartObject) | ({
801
969
  type: "custom";
802
970
  customType: ObjectType;
803
971
  } & Omit<CustomBoardObject, "type">);
@@ -826,6 +994,30 @@ type BoardObjectInput = {
826
994
  type: "timer";
827
995
  id?: string;
828
996
  } & Omit<KitchenTimer, "id">) | ({
997
+ type: "rectangle";
998
+ id?: string;
999
+ } & Omit<RectangleObject, "id">) | ({
1000
+ type: "ellipse";
1001
+ id?: string;
1002
+ } & Omit<EllipseObject, "id">) | ({
1003
+ type: "group";
1004
+ id?: string;
1005
+ } & Omit<GroupObject, "id">) | ({
1006
+ type: "line";
1007
+ id?: string;
1008
+ } & Omit<LineObject, "id">) | ({
1009
+ type: "arrow";
1010
+ id?: string;
1011
+ } & Omit<ArrowObject, "id">) | ({
1012
+ type: "polygon";
1013
+ id?: string;
1014
+ } & Omit<PolygonObject, "id">) | ({
1015
+ type: "star";
1016
+ id?: string;
1017
+ } & Omit<StarObject, "id">) | ({
1018
+ type: "heart";
1019
+ id?: string;
1020
+ } & Omit<HeartObject, "id">) | ({
829
1021
  type: "custom";
830
1022
  id?: string;
831
1023
  customType: ObjectType;
@@ -887,7 +1079,16 @@ interface ControllerOp {
887
1079
  id: string;
888
1080
  schemaVersion: 1;
889
1081
  kind: "upsert" | "restore" | "remove";
890
- objectType: "stroke" | "note" | "text" | "table" | "image" | "timer" | "custom";
1082
+ objectType: "stroke" | "note" | "text" | "table" | "image" | "timer" | "rectangle" | "ellipse" | "group" | "line" | "arrow" | "polygon" | "star" | "heart" | "custom"
1083
+ /**
1084
+ * A whole-document paint-order sync (Phase 3), not a per-object type —
1085
+ * `objectId` is always the fixed sentinel `"order"` and `payload` is
1086
+ * `{ order: string[] }`. The only `objectType` with no matching
1087
+ * `BoardObject`/document collection; kept in this same union (rather
1088
+ * than a separate wire message) so it flows through the existing
1089
+ * `PersistenceAdapter`/`CollaborationAdapter` opaquely, unchanged.
1090
+ */
1091
+ | "order";
891
1092
  objectId: string;
892
1093
  payload?: unknown;
893
1094
  }
@@ -1000,6 +1201,56 @@ interface BoardController {
1000
1201
  * Unknown ids are silently skipped, matching `remove`'s convention.
1001
1202
  */
1002
1203
  duplicate(ids: readonly string[]): readonly string[];
1204
+ /**
1205
+ * Creates a new Group referencing `ids` as its children and returns its
1206
+ * id, as one undoable step. Unknown ids are silently skipped, matching
1207
+ * `duplicate`/`remove`'s convention. A child id that's itself a group
1208
+ * makes a nested group — expanding nested groups into their leaf
1209
+ * members is always the caller's job, never assumed here (matches the
1210
+ * document-model `GroupObject` itself).
1211
+ */
1212
+ group(ids: readonly string[]): string;
1213
+ /**
1214
+ * Dissolves one group, returning its immediate children's ids (a nested
1215
+ * subgroup among them stays intact, itself still a group) — the group
1216
+ * record itself is removed, the children are untouched. A no-op
1217
+ * (returns `[]`) if `groupId` isn't a group.
1218
+ */
1219
+ ungroup(groupId: string): readonly string[];
1220
+ /**
1221
+ * Aligns every given object's matching edge/center to the corresponding
1222
+ * edge/center of their combined bounding box, as one undoable step.
1223
+ * `"top"`/`"bottom"` follow board space's Y-up convention (`"top"` is
1224
+ * the larger Y). Ids that don't resolve, or resolve to a Group (which
1225
+ * has no position of its own), are skipped. A no-op under 2 resolvable
1226
+ * ids — there's nothing to align relative to.
1227
+ */
1228
+ align(ids: readonly string[], edge: "left" | "right" | "top" | "bottom" | "centerX" | "centerY"): void;
1229
+ /**
1230
+ * Spaces the middle objects' centers evenly between the first and last
1231
+ * (sorted along `axis`), as one undoable step — the two endpoints don't
1232
+ * move. Ids that don't resolve, or resolve to a Group, are skipped. A
1233
+ * no-op under 3 resolvable ids — there's no "middle" to distribute.
1234
+ */
1235
+ distribute(ids: readonly string[], axis: "x" | "y"): void;
1236
+ /**
1237
+ * Snapshots `ids` (recursively expanded through any group, same as
1238
+ * `duplicate`) into an internal in-memory clipboard — never
1239
+ * `navigator.clipboard`, scoped to this one controller instance and
1240
+ * replaced wholesale by the next `copy`/`cut`. Read-only; works even
1241
+ * on a read-only board.
1242
+ */
1243
+ copy(ids: readonly string[]): void;
1244
+ /** `copy`, then removes every resolved object (recursively through any group) as one undoable step. */
1245
+ cut(ids: readonly string[]): void;
1246
+ /**
1247
+ * Clones the current clipboard contents onto the board as one undoable
1248
+ * step, offset the same small cascade `duplicate` uses (no cursor
1249
+ * position to paste relative to yet). Returns the new top-level ids —
1250
+ * a pasted group's own id stands for its (also-pasted) children, which
1251
+ * aren't listed separately. `[]` when the clipboard is empty.
1252
+ */
1253
+ paste(): readonly string[];
1003
1254
  table: {
1004
1255
  addRow(tableId: string): void;
1005
1256
  addCol(tableId: string): void;
@@ -1010,6 +1261,14 @@ interface BoardController {
1010
1261
  };
1011
1262
  select(ids: readonly string[]): void;
1012
1263
  import(document: SerializedBoardDocument): readonly string[];
1264
+ /**
1265
+ * Toggle lock state for the current selection (or focused note/text/
1266
+ * table/image/timer), matching whatever a single Host lock/unlock
1267
+ * control already does per object type. A no-op with nothing selected,
1268
+ * on a headless board, or when every actionable target is locked by
1269
+ * another collaborator who isn't the current lock holder.
1270
+ */
1271
+ toggleSelectionLock(): void;
1013
1272
  };
1014
1273
  readonly query: {
1015
1274
  get(id: string): DeepReadonly<BoardObject> | undefined;