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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -151,7 +151,11 @@ function validateStrokes(raw) {
151
151
  for (let index = 0; index < raw.length; index += 1) {
152
152
  const value = raw[index];
153
153
  const path = `strokes[${index}]`;
154
- if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, ["marker", "highlighter"]) || !validLock(value) || !optionalString(value.clusterId) || !validMatrix(value.matrix) || !Array.isArray(value.points)) return validation("invalid stroke", path);
154
+ if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, [
155
+ "marker",
156
+ "highlighter",
157
+ "shape"
158
+ ]) || !validLock(value) || !optionalString(value.clusterId) || !validMatrix(value.matrix) || !Array.isArray(value.points)) return validation("invalid stroke", path);
155
159
  const points = [];
156
160
  for (let pointIndex = 0; pointIndex < value.points.length; pointIndex += 1) {
157
161
  const point = normalizePoint(value.points[pointIndex]);
@@ -432,9 +436,21 @@ var HIGHLIGHT_COLORS = {
432
436
  };
433
437
  var NOTE_COLORS = {
434
438
  yellow: "#FDE68A",
439
+ amber: "#FCD34D",
440
+ orange: "#FDBA74",
441
+ peach: "#FED7AA",
442
+ red: "#FCA5A5",
443
+ rose: "#FECACA",
435
444
  pink: "#FBCFE8",
445
+ magenta: "#F9A8D4",
446
+ purple: "#D8B4FE",
447
+ lavender: "#E9D5FF",
436
448
  blue: "#BFDBFE",
437
- green: "#BBF7D0"
449
+ cornflower: "#93C5FD",
450
+ sky: "#7DD3FC",
451
+ cyan: "#BAE6FD",
452
+ green: "#BBF7D0",
453
+ mint: "#86EFAC"
438
454
  };
439
455
  var TEXT_DEFAULT_SIZE = 1.8;
440
456
  function cloneText(block) {
@@ -35964,12 +35980,16 @@ var FRAGMENT = `
35964
35980
  return max(lineAlpha2.x, lineAlpha2.y);
35965
35981
  }
35966
35982
 
35967
- // Distance from p's nearest grid intersection, in world units. Dot radius
35968
- // matches the configured line width in pixels, same anti-aliasing.
35983
+ // Distance from p's nearest grid intersection, in world units. A line's
35984
+ // width reads as visible even at 1px because it spans the whole screen;
35985
+ // an isolated dot at that same 1px radius gets almost entirely eaten by
35986
+ // anti-aliasing and all but disappears. Scaling the radius up keeps a
35987
+ // dot's on-screen weight comparable to a line drawn with the same
35988
+ // uGridLineWidthPx, rather than using it literally as a radius.
35969
35989
  float gridDotAlpha(vec2 p) {
35970
35990
  vec2 cell = mod(p, uGridSpacing) - uGridSpacing * 0.5;
35971
35991
  float dist = length(cell);
35972
- float radius = uGridLineWidthPx * uWorldPerPixel;
35992
+ float radius = uGridLineWidthPx * uWorldPerPixel * 1.75;
35973
35993
  float aa = uWorldPerPixel;
35974
35994
  return 1.0 - smoothstep(radius - aa * 0.5, radius + aa * 0.5, dist);
35975
35995
  }
@@ -36546,6 +36566,7 @@ var StrokeRenderer = class {
36546
36566
  if (mesh) {
36547
36567
  mesh.geometry.dispose();
36548
36568
  mesh.geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke));
36569
+ mesh.material = stroke.tool === "highlighter" ? this.materials.highlight(stroke.color) : this.materials.get(stroke.color);
36549
36570
  }
36550
36571
  }
36551
36572
  for (const stroke of change.transformed) {
@@ -44725,7 +44746,7 @@ function distToSegmentSq(p, a, b) {
44725
44746
  }
44726
44747
  //#endregion
44727
44748
  //#region src/interaction/tools/eraserTool.ts
44728
- var DECAY_PER_PASS = .4;
44749
+ var DECAY_PER_PASS = 1;
44729
44750
  /** A point can take another decay pass after this long — rubbing works. */
44730
44751
  var REARM_MS = 250;
44731
44752
  var EraserTool = class {
@@ -44819,19 +44840,11 @@ var EraserTool = class {
44819
44840
  const runs = partitionByErasure(current.points);
44820
44841
  const survives = runs.filter((r) => r.survives && r.points.length >= 2);
44821
44842
  const keepId = survives.length === 1 && survives[0].points.length === current.points.length;
44822
- const m = current.matrix;
44823
44843
  for (const run of runs) if (run.survives && run.points.length >= 2) after.push({
44824
44844
  ...current,
44825
44845
  id: keepId ? current.id : crypto.randomUUID(),
44826
44846
  points: run.points
44827
44847
  });
44828
- else if (!run.survives) {
44829
- const world = m ? run.points.map((p) => ({
44830
- ...p,
44831
- ...apply(m, p)
44832
- })) : run.points;
44833
- this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1), current.tool !== "shape");
44834
- }
44835
44848
  }
44836
44849
  doc.removeStrokes(before.map((s) => s.id));
44837
44850
  doc.addStrokes(after.map(cloneStroke));
@@ -45039,10 +45052,10 @@ function buildShape(kind, a, b, step, constrain) {
45039
45052
  case "ellipse": return [ellipse(a, targetB, step)];
45040
45053
  case "line": return [sampleSegment(a, constrain ? snappedEnd(a, b) : b, step)];
45041
45054
  case "arrow": return arrow(a, constrain ? snappedEnd(a, b) : b, step);
45042
- case "triangle": return [polygon(3, a, targetB, step, -Math.PI / 2)];
45055
+ case "triangle": return [polygon(3, a, targetB, step, Math.PI / 2)];
45043
45056
  case "diamond": return [polygon(4, a, targetB, step, 0)];
45044
- case "pentagon": return [polygon(5, a, targetB, step, -Math.PI / 2)];
45045
- case "hexagon": return [polygon(6, a, targetB, step, -Math.PI / 2)];
45057
+ case "pentagon": return [polygon(5, a, targetB, step, Math.PI / 2)];
45058
+ case "hexagon": return [polygon(6, a, targetB, step, Math.PI / 2)];
45046
45059
  case "octagon": return [polygon(8, a, targetB, step, Math.PI / 8)];
45047
45060
  case "star": return [star(5, a, targetB, step)];
45048
45061
  case "heart": return [heart(a, targetB, step)];
@@ -45241,6 +45254,7 @@ var ShapeTool = class {
45241
45254
  this.ctx.clusters.assign(strokes[0]);
45242
45255
  for (const s of strokes) s.clusterId = strokes[0].clusterId;
45243
45256
  this.ctx.history.execute(new AddStrokesCommand(strokes));
45257
+ this.ctx.selection.set(strokes.map((s) => s.id));
45244
45258
  this.ctx.setTool?.("select");
45245
45259
  }
45246
45260
  this.ctx.transition("idle");
@@ -45822,6 +45836,13 @@ var HANDLE_HIT_PX = 12;
45822
45836
  var ROTATE_OFFSET_PX = 26;
45823
45837
  var ROTATE_HIT_PX = 14;
45824
45838
  var COLOR = 2450411;
45839
+ /**
45840
+ * Visual-only outset between the selected object and the gizmo's outline/
45841
+ * handles — the object's actual bounding box (used for resize math, see
45842
+ * `anchorPoint`/`handlePoint`'s callers in `selectTool.ts`) never changes;
45843
+ * only where the outline and handle meshes are *drawn* and hit-tested does.
45844
+ */
45845
+ var GAP_PX = 8;
45825
45846
  var SelectionGizmo = class {
45826
45847
  constructor(scene) {
45827
45848
  this.group = new Group();
@@ -45836,7 +45857,7 @@ var SelectionGizmo = class {
45836
45857
  this.outline = new LineLoop(new BufferGeometry(), this.lineMaterial);
45837
45858
  this.outline.position.z = GIZMO_Z;
45838
45859
  this.outline.frustumCulled = false;
45839
- const handleGeometry = new PlaneGeometry(1, 1);
45860
+ const handleGeometry = new CircleGeometry(.5, 20);
45840
45861
  const handleMaterial = new MeshBasicMaterial({ color: COLOR });
45841
45862
  const makeHandle = () => new Mesh(handleGeometry, handleMaterial);
45842
45863
  this.handles = {
@@ -45864,18 +45885,19 @@ var SelectionGizmo = class {
45864
45885
  if (!box) return;
45865
45886
  this.lineMaterial.color.setHex(locked ? 16096779 : COLOR);
45866
45887
  this.rotateHandle.visible = !locked;
45888
+ const visual = this.visualBox(box, wpp);
45867
45889
  const positions = new Float32Array([
45868
- box.minX,
45869
- box.minY,
45890
+ visual.minX,
45891
+ visual.minY,
45870
45892
  GIZMO_Z,
45871
- box.maxX,
45872
- box.minY,
45893
+ visual.maxX,
45894
+ visual.minY,
45873
45895
  GIZMO_Z,
45874
- box.maxX,
45875
- box.maxY,
45896
+ visual.maxX,
45897
+ visual.maxY,
45876
45898
  GIZMO_Z,
45877
- box.minX,
45878
- box.maxY,
45899
+ visual.minX,
45900
+ visual.maxY,
45879
45901
  GIZMO_Z
45880
45902
  ]);
45881
45903
  this.outline.geometry.dispose();
@@ -45887,17 +45909,27 @@ var SelectionGizmo = class {
45887
45909
  for (const [handle, mesh] of Object.entries(this.handles)) {
45888
45910
  mesh.visible = !locked;
45889
45911
  if (!locked) {
45890
- const p = this.handlePoint(handle, box);
45912
+ const p = this.handlePoint(handle, visual);
45891
45913
  mesh.position.set(p.x, p.y, GIZMO_Z);
45892
45914
  mesh.scale.set(size, size, 1);
45893
45915
  }
45894
45916
  }
45895
45917
  if (!locked) {
45896
- const rotate = this.rotatePoint(box, wpp);
45918
+ const rotate = this.rotatePoint(visual, wpp);
45897
45919
  this.rotateHandle.position.set(rotate.x, rotate.y, GIZMO_Z);
45898
45920
  this.rotateHandle.scale.set(size, size, 1);
45899
45921
  }
45900
45922
  }
45923
+ /** The object's real bbox outset by `GAP_PX`, screen-constant — where the outline/handles are drawn and hit-tested. Never used for resize math (see `anchorPoint`). */
45924
+ visualBox(box, wpp) {
45925
+ const gap = GAP_PX * wpp;
45926
+ return {
45927
+ minX: box.minX - gap,
45928
+ minY: box.minY - gap,
45929
+ maxX: box.maxX + gap,
45930
+ maxY: box.maxY + gap
45931
+ };
45932
+ }
45901
45933
  handlePoint(handle, box) {
45902
45934
  const midX = (box.minX + box.maxX) / 2;
45903
45935
  const midY = (box.minY + box.maxY) / 2;
@@ -45985,7 +46017,8 @@ var SelectionGizmo = class {
45985
46017
  if (!this.box || this.isLocked) return null;
45986
46018
  const handleR = HANDLE_HIT_PX * this.wpp;
45987
46019
  const edgeSlop = 6 * this.wpp;
45988
- const rotate = this.rotatePoint(this.box, this.wpp);
46020
+ const visual = this.visualBox(this.box, this.wpp);
46021
+ const rotate = this.rotatePoint(visual, this.wpp);
45989
46022
  if (Math.hypot(p.x - rotate.x, p.y - rotate.y) < ROTATE_HIT_PX * this.wpp) return { kind: "rotate" };
45990
46023
  for (const handle of [
45991
46024
  "nw",
@@ -45997,13 +46030,13 @@ var SelectionGizmo = class {
45997
46030
  "e",
45998
46031
  "w"
45999
46032
  ]) {
46000
- const hp = this.handlePoint(handle, this.box);
46033
+ const hp = this.handlePoint(handle, visual);
46001
46034
  if (Math.abs(p.x - hp.x) < handleR && Math.abs(p.y - hp.y) < handleR) return {
46002
46035
  kind: "scale",
46003
46036
  handle
46004
46037
  };
46005
46038
  }
46006
- const { minX, maxX, minY, maxY } = this.box;
46039
+ const { minX, maxX, minY, maxY } = visual;
46007
46040
  const inX = p.x >= minX - edgeSlop && p.x <= maxX + edgeSlop;
46008
46041
  const inY = p.y >= minY - edgeSlop && p.y <= maxY + edgeSlop;
46009
46042
  if (inX && Math.abs(p.y - maxY) < edgeSlop) return {
@@ -46023,7 +46056,7 @@ var SelectionGizmo = class {
46023
46056
  handle: "w"
46024
46057
  };
46025
46058
  const pad = 2 * this.wpp;
46026
- if (p.x >= minX - pad && p.x <= maxX + pad && p.y >= minY - pad && p.y <= maxY + pad) return { kind: "inside" };
46059
+ if (p.x >= this.box.minX - pad && p.x <= this.box.maxX + pad && p.y >= this.box.minY - pad && p.y <= this.box.maxY + pad) return { kind: "inside" };
46027
46060
  return null;
46028
46061
  }
46029
46062
  rotatePoint(box, wpp) {
@@ -48256,6 +48289,112 @@ function createBoardControllerWithEngine(options, existingEngine) {
48256
48289
  })),
48257
48290
  ...[...document.allCustomObjects()].map((value) => clone(toBoardObject(value)))
48258
48291
  ];
48292
+ const projectToScreen = (point) => engine ? engine.boardToScreen(point) : {
48293
+ x: (point.x - view.x) * view.zoom,
48294
+ y: (view.y - point.y) * view.zoom
48295
+ };
48296
+ const lockFields = (item) => ({
48297
+ locked: !!item.locked,
48298
+ ...item.lockedBy ? { lockedBy: item.lockedBy } : {},
48299
+ ...item.lockedByName ? { lockedByName: item.lockedByName } : {}
48300
+ });
48301
+ const lockFieldsMany = (items) => {
48302
+ const locked = items.filter((i) => i.locked);
48303
+ if (locked.length === 0) return { locked: false };
48304
+ const lockedBys = [...new Set(locked.map((i) => i.lockedBy).filter((v) => !!v))];
48305
+ const names = [...new Set(locked.map((i) => i.lockedByName).filter((v) => !!v))];
48306
+ return {
48307
+ locked: true,
48308
+ ...lockedBys.length === 1 ? { lockedBy: lockedBys[0] } : {},
48309
+ ...names.length === 1 ? { lockedByName: names[0] } : {}
48310
+ };
48311
+ };
48312
+ const strokeClusterAnchor = (strokes) => {
48313
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
48314
+ for (const s of strokes) for (const p of s.points) {
48315
+ const world = s.matrix ? apply(s.matrix, p) : p;
48316
+ minX = Math.min(minX, world.x);
48317
+ maxX = Math.max(maxX, world.x);
48318
+ minY = Math.min(minY, world.y);
48319
+ maxY = Math.max(maxY, world.y);
48320
+ }
48321
+ if (!Number.isFinite(minX)) return projectToScreen({
48322
+ x: 0,
48323
+ y: 0
48324
+ });
48325
+ return projectToScreen({
48326
+ x: (minX + maxX) / 2,
48327
+ y: maxY + 1.4
48328
+ });
48329
+ };
48330
+ /** Per-type anchor formulas mirror `ScrawlEngine.getSelectedItemInfo` exactly, so a Host-built toolbar lands in the same spot the SDK's own would. */
48331
+ const anchorFor = (obj) => {
48332
+ switch (obj.type) {
48333
+ case "note": return projectToScreen({
48334
+ x: obj.x,
48335
+ y: obj.y + obj.size / 2 + 1
48336
+ });
48337
+ case "text": {
48338
+ const { width } = measureTextBlock(obj.text, obj.fontSize);
48339
+ return projectToScreen({
48340
+ x: obj.x + width / 2,
48341
+ y: obj.y + 1
48342
+ });
48343
+ }
48344
+ case "table": {
48345
+ const { width } = measureTable(obj);
48346
+ return projectToScreen({
48347
+ x: obj.x + width / 2,
48348
+ y: obj.y + 1.2
48349
+ });
48350
+ }
48351
+ case "image": return projectToScreen({
48352
+ x: obj.x,
48353
+ y: obj.y + obj.height / 2 + 1.2
48354
+ });
48355
+ case "timer": return projectToScreen({
48356
+ x: obj.x,
48357
+ y: obj.y + obj.size / 2 + 1.2
48358
+ });
48359
+ case "custom": {
48360
+ const b = obj.fallback.bounds;
48361
+ return projectToScreen(apply(obj.transform, {
48362
+ x: b.x + b.width / 2,
48363
+ y: b.y + b.height + 1
48364
+ }));
48365
+ }
48366
+ case "stroke": return strokeClusterAnchor([obj]);
48367
+ }
48368
+ };
48369
+ const computeFocusedItem = () => {
48370
+ if (selection.length === 0) return null;
48371
+ if (selection.length === 1) {
48372
+ const obj = readObject(selection[0]);
48373
+ if (!obj) return null;
48374
+ if (obj.type === "custom") return {
48375
+ type: "custom",
48376
+ id: obj.id,
48377
+ locked: false,
48378
+ screenPosition: anchorFor(obj)
48379
+ };
48380
+ return {
48381
+ type: obj.type,
48382
+ id: obj.id,
48383
+ ...lockFields(obj),
48384
+ screenPosition: anchorFor(obj)
48385
+ };
48386
+ }
48387
+ const objects = selection.map(readObject);
48388
+ if (!objects.every((o) => o?.type === "stroke")) return null;
48389
+ const clusterId = objects[0].clusterId;
48390
+ if (!clusterId || !objects.every((o) => o.clusterId === clusterId)) return null;
48391
+ return {
48392
+ type: "stroke",
48393
+ id: objects[0].id,
48394
+ ...lockFieldsMany(objects),
48395
+ screenPosition: strokeClusterAnchor(objects)
48396
+ };
48397
+ };
48259
48398
  const updateObject = (id, patch) => {
48260
48399
  assertMutable();
48261
48400
  const before = readObject(id);
@@ -48297,6 +48436,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
48297
48436
  zoom: view.zoom,
48298
48437
  readOnly,
48299
48438
  selection: Object.freeze([...selection]),
48439
+ focusedItem: computeFocusedItem(),
48300
48440
  strokeCount: [...document.all()].length,
48301
48441
  objectCount: allObjects().length,
48302
48442
  canUndo: history.canUndo,
@@ -48711,10 +48851,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
48711
48851
  changed();
48712
48852
  },
48713
48853
  get: () => ({ ...view }),
48714
- boardToScreen: (point) => engine ? engine.boardToScreen(point) : {
48715
- x: (point.x - view.x) * view.zoom,
48716
- y: (view.y - point.y) * view.zoom
48717
- },
48854
+ boardToScreen: (point) => projectToScreen(point),
48718
48855
  screenToBoard: (point) => engine ? engine.screenToBoard(point.x, point.y) ?? {
48719
48856
  x: point.x,
48720
48857
  y: point.y
@@ -48751,6 +48888,81 @@ function createBoardControllerWithEngine(options, existingEngine) {
48751
48888
  update(id, patch) {
48752
48889
  updateObject(id, patch);
48753
48890
  },
48891
+ duplicate(ids) {
48892
+ assertMutable();
48893
+ const objects = [...new Set(ids)].map(readObject).filter((o) => !!o);
48894
+ if (objects.length === 0) return [];
48895
+ const DX = 1.5;
48896
+ const DY = -1.5;
48897
+ const newClusterIds = /* @__PURE__ */ new Map();
48898
+ const clusterIdFor = (oldClusterId) => {
48899
+ let next = newClusterIds.get(oldClusterId);
48900
+ if (!next) {
48901
+ next = crypto.randomUUID();
48902
+ newClusterIds.set(oldClusterId, next);
48903
+ }
48904
+ return next;
48905
+ };
48906
+ const commands = [];
48907
+ const newIds = [];
48908
+ for (const obj of objects) {
48909
+ const id = createId();
48910
+ newIds.push(id);
48911
+ const rest = { ...obj };
48912
+ delete rest.locked;
48913
+ delete rest.lockedBy;
48914
+ delete rest.lockedByName;
48915
+ let duplicated;
48916
+ if (obj.type === "stroke") {
48917
+ const matrix = obj.matrix ?? [
48918
+ 1,
48919
+ 0,
48920
+ 0,
48921
+ 1,
48922
+ 0,
48923
+ 0
48924
+ ];
48925
+ duplicated = {
48926
+ ...rest,
48927
+ id,
48928
+ matrix: [
48929
+ matrix[0],
48930
+ matrix[1],
48931
+ matrix[2],
48932
+ matrix[3],
48933
+ matrix[4] + DX,
48934
+ matrix[5] + DY
48935
+ ],
48936
+ ...obj.clusterId ? { clusterId: clusterIdFor(obj.clusterId) } : {}
48937
+ };
48938
+ } else if (obj.type === "custom") {
48939
+ const t = obj.transform;
48940
+ duplicated = {
48941
+ ...rest,
48942
+ id,
48943
+ transform: [
48944
+ t[0],
48945
+ t[1],
48946
+ t[2],
48947
+ t[3],
48948
+ t[4] + DX,
48949
+ t[5] + DY
48950
+ ]
48951
+ };
48952
+ delete duplicated.lock;
48953
+ } else duplicated = {
48954
+ ...rest,
48955
+ id,
48956
+ x: obj.x + DX,
48957
+ y: obj.y + DY
48958
+ };
48959
+ validateBoardObject(duplicated.type, duplicated);
48960
+ commands.push(addCommand(duplicated.type, duplicated));
48961
+ }
48962
+ executeHistory(() => history.execute(new CommandBatch("duplicate content", commands)));
48963
+ changed();
48964
+ return newIds;
48965
+ },
48754
48966
  table: {
48755
48967
  addRow(tableId) {
48756
48968
  const table = document.getTable(tableId);
@@ -49698,14 +49910,17 @@ function InlineEditors({ controller, renderPortal }) {
49698
49910
  if (editor?.kind === "text") {
49699
49911
  const text = editor.text;
49700
49912
  if (editor.id === null) {
49701
- if (text.trim()) controller.content.add({
49702
- type: "text",
49703
- x: editor.boardX,
49704
- y: editor.boardY,
49705
- text,
49706
- color: editor.color,
49707
- fontSize: TEXT_DEFAULT_SIZE
49708
- });
49913
+ if (text.trim()) {
49914
+ const newId = controller.content.add({
49915
+ type: "text",
49916
+ x: editor.boardX,
49917
+ y: editor.boardY,
49918
+ text,
49919
+ color: editor.color,
49920
+ fontSize: TEXT_DEFAULT_SIZE
49921
+ });
49922
+ controller.content.select([newId]);
49923
+ }
49709
49924
  } else if (!text.trim()) controller.content.remove([editor.id]);
49710
49925
  else controller.content.update(editor.id, { text });
49711
49926
  }
@@ -49900,6 +50115,366 @@ function InlineEditors({ controller, renderPortal }) {
49900
50115
  }))] });
49901
50116
  }
49902
50117
  //#endregion
50118
+ //#region src/ui/toolbar/focused-item-toolbar.tsx
50119
+ var NOTE_SIZES = [
50120
+ ["Small", 7],
50121
+ ["Medium", 10],
50122
+ ["Large", 14]
50123
+ ];
50124
+ var TEXT_SIZES = [
50125
+ ["Small", 1.2],
50126
+ ["Medium", 1.8],
50127
+ ["Large", 2.6]
50128
+ ];
50129
+ var SHAPE_WIDTHS = [
50130
+ ["Thin", .1],
50131
+ ["Medium", .15],
50132
+ ["Thick", .3]
50133
+ ];
50134
+ /** Matches the native selection gizmo's own outset (renderer/selection/gizmo.ts's GAP_PX), for a consistent look. */
50135
+ var HANDLE_GAP_PX = 8;
50136
+ var MIN_SIZE = 4;
50137
+ var MAX_SIZE = 30;
50138
+ var HANDLE_CORNERS = [
50139
+ "nw",
50140
+ "ne",
50141
+ "sw",
50142
+ "se"
50143
+ ];
50144
+ /**
50145
+ * Floating toolbar above the focused note, shape, or text block — Colour,
50146
+ * Size (note/text) or Width (shape), Lock/Unlock, Duplicate, Delete.
50147
+ * Table/image/timer/custom objects and plain (non-shape) ink strokes never
50148
+ * get a toolbar here — out of scope for this destination.
50149
+ *
50150
+ * Lock/Unlock carries no per-user ownership gating: nothing else in this
50151
+ * SDK enforces lock ownership either (`content.update` never checks
50152
+ * `locked`, and the engine's own internal unlock check has no way for a
50153
+ * Host to ever supply a real user id) — locking is advisory UI state
50154
+ * throughout, and this toolbar matches that rather than inventing an
50155
+ * enforcement story alone.
50156
+ *
50157
+ * A shape can be several strokes sharing one `clusterId` (e.g. an arrow's
50158
+ * shaft + head) — every action here applies to the whole cluster. Resolved
50159
+ * via `query.all()` + `clusterId`, not `snapshot.selection`: a canvas click
50160
+ * on one member selects every member internally, but the public selection
50161
+ * bridge only ever reports one id (`FocusedItem.id` is singular by design),
50162
+ * so reconstructing the cluster from the document is the reliable path
50163
+ * regardless of how the selection was made.
50164
+ *
50165
+ * A focused note also gets drag-to-resize corner handles — notes have no
50166
+ * gizmo of their own (the native selection gizmo is ink-stroke/shape-only),
50167
+ * so this is that capability's default-ui-owned equivalent. Proportional
50168
+ * (always-square) resize from the note's own centre, clamped to
50169
+ * `[MIN_SIZE, MAX_SIZE]`, independent of which corner is grabbed.
50170
+ */
50171
+ function FocusedItemToolbar({ controller, snapshot }) {
50172
+ const focused = snapshot.focusedItem;
50173
+ if (!focused) return null;
50174
+ const obj = controller.query.get(focused.id);
50175
+ if (!obj) return null;
50176
+ if (obj.type === "note") return /* @__PURE__ */ jsx(NoteToolbar, {
50177
+ controller,
50178
+ snapshot,
50179
+ note: obj,
50180
+ locked: focused.locked,
50181
+ anchor: focused.screenPosition
50182
+ });
50183
+ if (obj.type === "text") return /* @__PURE__ */ jsx(TextToolbar, {
50184
+ controller,
50185
+ snapshot,
50186
+ text: obj,
50187
+ locked: focused.locked,
50188
+ anchor: focused.screenPosition
50189
+ });
50190
+ if (obj.type === "stroke" && obj.tool === "shape") return /* @__PURE__ */ jsx(ShapeToolbar, {
50191
+ controller,
50192
+ snapshot,
50193
+ stroke: obj,
50194
+ locked: focused.locked,
50195
+ anchor: focused.screenPosition
50196
+ });
50197
+ return null;
50198
+ }
50199
+ function ToolbarShell({ anchor, children }) {
50200
+ if (!anchor) return null;
50201
+ return /* @__PURE__ */ jsx("div", {
50202
+ "aria-label": "Selected object",
50203
+ className: "scrawl-board__focus-toolbar",
50204
+ role: "toolbar",
50205
+ style: {
50206
+ left: anchor.x,
50207
+ top: anchor.y
50208
+ },
50209
+ children
50210
+ });
50211
+ }
50212
+ function NoteToolbar({ controller, snapshot, note, locked, anchor }) {
50213
+ const mutable = snapshot.status === "ready" && !snapshot.readOnly;
50214
+ const locked_ = !mutable || locked;
50215
+ const half = note.size / 2;
50216
+ const cornerA = controller.view.boardToScreen({
50217
+ x: note.x - half,
50218
+ y: note.y + half
50219
+ });
50220
+ const cornerB = controller.view.boardToScreen({
50221
+ x: note.x + half,
50222
+ y: note.y - half
50223
+ });
50224
+ const outline = {
50225
+ left: Math.min(cornerA.x, cornerB.x) - HANDLE_GAP_PX,
50226
+ top: Math.min(cornerA.y, cornerB.y) - HANDLE_GAP_PX,
50227
+ right: Math.max(cornerA.x, cornerB.x) + HANDLE_GAP_PX,
50228
+ bottom: Math.max(cornerA.y, cornerB.y) + HANDLE_GAP_PX
50229
+ };
50230
+ const handlePoints = {
50231
+ nw: {
50232
+ x: outline.left,
50233
+ y: outline.top
50234
+ },
50235
+ ne: {
50236
+ x: outline.right,
50237
+ y: outline.top
50238
+ },
50239
+ sw: {
50240
+ x: outline.left,
50241
+ y: outline.bottom
50242
+ },
50243
+ se: {
50244
+ x: outline.right,
50245
+ y: outline.bottom
50246
+ }
50247
+ };
50248
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
50249
+ /* @__PURE__ */ jsx("div", {
50250
+ "aria-hidden": "true",
50251
+ className: "scrawl-board__focus-outline",
50252
+ "data-locked": locked,
50253
+ style: {
50254
+ left: outline.left,
50255
+ top: outline.top,
50256
+ width: outline.right - outline.left,
50257
+ height: outline.bottom - outline.top
50258
+ }
50259
+ }),
50260
+ mutable && !locked && HANDLE_CORNERS.map((corner) => /* @__PURE__ */ jsx(ResizeHandle, {
50261
+ controller,
50262
+ note,
50263
+ corner,
50264
+ x: handlePoints[corner].x,
50265
+ y: handlePoints[corner].y
50266
+ }, corner)),
50267
+ /* @__PURE__ */ jsxs(ToolbarShell, {
50268
+ anchor,
50269
+ children: [
50270
+ /* @__PURE__ */ jsxs("div", {
50271
+ className: "scrawl-board__style-group",
50272
+ children: [/* @__PURE__ */ jsx("span", { children: "Size" }), NOTE_SIZES.map(([label, size]) => /* @__PURE__ */ jsx("button", {
50273
+ "aria-label": `${label} note`,
50274
+ "aria-pressed": note.size === size,
50275
+ disabled: locked_,
50276
+ onClick: () => controller.content.update(note.id, { size }),
50277
+ type: "button",
50278
+ children: label[0]
50279
+ }, label))]
50280
+ }),
50281
+ /* @__PURE__ */ jsxs("div", {
50282
+ className: "scrawl-board__style-group",
50283
+ children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(NOTE_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
50284
+ "aria-label": `${name} note`,
50285
+ "aria-pressed": note.color.toLowerCase() === value.toLowerCase(),
50286
+ disabled: locked_,
50287
+ onClick: () => controller.content.update(note.id, { color: value }),
50288
+ type: "button",
50289
+ children: /* @__PURE__ */ jsx("span", {
50290
+ "aria-hidden": "true",
50291
+ style: { backgroundColor: value }
50292
+ })
50293
+ }, value))]
50294
+ }),
50295
+ /* @__PURE__ */ jsx(LockDuplicateDelete, {
50296
+ controller,
50297
+ mutable,
50298
+ locked,
50299
+ ids: [note.id]
50300
+ })
50301
+ ]
50302
+ })
50303
+ ] });
50304
+ }
50305
+ /**
50306
+ * One corner handle — drag distance from the note's own centre (its `x`/`y`
50307
+ * already *is* the centre) drives a proportional, always-square resize,
50308
+ * independent of which corner is grabbed. `note` is fresh every render (a
50309
+ * new `content.update` during drag triggers a new snapshot), so the
50310
+ * closure never goes stale.
50311
+ */
50312
+ function ResizeHandle({ controller, note, corner, x, y }) {
50313
+ const [dragging, setDragging] = useState(false);
50314
+ const nudge = (delta) => {
50315
+ const half = Math.max(MIN_SIZE / 2, Math.min(MAX_SIZE / 2, note.size / 2 + delta));
50316
+ controller.content.update(note.id, { size: half * 2 });
50317
+ };
50318
+ return /* @__PURE__ */ jsx("span", {
50319
+ "aria-label": `Resize note (${corner})`,
50320
+ "aria-valuemax": MAX_SIZE,
50321
+ "aria-valuemin": MIN_SIZE,
50322
+ "aria-valuenow": note.size,
50323
+ role: "slider",
50324
+ tabIndex: 0,
50325
+ className: "scrawl-board__focus-handle",
50326
+ "data-orientation": corner === "nw" || corner === "se" ? "nwse" : "nesw",
50327
+ style: {
50328
+ left: x,
50329
+ top: y
50330
+ },
50331
+ onKeyDown: (event) => {
50332
+ if (event.key === "ArrowUp" || event.key === "ArrowRight") {
50333
+ event.preventDefault();
50334
+ nudge(.5);
50335
+ } else if (event.key === "ArrowDown" || event.key === "ArrowLeft") {
50336
+ event.preventDefault();
50337
+ nudge(-.5);
50338
+ }
50339
+ },
50340
+ onPointerDown: (event) => {
50341
+ event.stopPropagation();
50342
+ event.currentTarget.setPointerCapture(event.pointerId);
50343
+ setDragging(true);
50344
+ },
50345
+ onPointerMove: (event) => {
50346
+ if (!dragging) return;
50347
+ const board = controller.view.screenToBoard({
50348
+ x: event.clientX,
50349
+ y: event.clientY
50350
+ });
50351
+ const half = Math.max(MIN_SIZE / 2, Math.min(MAX_SIZE / 2, Math.max(Math.abs(board.x - note.x), Math.abs(board.y - note.y))));
50352
+ controller.content.update(note.id, { size: half * 2 });
50353
+ },
50354
+ onPointerUp: (event) => {
50355
+ event.currentTarget.releasePointerCapture(event.pointerId);
50356
+ setDragging(false);
50357
+ }
50358
+ });
50359
+ }
50360
+ function TextToolbar({ controller, snapshot, text, locked, anchor }) {
50361
+ const mutable = snapshot.status === "ready" && !snapshot.readOnly;
50362
+ const locked_ = !mutable || locked;
50363
+ return /* @__PURE__ */ jsxs(ToolbarShell, {
50364
+ anchor,
50365
+ children: [
50366
+ /* @__PURE__ */ jsxs("div", {
50367
+ className: "scrawl-board__style-group",
50368
+ children: [/* @__PURE__ */ jsx("span", { children: "Size" }), TEXT_SIZES.map(([label, fontSize]) => /* @__PURE__ */ jsx("button", {
50369
+ "aria-label": `${label} text`,
50370
+ "aria-pressed": Math.abs(text.fontSize - fontSize) < .001,
50371
+ disabled: locked_,
50372
+ onClick: () => controller.content.update(text.id, { fontSize }),
50373
+ type: "button",
50374
+ children: label[0]
50375
+ }, label))]
50376
+ }),
50377
+ /* @__PURE__ */ jsxs("div", {
50378
+ className: "scrawl-board__style-group",
50379
+ children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(INK_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
50380
+ "aria-label": `${name} text`,
50381
+ "aria-pressed": text.color.toLowerCase() === value.toLowerCase(),
50382
+ disabled: locked_,
50383
+ onClick: () => controller.content.update(text.id, { color: value }),
50384
+ type: "button",
50385
+ children: /* @__PURE__ */ jsx("span", {
50386
+ "aria-hidden": "true",
50387
+ style: { backgroundColor: value }
50388
+ })
50389
+ }, value))]
50390
+ }),
50391
+ /* @__PURE__ */ jsx(LockDuplicateDelete, {
50392
+ controller,
50393
+ mutable,
50394
+ locked,
50395
+ ids: [text.id]
50396
+ })
50397
+ ]
50398
+ });
50399
+ }
50400
+ function ShapeToolbar({ controller, snapshot, stroke, locked, anchor }) {
50401
+ const mutable = snapshot.status === "ready" && !snapshot.readOnly;
50402
+ const locked_ = !mutable || locked;
50403
+ const ids = (stroke.clusterId ? controller.query.all().filter((o) => o.type === "stroke" && o.clusterId === stroke.clusterId) : [stroke]).map((m) => m.id);
50404
+ const applyToAll = (patch) => {
50405
+ for (const id of ids) controller.content.update(id, patch);
50406
+ };
50407
+ return /* @__PURE__ */ jsxs(ToolbarShell, {
50408
+ anchor,
50409
+ children: [
50410
+ /* @__PURE__ */ jsxs("div", {
50411
+ className: "scrawl-board__style-group",
50412
+ children: [/* @__PURE__ */ jsx("span", { children: "Width" }), SHAPE_WIDTHS.map(([label, width]) => /* @__PURE__ */ jsx("button", {
50413
+ "aria-label": `${label} stroke`,
50414
+ "aria-pressed": Math.abs(stroke.baseWidth - width) < .001,
50415
+ disabled: locked_,
50416
+ onClick: () => applyToAll({ baseWidth: width }),
50417
+ type: "button",
50418
+ children: label
50419
+ }, label))]
50420
+ }),
50421
+ /* @__PURE__ */ jsxs("div", {
50422
+ className: "scrawl-board__style-group",
50423
+ children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(INK_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
50424
+ "aria-label": `${name} shape`,
50425
+ "aria-pressed": stroke.color.toLowerCase() === value.toLowerCase(),
50426
+ disabled: locked_,
50427
+ onClick: () => applyToAll({ color: value }),
50428
+ type: "button",
50429
+ children: /* @__PURE__ */ jsx("span", {
50430
+ "aria-hidden": "true",
50431
+ style: { backgroundColor: value }
50432
+ })
50433
+ }, value))]
50434
+ }),
50435
+ /* @__PURE__ */ jsx(LockDuplicateDelete, {
50436
+ controller,
50437
+ mutable,
50438
+ locked,
50439
+ ids
50440
+ })
50441
+ ]
50442
+ });
50443
+ }
50444
+ function LockDuplicateDelete({ controller, mutable, locked, ids }) {
50445
+ return /* @__PURE__ */ jsxs("div", {
50446
+ className: "scrawl-board__style-group",
50447
+ children: [
50448
+ /* @__PURE__ */ jsx("button", {
50449
+ "aria-label": locked ? "Unlock" : "Lock",
50450
+ disabled: !mutable,
50451
+ onClick: () => {
50452
+ for (const id of ids) controller.content.update(id, locked ? { locked: false } : { locked: true });
50453
+ },
50454
+ type: "button",
50455
+ children: locked ? "Unlock" : "Lock"
50456
+ }),
50457
+ /* @__PURE__ */ jsx("button", {
50458
+ "aria-label": "Duplicate",
50459
+ disabled: !mutable || locked,
50460
+ onClick: () => {
50461
+ const newIds = controller.content.duplicate(ids);
50462
+ if (newIds.length) controller.content.select(newIds);
50463
+ },
50464
+ type: "button",
50465
+ children: "Duplicate"
50466
+ }),
50467
+ /* @__PURE__ */ jsx("button", {
50468
+ "aria-label": "Delete",
50469
+ disabled: !mutable || locked,
50470
+ onClick: () => controller.content.remove(ids),
50471
+ type: "button",
50472
+ children: "Delete"
50473
+ })
50474
+ ]
50475
+ });
50476
+ }
50477
+ //#endregion
49903
50478
  //#region src/ui/toolbar/style-shelf.tsx
49904
50479
  var ERASER_RADII = [
49905
50480
  ["Fine", 1],
@@ -49938,7 +50513,7 @@ var SHELF_TOOLS = /* @__PURE__ */ new Set([
49938
50513
  "timer"
49939
50514
  ]);
49940
50515
  /** Contextual per-tool style controls — visible while a styleable tool is active. */
49941
- function StyleShelf({ controller, snapshot }) {
50516
+ function StyleShelf({ controller, snapshot, anchor }) {
49942
50517
  if (!SHELF_TOOLS.has(snapshot.tool)) return null;
49943
50518
  const mutable = snapshot.status === "ready" && !snapshot.readOnly;
49944
50519
  const { style } = snapshot;
@@ -49946,7 +50521,12 @@ function StyleShelf({ controller, snapshot }) {
49946
50521
  return /* @__PURE__ */ jsxs("div", {
49947
50522
  "aria-label": "Board style",
49948
50523
  className: "scrawl-board__style-shelf",
50524
+ "data-anchored": !!anchor,
49949
50525
  role: "toolbar",
50526
+ style: anchor ? {
50527
+ left: anchor.x,
50528
+ top: anchor.y
50529
+ } : void 0,
49950
50530
  children: [
49951
50531
  snapshot.tool === "highlighter" && /* @__PURE__ */ jsxs("div", {
49952
50532
  className: "scrawl-board__style-group",
@@ -50176,6 +50756,7 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
50176
50756
  const [panel, setPanel] = useState(null);
50177
50757
  const [query, setQuery] = useState("");
50178
50758
  const [announcement, setAnnouncement] = useState("Board controls ready");
50759
+ const [styleShelfAnchor, setStyleShelfAnchor] = useState(void 0);
50179
50760
  const importRef = useRef(null);
50180
50761
  const openerRef = useRef(null);
50181
50762
  const searchRef = useRef(null);
@@ -50186,6 +50767,25 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
50186
50767
  useEffect(() => {
50187
50768
  if (panel === "search") searchRef.current?.focus();
50188
50769
  }, [panel]);
50770
+ useEffect(() => {
50771
+ const recompute = () => {
50772
+ const root = chromeRef.current;
50773
+ const button = root?.querySelector(".scrawl-board__tools button[aria-pressed=\"true\"]");
50774
+ if (!root || !button) {
50775
+ setStyleShelfAnchor(void 0);
50776
+ return;
50777
+ }
50778
+ const buttonRect = button.getBoundingClientRect();
50779
+ const rootRect = root.getBoundingClientRect();
50780
+ setStyleShelfAnchor({
50781
+ x: buttonRect.left + buttonRect.width / 2 - rootRect.left,
50782
+ y: buttonRect.top - rootRect.top
50783
+ });
50784
+ };
50785
+ recompute();
50786
+ window.addEventListener("resize", recompute);
50787
+ return () => window.removeEventListener("resize", recompute);
50788
+ }, [snapshot.tool]);
50189
50789
  const open = (next, opener) => {
50190
50790
  openerRef.current = opener;
50191
50791
  setPanel(next);
@@ -50414,9 +51014,14 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
50414
51014
  ]
50415
51015
  }),
50416
51016
  renderSlot(slots?.stylePanel, enabled("styleShelf") && /* @__PURE__ */ jsx(StyleShelf, {
51017
+ anchor: styleShelfAnchor,
50417
51018
  controller,
50418
51019
  snapshot
50419
51020
  })),
51021
+ enabled("focusedItemToolbar") && /* @__PURE__ */ jsx(FocusedItemToolbar, {
51022
+ controller,
51023
+ snapshot
51024
+ }),
50420
51025
  ContextMenu && /* @__PURE__ */ jsx(ContextMenu, {
50421
51026
  controller,
50422
51027
  snapshot
@@ -51059,4 +51664,4 @@ function ScrawlBoard({ documentId, initialDocument, onReady, className, style })
51059
51664
  });
51060
51665
  }
51061
51666
  //#endregion
51062
- export { ASSET_CACHE_BYTES_DEFAULT, ASSET_CACHE_BYTES_MAX, ASSET_CACHE_BYTES_MIN, ASSET_EXPORT_MAX_DECODED_MEGAPIXELS, ASSET_EXPORT_MAX_ENCODED_BYTES, ASSET_MAX_CONCURRENT_RESOLUTIONS, ASSET_MAX_DECODED_MEGAPIXELS, ASSET_MAX_DIMENSION_PX, ASSET_MAX_ENCODED_BYTES, ASSET_REF_MAX_BYTES, ASSET_REF_PATTERN, AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, AssetResolutionError, BEACON_INSET, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, DefaultBoardChrome, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, InlineEditors, LockItemsCommand, MIN_WIDTH_FACTOR, MultiplayerCursors, NOTE_COLORS, NOTE_DEFAULT_SIZE, NOTE_DEFAULT_Z, NOTE_MAX_Z, NOTE_MIN_Z, NOTE_PEEL_STEP, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, STAMPS, STAMP_SIZE, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, SpatialIndex, StyleShelf, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, UpdateImageCommand, UpdateNoteCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, assetRef, avgScale, canUnlockItem, changeToOps, clampAssetCacheBytes, cloneCustomObject, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, createBoardController, createLocalBoard, documentId, documentToSVG, formatTimer, invert, isAssetRef, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, resolveScrawlTheme, ribbonEdges, rotationAbout, scalingAbout, scrawlThemePresets, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
51667
+ export { ASSET_CACHE_BYTES_DEFAULT, ASSET_CACHE_BYTES_MAX, ASSET_CACHE_BYTES_MIN, ASSET_EXPORT_MAX_DECODED_MEGAPIXELS, ASSET_EXPORT_MAX_ENCODED_BYTES, ASSET_MAX_CONCURRENT_RESOLUTIONS, ASSET_MAX_DECODED_MEGAPIXELS, ASSET_MAX_DIMENSION_PX, ASSET_MAX_ENCODED_BYTES, ASSET_REF_MAX_BYTES, ASSET_REF_PATTERN, AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, AssetResolutionError, BEACON_INSET, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, DefaultBoardChrome, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, FocusedItemToolbar, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, InlineEditors, LockItemsCommand, MIN_WIDTH_FACTOR, MultiplayerCursors, NOTE_COLORS, NOTE_DEFAULT_SIZE, NOTE_DEFAULT_Z, NOTE_MAX_Z, NOTE_MIN_Z, NOTE_PEEL_STEP, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, STAMPS, STAMP_SIZE, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, SpatialIndex, StyleShelf, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, UpdateImageCommand, UpdateNoteCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, assetRef, avgScale, canUnlockItem, changeToOps, clampAssetCacheBytes, cloneCustomObject, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, createBoardController, createLocalBoard, documentId, documentToSVG, formatTimer, invert, isAssetRef, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, resolveScrawlTheme, ribbonEdges, rotationAbout, scalingAbout, scrawlThemePresets, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };