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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.d.ts CHANGED
@@ -5,6 +5,9 @@ type StrokeId = string & {
5
5
 
6
6
  /** Wire grammar: `asset:<namespace>:<opaque-id>`. Interpreted only by the Host. */
7
7
  type AssetRef = string;
8
+ declare function isAssetRef(value: unknown): value is AssetRef;
9
+ /** Throws on malformed input; use `isAssetRef` where a boolean is wanted instead. */
10
+ declare function assetRef(value: string): AssetRef;
8
11
  type AssetKind = "image";
9
12
  type AssetPurpose = "render" | "thumbnail" | "export";
10
13
  interface AssetResolveRequest {
@@ -46,6 +49,12 @@ interface AssetIngestor {
46
49
  ingest(request: AssetIngestRequest): Promise<AssetIngestResult>;
47
50
  }
48
51
  type AssetResolutionErrorCode = "resolver-unavailable" | "not-found" | "forbidden" | "offline" | "unsupported-type" | "too-large" | "invalid-content" | "decode-failed" | "budget-exceeded" | "aborted" | "unknown";
52
+ declare class AssetResolutionError extends Error {
53
+ readonly code: AssetResolutionErrorCode;
54
+ readonly retryable: boolean;
55
+ readonly ref?: AssetRef | undefined;
56
+ constructor(code: AssetResolutionErrorCode, retryable: boolean, message: string, ref?: AssetRef | undefined);
57
+ }
49
58
  /** Runtime event for a resolution/ingestion failure — never carries credentials or a fetchable location. */
50
59
  interface AssetDiagnostic {
51
60
  code: AssetResolutionErrorCode;
@@ -54,6 +63,9 @@ interface AssetDiagnostic {
54
63
  objectKind: "image" | "custom";
55
64
  retryable: boolean;
56
65
  }
66
+ declare const SUPPORTED_ASSET_MEDIA_TYPES: readonly ["image/png", "image/jpeg", "image/webp"];
67
+ type SupportedAssetMediaType = (typeof SUPPORTED_ASSET_MEDIA_TYPES)[number];
68
+ declare function clampAssetCacheBytes(value: number | undefined): number;
57
69
 
58
70
  type Mat2x3 = [number, number, number, number, number, number];
59
71
 
@@ -101,6 +113,7 @@ interface CustomBoardObject {
101
113
  };
102
114
  props: JsonValue;
103
115
  }
116
+ declare function cloneCustomObject(object: CustomBoardObject): CustomBoardObject;
104
117
  /**
105
118
  * The read-only view handed to `describe`. Deep-readonly by construction
106
119
  * (not derived via a shallow `Readonly<>`) because `describe` must treat its
@@ -867,22 +880,31 @@ interface CreateBoardControllerOptions {
867
880
  * Trusted Custom tool/object registrations (ticket #22, design:
868
881
  * docs/research/extension-contracts.md). Validated atomically at
869
882
  * construction; registration failure throws before any controller is
870
- * returned. Not yet re-exported from a public package entry point —
871
- * internal-only until the reference Extension proves the seam.
883
+ * returned.
872
884
  */
873
885
  extensions?: readonly ScrawlExtension[];
874
886
  /**
875
887
  * Optional Host-managed Asset capabilities (ticket #23, design:
876
888
  * docs/research/asset-resolution-resource-policy.md). Without a
877
889
  * resolver, referenced Assets preserve their Document geometry and
878
- * render an accessible placeholder. Not yet re-exported from a public
879
- * package entry point — internal-only until the reference resolver
880
- * proves the seam, matching how `extensions` is scoped.
890
+ * render an accessible placeholder.
881
891
  */
882
892
  assetResolver?: AssetResolver;
883
893
  assetIngestor?: AssetIngestor;
884
894
  /** Clamped to 64–512MiB; defaults to 256MiB. */
885
895
  assetCacheBytes?: number;
896
+ /**
897
+ * The rendered board surface's color and reference grid. Defaults to the
898
+ * light theme preset's values; `<Scrawl>` keeps this current across theme
899
+ * changes via `boardTheme.set` below — a headless/browser-tier Host that
900
+ * doesn't use the React theme system can set this directly instead.
901
+ */
902
+ boardTheme?: {
903
+ surface?: string;
904
+ gridMode?: "none" | "line" | "dot";
905
+ gridColor?: string;
906
+ gridSpacing?: number;
907
+ };
886
908
  }
887
909
  interface BoardController {
888
910
  readonly document: ReadonlyBoardDocument;
@@ -908,6 +930,15 @@ interface BoardController {
908
930
  undo(): void;
909
931
  redo(): void;
910
932
  };
933
+ readonly boardTheme: {
934
+ /** Live update of the board surface color/grid — the controller's identity stays fixed across theme changes. */
935
+ set(theme: {
936
+ surface?: string;
937
+ gridMode?: "none" | "line" | "dot";
938
+ gridColor?: string;
939
+ gridSpacing?: number;
940
+ }): void;
941
+ };
911
942
  readonly view: {
912
943
  fit(): void;
913
944
  zoomTo(value: number): void;
@@ -1008,5 +1039,5 @@ type LocalBoard = {
1008
1039
  };
1009
1040
  declare function createLocalBoard(options: LocalBoardOptions): LocalBoard;
1010
1041
 
1011
- export { STAMPS, createBoardController, createLocalBoard, isStampKind, stampDataUrl };
1012
- export type { ApplyOpsResult, BoardController, BoardControllerError, BoardEventMap, BoardObject, BoardObjectInput, BoardObjectPatch, BoardSnapshot, BoardStyle, BoardView, BuiltInTool, CollaborationAdapter, CollaborationReceiver, CollaborationSession, CollaborationSnapshot, CollaboratorIdentity, CommentMarker, ControllerOp, CreateBoardControllerOptions, DeepReadonly, DocumentContext, LoadResult, LocalBoard, LocalBoardOptions, LocalBoardSnapshot, PersistenceAdapter, PersistenceDiagnostic, PersistenceSnapshot, PresenceCursor, PresenceUser, PresenceView, ReadonlyBoardDocument, ReadonlyDocumentChange, ScreenPoint, ScreenRect, StampKind };
1042
+ export { AssetResolutionError, STAMPS, SUPPORTED_ASSET_MEDIA_TYPES, assetRef, clampAssetCacheBytes, cloneCustomObject, createBoardController, createLocalBoard, isAssetRef, isStampKind, stampDataUrl };
1043
+ export type { ApplyOpsResult, AssetDiagnostic, AssetIngestRequest, AssetIngestResult, AssetIngestor, AssetKind, AssetPurpose, AssetRef, AssetResolutionErrorCode, AssetResolveRequest, AssetResolveResult, AssetResolver, BoardController, BoardControllerError, BoardEventMap, BoardKeyInput, BoardObject, BoardObjectInput, BoardObjectPatch, BoardPointerInput, BoardScene, BoardSnapshot, BoardStyle, BoardView, BuiltInTool, CollaborationAdapter, CollaborationReceiver, CollaborationSession, CollaborationSnapshot, CollaboratorIdentity, CommentMarker, ControllerOp, CreateBoardControllerOptions, CustomBoardObject, CustomObjectAddInput, CustomObjectDefinition, CustomTool, CustomToolDefinition, DeepReadonly, DocumentContext, ExtensionCommand, ExtensionDiagnostic, ExtensionHitResult, ExtensionId, ExtensionRequirement, InputModifiers, JsonObject, JsonValue, LoadResult, LocalBoard, LocalBoardOptions, LocalBoardSnapshot, Mat2x3, ObjectDescribeContext, ObjectIntent, ObjectType, PersistenceAdapter, PersistenceDiagnostic, PersistenceSnapshot, PresenceCursor, PresenceUser, PresenceView, QueryableBoardObject, ReadonlyBoardDocument, ReadonlyCustomObject, ReadonlyDocumentChange, SceneEllipse, SceneGroup, SceneImage, ScenePath, SceneRect, SceneText, ScrawlExtension, ScreenPoint, ScreenRect, StampKind, SupportedAssetMediaType, ToolCancelReason, ToolCapabilities, ToolCursor, ToolId };
package/dist/browser.js CHANGED
@@ -20,6 +20,11 @@ var AssetResolutionError = class extends Error {
20
20
  var ASSET_CACHE_BYTES_DEFAULT = 256 * 1024 * 1024;
21
21
  var ASSET_CACHE_BYTES_MIN = 64 * 1024 * 1024;
22
22
  var ASSET_CACHE_BYTES_MAX = 512 * 1024 * 1024;
23
+ var SUPPORTED_ASSET_MEDIA_TYPES = [
24
+ "image/png",
25
+ "image/jpeg",
26
+ "image/webp"
27
+ ];
23
28
  function clampAssetCacheBytes(value) {
24
29
  if (value === void 0 || !Number.isFinite(value)) return ASSET_CACHE_BYTES_DEFAULT;
25
30
  return Math.min(ASSET_CACHE_BYTES_MAX, Math.max(ASSET_CACHE_BYTES_MIN, Math.floor(value)));
@@ -442,7 +447,6 @@ function measureTable(table) {
442
447
  height: table.rowHeights.reduce((sum, h) => sum + h, 0)
443
448
  };
444
449
  }
445
- var BOARD_COLOR = "#FFFFFF";
446
450
  var FOG_COLOR = "#FFFFFF";
447
451
  function cloneImage(img) {
448
452
  return { ...img };
@@ -1895,12 +1899,19 @@ var LIGHT = {
1895
1899
  var MARGIN = 4;
1896
1900
  var NOTE_FONT_RATIO = 44 / 512;
1897
1901
  var NOTE_PAD_RATIO = 40 / 512;
1902
+ /** Matches the theme system's light-preset `boardSurface` default (theme.ts) — kept in sync manually, since core-internal stays decoupled from the theme layer. */
1903
+ var DEFAULT_BACKGROUND_COLOR = "#f5f5f3";
1898
1904
  /**
1899
1905
  * `registry` is optional and only enables rendering Custom objects through
1900
1906
  * their own `describe()` — without it (or for an object whose extension
1901
1907
  * isn't in it), Custom objects still export via the standard fallback
1902
1908
  * placeholder (`fallback.bounds`/`label`), never silently dropped.
1903
1909
  *
1910
+ * `backgroundColor` should be the Host's actual resolved `boardTheme.surface`
1911
+ * so an export matches what was on screen; defaults to the theme system's
1912
+ * own light-preset default when the caller doesn't have one on hand. The
1913
+ * board's reference grid, if any, is a screen-only aid and never exported.
1914
+ *
1904
1915
  * `resolvedAssets` (ticket #23) maps an Asset reference to an already-
1905
1916
  * resolved `data:` URI — see `assetExport.ts`'s `exportDocumentSVGWithAssets`,
1906
1917
  * which is the only intended caller that ever passes one. Without it, every
@@ -1908,7 +1919,7 @@ var NOTE_PAD_RATIO = 40 / 512;
1908
1919
  * placeholder instead of guessing at a URL; a legacy `src`-backed image is
1909
1920
  * unaffected either way.
1910
1921
  */
1911
- function documentToSVG(doc, now = 0, registry, resolvedAssets) {
1922
+ function documentToSVG(doc, now = 0, registry, backgroundColor = DEFAULT_BACKGROUND_COLOR, resolvedAssets) {
1912
1923
  const notes = doc.notes ?? [];
1913
1924
  const texts = doc.textBlocks ?? [];
1914
1925
  const tables = doc.tables ?? [];
@@ -1921,7 +1932,7 @@ function documentToSVG(doc, now = 0, registry, resolvedAssets) {
1921
1932
  const highlights = doc.strokes.filter((s) => s.tool === "highlighter");
1922
1933
  const ink = doc.strokes.filter((s) => s.tool !== "highlighter");
1923
1934
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${view}">\n<defs>\n${defs}\n</defs>\n${[
1924
- `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${BOARD_COLOR}"/>`,
1935
+ `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${backgroundColor}"/>`,
1925
1936
  ...images.map((image) => imageToElement(image, resolvedAssets)),
1926
1937
  ...tables.map(tableToGroup),
1927
1938
  ...highlights.flatMap(strokeToPaths),
@@ -2837,7 +2848,7 @@ var SvgAssetExportError = class extends Error {
2837
2848
  this.name = "SvgAssetExportError";
2838
2849
  }
2839
2850
  };
2840
- async function exportDocumentSVGWithAssets(doc, now, registry, assetController, options) {
2851
+ async function exportDocumentSVGWithAssets(doc, now, registry, assetController, backgroundColor, options) {
2841
2852
  const refs = collectDocumentAssetRefs(doc, registry);
2842
2853
  const resolvedAssets = /* @__PURE__ */ new Map();
2843
2854
  const missingAssets = [];
@@ -2873,7 +2884,7 @@ async function exportDocumentSVGWithAssets(doc, now, registry, assetController,
2873
2884
  }
2874
2885
  if (missingAssets.length > 0 && options?.missingAssets !== "placeholder") throw new SvgAssetExportError(missingAssets);
2875
2886
  return {
2876
- svg: documentToSVG(doc, now, registry, resolvedAssets),
2887
+ svg: documentToSVG(doc, now, registry, backgroundColor, resolvedAssets),
2877
2888
  missingAssets
2878
2889
  };
2879
2890
  }
@@ -35794,6 +35805,21 @@ var FOG_GLSL = `
35794
35805
  `;
35795
35806
  //#endregion
35796
35807
  //#region ../../src/scrawl/engine/boardSurface.ts
35808
+ var GRID_MODE_CODE = {
35809
+ none: 0,
35810
+ line: 1,
35811
+ dot: 2
35812
+ };
35813
+ /** Used when a caller doesn't supply one (e.g. ScrawlEngine constructed without a Host theme). */
35814
+ var DEFAULT_BOARD_SURFACE_THEME = {
35815
+ color: "#f5f5f3",
35816
+ gridMode: "none",
35817
+ gridColor: "#deded8",
35818
+ gridSpacing: 5
35819
+ };
35820
+ /** Grid alpha fades out below this on-screen spacing (px) — otherwise it aliases into a solid wash when zoomed out. */
35821
+ var GRID_FADE_MIN_PX = 3;
35822
+ var GRID_FADE_MAX_PX = 8;
35797
35823
  var VERTEX$1 = `
35798
35824
  varying vec3 vWorldPos;
35799
35825
  void main() {
@@ -35807,6 +35833,11 @@ var FRAGMENT = `
35807
35833
  uniform vec3 uFogColor;
35808
35834
  uniform float uFogNear;
35809
35835
  uniform float uFogFar;
35836
+ uniform float uGridMode;
35837
+ uniform vec3 uGridColor;
35838
+ uniform float uGridSpacing;
35839
+ uniform float uGridLineWorld;
35840
+ uniform float uGridFade;
35810
35841
  varying vec3 vWorldPos;
35811
35842
 
35812
35843
  float hash(vec2 p) {
@@ -35825,6 +35856,22 @@ var FRAGMENT = `
35825
35856
 
35826
35857
  ${FOG_GLSL}
35827
35858
 
35859
+ // Distance from p's nearest axis-aligned grid line, in world units.
35860
+ float gridLineAlpha(vec2 p) {
35861
+ vec2 cell = mod(p, uGridSpacing);
35862
+ vec2 distToLine = min(cell, uGridSpacing - cell);
35863
+ vec2 lineAlpha2 = 1.0 - smoothstep(uGridLineWorld, uGridLineWorld * 2.0, distToLine);
35864
+ return max(lineAlpha2.x, lineAlpha2.y);
35865
+ }
35866
+
35867
+ // Distance from p's nearest grid intersection, in world units.
35868
+ float gridDotAlpha(vec2 p) {
35869
+ vec2 cell = mod(p, uGridSpacing) - uGridSpacing * 0.5;
35870
+ float dist = length(cell);
35871
+ float radius = uGridLineWorld * 1.5;
35872
+ return 1.0 - smoothstep(radius, radius * 2.0, dist);
35873
+ }
35874
+
35828
35875
  void main() {
35829
35876
  vec3 col = uColor;
35830
35877
 
@@ -35842,16 +35889,26 @@ var FRAGMENT = `
35842
35889
  float spec = pow(max(dot(N, H), 0.0), 90.0);
35843
35890
  col += (fres * 0.18 + spec * 0.06) * vec3(1.0);
35844
35891
 
35892
+ if (uGridMode > 0.5 && uGridFade > 0.001) {
35893
+ float gridAlpha = (uGridMode < 1.5 ? gridLineAlpha(vWorldPos.xy) : gridDotAlpha(vWorldPos.xy)) * uGridFade;
35894
+ col = mix(col, uGridColor, gridAlpha);
35895
+ }
35896
+
35845
35897
  col = applyBoardFog(col, vWorldPos);
35846
35898
  gl_FragColor = vec4(col, 1.0);
35847
35899
  }
35848
35900
  `;
35849
- function createBoardSurface(fog) {
35901
+ function createBoardSurface(fog, theme) {
35850
35902
  const material = new ShaderMaterial({
35851
35903
  vertexShader: VERTEX$1,
35852
35904
  fragmentShader: FRAGMENT,
35853
35905
  uniforms: {
35854
- uColor: { value: new Color(BOARD_COLOR) },
35906
+ uColor: { value: new Color(theme.color) },
35907
+ uGridMode: { value: GRID_MODE_CODE[theme.gridMode] },
35908
+ uGridColor: { value: new Color(theme.gridColor) },
35909
+ uGridSpacing: { value: Math.max(.001, theme.gridSpacing) },
35910
+ uGridLineWorld: { value: .05 },
35911
+ uGridFade: { value: 0 },
35855
35912
  ...fog
35856
35913
  }
35857
35914
  });
@@ -35859,11 +35916,32 @@ function createBoardSurface(fog) {
35859
35916
  mesh.name = "board-surface";
35860
35917
  return mesh;
35861
35918
  }
35862
- /** Keep the plane centered under the view and larger than the fog horizon. */
35863
- function updateBoardSurface(mesh, centerX, centerY, fogFar) {
35919
+ /** Applies a theme change (initial or live) everything except the per-frame fade/line-width. */
35920
+ function setBoardSurfaceTheme(mesh, theme) {
35921
+ const uniforms = mesh.material.uniforms;
35922
+ uniforms.uColor.value.set(theme.color);
35923
+ uniforms.uGridMode.value = GRID_MODE_CODE[theme.gridMode];
35924
+ uniforms.uGridColor.value.set(theme.gridColor);
35925
+ uniforms.uGridSpacing.value = Math.max(.001, theme.gridSpacing);
35926
+ }
35927
+ /** Keep the plane centered under the view, larger than the fog horizon, and the grid's on-screen weight/fade current. */
35928
+ function updateBoardSurface(mesh, centerX, centerY, fogFar, worldPerPixel) {
35864
35929
  mesh.position.set(centerX, centerY, 0);
35865
35930
  const size = fogFar * 2.5;
35866
35931
  mesh.scale.set(size, size, 1);
35932
+ const uniforms = mesh.material.uniforms;
35933
+ uniforms.uGridLineWorld.value = worldPerPixel;
35934
+ const t = (uniforms.uGridSpacing.value / Math.max(1e-6, worldPerPixel) - GRID_FADE_MIN_PX) / (GRID_FADE_MAX_PX - GRID_FADE_MIN_PX);
35935
+ uniforms.uGridFade.value = MathUtils.clamp(t, 0, 1);
35936
+ }
35937
+ /** Temporarily zeroes the grid's contribution (for export snapshots) and returns a restore function. */
35938
+ function suppressBoardSurfaceGrid(mesh) {
35939
+ const uniforms = mesh.material.uniforms;
35940
+ const previous = uniforms.uGridFade.value;
35941
+ uniforms.uGridFade.value = 0;
35942
+ return () => {
35943
+ uniforms.uGridFade.value = previous;
35944
+ };
35867
35945
  }
35868
35946
  var MIN_HEIGHT = 40;
35869
35947
  var MAX_HEIGHT = 1500;
@@ -36027,7 +36105,7 @@ var CameraRig = class {
36027
36105
  //#endregion
36028
36106
  //#region ../../src/scrawl/engine/scene.ts
36029
36107
  var SceneRig = class {
36030
- constructor(canvas) {
36108
+ constructor(canvas, boardTheme) {
36031
36109
  this.renderer = new WebGLRenderer({
36032
36110
  canvas,
36033
36111
  antialias: true
@@ -36037,7 +36115,7 @@ var SceneRig = class {
36037
36115
  this.scene = new Scene();
36038
36116
  this.cameraRig = new CameraRig(canvas.clientWidth / Math.max(1, canvas.clientHeight));
36039
36117
  this.fog = createFogUniforms(FOG_COLOR);
36040
- this.board = createBoardSurface(this.fog);
36118
+ this.board = createBoardSurface(this.fog, boardTheme);
36041
36119
  this.scene.add(this.board);
36042
36120
  this.inkGroup = new Group();
36043
36121
  this.inkGroup.name = "ink";
@@ -36054,9 +36132,17 @@ var SceneRig = class {
36054
36132
  renderFrame(dtMs) {
36055
36133
  this.cameraRig.update(dtMs);
36056
36134
  updateFogForCamera(this.fog, this.cameraRig.height);
36057
- updateBoardSurface(this.board, this.cameraRig.center.x, this.cameraRig.center.y, this.fog.uFogFar.value);
36135
+ updateBoardSurface(this.board, this.cameraRig.center.x, this.cameraRig.center.y, this.fog.uFogFar.value, this.cameraRig.worldPerPixel());
36058
36136
  this.renderer.render(this.scene, this.cameraRig.camera);
36059
36137
  }
36138
+ /** Live theme update — the board controller's identity stays fixed across theme changes; this is how a new one reaches the render engine. */
36139
+ applyBoardTheme(theme) {
36140
+ setBoardSurfaceTheme(this.board, theme);
36141
+ }
36142
+ /** Zeroes the grid for one render pass (PNG export never bakes it in) and returns a restore function. */
36143
+ suppressGrid() {
36144
+ return suppressBoardSurfaceGrid(this.board);
36145
+ }
36060
36146
  dispose() {
36061
36147
  this.board.geometry.dispose();
36062
36148
  this.board.material.dispose();
@@ -46505,9 +46591,10 @@ function emptyExtensionRegistry() {
46505
46591
  * React never reaches past this class.
46506
46592
  */
46507
46593
  var ScrawlEngine = class {
46508
- constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0)) {
46594
+ constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0), boardTheme = DEFAULT_BOARD_SURFACE_THEME) {
46509
46595
  this.canvas = canvas;
46510
46596
  this.extensionRegistry = extensionRegistry;
46597
+ this.boardTheme = boardTheme;
46511
46598
  this.onToolChange = null;
46512
46599
  this.onZoomChange = null;
46513
46600
  this.onAuditEvent = null;
@@ -46554,7 +46641,7 @@ var ScrawlEngine = class {
46554
46641
  this.stampKind = "star";
46555
46642
  this.timerDurationMs = TIMER_DEFAULT_DURATION_MS;
46556
46643
  this.readOnly = false;
46557
- this.scene = new SceneRig(canvas);
46644
+ this.scene = new SceneRig(canvas, boardTheme);
46558
46645
  this.document = new BoardDocument(documentId(documentId$1));
46559
46646
  this.history = new History(this.document);
46560
46647
  this.history.onCommand = (command, kind) => {
@@ -46861,6 +46948,11 @@ var ScrawlEngine = class {
46861
46948
  height: rig.height
46862
46949
  };
46863
46950
  }
46951
+ /** Live update of the board surface color/grid — the engine's identity stays fixed across theme changes. */
46952
+ applyBoardTheme(theme) {
46953
+ this.boardTheme = theme;
46954
+ this.scene.applyBoardTheme(theme);
46955
+ }
46864
46956
  /**
46865
46957
  * Snap to a peer's camera. No-op while drawing so pointer-to-board
46866
46958
  * mapping does not warp the in-progress stroke.
@@ -46879,7 +46971,7 @@ var ScrawlEngine = class {
46879
46971
  }
46880
46972
  /** The saved document as standalone SVG — a pure walk of the wire format. */
46881
46973
  exportSVG() {
46882
- return documentToSVG(this.document.toJSON(), Date.now(), this.extensionRegistry);
46974
+ return documentToSVG(this.document.toJSON(), Date.now(), this.extensionRegistry, this.boardTheme.color);
46883
46975
  }
46884
46976
  /**
46885
46977
  * Content-framed PNG snapshot. Renders once offscreen-style with UI props
@@ -46915,6 +47007,7 @@ var ScrawlEngine = class {
46915
47007
  this.notesRenderer.setFocus(null);
46916
47008
  this.textsRenderer.setFocus(null);
46917
47009
  this.renderer.setGhostsVisible(false);
47010
+ const restoreGrid = this.scene.suppressGrid();
46918
47011
  try {
46919
47012
  rig.yaw = 0;
46920
47013
  rig.pitch = 0;
@@ -46925,6 +47018,7 @@ var ScrawlEngine = class {
46925
47018
  const dataUrl = this.scene.renderer.domElement.toDataURL("image/png");
46926
47019
  return await (await fetch(dataUrl)).blob();
46927
47020
  } finally {
47021
+ restoreGrid();
46928
47022
  rig.center.x = saved.x;
46929
47023
  rig.center.y = saved.y;
46930
47024
  rig.height = saved.height;
@@ -47934,7 +48028,13 @@ function createBoardControllerWithEngine(options, existingEngine) {
47934
48028
  }
47935
48029
  const extensionRegistry = registryResult.registry;
47936
48030
  const assetController = new AssetController(options.assetResolver, { cacheBytes: options.assetCacheBytes });
47937
- const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController) : null);
48031
+ let currentBoardTheme = {
48032
+ color: options.boardTheme?.surface ?? DEFAULT_BOARD_SURFACE_THEME.color,
48033
+ gridMode: options.boardTheme?.gridMode ?? DEFAULT_BOARD_SURFACE_THEME.gridMode,
48034
+ gridColor: options.boardTheme?.gridColor ?? DEFAULT_BOARD_SURFACE_THEME.gridColor,
48035
+ gridSpacing: options.boardTheme?.gridSpacing ?? DEFAULT_BOARD_SURFACE_THEME.gridSpacing
48036
+ };
48037
+ const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController, currentBoardTheme) : null);
47938
48038
  const document = engine?.document ?? new BoardDocument(documentId(options.document.id));
47939
48039
  const history = engine?.history ?? new History(document);
47940
48040
  const createId = options.createId ?? (() => crypto.randomUUID());
@@ -48418,6 +48518,15 @@ function createBoardControllerWithEngine(options, existingEngine) {
48418
48518
  changed();
48419
48519
  }
48420
48520
  },
48521
+ boardTheme: { set(theme) {
48522
+ currentBoardTheme = {
48523
+ color: theme.surface ?? currentBoardTheme.color,
48524
+ gridMode: theme.gridMode ?? currentBoardTheme.gridMode,
48525
+ gridColor: theme.gridColor ?? currentBoardTheme.gridColor,
48526
+ gridSpacing: theme.gridSpacing ?? currentBoardTheme.gridSpacing
48527
+ };
48528
+ engine?.applyBoardTheme(currentBoardTheme);
48529
+ } },
48421
48530
  view: {
48422
48531
  fit() {
48423
48532
  assertActive();
@@ -48722,10 +48831,10 @@ function createBoardControllerWithEngine(options, existingEngine) {
48722
48831
  gather: (nextView) => engine?.applyGatherView(nextView) ?? false
48723
48832
  },
48724
48833
  export: {
48725
- svg: () => engine ? engine.exportSVG() : documentToSVG(document.toJSON(), Date.now(), extensionRegistry),
48834
+ svg: () => engine ? engine.exportSVG() : documentToSVG(document.toJSON(), Date.now(), extensionRegistry, currentBoardTheme.color),
48726
48835
  png: (options) => engine ? engine.exportPNG(options?.maxSide, { awaitAssets: options?.awaitAssets }) : Promise.resolve(null),
48727
48836
  json: () => document.toJSON(),
48728
- svgAsync: (svgOptions) => exportDocumentSVGWithAssets(document.toJSON(), Date.now(), extensionRegistry, assetController, svgOptions)
48837
+ svgAsync: (svgOptions) => exportDocumentSVGWithAssets(document.toJSON(), Date.now(), extensionRegistry, assetController, currentBoardTheme.color, svgOptions)
48729
48838
  },
48730
48839
  assets: { async ingest(bytes, mediaType, name, signal) {
48731
48840
  assertActive();
@@ -49187,4 +49296,4 @@ function createLocalBoard(options) {
49187
49296
  };
49188
49297
  }
49189
49298
  //#endregion
49190
- export { STAMPS, createBoardController, createLocalBoard, isStampKind, stampDataUrl };
49299
+ export { AssetResolutionError, STAMPS, SUPPORTED_ASSET_MEDIA_TYPES, assetRef, clampAssetCacheBytes, cloneCustomObject, createBoardController, createLocalBoard, isAssetRef, isStampKind, stampDataUrl };
package/dist/core.d.ts CHANGED
@@ -11,6 +11,79 @@ declare function strokeId(value: string): StrokeId;
11
11
 
12
12
  /** Wire grammar: `asset:<namespace>:<opaque-id>`. Interpreted only by the Host. */
13
13
  type AssetRef = string;
14
+ declare const ASSET_REF_PATTERN: RegExp;
15
+ /** Hard ceiling on a reference's own wire length — independent of any resource limit below. */
16
+ declare const ASSET_REF_MAX_BYTES = 512;
17
+ declare function isAssetRef(value: unknown): value is AssetRef;
18
+ /** Throws on malformed input; use `isAssetRef` where a boolean is wanted instead. */
19
+ declare function assetRef(value: string): AssetRef;
20
+ type AssetKind = "image";
21
+ type AssetPurpose = "render" | "thumbnail" | "export";
22
+ interface AssetResolveRequest {
23
+ ref: AssetRef;
24
+ kind: AssetKind;
25
+ purpose: AssetPurpose;
26
+ signal: AbortSignal;
27
+ }
28
+ interface AssetResolveResult {
29
+ bytes: Uint8Array;
30
+ mediaType: string;
31
+ }
32
+ /**
33
+ * Host-owned lookup. Scrawl passes only the reference, kind, purpose, and
34
+ * signal — never Document contents, an Extension instance, or credentials.
35
+ * The returned bytes are copied into SDK-owned storage and independently
36
+ * validated before use; a resolver's claimed `mediaType` is never trusted
37
+ * on its own (see `assetValidation.ts`).
38
+ */
39
+ interface AssetResolver {
40
+ resolve(request: AssetResolveRequest): Promise<AssetResolveResult>;
41
+ }
42
+ interface AssetIngestRequest {
43
+ bytes: Uint8Array;
44
+ mediaType: string;
45
+ name?: string;
46
+ signal: AbortSignal;
47
+ }
48
+ interface AssetIngestResult {
49
+ ref: AssetRef;
50
+ }
51
+ /**
52
+ * Host-owned upload/creation path. Scrawl validates the candidate against
53
+ * SDK limits before calling this, and validates the returned reference
54
+ * before it can enter any command. Cancellation or failure creates no
55
+ * Document object, Op, or history entry.
56
+ */
57
+ interface AssetIngestor {
58
+ ingest(request: AssetIngestRequest): Promise<AssetIngestResult>;
59
+ }
60
+ type AssetResolutionErrorCode = "resolver-unavailable" | "not-found" | "forbidden" | "offline" | "unsupported-type" | "too-large" | "invalid-content" | "decode-failed" | "budget-exceeded" | "aborted" | "unknown";
61
+ declare class AssetResolutionError extends Error {
62
+ readonly code: AssetResolutionErrorCode;
63
+ readonly retryable: boolean;
64
+ readonly ref?: AssetRef | undefined;
65
+ constructor(code: AssetResolutionErrorCode, retryable: boolean, message: string, ref?: AssetRef | undefined);
66
+ }
67
+ /** Runtime event for a resolution/ingestion failure — never carries credentials or a fetchable location. */
68
+ interface AssetDiagnostic {
69
+ code: AssetResolutionErrorCode;
70
+ ref?: AssetRef;
71
+ objectId: string;
72
+ objectKind: "image" | "custom";
73
+ retryable: boolean;
74
+ }
75
+ declare const ASSET_MAX_ENCODED_BYTES: number;
76
+ declare const ASSET_MAX_DIMENSION_PX = 8192;
77
+ declare const ASSET_MAX_DECODED_MEGAPIXELS = 40;
78
+ declare const ASSET_MAX_CONCURRENT_RESOLUTIONS = 6;
79
+ declare const ASSET_CACHE_BYTES_DEFAULT: number;
80
+ declare const ASSET_CACHE_BYTES_MIN: number;
81
+ declare const ASSET_CACHE_BYTES_MAX: number;
82
+ declare const ASSET_EXPORT_MAX_ENCODED_BYTES: number;
83
+ declare const ASSET_EXPORT_MAX_DECODED_MEGAPIXELS = 100;
84
+ declare const SUPPORTED_ASSET_MEDIA_TYPES: readonly ["image/png", "image/jpeg", "image/webp"];
85
+ type SupportedAssetMediaType = (typeof SUPPORTED_ASSET_MEDIA_TYPES)[number];
86
+ declare function clampAssetCacheBytes(value: number | undefined): number;
14
87
 
15
88
  type Mat2x3 = [number, number, number, number, number, number];
16
89
  declare const IDENTITY: Mat2x3;
@@ -69,6 +142,7 @@ interface CustomBoardObject {
69
142
  };
70
143
  props: JsonValue;
71
144
  }
145
+ declare function cloneCustomObject(object: CustomBoardObject): CustomBoardObject;
72
146
  /**
73
147
  * The read-only view handed to `describe`. Deep-readonly by construction
74
148
  * (not derived via a shallow `Readonly<>`) because `describe` must treat its
@@ -106,7 +180,7 @@ interface CustomObjectDefinition<Props extends JsonValue = JsonValue> {
106
180
  parse(input: unknown, schemaVersion: number): Props;
107
181
  /** One pure, synchronous step per consecutive schema version. */
108
182
  migrate?: Readonly<Record<number, (oldProps: JsonValue) => JsonValue>>;
109
- describe(object: ReadonlyCustomObject<Props>, context: ObjectDescribeContext): BoardScene$1;
183
+ describe(object: ReadonlyCustomObject<Props>, context: ObjectDescribeContext): BoardScene;
110
184
  }
111
185
  interface SceneNodeBase {
112
186
  key: string;
@@ -138,9 +212,9 @@ interface SceneText extends SceneNodeBase {
138
212
  }
139
213
  interface SceneGroup extends SceneNodeBase {
140
214
  kind: "group";
141
- children: readonly BoardScene$1[];
215
+ children: readonly BoardScene[];
142
216
  }
143
- interface ScenePath$1 extends SceneNodeBase {
217
+ interface ScenePath extends SceneNodeBase {
144
218
  kind: "path";
145
219
  /** SVG-style path data, board-local coordinates. */
146
220
  d: string;
@@ -173,7 +247,7 @@ interface SceneEllipse extends SceneNodeBase {
173
247
  stroke?: string;
174
248
  strokeWidth?: number;
175
249
  }
176
- type BoardScene$1 = SceneGroup | ScenePath$1 | SceneText | SceneImage | SceneRect | SceneEllipse;
250
+ type BoardScene = SceneGroup | ScenePath | SceneText | SceneImage | SceneRect | SceneEllipse;
177
251
  type ToolCursor = "default" | "crosshair" | "pointer" | "grab" | "grabbing" | "text";
178
252
  type ToolCancelReason = "escape-key" | "tool-switched" | "pointer-lost" | "error";
179
253
  interface InputModifiers {
@@ -269,7 +343,7 @@ interface ToolCapabilities {
269
343
  };
270
344
  /** Session-only geometry — never enters Document/history/persistence/collaboration. */
271
345
  preview: {
272
- set(scene: BoardScene$1): void;
346
+ set(scene: BoardScene): void;
273
347
  clear(): void;
274
348
  };
275
349
  /** Constructs, validates, and commits one atomic command; controller derives Ops. */
@@ -279,6 +353,15 @@ interface ToolCapabilities {
279
353
  cancel(): void;
280
354
  };
281
355
  }
356
+ interface ExtensionDiagnostic {
357
+ extensionId: ExtensionId;
358
+ phase: "parse" | "migrate" | "describe" | "tool-lifecycle" | "react-companion" | "asset-resolution";
359
+ toolId?: ToolId;
360
+ objectId?: string;
361
+ objectType?: ObjectType;
362
+ recoverable: boolean;
363
+ cause: unknown;
364
+ }
282
365
 
283
366
  /** Who holds a lock. Stored on the item so only they can take it off. */
284
367
  interface LockHolder {
@@ -489,7 +572,6 @@ declare function measureTable(table: TableBlock): {
489
572
  width: number;
490
573
  height: number;
491
574
  };
492
- declare const BOARD_COLOR = "#FFFFFF";
493
575
  declare const FOG_COLOR = "#FFFFFF";
494
576
  /**
495
577
  * An imported image block on the board plane.
@@ -1048,6 +1130,11 @@ interface ExtensionRegistry {
1048
1130
  * isn't in it), Custom objects still export via the standard fallback
1049
1131
  * placeholder (`fallback.bounds`/`label`), never silently dropped.
1050
1132
  *
1133
+ * `backgroundColor` should be the Host's actual resolved `boardTheme.surface`
1134
+ * so an export matches what was on screen; defaults to the theme system's
1135
+ * own light-preset default when the caller doesn't have one on hand. The
1136
+ * board's reference grid, if any, is a screen-only aid and never exported.
1137
+ *
1051
1138
  * `resolvedAssets` (ticket #23) maps an Asset reference to an already-
1052
1139
  * resolved `data:` URI — see `assetExport.ts`'s `exportDocumentSVGWithAssets`,
1053
1140
  * which is the only intended caller that ever passes one. Without it, every
@@ -1055,30 +1142,14 @@ interface ExtensionRegistry {
1055
1142
  * placeholder instead of guessing at a URL; a legacy `src`-backed image is
1056
1143
  * unaffected either way.
1057
1144
  */
1058
- declare function documentToSVG(doc: SerializedDocument, now?: number, registry?: ExtensionRegistry, resolvedAssets?: ReadonlyMap<AssetRef, string>): string;
1145
+ declare function documentToSVG(doc: SerializedDocument, now?: number, registry?: ExtensionRegistry, backgroundColor?: string, resolvedAssets?: ReadonlyMap<AssetRef, string>): string;
1059
1146
 
1060
1147
  declare const SDK_PACKAGE_NAME = "@scrawl-board/board";
1061
1148
  declare const SDK_DEVELOPMENT_VERSION = "0.0.0-development";
1062
1149
 
1063
- type BoardBounds = {
1064
- minX: number;
1065
- minY: number;
1066
- maxX: number;
1067
- maxY: number;
1068
- };
1069
- type ScenePath = {
1070
- id: string;
1071
- color: string;
1072
- width: number;
1073
- points: readonly BoardPoint[];
1074
- };
1075
- type BoardScene = {
1076
- bounds: BoardBounds;
1077
- paths: readonly ScenePath[];
1078
- };
1079
1150
  type BoardStroke = Stroke;
1080
1151
  type SerializedBoardStroke = SerializedStroke;
1081
1152
  type SerializedBoardDocument = CurrentSerializedDocument;
1082
1153
 
1083
- export { AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, BEACON_INSET, BOARD_COLOR, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, LockItemsCommand, MIN_WIDTH_FACTOR, 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, SpatialIndex, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, UpdateImageCommand, UpdateNoteCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, avgScale, canUnlockItem, changeToOps, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, documentId, documentToSVG, formatTimer, invert, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, ribbonEdges, rotationAbout, scalingAbout, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation };
1084
- export type { BBox, BoardBounds, BoardPoint, BoardScene, BoardStroke, ClusterIdFactory, Command, CommandKind, CurrentSerializedDocument, CurrentSerializedStroke, DocumentChange, DocumentId, DocumentLoadResult, DocumentRecoveryCode, ImageBlock, KitchenTimer, LockHolder, LockTarget, Lockable, Mat2x3, NoteVote, Op, OpCollection, PresencePlacement, RibbonEdgePoint, ScenePath, SearchHit, SearchHitKind, SearchableBoard, SearchableComment, SerializedBoardDocument, SerializedBoardStroke, SerializedPoint, SerializedStroke, StampKind, StickyNote, Stroke, StrokeId, StrokePoint, StrokeTool, TableBlock, TextBlock, ViewportInset };
1154
+ 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, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, LockItemsCommand, MIN_WIDTH_FACTOR, 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, SpatialIndex, 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, documentId, documentToSVG, formatTimer, invert, isAssetRef, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, ribbonEdges, rotationAbout, scalingAbout, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation };
1155
+ export type { AssetDiagnostic, AssetIngestRequest, AssetIngestResult, AssetIngestor, AssetKind, AssetPurpose, AssetRef, AssetResolutionErrorCode, AssetResolveRequest, AssetResolveResult, AssetResolver, BBox, BoardKeyInput, BoardPoint, BoardPointerInput, BoardScene, BoardStroke, ClusterIdFactory, Command, CommandKind, CurrentSerializedDocument, CurrentSerializedStroke, CustomBoardObject, CustomObjectAddInput, CustomObjectDefinition, CustomTool, CustomToolDefinition, DocumentChange, DocumentId, DocumentLoadResult, DocumentRecoveryCode, ExtensionCommand, ExtensionDiagnostic, ExtensionHitResult, ExtensionId, ExtensionRequirement, ImageBlock, InputModifiers, JsonObject, JsonValue, KitchenTimer, LockHolder, LockTarget, Lockable, Mat2x3, NoteVote, ObjectDescribeContext, ObjectIntent, ObjectType, Op, OpCollection, PresencePlacement, QueryableBoardObject, ReadonlyCustomObject, RibbonEdgePoint, SceneEllipse, SceneGroup, SceneImage, ScenePath, SceneRect, SceneText, ScrawlExtension, SearchHit, SearchHitKind, SearchableBoard, SearchableComment, SerializedBoardDocument, SerializedBoardStroke, SerializedPoint, SerializedStroke, StampKind, StickyNote, Stroke, StrokeId, StrokePoint, StrokeTool, SupportedAssetMediaType, TableBlock, TextBlock, ToolCancelReason, ToolCapabilities, ToolCursor, ToolId, ViewportInset };