@scrawl-board/board 0.1.0-beta.8 → 0.1.0-beta.9

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.js CHANGED
@@ -5161,10 +5161,18 @@ function pointerToPlane(clientX, clientY, canvas, camera, z) {
5161
5161
  };
5162
5162
  }
5163
5163
  //#endregion
5164
+ //#region src/renderer/canvas/displayColor.ts
5165
+ function displayColor(hex) {
5166
+ return new THREE.Color().setStyle(hex, THREE.NoColorSpace);
5167
+ }
5168
+ function setDisplayColor(color, hex) {
5169
+ color.setStyle(hex, THREE.NoColorSpace);
5170
+ }
5171
+ //#endregion
5164
5172
  //#region src/renderer/canvas/fog.ts
5165
5173
  function createFogUniforms(color) {
5166
5174
  return {
5167
- uFogColor: { value: new THREE.Color(color) },
5175
+ uFogColor: { value: displayColor(color) },
5168
5176
  uFogNear: { value: 100 },
5169
5177
  uFogFar: { value: 900 }
5170
5178
  };
@@ -5301,9 +5309,9 @@ function createBoardSurface(fog, theme) {
5301
5309
  vertexShader: VERTEX$1,
5302
5310
  fragmentShader: FRAGMENT,
5303
5311
  uniforms: {
5304
- uColor: { value: new THREE.Color(theme.color) },
5312
+ uColor: { value: displayColor(theme.color) },
5305
5313
  uGridMode: { value: GRID_MODE_CODE[theme.gridMode] },
5306
- uGridColor: { value: new THREE.Color(theme.gridColor) },
5314
+ uGridColor: { value: displayColor(theme.gridColor) },
5307
5315
  uGridSpacing: { value: Math.max(.001, theme.gridSpacing) },
5308
5316
  uGridLineWidthPx: { value: Math.max(.1, theme.gridLineWidth) },
5309
5317
  uWorldPerPixel: { value: .05 },
@@ -5319,9 +5327,9 @@ function createBoardSurface(fog, theme) {
5319
5327
  /** Applies a theme change (initial or live) — everything except the per-frame fade/pixel-ratio. */
5320
5328
  function setBoardSurfaceTheme(mesh, theme) {
5321
5329
  const uniforms = mesh.material.uniforms;
5322
- uniforms.uColor.value.set(theme.color);
5330
+ setDisplayColor(uniforms.uColor.value, theme.color);
5323
5331
  uniforms.uGridMode.value = GRID_MODE_CODE[theme.gridMode];
5324
- uniforms.uGridColor.value.set(theme.gridColor);
5332
+ setDisplayColor(uniforms.uGridColor.value, theme.gridColor);
5325
5333
  uniforms.uGridSpacing.value = Math.max(.001, theme.gridSpacing);
5326
5334
  uniforms.uGridLineWidthPx.value = Math.max(.1, theme.gridLineWidth);
5327
5335
  uniforms.uSurfaceTexture.value = TEXTURE_CODE[theme.surfaceTexture];
@@ -5674,7 +5682,7 @@ var InkMaterials = class {
5674
5682
  vertexShader: VERTEX,
5675
5683
  fragmentShader,
5676
5684
  uniforms: {
5677
- uColor: { value: new THREE.Color(color) },
5685
+ uColor: { value: displayColor(color) },
5678
5686
  ...this.fog
5679
5687
  },
5680
5688
  transparent: true,
package/dist/index.d.ts CHANGED
@@ -2910,16 +2910,8 @@ interface DefaultBoardChromeProps {
2910
2910
  regions?: Partial<Record<DefaultUIRegion, boolean>>;
2911
2911
  slots?: DefaultUISlots;
2912
2912
  /**
2913
- * Swap in your own icon for a tool's primary toolbar button — pass any
2914
- * icon from any library (`<Pencil />`, `<Icon icon="..." />`, an inline
2915
- * `<svg>`, whatever). A tool with no entry here keeps its default text
2916
- * label, so this is opt-in per tool, not all-or-nothing. Aria labeling
2917
- * and the keyboard-shortcut tooltip stay text-based regardless, so the
2918
- * board remains fully accessible even with icon-only buttons.
2919
- *
2920
- * Only affects the six primary toolbar buttons (select/marker/
2921
- * highlighter/eraser/note/text) — the "more tools" overflow menu is a
2922
- * native `<select>`, which can only render plain text `<option>`s.
2913
+ * Override any built-in tool icon. Tools without an override keep the
2914
+ * SDK's outline icon. Accessible labels and shortcut tooltips are preserved.
2923
2915
  */
2924
2916
  icons?: Partial<Record<BuiltInTool, ReactNode>>;
2925
2917
  }
@@ -3022,11 +3014,8 @@ interface StyleShelfProps {
3022
3014
  controller: BoardController;
3023
3015
  snapshot: BoardSnapshot;
3024
3016
  /**
3025
- * Root-relative screen point (e.g. the active tool button's own position)
3026
- * to anchor above instead of the default: centred over the whole
3027
- * `.scrawl-board__tools` toolbar. `DefaultBoardChrome` supplies this
3028
- * automatically; omit it when rendering `StyleShelf` standalone (its own
3029
- * layout has no toolbar button to anchor to) to keep the centred default.
3017
+ * Root-relative point beside the active tool. DefaultBoardChrome supplies
3018
+ * this automatically and keeps the shelf within the board bounds.
3030
3019
  */
3031
3020
  anchor?: {
3032
3021
  x: number;
@@ -3072,7 +3061,7 @@ interface ScrawlProps extends Omit<CreateBoardControllerOptions, "canvas"> {
3072
3061
  /** Provide null for a headless Board, or an existing canvas to control its identity. */
3073
3062
  canvas?: HTMLCanvasElement | null;
3074
3063
  /**
3075
- * Per-tool icon override for the default toolbar's primary buttons —
3064
+ * Per-tool icon override for the default toolbar and More tools buttons —
3076
3065
  * only applies when you don't supply `children` (i.e. you're using the
3077
3066
  * SDK's default UI). See DefaultBoardChromeProps.icons.
3078
3067
  */
@@ -3104,7 +3093,7 @@ interface ScrawlDefaultUIProps {
3104
3093
  regions?: Partial<Record<DefaultUIRegion, boolean>>;
3105
3094
  /** Replace (component) or hide (null) a coarse region; omit for the SDK default. */
3106
3095
  slots?: DefaultUISlots;
3107
- /** Per-tool icon override for the primary toolbar buttons — see DefaultBoardChromeProps.icons. */
3096
+ /** Per-tool icon override for the toolbar and More tools buttons — see DefaultBoardChromeProps.icons. */
3108
3097
  icons?: Partial<Record<BuiltInTool, ReactNode>>;
3109
3098
  }
3110
3099
  declare function ScrawlDefaultUI({ className, style, regions, slots, icons }: ScrawlDefaultUIProps): react.JSX.Element;
package/dist/index.js CHANGED
@@ -5319,10 +5319,18 @@ function pointerToPlane(clientX, clientY, canvas, camera, z) {
5319
5319
  };
5320
5320
  }
5321
5321
  //#endregion
5322
+ //#region src/renderer/canvas/displayColor.ts
5323
+ function displayColor(hex) {
5324
+ return new THREE.Color().setStyle(hex, THREE.NoColorSpace);
5325
+ }
5326
+ function setDisplayColor(color, hex) {
5327
+ color.setStyle(hex, THREE.NoColorSpace);
5328
+ }
5329
+ //#endregion
5322
5330
  //#region src/renderer/canvas/fog.ts
5323
5331
  function createFogUniforms(color) {
5324
5332
  return {
5325
- uFogColor: { value: new THREE.Color(color) },
5333
+ uFogColor: { value: displayColor(color) },
5326
5334
  uFogNear: { value: 100 },
5327
5335
  uFogFar: { value: 900 }
5328
5336
  };
@@ -5459,9 +5467,9 @@ function createBoardSurface(fog, theme) {
5459
5467
  vertexShader: VERTEX$1,
5460
5468
  fragmentShader: FRAGMENT,
5461
5469
  uniforms: {
5462
- uColor: { value: new THREE.Color(theme.color) },
5470
+ uColor: { value: displayColor(theme.color) },
5463
5471
  uGridMode: { value: GRID_MODE_CODE[theme.gridMode] },
5464
- uGridColor: { value: new THREE.Color(theme.gridColor) },
5472
+ uGridColor: { value: displayColor(theme.gridColor) },
5465
5473
  uGridSpacing: { value: Math.max(.001, theme.gridSpacing) },
5466
5474
  uGridLineWidthPx: { value: Math.max(.1, theme.gridLineWidth) },
5467
5475
  uWorldPerPixel: { value: .05 },
@@ -5477,9 +5485,9 @@ function createBoardSurface(fog, theme) {
5477
5485
  /** Applies a theme change (initial or live) — everything except the per-frame fade/pixel-ratio. */
5478
5486
  function setBoardSurfaceTheme(mesh, theme) {
5479
5487
  const uniforms = mesh.material.uniforms;
5480
- uniforms.uColor.value.set(theme.color);
5488
+ setDisplayColor(uniforms.uColor.value, theme.color);
5481
5489
  uniforms.uGridMode.value = GRID_MODE_CODE[theme.gridMode];
5482
- uniforms.uGridColor.value.set(theme.gridColor);
5490
+ setDisplayColor(uniforms.uGridColor.value, theme.gridColor);
5483
5491
  uniforms.uGridSpacing.value = Math.max(.001, theme.gridSpacing);
5484
5492
  uniforms.uGridLineWidthPx.value = Math.max(.1, theme.gridLineWidth);
5485
5493
  uniforms.uSurfaceTexture.value = TEXTURE_CODE[theme.surfaceTexture];
@@ -5832,7 +5840,7 @@ var InkMaterials = class {
5832
5840
  vertexShader: VERTEX,
5833
5841
  fragmentShader,
5834
5842
  uniforms: {
5835
- uColor: { value: new THREE.Color(color) },
5843
+ uColor: { value: displayColor(color) },
5836
5844
  ...this.fog
5837
5845
  },
5838
5846
  transparent: true,
@@ -23048,6 +23056,50 @@ function LockDuplicateDelete({ controller, mutable, locked, ids }) {
23048
23056
  });
23049
23057
  }
23050
23058
  //#endregion
23059
+ //#region src/ui/toolbar/tool-icon.tsx
23060
+ var PATHS = {
23061
+ select: "m5 3 14 9-7 1-3 7Z",
23062
+ marker: "m4 16 11-11 4 4L8 20H4Zm9-9 4 4",
23063
+ highlighter: "m5 14 8-10 6 5-8 10Zm0 0 6 5M4 20h8",
23064
+ eraser: "m3 14 9-10 9 8-7 8H9Zm4-4 9 8M14 20h7",
23065
+ note: "M5 3h14v12l-6 6H5ZM13 21v-6h6",
23066
+ text: "M4 5h16M12 5v15M8 20h8M4 5v3M20 5v3",
23067
+ pan: "M8 12V5a2 2 0 0 1 4 0v6-7a2 2 0 0 1 4 0v7-5a2 2 0 0 1 4 0v9c0 8-11 9-14 3l-3-5a2 2 0 0 1 3-2l2 2",
23068
+ table: "M3 4h18v16H3ZM3 10h18M9 4v16",
23069
+ image: "M3 4h18v16H3Zm0 12 5-5 5 5 3-3 5 5M15 8h.01",
23070
+ comment: "M4 4h16v12H9l-5 4Z",
23071
+ stamp: "M8 14v-3a5 5 0 1 1 8 0v3l4 3v3H4v-3ZM4 17h16",
23072
+ timer: "M9 2h6M12 6a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm0 3v5l3 2",
23073
+ "shape:line": "M4 20 20 4",
23074
+ "shape:arrow": "M4 20 20 4M9 4h11v11",
23075
+ "shape:rect": "M4 5h16v14H4Z",
23076
+ "shape:ellipse": "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z",
23077
+ "shape:triangle": "m12 3 10 18H2Z",
23078
+ "shape:diamond": "m12 2 10 10-10 10L2 12Z",
23079
+ "shape:pentagon": "m12 2 10 8-4 12H6L2 10Z",
23080
+ "shape:hexagon": "m7 3 10 0 5 9-5 9H7l-5-9Z",
23081
+ "shape:octagon": "M8 2h8l6 6v8l-6 6H8l-6-6V8Z",
23082
+ "shape:star": "m12 2 3 7 7 1-5 5 1 7-6-4-6 4 1-7-5-5 7-1Z",
23083
+ "shape:heart": "M12 21 3 12C-3 3 8-1 12 6c4-7 15-3 9 6Z",
23084
+ more: "M12 4v16M4 12h16",
23085
+ undo: "M9 5 4 10l5 5M4 10h10a6 6 0 0 1 6 6v3",
23086
+ redo: "m15 5 5 5-5 5M20 10H10a6 6 0 0 0-6 6v3"
23087
+ };
23088
+ function ToolIcon({ tool }) {
23089
+ return /* @__PURE__ */ jsx("svg", {
23090
+ "aria-hidden": "true",
23091
+ width: "22",
23092
+ height: "22",
23093
+ viewBox: "0 0 24 24",
23094
+ fill: "none",
23095
+ stroke: "currentColor",
23096
+ strokeWidth: "1.7",
23097
+ strokeLinecap: "round",
23098
+ strokeLinejoin: "round",
23099
+ children: /* @__PURE__ */ jsx("path", { d: PATHS[tool] ?? PATHS.more })
23100
+ });
23101
+ }
23102
+ //#endregion
23051
23103
  //#region src/ui/toolbar/style-shelf.tsx
23052
23104
  var ERASER_RADII = [
23053
23105
  ["Fine", 1],
@@ -23096,6 +23148,7 @@ function StyleShelf({ controller, snapshot, anchor }) {
23096
23148
  className: "scrawl-board__style-shelf",
23097
23149
  "data-anchored": !!anchor,
23098
23150
  role: "toolbar",
23151
+ "aria-orientation": "vertical",
23099
23152
  style: anchor ? {
23100
23153
  left: anchor.x,
23101
23154
  top: anchor.y
@@ -23132,17 +23185,29 @@ function StyleShelf({ controller, snapshot, anchor }) {
23132
23185
  snapshot.tool === "eraser" && /* @__PURE__ */ jsxs("div", {
23133
23186
  className: "scrawl-board__style-group",
23134
23187
  children: [/* @__PURE__ */ jsx("span", { children: "Eraser size" }), ERASER_RADII.map(([label, radius]) => /* @__PURE__ */ jsx("button", {
23188
+ "aria-label": label,
23189
+ title: label,
23135
23190
  "aria-pressed": style.eraserRadius === radius,
23136
23191
  disabled: !mutable,
23137
23192
  onClick: () => controller.style.set({ eraserRadius: radius }),
23138
23193
  type: "button",
23139
- children: label
23194
+ children: /* @__PURE__ */ jsx("span", {
23195
+ "aria-hidden": "true",
23196
+ style: {
23197
+ backgroundColor: "currentColor",
23198
+ width: 4 + radius * 3,
23199
+ height: 4 + radius * 3
23200
+ }
23201
+ })
23140
23202
  }, label))]
23141
23203
  }),
23142
23204
  snapshot.tool === "table" && /* @__PURE__ */ jsxs("div", {
23143
23205
  className: "scrawl-board__style-group",
23144
23206
  children: [
23145
- /* @__PURE__ */ jsx("span", { children: "Rows" }),
23207
+ /* @__PURE__ */ jsxs("span", {
23208
+ "aria-live": "polite",
23209
+ children: ["Rows: ", style.tableRows]
23210
+ }),
23146
23211
  /* @__PURE__ */ jsx("button", {
23147
23212
  "aria-label": "Fewer rows",
23148
23213
  disabled: !mutable,
@@ -23150,10 +23215,6 @@ function StyleShelf({ controller, snapshot, anchor }) {
23150
23215
  type: "button",
23151
23216
  children: "−"
23152
23217
  }),
23153
- /* @__PURE__ */ jsx("span", {
23154
- "aria-live": "polite",
23155
- children: style.tableRows
23156
- }),
23157
23218
  /* @__PURE__ */ jsx("button", {
23158
23219
  "aria-label": "More rows",
23159
23220
  disabled: !mutable,
@@ -23161,7 +23222,10 @@ function StyleShelf({ controller, snapshot, anchor }) {
23161
23222
  type: "button",
23162
23223
  children: "+"
23163
23224
  }),
23164
- /* @__PURE__ */ jsx("span", { children: "Cols" }),
23225
+ /* @__PURE__ */ jsxs("span", {
23226
+ "aria-live": "polite",
23227
+ children: ["Cols: ", style.tableCols]
23228
+ }),
23165
23229
  /* @__PURE__ */ jsx("button", {
23166
23230
  "aria-label": "Fewer columns",
23167
23231
  disabled: !mutable,
@@ -23169,10 +23233,6 @@ function StyleShelf({ controller, snapshot, anchor }) {
23169
23233
  type: "button",
23170
23234
  children: "−"
23171
23235
  }),
23172
- /* @__PURE__ */ jsx("span", {
23173
- "aria-live": "polite",
23174
- children: style.tableCols
23175
- }),
23176
23236
  /* @__PURE__ */ jsx("button", {
23177
23237
  "aria-label": "More columns",
23178
23238
  disabled: !mutable,
@@ -23327,6 +23387,7 @@ var INKS = [
23327
23387
  ];
23328
23388
  function DefaultBoardChrome({ controller, snapshot, renderPortal, className, style, regions, slots, icons }) {
23329
23389
  const [panel, setPanel] = useState(null);
23390
+ const [moreOpen, setMoreOpen] = useState(false);
23330
23391
  const [query, setQuery] = useState("");
23331
23392
  const [announcement, setAnnouncement] = useState("Board controls ready");
23332
23393
  const [styleShelfAnchor, setStyleShelfAnchor] = useState(void 0);
@@ -23341,25 +23402,46 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23341
23402
  if (panel === "search") searchRef.current?.focus();
23342
23403
  }, [panel]);
23343
23404
  useEffect(() => {
23405
+ const root = chromeRef.current;
23406
+ if (!root) return;
23344
23407
  const recompute = () => {
23345
- const root = chromeRef.current;
23346
- const button = root?.querySelector(".scrawl-board__tools button[aria-pressed=\"true\"]");
23347
- if (!root || !button) {
23408
+ const button = root.querySelector(".scrawl-board__tools button[aria-pressed=\"true\"]") ?? root.querySelector("[aria-label=\"More Board tools\"]");
23409
+ if (!button) {
23348
23410
  setStyleShelfAnchor(void 0);
23349
23411
  return;
23350
23412
  }
23351
23413
  const buttonRect = button.getBoundingClientRect();
23352
23414
  const rootRect = root.getBoundingClientRect();
23415
+ const shelf = root.querySelector(".scrawl-board__style-shelf");
23353
23416
  setStyleShelfAnchor({
23354
- x: buttonRect.left + buttonRect.width / 2 - rootRect.left,
23355
- y: buttonRect.top - rootRect.top
23417
+ x: buttonRect.right - rootRect.left + 12,
23418
+ y: Math.max(12, Math.min(buttonRect.top - rootRect.top, rootRect.height - (shelf?.offsetHeight ?? 0) - 12))
23356
23419
  });
23357
23420
  };
23358
23421
  recompute();
23422
+ const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(recompute);
23423
+ observer?.observe(root);
23424
+ const shelf = root.querySelector(".scrawl-board__style-shelf");
23425
+ if (shelf) observer?.observe(shelf);
23426
+ root.addEventListener("scroll", recompute, true);
23359
23427
  window.addEventListener("resize", recompute);
23360
- return () => window.removeEventListener("resize", recompute);
23361
- }, [snapshot.tool]);
23428
+ return () => {
23429
+ observer?.disconnect();
23430
+ root.removeEventListener("scroll", recompute, true);
23431
+ window.removeEventListener("resize", recompute);
23432
+ };
23433
+ }, [snapshot.tool, moreOpen]);
23434
+ useEffect(() => {
23435
+ if (!moreOpen) return;
23436
+ chromeRef.current?.querySelector(".scrawl-board__more-tools button")?.focus();
23437
+ const dismiss = (event) => {
23438
+ if (!event.target.closest(".scrawl-board__more-tools, [aria-label=\"More Board tools\"]")) setMoreOpen(false);
23439
+ };
23440
+ document.addEventListener("pointerdown", dismiss);
23441
+ return () => document.removeEventListener("pointerdown", dismiss);
23442
+ }, [moreOpen]);
23362
23443
  const open = (next, opener) => {
23444
+ setMoreOpen(false);
23363
23445
  openerRef.current = opener;
23364
23446
  setPanel(next);
23365
23447
  };
@@ -23368,6 +23450,8 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23368
23450
  queueMicrotask(() => openerRef.current?.focus());
23369
23451
  }, []);
23370
23452
  const chooseTool = (tool) => {
23453
+ if (moreOpen) chromeRef.current?.querySelector("[aria-label=\"More Board tools\"]")?.focus();
23454
+ setMoreOpen(false);
23371
23455
  controller.tools.select(tool);
23372
23456
  const label = [...PRIMARY_TOOLS, ...MORE_TOOLS].find((item) => item.tool === tool)?.label ?? tool;
23373
23457
  setAnnouncement(`${label} tool selected`);
@@ -23375,6 +23459,13 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23375
23459
  const onKeyDown = (event) => {
23376
23460
  const editing = event.target.matches("input, textarea, select, [contenteditable=true]");
23377
23461
  const modifier = event.metaKey || event.ctrlKey;
23462
+ if (event.key === "Escape" && moreOpen) {
23463
+ event.preventDefault();
23464
+ event.stopPropagation();
23465
+ setMoreOpen(false);
23466
+ chromeRef.current?.querySelector("[aria-label=\"More Board tools\"]")?.focus();
23467
+ return;
23468
+ }
23378
23469
  if (event.key === "Escape" && panel) {
23379
23470
  event.preventDefault();
23380
23471
  event.stopPropagation();
@@ -23491,6 +23582,7 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23491
23582
  "aria-label": "Board tools",
23492
23583
  className: "scrawl-board__tools",
23493
23584
  role: "toolbar",
23585
+ "aria-orientation": "vertical",
23494
23586
  children: [
23495
23587
  enabled("tools") && /* @__PURE__ */ jsxs(Fragment, { children: [PRIMARY_TOOLS.map(({ tool, label, key }) => /* @__PURE__ */ jsx("button", {
23496
23588
  "aria-label": `${label} tool`,
@@ -23499,37 +23591,31 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23499
23591
  onClick: () => chooseTool(tool),
23500
23592
  title: `${label} (${key})`,
23501
23593
  type: "button",
23502
- children: icons?.[tool] ? /* @__PURE__ */ jsx("span", {
23594
+ children: /* @__PURE__ */ jsx("span", {
23503
23595
  "aria-hidden": "true",
23504
- children: icons[tool]
23505
- }) : label
23506
- }, tool)), /* @__PURE__ */ jsxs("label", {
23507
- className: "scrawl-board__tool-select",
23508
- children: [/* @__PURE__ */ jsx("span", { children: "More tools" }), /* @__PURE__ */ jsxs("select", {
23509
- "aria-label": "More Board tools",
23510
- disabled: !mutable,
23511
- onChange: (event) => chooseTool(event.currentTarget.value),
23512
- value: MORE_TOOLS.some((item) => item.tool === snapshot.tool) ? snapshot.tool : "",
23513
- children: [/* @__PURE__ */ jsx("option", {
23514
- value: "",
23515
- children: "Choose"
23516
- }), MORE_TOOLS.map(({ tool, label }) => /* @__PURE__ */ jsx("option", {
23517
- value: tool,
23518
- children: label
23519
- }, tool))]
23520
- })]
23596
+ children: icons?.[tool] ?? /* @__PURE__ */ jsx(ToolIcon, { tool })
23597
+ })
23598
+ }, tool)), /* @__PURE__ */ jsx("button", {
23599
+ "aria-label": "More Board tools",
23600
+ "aria-expanded": moreOpen,
23601
+ disabled: !mutable,
23602
+ onClick: () => setMoreOpen((value) => !value),
23603
+ title: "More tools",
23604
+ type: "button",
23605
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "more" })
23521
23606
  })] }),
23522
- enabled("style") && /* @__PURE__ */ jsxs("button", {
23607
+ enabled("style") && /* @__PURE__ */ jsx("button", {
23523
23608
  "aria-label": "Ink style",
23524
23609
  "aria-expanded": panel === "style",
23525
23610
  disabled: !mutable,
23611
+ title: "Ink style",
23526
23612
  onClick: (event) => open("style", event.currentTarget),
23527
23613
  type: "button",
23528
- children: [/* @__PURE__ */ jsx("span", {
23614
+ children: /* @__PURE__ */ jsx("span", {
23529
23615
  "aria-hidden": "true",
23530
23616
  className: "scrawl-board__ink-sample",
23531
23617
  style: { backgroundColor: snapshot.style.inkColor }
23532
- }), "Ink"]
23618
+ })
23533
23619
  }),
23534
23620
  enabled("history") && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
23535
23621
  "aria-label": "Undo",
@@ -23538,8 +23624,9 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23538
23624
  controller.history.undo();
23539
23625
  setAnnouncement("Undo");
23540
23626
  },
23627
+ title: "Undo",
23541
23628
  type: "button",
23542
- children: "Undo"
23629
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "undo" })
23543
23630
  }), /* @__PURE__ */ jsx("button", {
23544
23631
  "aria-label": "Redo",
23545
23632
  disabled: !snapshot.canRedo || !mutable,
@@ -23547,11 +23634,31 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23547
23634
  controller.history.redo();
23548
23635
  setAnnouncement("Redo");
23549
23636
  },
23637
+ title: "Redo",
23550
23638
  type: "button",
23551
- children: "Redo"
23639
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "redo" })
23552
23640
  })] })
23553
23641
  ]
23554
23642
  })),
23643
+ moreOpen && enabled("tools") && slots?.toolbar !== null && /* @__PURE__ */ jsx("div", {
23644
+ "aria-label": "More tools",
23645
+ className: "scrawl-board__more-tools",
23646
+ role: "toolbar",
23647
+ "aria-orientation": "vertical",
23648
+ children: MORE_TOOLS.map(({ tool, label }) => /* @__PURE__ */ jsx("button", {
23649
+ "aria-label": `${label} tool`,
23650
+ "aria-pressed": snapshot.tool === tool,
23651
+ disabled: !mutable,
23652
+ "data-tool": tool,
23653
+ onClick: () => chooseTool(tool),
23654
+ title: label,
23655
+ type: "button",
23656
+ children: /* @__PURE__ */ jsx("span", {
23657
+ "aria-hidden": "true",
23658
+ children: icons?.[tool] ?? /* @__PURE__ */ jsx(ToolIcon, { tool })
23659
+ })
23660
+ }, tool))
23661
+ }),
23555
23662
  enabled("view") && /* @__PURE__ */ jsxs("div", {
23556
23663
  "aria-label": "Board view",
23557
23664
  className: "scrawl-board__view",
@@ -23586,7 +23693,7 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23586
23693
  })
23587
23694
  ]
23588
23695
  }),
23589
- renderSlot(slots?.stylePanel, enabled("styleShelf") && /* @__PURE__ */ jsx(StyleShelf, {
23696
+ renderSlot(slots?.stylePanel, enabled("styleShelf") && !moreOpen && /* @__PURE__ */ jsx(StyleShelf, {
23590
23697
  anchor: styleShelfAnchor,
23591
23698
  controller,
23592
23699
  snapshot
package/dist/react.d.ts CHANGED
@@ -1735,16 +1735,8 @@ interface DefaultBoardChromeProps {
1735
1735
  regions?: Partial<Record<DefaultUIRegion, boolean>>;
1736
1736
  slots?: DefaultUISlots;
1737
1737
  /**
1738
- * Swap in your own icon for a tool's primary toolbar button — pass any
1739
- * icon from any library (`<Pencil />`, `<Icon icon="..." />`, an inline
1740
- * `<svg>`, whatever). A tool with no entry here keeps its default text
1741
- * label, so this is opt-in per tool, not all-or-nothing. Aria labeling
1742
- * and the keyboard-shortcut tooltip stay text-based regardless, so the
1743
- * board remains fully accessible even with icon-only buttons.
1744
- *
1745
- * Only affects the six primary toolbar buttons (select/marker/
1746
- * highlighter/eraser/note/text) — the "more tools" overflow menu is a
1747
- * native `<select>`, which can only render plain text `<option>`s.
1738
+ * Override any built-in tool icon. Tools without an override keep the
1739
+ * SDK's outline icon. Accessible labels and shortcut tooltips are preserved.
1748
1740
  */
1749
1741
  icons?: Partial<Record<BuiltInTool, ReactNode>>;
1750
1742
  }
@@ -1847,11 +1839,8 @@ interface StyleShelfProps {
1847
1839
  controller: BoardController;
1848
1840
  snapshot: BoardSnapshot;
1849
1841
  /**
1850
- * Root-relative screen point (e.g. the active tool button's own position)
1851
- * to anchor above instead of the default: centred over the whole
1852
- * `.scrawl-board__tools` toolbar. `DefaultBoardChrome` supplies this
1853
- * automatically; omit it when rendering `StyleShelf` standalone (its own
1854
- * layout has no toolbar button to anchor to) to keep the centred default.
1842
+ * Root-relative point beside the active tool. DefaultBoardChrome supplies
1843
+ * this automatically and keeps the shelf within the board bounds.
1855
1844
  */
1856
1845
  anchor?: {
1857
1846
  x: number;
@@ -1897,7 +1886,7 @@ interface ScrawlProps extends Omit<CreateBoardControllerOptions, "canvas"> {
1897
1886
  /** Provide null for a headless Board, or an existing canvas to control its identity. */
1898
1887
  canvas?: HTMLCanvasElement | null;
1899
1888
  /**
1900
- * Per-tool icon override for the default toolbar's primary buttons —
1889
+ * Per-tool icon override for the default toolbar and More tools buttons —
1901
1890
  * only applies when you don't supply `children` (i.e. you're using the
1902
1891
  * SDK's default UI). See DefaultBoardChromeProps.icons.
1903
1892
  */
@@ -1929,7 +1918,7 @@ interface ScrawlDefaultUIProps {
1929
1918
  regions?: Partial<Record<DefaultUIRegion, boolean>>;
1930
1919
  /** Replace (component) or hide (null) a coarse region; omit for the SDK default. */
1931
1920
  slots?: DefaultUISlots;
1932
- /** Per-tool icon override for the primary toolbar buttons — see DefaultBoardChromeProps.icons. */
1921
+ /** Per-tool icon override for the toolbar and More tools buttons — see DefaultBoardChromeProps.icons. */
1933
1922
  icons?: Partial<Record<BuiltInTool, ReactNode>>;
1934
1923
  }
1935
1924
  declare function ScrawlDefaultUI({ className, style, regions, slots, icons }: ScrawlDefaultUIProps): react.JSX.Element;
package/dist/react.js CHANGED
@@ -5203,10 +5203,18 @@ function pointerToPlane(clientX, clientY, canvas, camera, z) {
5203
5203
  };
5204
5204
  }
5205
5205
  //#endregion
5206
+ //#region src/renderer/canvas/displayColor.ts
5207
+ function displayColor(hex) {
5208
+ return new THREE.Color().setStyle(hex, THREE.NoColorSpace);
5209
+ }
5210
+ function setDisplayColor(color, hex) {
5211
+ color.setStyle(hex, THREE.NoColorSpace);
5212
+ }
5213
+ //#endregion
5206
5214
  //#region src/renderer/canvas/fog.ts
5207
5215
  function createFogUniforms(color) {
5208
5216
  return {
5209
- uFogColor: { value: new THREE.Color(color) },
5217
+ uFogColor: { value: displayColor(color) },
5210
5218
  uFogNear: { value: 100 },
5211
5219
  uFogFar: { value: 900 }
5212
5220
  };
@@ -5343,9 +5351,9 @@ function createBoardSurface(fog, theme) {
5343
5351
  vertexShader: VERTEX$1,
5344
5352
  fragmentShader: FRAGMENT,
5345
5353
  uniforms: {
5346
- uColor: { value: new THREE.Color(theme.color) },
5354
+ uColor: { value: displayColor(theme.color) },
5347
5355
  uGridMode: { value: GRID_MODE_CODE[theme.gridMode] },
5348
- uGridColor: { value: new THREE.Color(theme.gridColor) },
5356
+ uGridColor: { value: displayColor(theme.gridColor) },
5349
5357
  uGridSpacing: { value: Math.max(.001, theme.gridSpacing) },
5350
5358
  uGridLineWidthPx: { value: Math.max(.1, theme.gridLineWidth) },
5351
5359
  uWorldPerPixel: { value: .05 },
@@ -5361,9 +5369,9 @@ function createBoardSurface(fog, theme) {
5361
5369
  /** Applies a theme change (initial or live) — everything except the per-frame fade/pixel-ratio. */
5362
5370
  function setBoardSurfaceTheme(mesh, theme) {
5363
5371
  const uniforms = mesh.material.uniforms;
5364
- uniforms.uColor.value.set(theme.color);
5372
+ setDisplayColor(uniforms.uColor.value, theme.color);
5365
5373
  uniforms.uGridMode.value = GRID_MODE_CODE[theme.gridMode];
5366
- uniforms.uGridColor.value.set(theme.gridColor);
5374
+ setDisplayColor(uniforms.uGridColor.value, theme.gridColor);
5367
5375
  uniforms.uGridSpacing.value = Math.max(.001, theme.gridSpacing);
5368
5376
  uniforms.uGridLineWidthPx.value = Math.max(.1, theme.gridLineWidth);
5369
5377
  uniforms.uSurfaceTexture.value = TEXTURE_CODE[theme.surfaceTexture];
@@ -5716,7 +5724,7 @@ var InkMaterials = class {
5716
5724
  vertexShader: VERTEX,
5717
5725
  fragmentShader,
5718
5726
  uniforms: {
5719
- uColor: { value: new THREE.Color(color) },
5727
+ uColor: { value: displayColor(color) },
5720
5728
  ...this.fog
5721
5729
  },
5722
5730
  transparent: true,
@@ -22765,6 +22773,50 @@ function LockDuplicateDelete({ controller, mutable, locked, ids }) {
22765
22773
  });
22766
22774
  }
22767
22775
  //#endregion
22776
+ //#region src/ui/toolbar/tool-icon.tsx
22777
+ var PATHS = {
22778
+ select: "m5 3 14 9-7 1-3 7Z",
22779
+ marker: "m4 16 11-11 4 4L8 20H4Zm9-9 4 4",
22780
+ highlighter: "m5 14 8-10 6 5-8 10Zm0 0 6 5M4 20h8",
22781
+ eraser: "m3 14 9-10 9 8-7 8H9Zm4-4 9 8M14 20h7",
22782
+ note: "M5 3h14v12l-6 6H5ZM13 21v-6h6",
22783
+ text: "M4 5h16M12 5v15M8 20h8M4 5v3M20 5v3",
22784
+ pan: "M8 12V5a2 2 0 0 1 4 0v6-7a2 2 0 0 1 4 0v7-5a2 2 0 0 1 4 0v9c0 8-11 9-14 3l-3-5a2 2 0 0 1 3-2l2 2",
22785
+ table: "M3 4h18v16H3ZM3 10h18M9 4v16",
22786
+ image: "M3 4h18v16H3Zm0 12 5-5 5 5 3-3 5 5M15 8h.01",
22787
+ comment: "M4 4h16v12H9l-5 4Z",
22788
+ stamp: "M8 14v-3a5 5 0 1 1 8 0v3l4 3v3H4v-3ZM4 17h16",
22789
+ timer: "M9 2h6M12 6a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm0 3v5l3 2",
22790
+ "shape:line": "M4 20 20 4",
22791
+ "shape:arrow": "M4 20 20 4M9 4h11v11",
22792
+ "shape:rect": "M4 5h16v14H4Z",
22793
+ "shape:ellipse": "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z",
22794
+ "shape:triangle": "m12 3 10 18H2Z",
22795
+ "shape:diamond": "m12 2 10 10-10 10L2 12Z",
22796
+ "shape:pentagon": "m12 2 10 8-4 12H6L2 10Z",
22797
+ "shape:hexagon": "m7 3 10 0 5 9-5 9H7l-5-9Z",
22798
+ "shape:octagon": "M8 2h8l6 6v8l-6 6H8l-6-6V8Z",
22799
+ "shape:star": "m12 2 3 7 7 1-5 5 1 7-6-4-6 4 1-7-5-5 7-1Z",
22800
+ "shape:heart": "M12 21 3 12C-3 3 8-1 12 6c4-7 15-3 9 6Z",
22801
+ more: "M12 4v16M4 12h16",
22802
+ undo: "M9 5 4 10l5 5M4 10h10a6 6 0 0 1 6 6v3",
22803
+ redo: "m15 5 5 5-5 5M20 10H10a6 6 0 0 0-6 6v3"
22804
+ };
22805
+ function ToolIcon({ tool }) {
22806
+ return /* @__PURE__ */ jsx("svg", {
22807
+ "aria-hidden": "true",
22808
+ width: "22",
22809
+ height: "22",
22810
+ viewBox: "0 0 24 24",
22811
+ fill: "none",
22812
+ stroke: "currentColor",
22813
+ strokeWidth: "1.7",
22814
+ strokeLinecap: "round",
22815
+ strokeLinejoin: "round",
22816
+ children: /* @__PURE__ */ jsx("path", { d: PATHS[tool] ?? PATHS.more })
22817
+ });
22818
+ }
22819
+ //#endregion
22768
22820
  //#region src/ui/toolbar/style-shelf.tsx
22769
22821
  var ERASER_RADII = [
22770
22822
  ["Fine", 1],
@@ -22813,6 +22865,7 @@ function StyleShelf({ controller, snapshot, anchor }) {
22813
22865
  className: "scrawl-board__style-shelf",
22814
22866
  "data-anchored": !!anchor,
22815
22867
  role: "toolbar",
22868
+ "aria-orientation": "vertical",
22816
22869
  style: anchor ? {
22817
22870
  left: anchor.x,
22818
22871
  top: anchor.y
@@ -22849,17 +22902,29 @@ function StyleShelf({ controller, snapshot, anchor }) {
22849
22902
  snapshot.tool === "eraser" && /* @__PURE__ */ jsxs("div", {
22850
22903
  className: "scrawl-board__style-group",
22851
22904
  children: [/* @__PURE__ */ jsx("span", { children: "Eraser size" }), ERASER_RADII.map(([label, radius]) => /* @__PURE__ */ jsx("button", {
22905
+ "aria-label": label,
22906
+ title: label,
22852
22907
  "aria-pressed": style.eraserRadius === radius,
22853
22908
  disabled: !mutable,
22854
22909
  onClick: () => controller.style.set({ eraserRadius: radius }),
22855
22910
  type: "button",
22856
- children: label
22911
+ children: /* @__PURE__ */ jsx("span", {
22912
+ "aria-hidden": "true",
22913
+ style: {
22914
+ backgroundColor: "currentColor",
22915
+ width: 4 + radius * 3,
22916
+ height: 4 + radius * 3
22917
+ }
22918
+ })
22857
22919
  }, label))]
22858
22920
  }),
22859
22921
  snapshot.tool === "table" && /* @__PURE__ */ jsxs("div", {
22860
22922
  className: "scrawl-board__style-group",
22861
22923
  children: [
22862
- /* @__PURE__ */ jsx("span", { children: "Rows" }),
22924
+ /* @__PURE__ */ jsxs("span", {
22925
+ "aria-live": "polite",
22926
+ children: ["Rows: ", style.tableRows]
22927
+ }),
22863
22928
  /* @__PURE__ */ jsx("button", {
22864
22929
  "aria-label": "Fewer rows",
22865
22930
  disabled: !mutable,
@@ -22867,10 +22932,6 @@ function StyleShelf({ controller, snapshot, anchor }) {
22867
22932
  type: "button",
22868
22933
  children: "−"
22869
22934
  }),
22870
- /* @__PURE__ */ jsx("span", {
22871
- "aria-live": "polite",
22872
- children: style.tableRows
22873
- }),
22874
22935
  /* @__PURE__ */ jsx("button", {
22875
22936
  "aria-label": "More rows",
22876
22937
  disabled: !mutable,
@@ -22878,7 +22939,10 @@ function StyleShelf({ controller, snapshot, anchor }) {
22878
22939
  type: "button",
22879
22940
  children: "+"
22880
22941
  }),
22881
- /* @__PURE__ */ jsx("span", { children: "Cols" }),
22942
+ /* @__PURE__ */ jsxs("span", {
22943
+ "aria-live": "polite",
22944
+ children: ["Cols: ", style.tableCols]
22945
+ }),
22882
22946
  /* @__PURE__ */ jsx("button", {
22883
22947
  "aria-label": "Fewer columns",
22884
22948
  disabled: !mutable,
@@ -22886,10 +22950,6 @@ function StyleShelf({ controller, snapshot, anchor }) {
22886
22950
  type: "button",
22887
22951
  children: "−"
22888
22952
  }),
22889
- /* @__PURE__ */ jsx("span", {
22890
- "aria-live": "polite",
22891
- children: style.tableCols
22892
- }),
22893
22953
  /* @__PURE__ */ jsx("button", {
22894
22954
  "aria-label": "More columns",
22895
22955
  disabled: !mutable,
@@ -23044,6 +23104,7 @@ var INKS = [
23044
23104
  ];
23045
23105
  function DefaultBoardChrome({ controller, snapshot, renderPortal, className, style, regions, slots, icons }) {
23046
23106
  const [panel, setPanel] = useState(null);
23107
+ const [moreOpen, setMoreOpen] = useState(false);
23047
23108
  const [query, setQuery] = useState("");
23048
23109
  const [announcement, setAnnouncement] = useState("Board controls ready");
23049
23110
  const [styleShelfAnchor, setStyleShelfAnchor] = useState(void 0);
@@ -23058,25 +23119,46 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23058
23119
  if (panel === "search") searchRef.current?.focus();
23059
23120
  }, [panel]);
23060
23121
  useEffect(() => {
23122
+ const root = chromeRef.current;
23123
+ if (!root) return;
23061
23124
  const recompute = () => {
23062
- const root = chromeRef.current;
23063
- const button = root?.querySelector(".scrawl-board__tools button[aria-pressed=\"true\"]");
23064
- if (!root || !button) {
23125
+ const button = root.querySelector(".scrawl-board__tools button[aria-pressed=\"true\"]") ?? root.querySelector("[aria-label=\"More Board tools\"]");
23126
+ if (!button) {
23065
23127
  setStyleShelfAnchor(void 0);
23066
23128
  return;
23067
23129
  }
23068
23130
  const buttonRect = button.getBoundingClientRect();
23069
23131
  const rootRect = root.getBoundingClientRect();
23132
+ const shelf = root.querySelector(".scrawl-board__style-shelf");
23070
23133
  setStyleShelfAnchor({
23071
- x: buttonRect.left + buttonRect.width / 2 - rootRect.left,
23072
- y: buttonRect.top - rootRect.top
23134
+ x: buttonRect.right - rootRect.left + 12,
23135
+ y: Math.max(12, Math.min(buttonRect.top - rootRect.top, rootRect.height - (shelf?.offsetHeight ?? 0) - 12))
23073
23136
  });
23074
23137
  };
23075
23138
  recompute();
23139
+ const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(recompute);
23140
+ observer?.observe(root);
23141
+ const shelf = root.querySelector(".scrawl-board__style-shelf");
23142
+ if (shelf) observer?.observe(shelf);
23143
+ root.addEventListener("scroll", recompute, true);
23076
23144
  window.addEventListener("resize", recompute);
23077
- return () => window.removeEventListener("resize", recompute);
23078
- }, [snapshot.tool]);
23145
+ return () => {
23146
+ observer?.disconnect();
23147
+ root.removeEventListener("scroll", recompute, true);
23148
+ window.removeEventListener("resize", recompute);
23149
+ };
23150
+ }, [snapshot.tool, moreOpen]);
23151
+ useEffect(() => {
23152
+ if (!moreOpen) return;
23153
+ chromeRef.current?.querySelector(".scrawl-board__more-tools button")?.focus();
23154
+ const dismiss = (event) => {
23155
+ if (!event.target.closest(".scrawl-board__more-tools, [aria-label=\"More Board tools\"]")) setMoreOpen(false);
23156
+ };
23157
+ document.addEventListener("pointerdown", dismiss);
23158
+ return () => document.removeEventListener("pointerdown", dismiss);
23159
+ }, [moreOpen]);
23079
23160
  const open = (next, opener) => {
23161
+ setMoreOpen(false);
23080
23162
  openerRef.current = opener;
23081
23163
  setPanel(next);
23082
23164
  };
@@ -23085,6 +23167,8 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23085
23167
  queueMicrotask(() => openerRef.current?.focus());
23086
23168
  }, []);
23087
23169
  const chooseTool = (tool) => {
23170
+ if (moreOpen) chromeRef.current?.querySelector("[aria-label=\"More Board tools\"]")?.focus();
23171
+ setMoreOpen(false);
23088
23172
  controller.tools.select(tool);
23089
23173
  const label = [...PRIMARY_TOOLS, ...MORE_TOOLS].find((item) => item.tool === tool)?.label ?? tool;
23090
23174
  setAnnouncement(`${label} tool selected`);
@@ -23092,6 +23176,13 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23092
23176
  const onKeyDown = (event) => {
23093
23177
  const editing = event.target.matches("input, textarea, select, [contenteditable=true]");
23094
23178
  const modifier = event.metaKey || event.ctrlKey;
23179
+ if (event.key === "Escape" && moreOpen) {
23180
+ event.preventDefault();
23181
+ event.stopPropagation();
23182
+ setMoreOpen(false);
23183
+ chromeRef.current?.querySelector("[aria-label=\"More Board tools\"]")?.focus();
23184
+ return;
23185
+ }
23095
23186
  if (event.key === "Escape" && panel) {
23096
23187
  event.preventDefault();
23097
23188
  event.stopPropagation();
@@ -23208,6 +23299,7 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23208
23299
  "aria-label": "Board tools",
23209
23300
  className: "scrawl-board__tools",
23210
23301
  role: "toolbar",
23302
+ "aria-orientation": "vertical",
23211
23303
  children: [
23212
23304
  enabled("tools") && /* @__PURE__ */ jsxs(Fragment, { children: [PRIMARY_TOOLS.map(({ tool, label, key }) => /* @__PURE__ */ jsx("button", {
23213
23305
  "aria-label": `${label} tool`,
@@ -23216,37 +23308,31 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23216
23308
  onClick: () => chooseTool(tool),
23217
23309
  title: `${label} (${key})`,
23218
23310
  type: "button",
23219
- children: icons?.[tool] ? /* @__PURE__ */ jsx("span", {
23311
+ children: /* @__PURE__ */ jsx("span", {
23220
23312
  "aria-hidden": "true",
23221
- children: icons[tool]
23222
- }) : label
23223
- }, tool)), /* @__PURE__ */ jsxs("label", {
23224
- className: "scrawl-board__tool-select",
23225
- children: [/* @__PURE__ */ jsx("span", { children: "More tools" }), /* @__PURE__ */ jsxs("select", {
23226
- "aria-label": "More Board tools",
23227
- disabled: !mutable,
23228
- onChange: (event) => chooseTool(event.currentTarget.value),
23229
- value: MORE_TOOLS.some((item) => item.tool === snapshot.tool) ? snapshot.tool : "",
23230
- children: [/* @__PURE__ */ jsx("option", {
23231
- value: "",
23232
- children: "Choose"
23233
- }), MORE_TOOLS.map(({ tool, label }) => /* @__PURE__ */ jsx("option", {
23234
- value: tool,
23235
- children: label
23236
- }, tool))]
23237
- })]
23313
+ children: icons?.[tool] ?? /* @__PURE__ */ jsx(ToolIcon, { tool })
23314
+ })
23315
+ }, tool)), /* @__PURE__ */ jsx("button", {
23316
+ "aria-label": "More Board tools",
23317
+ "aria-expanded": moreOpen,
23318
+ disabled: !mutable,
23319
+ onClick: () => setMoreOpen((value) => !value),
23320
+ title: "More tools",
23321
+ type: "button",
23322
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "more" })
23238
23323
  })] }),
23239
- enabled("style") && /* @__PURE__ */ jsxs("button", {
23324
+ enabled("style") && /* @__PURE__ */ jsx("button", {
23240
23325
  "aria-label": "Ink style",
23241
23326
  "aria-expanded": panel === "style",
23242
23327
  disabled: !mutable,
23328
+ title: "Ink style",
23243
23329
  onClick: (event) => open("style", event.currentTarget),
23244
23330
  type: "button",
23245
- children: [/* @__PURE__ */ jsx("span", {
23331
+ children: /* @__PURE__ */ jsx("span", {
23246
23332
  "aria-hidden": "true",
23247
23333
  className: "scrawl-board__ink-sample",
23248
23334
  style: { backgroundColor: snapshot.style.inkColor }
23249
- }), "Ink"]
23335
+ })
23250
23336
  }),
23251
23337
  enabled("history") && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
23252
23338
  "aria-label": "Undo",
@@ -23255,8 +23341,9 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23255
23341
  controller.history.undo();
23256
23342
  setAnnouncement("Undo");
23257
23343
  },
23344
+ title: "Undo",
23258
23345
  type: "button",
23259
- children: "Undo"
23346
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "undo" })
23260
23347
  }), /* @__PURE__ */ jsx("button", {
23261
23348
  "aria-label": "Redo",
23262
23349
  disabled: !snapshot.canRedo || !mutable,
@@ -23264,11 +23351,31 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23264
23351
  controller.history.redo();
23265
23352
  setAnnouncement("Redo");
23266
23353
  },
23354
+ title: "Redo",
23267
23355
  type: "button",
23268
- children: "Redo"
23356
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "redo" })
23269
23357
  })] })
23270
23358
  ]
23271
23359
  })),
23360
+ moreOpen && enabled("tools") && slots?.toolbar !== null && /* @__PURE__ */ jsx("div", {
23361
+ "aria-label": "More tools",
23362
+ className: "scrawl-board__more-tools",
23363
+ role: "toolbar",
23364
+ "aria-orientation": "vertical",
23365
+ children: MORE_TOOLS.map(({ tool, label }) => /* @__PURE__ */ jsx("button", {
23366
+ "aria-label": `${label} tool`,
23367
+ "aria-pressed": snapshot.tool === tool,
23368
+ disabled: !mutable,
23369
+ "data-tool": tool,
23370
+ onClick: () => chooseTool(tool),
23371
+ title: label,
23372
+ type: "button",
23373
+ children: /* @__PURE__ */ jsx("span", {
23374
+ "aria-hidden": "true",
23375
+ children: icons?.[tool] ?? /* @__PURE__ */ jsx(ToolIcon, { tool })
23376
+ })
23377
+ }, tool))
23378
+ }),
23272
23379
  enabled("view") && /* @__PURE__ */ jsxs("div", {
23273
23380
  "aria-label": "Board view",
23274
23381
  className: "scrawl-board__view",
@@ -23303,7 +23410,7 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
23303
23410
  })
23304
23411
  ]
23305
23412
  }),
23306
- renderSlot(slots?.stylePanel, enabled("styleShelf") && /* @__PURE__ */ jsx(StyleShelf, {
23413
+ renderSlot(slots?.stylePanel, enabled("styleShelf") && !moreOpen && /* @__PURE__ */ jsx(StyleShelf, {
23307
23414
  anchor: styleShelfAnchor,
23308
23415
  controller,
23309
23416
  snapshot
package/dist/styles.css CHANGED
@@ -7,6 +7,7 @@
7
7
  --scrawl-color-text-muted: #62625c;
8
8
  --scrawl-color-edge: #d4d4cc;
9
9
  --scrawl-color-focus: #1856a3;
10
+ --scrawl-color-primary: #1856a3;
10
11
  --scrawl-color-selection: #d9e8f7;
11
12
  --scrawl-color-danger: #a3302b;
12
13
  --scrawl-color-warning: #805400;
@@ -102,9 +103,10 @@
102
103
  [data-scrawl-root] .scrawl-board__actions,
103
104
  [data-scrawl-root] .scrawl-board__tools,
104
105
  [data-scrawl-root] .scrawl-board__view,
106
+ [data-scrawl-root] .scrawl-board__more-tools,
105
107
  [data-scrawl-root] .scrawl-board__style-shelf {
106
108
  align-items: center;
107
- background: var(--scrawl-color-surface-raised);
109
+ background: var(--scrawl-color-surface);
108
110
  border: 1px solid color-mix(in srgb, var(--scrawl-color-surface-raised), var(--scrawl-color-text) 12%);
109
111
  border-radius: var(--scrawl-radius-panel);
110
112
  display: flex;
@@ -115,25 +117,45 @@
115
117
  }
116
118
 
117
119
  [data-scrawl-root] .scrawl-board__actions { inset-block-start: 12px; inset-inline-end: 12px; }
118
- [data-scrawl-root] .scrawl-board__tools { inset-block-end: 12px; inset-inline-start: 50%; max-inline-size: calc(100% - 24px); transform: translateX(-50%); }
120
+ [data-scrawl-root] .scrawl-board__tools {
121
+ inset-block-start: 50%;
122
+ inset-inline-start: 12px;
123
+ inline-size: 54px;
124
+ max-block-size: calc(100% - 144px);
125
+ overflow-y: auto;
126
+ flex-direction: column;
127
+ transform: translateY(-50%);
128
+ box-shadow: var(--scrawl-elevation-low);
129
+ }
130
+ [data-scrawl-root] .scrawl-board__tools > button { flex: 0 0 44px; inline-size: 44px; }
131
+ [data-scrawl-root] .scrawl-board__tools button,
132
+ [data-scrawl-root] .scrawl-board__more-tools button { display: flex; align-items: center; justify-content: center; }
133
+ [data-scrawl-root] .scrawl-board__tools svg,
134
+ [data-scrawl-root] .scrawl-board__more-tools svg { display: block; inline-size: 22px; block-size: 22px; }
135
+ [data-scrawl-root] .scrawl-board__tools .scrawl-board__ink-sample { margin: 0; }
136
+ [data-scrawl-root] .scrawl-board__tools :focus-visible,
137
+ [data-scrawl-root] .scrawl-board__more-tools :focus-visible,
138
+ [data-scrawl-root] .scrawl-board__style-shelf :focus-visible { outline-offset: -3px; }
119
139
  [data-scrawl-root] .scrawl-board__view { inset-block-end: 12px; inset-inline-end: 12px; }
140
+ [data-scrawl-root] .scrawl-board__more-tools,
120
141
  [data-scrawl-root] .scrawl-board__style-shelf {
121
- gap: 12px;
122
- inset-block-end: 68px;
123
- inset-inline-start: 50%;
124
- max-inline-size: calc(100% - 24px);
125
- overflow-x: auto;
126
- transform: translateX(-50%);
142
+ inset-inline-start: 78px;
143
+ inset-block-start: 72px;
144
+ inline-size: 106px;
145
+ max-block-size: calc(100% - 84px);
146
+ overflow-y: auto;
147
+ box-shadow: var(--scrawl-elevation-low);
127
148
  }
128
- /* `anchor` supplied (DefaultBoardChrome's own toolbar): follow the active
129
- tool button's own position (set inline as left/top) instead of centring
130
- over the whole toolbar — inset-block-end must yield explicitly, since an
131
- inline `top` alongside a stylesheet `bottom` would otherwise stretch the
132
- box rather than let `top` alone govern its position. */
133
- [data-scrawl-root] .scrawl-board__style-shelf[data-anchored="true"] {
134
- inset-block-end: auto;
135
- transform: translate(-50%, calc(-100% - 12px));
149
+ [data-scrawl-root] .scrawl-board__more-tools { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px; }
150
+ [data-scrawl-root] .scrawl-board__style-shelf .scrawl-board__style-group {
151
+ display: grid;
152
+ grid-template-columns: repeat(2, minmax(0, 1fr));
153
+ gap: 4px;
154
+ inline-size: 100%;
155
+ white-space: normal;
136
156
  }
157
+ [data-scrawl-root] .scrawl-board__style-shelf .scrawl-board__style-group > span { grid-column: 1 / -1; text-align: center; }
158
+ [data-scrawl-root] .scrawl-board__style-shelf .scrawl-board__style-group button { padding: 4px; font-size: 12px; }
137
159
  [data-scrawl-root] .scrawl-board__style-group { align-items: center; display: flex; gap: 6px; white-space: nowrap; }
138
160
  [data-scrawl-root] .scrawl-board__style-group > span:first-child { color: var(--scrawl-color-text-muted); font-size: 0.75rem; font-weight: var(--scrawl-font-weight-strong); text-transform: uppercase; }
139
161
  [data-scrawl-root] .scrawl-board__style-group button { align-items: center; display: flex; justify-content: center; padding: 6px 10px; }
@@ -221,30 +243,6 @@
221
243
  [data-scrawl-root] [data-scrawl-default-ui] button:disabled,
222
244
  [data-scrawl-root] [data-scrawl-default-ui] select:disabled { color: var(--scrawl-color-text-muted); cursor: not-allowed; }
223
245
 
224
- [data-scrawl-root] .scrawl-board__tool-select {
225
- align-items: center;
226
- display: flex;
227
- min-block-size: 44px;
228
- }
229
-
230
- [data-scrawl-root] .scrawl-board__tool-select > span { block-size: 1px; clip: rect(0, 0, 0, 0); inline-size: 1px; overflow: hidden; position: absolute; }
231
- [data-scrawl-root] .scrawl-board__tool-select select {
232
- -webkit-appearance: none;
233
- appearance: none;
234
- /* Chevron drawn from two gradient triangles rather than an embedded asset
235
- reference (disallowed by this stylesheet's policy, see styles.test.ts),
236
- and currentColor keeps it Theme-aware where a bitmap/SVG asset would
237
- need its own colour. */
238
- background-image:
239
- linear-gradient(45deg, transparent 50%, currentColor 50%),
240
- linear-gradient(135deg, currentColor 50%, transparent 50%);
241
- background-position: right 14px center, right 9px center;
242
- background-repeat: no-repeat;
243
- background-size: 5px 5px, 5px 5px;
244
- min-block-size: 44px;
245
- padding-inline: 8px 24px;
246
- }
247
-
248
246
  [data-scrawl-root] .scrawl-board__ink-sample,
249
247
  [data-scrawl-portal] .scrawl-board__ink-options span {
250
248
  border: 1px solid color-mix(in srgb, currentColor, transparent 72%);
@@ -334,14 +332,16 @@
334
332
  }
335
333
  [data-scrawl-portal] .scrawl-board__inline-editor-actions button { min-block-size: auto; min-inline-size: auto; padding: 2px 6px; }
336
334
 
337
- @media (max-width: 60rem) {
338
- [data-scrawl-root] .scrawl-board__actions,
339
- [data-scrawl-root] .scrawl-board__tools { inline-size: calc(100% - 24px); overflow-x: auto; }
340
- [data-scrawl-root] .scrawl-board__actions { justify-content: flex-end; }
341
- [data-scrawl-root] .scrawl-board__tools { flex-wrap: nowrap; inset-inline-start: 12px; transform: none; }
342
- [data-scrawl-root] .scrawl-board__tools :focus-visible { outline-offset: -3px; }
343
- [data-scrawl-root] .scrawl-board__view { inset-block-end: 84px; }
335
+ [data-scrawl-portal] .scrawl-board__panel[aria-label="Ink style"] {
336
+ inset-inline-start: 78px;
337
+ inset-block-start: 72px;
338
+ inset-block-end: auto;
339
+ inline-size: 156px;
340
+ max-block-size: calc(100% - 84px);
341
+ transform: none;
344
342
  }
343
+ [data-scrawl-portal] .scrawl-board__panel[aria-label="Ink style"] .scrawl-board__panel-heading { flex-direction: column; align-items: stretch; }
344
+ [data-scrawl-portal] .scrawl-board__ink-options { grid-template-columns: 1fr; }
345
345
 
346
346
  [data-scrawl-root] .scrawl-board__status, [data-scrawl-portal] .scrawl-board__status {
347
347
  block-size: 1px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrawl-board/board",
3
- "version": "0.1.0-beta.8",
3
+ "version": "0.1.0-beta.9",
4
4
  "description": "The Scrawl Board SDK: an embeddable, collaborative infinite-canvas whiteboard for React and vanilla JS/TS apps.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {