@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/index.js CHANGED
@@ -3,6 +3,8 @@ import { createPortal } from "react-dom";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/core-internal/assets.ts
5
5
  var ASSET_REF_PATTERN = /^asset:[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?:[A-Za-z0-9._~-]+$/;
6
+ /** Hard ceiling on a reference's own wire length — independent of any resource limit below. */
7
+ var ASSET_REF_MAX_BYTES = 512;
6
8
  function isAssetRef(value) {
7
9
  return typeof value === "string" && ASSET_REF_PATTERN.test(value) && new TextEncoder().encode(value).length <= 512;
8
10
  }
@@ -20,9 +22,20 @@ var AssetResolutionError = class extends Error {
20
22
  this.name = "AssetResolutionError";
21
23
  }
22
24
  };
25
+ var ASSET_MAX_ENCODED_BYTES = 20 * 1024 * 1024;
26
+ var ASSET_MAX_DIMENSION_PX = 8192;
27
+ var ASSET_MAX_DECODED_MEGAPIXELS = 40;
28
+ var ASSET_MAX_CONCURRENT_RESOLUTIONS = 6;
23
29
  var ASSET_CACHE_BYTES_DEFAULT = 256 * 1024 * 1024;
24
30
  var ASSET_CACHE_BYTES_MIN = 64 * 1024 * 1024;
25
31
  var ASSET_CACHE_BYTES_MAX = 512 * 1024 * 1024;
32
+ var ASSET_EXPORT_MAX_ENCODED_BYTES = 100 * 1024 * 1024;
33
+ var ASSET_EXPORT_MAX_DECODED_MEGAPIXELS = 100;
34
+ var SUPPORTED_ASSET_MEDIA_TYPES = [
35
+ "image/png",
36
+ "image/jpeg",
37
+ "image/webp"
38
+ ];
26
39
  function clampAssetCacheBytes(value) {
27
40
  if (value === void 0 || !Number.isFinite(value)) return ASSET_CACHE_BYTES_DEFAULT;
28
41
  return Math.min(ASSET_CACHE_BYTES_MAX, Math.max(ASSET_CACHE_BYTES_MIN, Math.floor(value)));
@@ -466,7 +479,6 @@ function measureTable(table) {
466
479
  height: table.rowHeights.reduce((sum, h) => sum + h, 0)
467
480
  };
468
481
  }
469
- var BOARD_COLOR = "#FFFFFF";
470
482
  var FOG_COLOR = "#FFFFFF";
471
483
  function cloneImage(img) {
472
484
  return { ...img };
@@ -1963,12 +1975,19 @@ var LIGHT = {
1963
1975
  var MARGIN = 4;
1964
1976
  var NOTE_FONT_RATIO = 44 / 512;
1965
1977
  var NOTE_PAD_RATIO = 40 / 512;
1978
+ /** Matches the theme system's light-preset `boardSurface` default (theme.ts) — kept in sync manually, since core-internal stays decoupled from the theme layer. */
1979
+ var DEFAULT_BACKGROUND_COLOR = "#f5f5f3";
1966
1980
  /**
1967
1981
  * `registry` is optional and only enables rendering Custom objects through
1968
1982
  * their own `describe()` — without it (or for an object whose extension
1969
1983
  * isn't in it), Custom objects still export via the standard fallback
1970
1984
  * placeholder (`fallback.bounds`/`label`), never silently dropped.
1971
1985
  *
1986
+ * `backgroundColor` should be the Host's actual resolved `boardTheme.surface`
1987
+ * so an export matches what was on screen; defaults to the theme system's
1988
+ * own light-preset default when the caller doesn't have one on hand. The
1989
+ * board's reference grid, if any, is a screen-only aid and never exported.
1990
+ *
1972
1991
  * `resolvedAssets` (ticket #23) maps an Asset reference to an already-
1973
1992
  * resolved `data:` URI — see `assetExport.ts`'s `exportDocumentSVGWithAssets`,
1974
1993
  * which is the only intended caller that ever passes one. Without it, every
@@ -1976,7 +1995,7 @@ var NOTE_PAD_RATIO = 40 / 512;
1976
1995
  * placeholder instead of guessing at a URL; a legacy `src`-backed image is
1977
1996
  * unaffected either way.
1978
1997
  */
1979
- function documentToSVG(doc, now = 0, registry, resolvedAssets) {
1998
+ function documentToSVG(doc, now = 0, registry, backgroundColor = DEFAULT_BACKGROUND_COLOR, resolvedAssets) {
1980
1999
  const notes = doc.notes ?? [];
1981
2000
  const texts = doc.textBlocks ?? [];
1982
2001
  const tables = doc.tables ?? [];
@@ -1989,7 +2008,7 @@ function documentToSVG(doc, now = 0, registry, resolvedAssets) {
1989
2008
  const highlights = doc.strokes.filter((s) => s.tool === "highlighter");
1990
2009
  const ink = doc.strokes.filter((s) => s.tool !== "highlighter");
1991
2010
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${view}">\n<defs>\n${defs}\n</defs>\n${[
1992
- `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${BOARD_COLOR}"/>`,
2011
+ `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${backgroundColor}"/>`,
1993
2012
  ...images.map((image) => imageToElement(image, resolvedAssets)),
1994
2013
  ...tables.map(tableToGroup),
1995
2014
  ...highlights.flatMap(strokeToPaths),
@@ -2909,7 +2928,7 @@ var SvgAssetExportError = class extends Error {
2909
2928
  this.name = "SvgAssetExportError";
2910
2929
  }
2911
2930
  };
2912
- async function exportDocumentSVGWithAssets(doc, now, registry, assetController, options) {
2931
+ async function exportDocumentSVGWithAssets(doc, now, registry, assetController, backgroundColor, options) {
2913
2932
  const refs = collectDocumentAssetRefs(doc, registry);
2914
2933
  const resolvedAssets = /* @__PURE__ */ new Map();
2915
2934
  const missingAssets = [];
@@ -2945,7 +2964,7 @@ async function exportDocumentSVGWithAssets(doc, now, registry, assetController,
2945
2964
  }
2946
2965
  if (missingAssets.length > 0 && options?.missingAssets !== "placeholder") throw new SvgAssetExportError(missingAssets);
2947
2966
  return {
2948
- svg: documentToSVG(doc, now, registry, resolvedAssets),
2967
+ svg: documentToSVG(doc, now, registry, backgroundColor, resolvedAssets),
2949
2968
  missingAssets
2950
2969
  };
2951
2970
  }
@@ -35866,6 +35885,21 @@ var FOG_GLSL = `
35866
35885
  `;
35867
35886
  //#endregion
35868
35887
  //#region ../../src/scrawl/engine/boardSurface.ts
35888
+ var GRID_MODE_CODE = {
35889
+ none: 0,
35890
+ line: 1,
35891
+ dot: 2
35892
+ };
35893
+ /** Used when a caller doesn't supply one (e.g. ScrawlEngine constructed without a Host theme). */
35894
+ var DEFAULT_BOARD_SURFACE_THEME = {
35895
+ color: "#f5f5f3",
35896
+ gridMode: "none",
35897
+ gridColor: "#deded8",
35898
+ gridSpacing: 5
35899
+ };
35900
+ /** Grid alpha fades out below this on-screen spacing (px) — otherwise it aliases into a solid wash when zoomed out. */
35901
+ var GRID_FADE_MIN_PX = 3;
35902
+ var GRID_FADE_MAX_PX = 8;
35869
35903
  var VERTEX$1 = `
35870
35904
  varying vec3 vWorldPos;
35871
35905
  void main() {
@@ -35879,6 +35913,11 @@ var FRAGMENT = `
35879
35913
  uniform vec3 uFogColor;
35880
35914
  uniform float uFogNear;
35881
35915
  uniform float uFogFar;
35916
+ uniform float uGridMode;
35917
+ uniform vec3 uGridColor;
35918
+ uniform float uGridSpacing;
35919
+ uniform float uGridLineWorld;
35920
+ uniform float uGridFade;
35882
35921
  varying vec3 vWorldPos;
35883
35922
 
35884
35923
  float hash(vec2 p) {
@@ -35897,6 +35936,22 @@ var FRAGMENT = `
35897
35936
 
35898
35937
  ${FOG_GLSL}
35899
35938
 
35939
+ // Distance from p's nearest axis-aligned grid line, in world units.
35940
+ float gridLineAlpha(vec2 p) {
35941
+ vec2 cell = mod(p, uGridSpacing);
35942
+ vec2 distToLine = min(cell, uGridSpacing - cell);
35943
+ vec2 lineAlpha2 = 1.0 - smoothstep(uGridLineWorld, uGridLineWorld * 2.0, distToLine);
35944
+ return max(lineAlpha2.x, lineAlpha2.y);
35945
+ }
35946
+
35947
+ // Distance from p's nearest grid intersection, in world units.
35948
+ float gridDotAlpha(vec2 p) {
35949
+ vec2 cell = mod(p, uGridSpacing) - uGridSpacing * 0.5;
35950
+ float dist = length(cell);
35951
+ float radius = uGridLineWorld * 1.5;
35952
+ return 1.0 - smoothstep(radius, radius * 2.0, dist);
35953
+ }
35954
+
35900
35955
  void main() {
35901
35956
  vec3 col = uColor;
35902
35957
 
@@ -35914,16 +35969,26 @@ var FRAGMENT = `
35914
35969
  float spec = pow(max(dot(N, H), 0.0), 90.0);
35915
35970
  col += (fres * 0.18 + spec * 0.06) * vec3(1.0);
35916
35971
 
35972
+ if (uGridMode > 0.5 && uGridFade > 0.001) {
35973
+ float gridAlpha = (uGridMode < 1.5 ? gridLineAlpha(vWorldPos.xy) : gridDotAlpha(vWorldPos.xy)) * uGridFade;
35974
+ col = mix(col, uGridColor, gridAlpha);
35975
+ }
35976
+
35917
35977
  col = applyBoardFog(col, vWorldPos);
35918
35978
  gl_FragColor = vec4(col, 1.0);
35919
35979
  }
35920
35980
  `;
35921
- function createBoardSurface(fog) {
35981
+ function createBoardSurface(fog, theme) {
35922
35982
  const material = new ShaderMaterial({
35923
35983
  vertexShader: VERTEX$1,
35924
35984
  fragmentShader: FRAGMENT,
35925
35985
  uniforms: {
35926
- uColor: { value: new Color(BOARD_COLOR) },
35986
+ uColor: { value: new Color(theme.color) },
35987
+ uGridMode: { value: GRID_MODE_CODE[theme.gridMode] },
35988
+ uGridColor: { value: new Color(theme.gridColor) },
35989
+ uGridSpacing: { value: Math.max(.001, theme.gridSpacing) },
35990
+ uGridLineWorld: { value: .05 },
35991
+ uGridFade: { value: 0 },
35927
35992
  ...fog
35928
35993
  }
35929
35994
  });
@@ -35931,11 +35996,32 @@ function createBoardSurface(fog) {
35931
35996
  mesh.name = "board-surface";
35932
35997
  return mesh;
35933
35998
  }
35934
- /** Keep the plane centered under the view and larger than the fog horizon. */
35935
- function updateBoardSurface(mesh, centerX, centerY, fogFar) {
35999
+ /** Applies a theme change (initial or live) — everything except the per-frame fade/line-width. */
36000
+ function setBoardSurfaceTheme(mesh, theme) {
36001
+ const uniforms = mesh.material.uniforms;
36002
+ uniforms.uColor.value.set(theme.color);
36003
+ uniforms.uGridMode.value = GRID_MODE_CODE[theme.gridMode];
36004
+ uniforms.uGridColor.value.set(theme.gridColor);
36005
+ uniforms.uGridSpacing.value = Math.max(.001, theme.gridSpacing);
36006
+ }
36007
+ /** Keep the plane centered under the view, larger than the fog horizon, and the grid's on-screen weight/fade current. */
36008
+ function updateBoardSurface(mesh, centerX, centerY, fogFar, worldPerPixel) {
35936
36009
  mesh.position.set(centerX, centerY, 0);
35937
36010
  const size = fogFar * 2.5;
35938
36011
  mesh.scale.set(size, size, 1);
36012
+ const uniforms = mesh.material.uniforms;
36013
+ uniforms.uGridLineWorld.value = worldPerPixel;
36014
+ const t = (uniforms.uGridSpacing.value / Math.max(1e-6, worldPerPixel) - GRID_FADE_MIN_PX) / (GRID_FADE_MAX_PX - GRID_FADE_MIN_PX);
36015
+ uniforms.uGridFade.value = MathUtils.clamp(t, 0, 1);
36016
+ }
36017
+ /** Temporarily zeroes the grid's contribution (for export snapshots) and returns a restore function. */
36018
+ function suppressBoardSurfaceGrid(mesh) {
36019
+ const uniforms = mesh.material.uniforms;
36020
+ const previous = uniforms.uGridFade.value;
36021
+ uniforms.uGridFade.value = 0;
36022
+ return () => {
36023
+ uniforms.uGridFade.value = previous;
36024
+ };
35939
36025
  }
35940
36026
  var MIN_HEIGHT = 40;
35941
36027
  var MAX_HEIGHT = 1500;
@@ -36099,7 +36185,7 @@ var CameraRig = class {
36099
36185
  //#endregion
36100
36186
  //#region ../../src/scrawl/engine/scene.ts
36101
36187
  var SceneRig = class {
36102
- constructor(canvas) {
36188
+ constructor(canvas, boardTheme) {
36103
36189
  this.renderer = new WebGLRenderer({
36104
36190
  canvas,
36105
36191
  antialias: true
@@ -36109,7 +36195,7 @@ var SceneRig = class {
36109
36195
  this.scene = new Scene();
36110
36196
  this.cameraRig = new CameraRig(canvas.clientWidth / Math.max(1, canvas.clientHeight));
36111
36197
  this.fog = createFogUniforms(FOG_COLOR);
36112
- this.board = createBoardSurface(this.fog);
36198
+ this.board = createBoardSurface(this.fog, boardTheme);
36113
36199
  this.scene.add(this.board);
36114
36200
  this.inkGroup = new Group();
36115
36201
  this.inkGroup.name = "ink";
@@ -36126,9 +36212,17 @@ var SceneRig = class {
36126
36212
  renderFrame(dtMs) {
36127
36213
  this.cameraRig.update(dtMs);
36128
36214
  updateFogForCamera(this.fog, this.cameraRig.height);
36129
- updateBoardSurface(this.board, this.cameraRig.center.x, this.cameraRig.center.y, this.fog.uFogFar.value);
36215
+ updateBoardSurface(this.board, this.cameraRig.center.x, this.cameraRig.center.y, this.fog.uFogFar.value, this.cameraRig.worldPerPixel());
36130
36216
  this.renderer.render(this.scene, this.cameraRig.camera);
36131
36217
  }
36218
+ /** Live theme update — the board controller's identity stays fixed across theme changes; this is how a new one reaches the render engine. */
36219
+ applyBoardTheme(theme) {
36220
+ setBoardSurfaceTheme(this.board, theme);
36221
+ }
36222
+ /** Zeroes the grid for one render pass (PNG export never bakes it in) and returns a restore function. */
36223
+ suppressGrid() {
36224
+ return suppressBoardSurfaceGrid(this.board);
36225
+ }
36132
36226
  dispose() {
36133
36227
  this.board.geometry.dispose();
36134
36228
  this.board.material.dispose();
@@ -46577,9 +46671,10 @@ function emptyExtensionRegistry() {
46577
46671
  * React never reaches past this class.
46578
46672
  */
46579
46673
  var ScrawlEngine = class {
46580
- constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0)) {
46674
+ constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0), boardTheme = DEFAULT_BOARD_SURFACE_THEME) {
46581
46675
  this.canvas = canvas;
46582
46676
  this.extensionRegistry = extensionRegistry;
46677
+ this.boardTheme = boardTheme;
46583
46678
  this.onToolChange = null;
46584
46679
  this.onZoomChange = null;
46585
46680
  this.onAuditEvent = null;
@@ -46626,7 +46721,7 @@ var ScrawlEngine = class {
46626
46721
  this.stampKind = "star";
46627
46722
  this.timerDurationMs = TIMER_DEFAULT_DURATION_MS;
46628
46723
  this.readOnly = false;
46629
- this.scene = new SceneRig(canvas);
46724
+ this.scene = new SceneRig(canvas, boardTheme);
46630
46725
  this.document = new BoardDocument(documentId(documentId$1));
46631
46726
  this.history = new History(this.document);
46632
46727
  this.history.onCommand = (command, kind) => {
@@ -46933,6 +47028,11 @@ var ScrawlEngine = class {
46933
47028
  height: rig.height
46934
47029
  };
46935
47030
  }
47031
+ /** Live update of the board surface color/grid — the engine's identity stays fixed across theme changes. */
47032
+ applyBoardTheme(theme) {
47033
+ this.boardTheme = theme;
47034
+ this.scene.applyBoardTheme(theme);
47035
+ }
46936
47036
  /**
46937
47037
  * Snap to a peer's camera. No-op while drawing so pointer-to-board
46938
47038
  * mapping does not warp the in-progress stroke.
@@ -46951,7 +47051,7 @@ var ScrawlEngine = class {
46951
47051
  }
46952
47052
  /** The saved document as standalone SVG — a pure walk of the wire format. */
46953
47053
  exportSVG() {
46954
- return documentToSVG(this.document.toJSON(), Date.now(), this.extensionRegistry);
47054
+ return documentToSVG(this.document.toJSON(), Date.now(), this.extensionRegistry, this.boardTheme.color);
46955
47055
  }
46956
47056
  /**
46957
47057
  * Content-framed PNG snapshot. Renders once offscreen-style with UI props
@@ -46987,6 +47087,7 @@ var ScrawlEngine = class {
46987
47087
  this.notesRenderer.setFocus(null);
46988
47088
  this.textsRenderer.setFocus(null);
46989
47089
  this.renderer.setGhostsVisible(false);
47090
+ const restoreGrid = this.scene.suppressGrid();
46990
47091
  try {
46991
47092
  rig.yaw = 0;
46992
47093
  rig.pitch = 0;
@@ -46997,6 +47098,7 @@ var ScrawlEngine = class {
46997
47098
  const dataUrl = this.scene.renderer.domElement.toDataURL("image/png");
46998
47099
  return await (await fetch(dataUrl)).blob();
46999
47100
  } finally {
47101
+ restoreGrid();
47000
47102
  rig.center.x = saved.x;
47001
47103
  rig.center.y = saved.y;
47002
47104
  rig.height = saved.height;
@@ -48006,7 +48108,13 @@ function createBoardControllerWithEngine(options, existingEngine) {
48006
48108
  }
48007
48109
  const extensionRegistry = registryResult.registry;
48008
48110
  const assetController = new AssetController(options.assetResolver, { cacheBytes: options.assetCacheBytes });
48009
- const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController) : null);
48111
+ let currentBoardTheme = {
48112
+ color: options.boardTheme?.surface ?? DEFAULT_BOARD_SURFACE_THEME.color,
48113
+ gridMode: options.boardTheme?.gridMode ?? DEFAULT_BOARD_SURFACE_THEME.gridMode,
48114
+ gridColor: options.boardTheme?.gridColor ?? DEFAULT_BOARD_SURFACE_THEME.gridColor,
48115
+ gridSpacing: options.boardTheme?.gridSpacing ?? DEFAULT_BOARD_SURFACE_THEME.gridSpacing
48116
+ };
48117
+ const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController, currentBoardTheme) : null);
48010
48118
  const document = engine?.document ?? new BoardDocument(documentId(options.document.id));
48011
48119
  const history = engine?.history ?? new History(document);
48012
48120
  const createId = options.createId ?? (() => crypto.randomUUID());
@@ -48490,6 +48598,15 @@ function createBoardControllerWithEngine(options, existingEngine) {
48490
48598
  changed();
48491
48599
  }
48492
48600
  },
48601
+ boardTheme: { set(theme) {
48602
+ currentBoardTheme = {
48603
+ color: theme.surface ?? currentBoardTheme.color,
48604
+ gridMode: theme.gridMode ?? currentBoardTheme.gridMode,
48605
+ gridColor: theme.gridColor ?? currentBoardTheme.gridColor,
48606
+ gridSpacing: theme.gridSpacing ?? currentBoardTheme.gridSpacing
48607
+ };
48608
+ engine?.applyBoardTheme(currentBoardTheme);
48609
+ } },
48493
48610
  view: {
48494
48611
  fit() {
48495
48612
  assertActive();
@@ -48794,10 +48911,10 @@ function createBoardControllerWithEngine(options, existingEngine) {
48794
48911
  gather: (nextView) => engine?.applyGatherView(nextView) ?? false
48795
48912
  },
48796
48913
  export: {
48797
- svg: () => engine ? engine.exportSVG() : documentToSVG(document.toJSON(), Date.now(), extensionRegistry),
48914
+ svg: () => engine ? engine.exportSVG() : documentToSVG(document.toJSON(), Date.now(), extensionRegistry, currentBoardTheme.color),
48798
48915
  png: (options) => engine ? engine.exportPNG(options?.maxSide, { awaitAssets: options?.awaitAssets }) : Promise.resolve(null),
48799
48916
  json: () => document.toJSON(),
48800
- svgAsync: (svgOptions) => exportDocumentSVGWithAssets(document.toJSON(), Date.now(), extensionRegistry, assetController, svgOptions)
48917
+ svgAsync: (svgOptions) => exportDocumentSVGWithAssets(document.toJSON(), Date.now(), extensionRegistry, assetController, currentBoardTheme.color, svgOptions)
48801
48918
  },
48802
48919
  assets: { async ingest(bytes, mediaType, name, signal) {
48803
48920
  assertActive();
@@ -49284,7 +49401,11 @@ var scrawlThemePresets = Object.freeze({
49284
49401
  elevationHigh: "0 8px 20px rgba(28, 28, 26, 0.18)",
49285
49402
  motionDuration: 140,
49286
49403
  motionEasing: "cubic-bezier(0.2, 0, 0, 1)",
49287
- density: "comfortable"
49404
+ density: "comfortable",
49405
+ boardSurface: "#f5f5f3",
49406
+ gridMode: "none",
49407
+ gridColor: "#deded8",
49408
+ gridSpacing: 5
49288
49409
  }),
49289
49410
  dark: Object.freeze({
49290
49411
  surface: "#171816",
@@ -49309,7 +49430,11 @@ var scrawlThemePresets = Object.freeze({
49309
49430
  elevationHigh: "0 8px 20px rgba(0, 0, 0, 0.38)",
49310
49431
  motionDuration: 140,
49311
49432
  motionEasing: "cubic-bezier(0.2, 0, 0, 1)",
49312
- density: "comfortable"
49433
+ density: "comfortable",
49434
+ boardSurface: "#232420",
49435
+ gridMode: "none",
49436
+ gridColor: "#33352f",
49437
+ gridSpacing: 5
49313
49438
  })
49314
49439
  });
49315
49440
  var variableNames = {
@@ -49335,7 +49460,11 @@ var variableNames = {
49335
49460
  elevationHigh: "--scrawl-elevation-high",
49336
49461
  motionDuration: "--scrawl-motion-duration",
49337
49462
  motionEasing: "--scrawl-motion-easing",
49338
- density: "--scrawl-density"
49463
+ density: "--scrawl-density",
49464
+ boardSurface: "--scrawl-board-surface",
49465
+ gridMode: "--scrawl-grid-mode",
49466
+ gridColor: "--scrawl-grid-color",
49467
+ gridSpacing: "--scrawl-grid-spacing"
49339
49468
  };
49340
49469
  var colorTokens = /* @__PURE__ */ new Set([
49341
49470
  "surface",
@@ -49348,7 +49477,9 @@ var colorTokens = /* @__PURE__ */ new Set([
49348
49477
  "selection",
49349
49478
  "danger",
49350
49479
  "warning",
49351
- "success"
49480
+ "success",
49481
+ "boardSurface",
49482
+ "gridColor"
49352
49483
  ]);
49353
49484
  var knownTokens = new Set(Object.keys(scrawlThemePresets.light));
49354
49485
  function validateScrawlTheme(theme) {
@@ -49385,6 +49516,14 @@ function validateScrawlTheme(theme) {
49385
49516
  token,
49386
49517
  message: "Expected comfortable or compact"
49387
49518
  });
49519
+ else if (token === "gridMode" && value !== "none" && value !== "line" && value !== "dot") diagnostics.push({
49520
+ token,
49521
+ message: "Expected none, line, or dot"
49522
+ });
49523
+ else if (token === "gridSpacing" && (!finiteNumber(value) || value < .5 || value > 50)) diagnostics.push({
49524
+ token,
49525
+ message: "Expected spacing 0.5-50 board units"
49526
+ });
49388
49527
  else if ((token.endsWith("Family") || token.startsWith("elevation") || token === "motionEasing") && (typeof value !== "string" || !value.trim() || /[;{}]/.test(value))) diagnostics.push({
49389
49528
  token,
49390
49529
  message: "Expected a safe CSS value"
@@ -50540,6 +50679,14 @@ function ScrawlProvider({ controller, children, preset = "light", theme, portalC
50540
50679
  style
50541
50680
  ]);
50542
50681
  useEffect(() => reportThemeDiagnostics(resolved.diagnostics, onThemeDiagnostic), [resolved, onThemeDiagnostic]);
50682
+ useEffect(() => {
50683
+ controller.boardTheme.set({
50684
+ surface: resolved.values.boardSurface,
50685
+ gridMode: resolved.values.gridMode,
50686
+ gridColor: resolved.values.gridColor,
50687
+ gridSpacing: resolved.values.gridSpacing
50688
+ });
50689
+ }, [controller, resolved]);
50543
50690
  useDeferredDisposal(controller, disposeOnUnmount);
50544
50691
  const value = useMemo(() => ({
50545
50692
  controller,
@@ -50587,8 +50734,16 @@ function Scrawl({ children, preset, theme, portalContainer, className, style, on
50587
50734
  try {
50588
50735
  if (!runtimeRef.current) {
50589
50736
  const canvas = suppliedCanvas === void 0 ? ownedCanvasRef.current : suppliedCanvas;
50737
+ const resolved = resolveScrawlTheme(preset, theme);
50738
+ const boardTheme = {
50739
+ surface: resolved.values.boardSurface,
50740
+ gridMode: resolved.values.gridMode,
50741
+ gridColor: resolved.values.gridColor,
50742
+ gridSpacing: resolved.values.gridSpacing
50743
+ };
50590
50744
  controller = createBoardController({
50591
50745
  ...options,
50746
+ boardTheme,
50592
50747
  ...canvas ? { canvas } : {}
50593
50748
  });
50594
50749
  runtimeRef.current = {
@@ -50824,4 +50979,4 @@ function ScrawlBoard({ documentId, initialDocument, onReady, className, style })
50824
50979
  });
50825
50980
  }
50826
50981
  //#endregion
50827
- export { AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, BEACON_INSET, BOARD_COLOR, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, DefaultBoardChrome, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, InlineEditors, LockItemsCommand, MIN_WIDTH_FACTOR, MultiplayerCursors, NOTE_COLORS, NOTE_DEFAULT_SIZE, NOTE_DEFAULT_Z, NOTE_MAX_Z, NOTE_MIN_Z, NOTE_PEEL_STEP, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, STAMPS, STAMP_SIZE, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, SpatialIndex, StyleShelf, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, UpdateImageCommand, UpdateNoteCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, avgScale, canUnlockItem, changeToOps, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, createBoardController, createLocalBoard, documentId, documentToSVG, formatTimer, invert, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, resolveScrawlTheme, ribbonEdges, rotationAbout, scalingAbout, scrawlThemePresets, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
50982
+ 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 };
package/dist/react.d.ts CHANGED
@@ -8,6 +8,9 @@ type StrokeId = string & {
8
8
 
9
9
  /** Wire grammar: `asset:<namespace>:<opaque-id>`. Interpreted only by the Host. */
10
10
  type AssetRef = string;
11
+ declare function isAssetRef(value: unknown): value is AssetRef;
12
+ /** Throws on malformed input; use `isAssetRef` where a boolean is wanted instead. */
13
+ declare function assetRef(value: string): AssetRef;
11
14
  type AssetKind = "image";
12
15
  type AssetPurpose = "render" | "thumbnail" | "export";
13
16
  interface AssetResolveRequest {
@@ -49,6 +52,12 @@ interface AssetIngestor {
49
52
  ingest(request: AssetIngestRequest): Promise<AssetIngestResult>;
50
53
  }
51
54
  type AssetResolutionErrorCode = "resolver-unavailable" | "not-found" | "forbidden" | "offline" | "unsupported-type" | "too-large" | "invalid-content" | "decode-failed" | "budget-exceeded" | "aborted" | "unknown";
55
+ declare class AssetResolutionError extends Error {
56
+ readonly code: AssetResolutionErrorCode;
57
+ readonly retryable: boolean;
58
+ readonly ref?: AssetRef | undefined;
59
+ constructor(code: AssetResolutionErrorCode, retryable: boolean, message: string, ref?: AssetRef | undefined);
60
+ }
52
61
  /** Runtime event for a resolution/ingestion failure — never carries credentials or a fetchable location. */
53
62
  interface AssetDiagnostic {
54
63
  code: AssetResolutionErrorCode;
@@ -57,6 +66,9 @@ interface AssetDiagnostic {
57
66
  objectKind: "image" | "custom";
58
67
  retryable: boolean;
59
68
  }
69
+ declare const SUPPORTED_ASSET_MEDIA_TYPES: readonly ["image/png", "image/jpeg", "image/webp"];
70
+ type SupportedAssetMediaType = (typeof SUPPORTED_ASSET_MEDIA_TYPES)[number];
71
+ declare function clampAssetCacheBytes(value: number | undefined): number;
60
72
 
61
73
  type Mat2x3 = [number, number, number, number, number, number];
62
74
 
@@ -104,6 +116,7 @@ interface CustomBoardObject {
104
116
  };
105
117
  props: JsonValue;
106
118
  }
119
+ declare function cloneCustomObject(object: CustomBoardObject): CustomBoardObject;
107
120
  /**
108
121
  * The read-only view handed to `describe`. Deep-readonly by construction
109
122
  * (not derived via a shallow `Readonly<>`) because `describe` must treat its
@@ -863,22 +876,31 @@ interface CreateBoardControllerOptions {
863
876
  * Trusted Custom tool/object registrations (ticket #22, design:
864
877
  * docs/research/extension-contracts.md). Validated atomically at
865
878
  * construction; registration failure throws before any controller is
866
- * returned. Not yet re-exported from a public package entry point —
867
- * internal-only until the reference Extension proves the seam.
879
+ * returned.
868
880
  */
869
881
  extensions?: readonly ScrawlExtension[];
870
882
  /**
871
883
  * Optional Host-managed Asset capabilities (ticket #23, design:
872
884
  * docs/research/asset-resolution-resource-policy.md). Without a
873
885
  * resolver, referenced Assets preserve their Document geometry and
874
- * render an accessible placeholder. Not yet re-exported from a public
875
- * package entry point — internal-only until the reference resolver
876
- * proves the seam, matching how `extensions` is scoped.
886
+ * render an accessible placeholder.
877
887
  */
878
888
  assetResolver?: AssetResolver;
879
889
  assetIngestor?: AssetIngestor;
880
890
  /** Clamped to 64–512MiB; defaults to 256MiB. */
881
891
  assetCacheBytes?: number;
892
+ /**
893
+ * The rendered board surface's color and reference grid. Defaults to the
894
+ * light theme preset's values; `<Scrawl>` keeps this current across theme
895
+ * changes via `boardTheme.set` below — a headless/browser-tier Host that
896
+ * doesn't use the React theme system can set this directly instead.
897
+ */
898
+ boardTheme?: {
899
+ surface?: string;
900
+ gridMode?: "none" | "line" | "dot";
901
+ gridColor?: string;
902
+ gridSpacing?: number;
903
+ };
882
904
  }
883
905
  interface BoardController {
884
906
  readonly document: ReadonlyBoardDocument;
@@ -904,6 +926,15 @@ interface BoardController {
904
926
  undo(): void;
905
927
  redo(): void;
906
928
  };
929
+ readonly boardTheme: {
930
+ /** Live update of the board surface color/grid — the controller's identity stays fixed across theme changes. */
931
+ set(theme: {
932
+ surface?: string;
933
+ gridMode?: "none" | "line" | "dot";
934
+ gridColor?: string;
935
+ gridSpacing?: number;
936
+ }): void;
937
+ };
907
938
  readonly view: {
908
939
  fit(): void;
909
940
  zoomTo(value: number): void;
@@ -1000,6 +1031,7 @@ type LocalBoard = {
1000
1031
 
1001
1032
  type ScrawlThemePreset = "light" | "dark";
1002
1033
  type ScrawlDensity = "comfortable" | "compact";
1034
+ type ScrawlGridMode = "none" | "line" | "dot";
1003
1035
  interface ScrawlTheme {
1004
1036
  surface?: string;
1005
1037
  surfaceRaised?: string;
@@ -1024,6 +1056,13 @@ interface ScrawlTheme {
1024
1056
  motionDuration?: number;
1025
1057
  motionEasing?: string;
1026
1058
  density?: ScrawlDensity;
1059
+ /** The rendered board/canvas surface color — distinct from `surface` (UI chrome panels). */
1060
+ boardSurface?: string;
1061
+ /** `"none"` (default) keeps the board a plain surface; `"line"`/`"dot"` draw a zoom-adaptive reference grid. */
1062
+ gridMode?: ScrawlGridMode;
1063
+ gridColor?: string;
1064
+ /** Grid spacing in board units at 100% zoom. Ignored when `gridMode` is `"none"`. */
1065
+ gridSpacing?: number;
1027
1066
  }
1028
1067
  type ResolvedScrawlTheme = Required<ScrawlTheme>;
1029
1068
  interface ScrawlThemeDiagnostic {
@@ -1167,5 +1206,5 @@ type ScrawlBoardProps = {
1167
1206
  };
1168
1207
  declare function ScrawlBoard({ documentId, initialDocument, onReady, className, style }: ScrawlBoardProps): react.JSX.Element;
1169
1208
 
1170
- export { DefaultBoardChrome, InlineEditors, MultiplayerCursors, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, StyleShelf, resolveScrawlTheme, scrawlThemePresets, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
1171
- export type { BoardController, BoardSlotProps, BoardSnapshot, BoardStyle, CommentMarker, CreateBoardControllerOptions, DefaultBoardChromeProps, DefaultUIRegion, DefaultUISlot, DefaultUISlots, DialogSlotProps, InlineEditorsProps, LocalBoard, LocalBoardSnapshot, MultiplayerCursorsProps, PresenceCursor, PresenceUser, PresenceView, ScrawlBoardProps, ScrawlCanvasProps, ScrawlDefaultUIProps, ScrawlProps, ScrawlProviderProps, ScrawlResolvedTheme, ScrawlTheme, ScrawlThemeDiagnostic, ScrawlThemePreset, StyleShelfProps };
1209
+ export { AssetResolutionError, DefaultBoardChrome, InlineEditors, MultiplayerCursors, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, StyleShelf, assetRef, clampAssetCacheBytes, cloneCustomObject, isAssetRef, resolveScrawlTheme, scrawlThemePresets, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
1210
+ export type { AssetDiagnostic, AssetIngestRequest, AssetIngestResult, AssetIngestor, AssetKind, AssetPurpose, AssetRef, AssetResolutionErrorCode, AssetResolveRequest, AssetResolveResult, AssetResolver, BoardController, BoardKeyInput, BoardPointerInput, BoardScene, BoardSlotProps, BoardSnapshot, BoardStyle, CommentMarker, CreateBoardControllerOptions, CustomBoardObject, CustomObjectAddInput, CustomObjectDefinition, CustomTool, CustomToolDefinition, DefaultBoardChromeProps, DefaultUIRegion, DefaultUISlot, DefaultUISlots, DialogSlotProps, ExtensionCommand, ExtensionDiagnostic, ExtensionHitResult, ExtensionId, ExtensionRequirement, InlineEditorsProps, InputModifiers, JsonObject, JsonValue, LocalBoard, LocalBoardSnapshot, Mat2x3, MultiplayerCursorsProps, ObjectDescribeContext, ObjectIntent, ObjectType, PresenceCursor, PresenceUser, PresenceView, QueryableBoardObject, ReadonlyCustomObject, SceneEllipse, SceneGroup, SceneImage, ScenePath, SceneRect, SceneText, ScrawlBoardProps, ScrawlCanvasProps, ScrawlDefaultUIProps, ScrawlDensity, ScrawlExtension, ScrawlGridMode, ScrawlProps, ScrawlProviderProps, ScrawlResolvedTheme, ScrawlTheme, ScrawlThemeDiagnostic, ScrawlThemePreset, StyleShelfProps, SupportedAssetMediaType, ToolCancelReason, ToolCapabilities, ToolCursor, ToolId };