@q9labsai/chalk-whiteboard 5.2.0 → 5.2.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.
Files changed (39) hide show
  1. package/dist/assets.d.ts +13 -0
  2. package/dist/assets.js +10 -0
  3. package/dist/chunk-BN4VA2BS.js +20 -0
  4. package/dist/collab/view-limits.d.ts +1 -0
  5. package/dist/collab/view.d.ts +26 -0
  6. package/dist/embedded/chalk-whiteboard/Figtree-Regular.ttf +0 -0
  7. package/dist/embedded/chalk-whiteboard/Figtree-SemiBold.ttf +0 -0
  8. package/dist/embedded/chalk-whiteboard/asset-manifest.json +19 -4
  9. package/dist/embedded/chalk-whiteboard/chalk.css +44 -0
  10. package/dist/embedded/chalk-whiteboard/licenses/Figtree.txt +93 -0
  11. package/dist/embedded/chalk-whiteboard/renderer.js +1669 -1418
  12. package/dist/embedded/controller.d.ts +16 -14
  13. package/dist/embedded/index.d.ts +1 -1
  14. package/dist/embedded/index.js +127 -8
  15. package/dist/embedded/protocol.d.ts +86 -0
  16. package/dist/embedded/renderer-assets.d.ts +1 -0
  17. package/dist/embedded/renderer-bridge.d.ts +10 -0
  18. package/dist/embedded/renderer-entry.d.ts +2 -0
  19. package/dist/embedded/renderer.d.ts +0 -5
  20. package/dist/react/board/Board.d.ts +41 -0
  21. package/dist/react/board/BoardCursors.d.ts +23 -0
  22. package/dist/react/board/BoardIcon.d.ts +46 -0
  23. package/dist/react/board/BoardIsland.d.ts +22 -0
  24. package/dist/react/board/BoardMenu.d.ts +20 -0
  25. package/dist/react/board/BoardStyles.d.ts +2 -0
  26. package/dist/react/board/board-draw-policy.d.ts +10 -0
  27. package/dist/react/board/board-engine.d.ts +30 -0
  28. package/dist/react/board/board-history.d.ts +7 -0
  29. package/dist/react/board/board-model.d.ts +91 -0
  30. package/dist/react/board/board-toasts.d.ts +15 -0
  31. package/dist/react/board/board-view-versions.d.ts +1 -0
  32. package/dist/react/board/controls/ChalkMenu.d.ts +22 -0
  33. package/dist/react/board/controls/ChalkPopover.d.ts +13 -0
  34. package/dist/react/board/controls/ChalkTooltip.d.ts +5 -0
  35. package/dist/react/board/controls/index.d.ts +3 -0
  36. package/dist/react/board/useMediaQuery.d.ts +6 -0
  37. package/dist/react/index.d.ts +3 -0
  38. package/dist/react/index.js +1811 -18
  39. package/package.json +19 -5
@@ -1,3 +1,7 @@
1
+ import {
2
+ getWhiteboardAssets,
3
+ resolveExcalidrawAssetPath
4
+ } from "../chunk-BN4VA2BS.js";
1
5
  import {
2
6
  CANONICAL_INK
3
7
  } from "../chunk-IVFWOYSW.js";
@@ -172,11 +176,14 @@ async function waitForMathJaxRuntime() {
172
176
  throw new Error("Math renderer failed to initialize");
173
177
  }
174
178
  function configureMathJax(target, existing) {
179
+ const configuredPath = getWhiteboardAssets()?.mathJaxBasePath;
180
+ const basePath = configuredPath ? new URL(configuredPath, document.baseURI).href.replace(/\/$/, "") : void 0;
175
181
  target.MathJax = {
176
182
  ...existing ?? {},
177
- options: { ...existing?.options, enableMenu: false },
178
- startup: { ...existing?.startup, typeset: false },
179
- svg: { fontCache: "none", ...existing?.svg }
183
+ ...basePath ? { loader: { paths: { mathjax: basePath, fonts: `${basePath}/fonts` }, ...existing?.loader } } : {},
184
+ svg: { ...basePath ? { dynamicPrefix: `${basePath}/fonts/mathjax-newcm-font/svg/dynamic` } : {}, fontCache: "none", ...existing?.svg },
185
+ options: { ...basePath ? { worker: { path: `${basePath}/sre`, maps: `${basePath}/sre/mathmaps` } } : {}, ...existing?.options, enableMenu: false },
186
+ startup: { ...existing?.startup, typeset: false }
180
187
  };
181
188
  }
182
189
  function loadMathJaxScript() {
@@ -188,7 +195,8 @@ function loadMathJaxScript() {
188
195
  return;
189
196
  }
190
197
  const script = document.createElement("script");
191
- script.src = DEFAULT_MATHJAX_SCRIPT_URL;
198
+ const basePath = getWhiteboardAssets()?.mathJaxBasePath;
199
+ script.src = basePath ? `${basePath.replace(/\/$/, "")}/tex-svg.js` : DEFAULT_MATHJAX_SCRIPT_URL;
192
200
  script.async = true;
193
201
  script.dataset.chalkMathjax = "true";
194
202
  script.onload = () => resolve();
@@ -288,7 +296,7 @@ function useWhiteboardCanvasConfiguration(props) {
288
296
  canDraw,
289
297
  headless: props.headless === true,
290
298
  classNames,
291
- excalidrawCssPath: valueOr(props.excalidrawCssPath, DEFAULT_EXCALIDRAW_CSS),
299
+ excalidrawCssPath: valueOr(props.excalidrawCssPath, getWhiteboardAssets()?.excalidrawCssPath ?? DEFAULT_EXCALIDRAW_CSS),
292
300
  icons: valueOr(props.icons, {}),
293
301
  isCollaborating: Boolean(props.collab),
294
302
  isVisible: valueOr(props.isVisible, true),
@@ -300,7 +308,7 @@ function valueOr(value, fallback) {
300
308
  return value === void 0 ? fallback : value;
301
309
  }
302
310
  var STYLESHEET_ID = "chalk-excalidraw-styles";
303
- function preloadWhiteboardStylesheet(href = DEFAULT_EXCALIDRAW_CSS) {
311
+ function preloadWhiteboardStylesheet(href = getWhiteboardAssets()?.excalidrawCssPath ?? DEFAULT_EXCALIDRAW_CSS) {
304
312
  if (typeof document !== "undefined") boardStylesheet(href);
305
313
  }
306
314
  function boardStylesheet(href) {
@@ -325,7 +333,7 @@ function useExternalStylesheet(href) {
325
333
  const element = boardStylesheet(href);
326
334
  const fail = () => setError(`Failed to load ${href}`);
327
335
  const ownLink = isManagedStylesheet(element);
328
- if (!ownLink || element.dataset.state === "loaded") {
336
+ if (!ownLink || element.sheet || element.dataset.state === "loaded") {
329
337
  setLoaded(true);
330
338
  return;
331
339
  }
@@ -343,10 +351,11 @@ function useExternalStylesheet(href) {
343
351
  }, [href]);
344
352
  return { error, loaded };
345
353
  }
346
- function useExcalidrawModule(onLoadError, setLoadError) {
354
+ function useExcalidrawModule(cssPath, onLoadError, setLoadError) {
347
355
  const [excalidraw, setExcalidraw] = useState2(null);
348
356
  useEffect(() => {
349
357
  let mounted = true;
358
+ window.EXCALIDRAW_ASSET_PATH = resolveExcalidrawAssetPath(cssPath);
350
359
  import("@excalidraw/excalidraw").then((module) => {
351
360
  if (mounted) setExcalidraw(module);
352
361
  }).catch((reason) => {
@@ -357,7 +366,7 @@ function useExcalidrawModule(onLoadError, setLoadError) {
357
366
  return () => {
358
367
  mounted = false;
359
368
  };
360
- }, [onLoadError, setLoadError]);
369
+ }, [cssPath, onLoadError, setLoadError]);
361
370
  return excalidraw;
362
371
  }
363
372
  var TRANSPARENT = "transparent";
@@ -390,14 +399,15 @@ var UI_OPTIONS = {
390
399
  function useWhiteboardCollaboration(options) {
391
400
  const apiRef = useRef(null);
392
401
  const refs = useCollaborationRefs(options.collab, options.canDraw, options.inkColor);
402
+ const [committedApi, setCommittedApi] = useState2(null);
393
403
  const [engineReady, setEngineReady] = useState2(false);
394
404
  const initializeEngine = useCollaborationEngineLoader(refs, apiRef, options.headless, options.onLoadError, options.setLoadError, setEngineReady);
395
- const handleApiReady = useExcalidrawApiReady(apiRef, initializeEngine, options.onExcalidrawApiReady);
405
+ const handleApiReady = useExcalidrawApiReady(apiRef, setCommittedApi, options.onExcalidrawApiReady);
396
406
  const handlers = useExcalidrawSceneHandlers(apiRef, refs.engineRef, options.onSelectedLatexChange);
397
407
  useCollaborationPermissions(refs.engineRef, options.canDraw, options.collab?.canManage);
398
408
  useCollaborationInk(refs.engineRef, options.inkColor);
399
409
  useCollaborationDisposal(refs.engineRef, refs.isMountedRef);
400
- useCollaborationInitialization(options.collab, apiRef, refs.engineRef, initializeEngine);
410
+ useCollaborationInitialization(options.collab, committedApi, refs.engineRef, initializeEngine);
401
411
  return { ready: !options.collab || engineReady, apiRef, engineRef: refs.engineRef, handleApiReady, ...handlers };
402
412
  }
403
413
  function useCollaborationRefs(collab, canDraw, inkColor) {
@@ -446,18 +456,18 @@ function useCollaborationEngineLoader(refs, apiRef, headless, onLoadError, setLo
446
456
  });
447
457
  }, [apiRef, headless, onLoadError, refs, setLoadError, setEngineReady]);
448
458
  }
449
- function useExcalidrawApiReady(apiRef, initializeEngine, onExcalidrawApiReady) {
459
+ function useExcalidrawApiReady(apiRef, setCommittedApi, onExcalidrawApiReady) {
450
460
  const didReportApiRef = useRef(false);
451
461
  return useCallback(
452
462
  (api) => {
453
463
  apiRef.current = api;
454
- initializeEngine();
464
+ setCommittedApi(api);
455
465
  if (!didReportApiRef.current) {
456
466
  didReportApiRef.current = true;
457
467
  onExcalidrawApiReady?.(api);
458
468
  }
459
469
  },
460
- [apiRef, initializeEngine, onExcalidrawApiReady]
470
+ [apiRef, setCommittedApi, onExcalidrawApiReady]
461
471
  );
462
472
  }
463
473
  function useExcalidrawSceneHandlers(apiRef, engineRef, onSelectedLatexChange) {
@@ -509,15 +519,15 @@ function useCollaborationDisposal(engineRef, isMountedRef) {
509
519
  };
510
520
  }, [engineRef, isMountedRef]);
511
521
  }
512
- function useCollaborationInitialization(collab, apiRef, engineRef, initializeEngine) {
522
+ function useCollaborationInitialization(collab, committedApi, engineRef, initializeEngine) {
513
523
  useEffect(() => {
514
524
  if (!collab) {
515
525
  engineRef.current?.dispose();
516
526
  engineRef.current = null;
517
527
  return;
518
528
  }
519
- if (apiRef.current) initializeEngine();
520
- }, [apiRef, collab, engineRef, initializeEngine]);
529
+ if (committedApi) initializeEngine();
530
+ }, [committedApi, collab, engineRef, initializeEngine]);
521
531
  }
522
532
  function useWhiteboardMath({ apiRef, canDraw, excalidraw, isCollaborating, selectedLatex, setSelectedLatex, syncScene }) {
523
533
  const [isMathOpen, setIsMathOpen] = useState2(false);
@@ -549,7 +559,7 @@ function useWhiteboardCanvasState(props) {
549
559
  const configuration = useWhiteboardCanvasConfiguration(props);
550
560
  const [loadError, setLoadError] = useState2(null);
551
561
  const [selectedLatex, setSelectedLatex] = useState2(null);
552
- const excalidraw = useExcalidrawModule(props.onLoadError, setLoadError);
562
+ const excalidraw = useExcalidrawModule(configuration.excalidrawCssPath, props.onLoadError, setLoadError);
553
563
  const stylesheet = useExternalStylesheet(configuration.excalidrawCssPath);
554
564
  const collaboration = useWhiteboardCollaboration({
555
565
  canDraw: configuration.canDraw,
@@ -732,10 +742,1793 @@ function observeEngineHistory(api, canvas, onChange) {
732
742
  dispose();
733
743
  };
734
744
  }
745
+
746
+ // src/react/board/board-history.ts
747
+ import { useEffect as useEffect2, useState as useState3 } from "react";
748
+ function useEngineHistory(root, engine) {
749
+ const [history, setHistory] = useState3({ canUndo: false, canRedo: false });
750
+ useEffect2(() => {
751
+ const canvas = root.current?.querySelector(".excalidraw");
752
+ if (!engine || !canvas) return;
753
+ return observeEngineHistory(engine.api, canvas, setHistory);
754
+ }, [engine, root]);
755
+ return history;
756
+ }
757
+
758
+ // src/react/board/board-view-versions.ts
759
+ import { ChalkWhiteboardViewVersions } from "@q9labsai/chalk-client";
760
+
761
+ // src/react/board/Board.tsx
762
+ import { useCallback as useCallback3, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState8 } from "react";
763
+
764
+ // src/react/board/useMediaQuery.ts
765
+ import { useEffect as useEffect3, useState as useState4 } from "react";
766
+ function useMediaQuery(query) {
767
+ const [matches, setMatches] = useState4(() => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(query).matches : false);
768
+ useEffect3(() => {
769
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
770
+ return;
771
+ }
772
+ const mediaQuery = window.matchMedia(query);
773
+ const handleChange = () => setMatches(mediaQuery.matches);
774
+ handleChange();
775
+ mediaQuery.addEventListener("change", handleChange);
776
+ return () => mediaQuery.removeEventListener("change", handleChange);
777
+ }, [query]);
778
+ return matches;
779
+ }
780
+ function usePrefersReducedMotion() {
781
+ return useMediaQuery("(prefers-reduced-motion: reduce)");
782
+ }
783
+
784
+ // src/react/board/board-model.ts
785
+ var SURFACES = [
786
+ { id: "chalkboard", label: "Chalkboard" },
787
+ { id: "paper", label: "Paper" },
788
+ { id: "dots", label: "Dot grid" }
789
+ ];
790
+ var ISLAND_PLACES = [
791
+ { id: "left", label: "Left" },
792
+ { id: "bottom", label: "Bottom" },
793
+ { id: "top", label: "Top" }
794
+ ];
795
+ var DRAW_POLICIES = [
796
+ { id: "everyone", label: "Everyone" },
797
+ { id: "hosts", label: "Only hosts" },
798
+ { id: "picked", label: "Picked people" }
799
+ ];
800
+ var PEN_COLORS = [
801
+ { id: "ink", label: "Ink" },
802
+ { id: "red", label: "Red" },
803
+ { id: "orange", label: "Orange" },
804
+ { id: "green", label: "Green" },
805
+ { id: "blue", label: "Blue" },
806
+ { id: "violet", label: "Violet" }
807
+ ];
808
+ var PEN_HEX = { red: "#e23240", orange: "#c15a10", green: "#288a51", blue: "#1f7ad6", violet: "#935ae2" };
809
+ var NOTE_FILL = { ink: "#f6e27a", red: "#f2a3a8", orange: "#f4be8c", green: "#a8dbb8", blue: "#a9cdf2", violet: "#cdb8f2" };
810
+ var NOTE_TEXT = "#1b1c1e";
811
+ var NOTE_SIZE = { width: 170, height: 150 };
812
+ var NOTE_ROUNDNESS = { type: 3, value: 4 };
813
+ var PEN_WIDTHS = [
814
+ { id: "thin", label: "Thin", units: 2.5 },
815
+ { id: "medium", label: "Medium", units: 4.5 },
816
+ { id: "thick", label: "Thick", units: 8 }
817
+ ];
818
+ var SHAPES = [
819
+ { id: "rectangle", label: "Rectangle", key: "R" },
820
+ { id: "ellipse", label: "Ellipse", key: "O" },
821
+ { id: "diamond", label: "Diamond", key: "D" },
822
+ { id: "arrow", label: "Arrow", key: "A" },
823
+ { id: "line", label: "Line", key: "L" }
824
+ ];
825
+ var TOOL_KEYS = { v: "selection", h: "hand", p: "pen", u: "highlighter", r: "rectangle", o: "ellipse", d: "diamond", a: "arrow", l: "line", t: "text", n: "note", e: "eraser", k: "laser" };
826
+ var PERSON_COLORS = ["#1d70c5", "#8753d0", "#d02e3b", "#257f4b", "#b2530f", "#1b7979", "#c0307a", "#62751d", "#3b6fd4", "#a14fb5", "#b5512c", "#297f65"];
827
+ function personColor(index) {
828
+ return PERSON_COLORS[(index % PERSON_COLORS.length + PERSON_COLORS.length) % PERSON_COLORS.length];
829
+ }
830
+ var DARK_SURFACES = /* @__PURE__ */ new Set(["chalkboard", "dots-dark"]);
831
+ function surfaceKey(surface, theme) {
832
+ return surface === "dots" ? `dots-${theme}` : surface;
833
+ }
834
+ function isDarkSurface(surface, theme) {
835
+ return DARK_SURFACES.has(surfaceKey(surface, theme));
836
+ }
837
+ function surfaceInk(surface, theme) {
838
+ if (surface === "chalkboard") return "#f1f0ea";
839
+ if (surface === "paper") return "#1b1c1f";
840
+ return theme === "dark" ? "#ececee" : "#18181b";
841
+ }
842
+ function surfaceFill(surface, theme) {
843
+ if (surface === "chalkboard") return "#1c2622";
844
+ if (surface === "paper") return theme === "dark" ? "#eae6db" : "#f5f2ea";
845
+ return theme === "dark" ? "#151619" : "#ffffff";
846
+ }
847
+ function selectionColor(surface, theme) {
848
+ return isDarkSurface(surface, theme) ? "#7db8ff" : "#1f6fd0";
849
+ }
850
+ function penHex(color, surface, theme) {
851
+ return color === "ink" ? surfaceInk(surface, theme) : PEN_HEX[color];
852
+ }
853
+ function highlighterStyle(color, width) {
854
+ return { strokeColor: PEN_HEX[color === "ink" ? "orange" : color], strokeWidth: widthUnits(width) * 4, opacity: 40 };
855
+ }
856
+ var FREEHAND_SCALE = 4.25;
857
+ function engineStrokeWidth(px, freehand) {
858
+ return freehand ? px / FREEHAND_SCALE : px;
859
+ }
860
+ function widthUnits(width) {
861
+ return PEN_WIDTHS.find((entry) => entry.id === width).units;
862
+ }
863
+ var MIN_ZOOM = 0.1;
864
+ var MAX_ZOOM = 4;
865
+ function clampZoom(zoom) {
866
+ return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
867
+ }
868
+ function visibleView(camera, viewport, bounds) {
869
+ const { maxCoordinate, maxExtent } = bounds;
870
+ return {
871
+ x: Math.max(-maxCoordinate, Math.min(maxCoordinate, -camera.scrollX)),
872
+ y: Math.max(-maxCoordinate, Math.min(maxCoordinate, -camera.scrollY)),
873
+ width: Math.min(maxExtent, viewport.width / camera.zoom),
874
+ height: Math.min(maxExtent, viewport.height / camera.zoom)
875
+ };
876
+ }
877
+ function cameraForView(view, viewport, zoom) {
878
+ return { zoom, scrollX: viewport.width / (2 * zoom) - (view.x + view.width / 2), scrollY: viewport.height / (2 * zoom) - (view.y + view.height / 2) };
879
+ }
880
+ function zoomAroundCentre(camera, viewport, zoom) {
881
+ const next = clampZoom(zoom);
882
+ const centreX = viewport.width / 2 / camera.zoom - camera.scrollX;
883
+ const centreY = viewport.height / 2 / camera.zoom - camera.scrollY;
884
+ return { zoom: next, scrollX: viewport.width / 2 / next - centreX, scrollY: viewport.height / 2 / next - centreY };
885
+ }
886
+ function sameCamera(left, right) {
887
+ return Math.abs(left.zoom - right.zoom) < 1e-4 && Math.abs(left.scrollX - right.scrollX) < 0.5 && Math.abs(left.scrollY - right.scrollY) < 0.5;
888
+ }
889
+ function mixCamera(from, to, progress) {
890
+ const zoom = Math.exp(Math.log(from.zoom) + (Math.log(to.zoom) - Math.log(from.zoom)) * progress);
891
+ return { zoom, scrollX: from.scrollX + (to.scrollX - from.scrollX) * progress, scrollY: from.scrollY + (to.scrollY - from.scrollY) * progress };
892
+ }
893
+ function springStep(offset, velocity, rate, seconds) {
894
+ const decay = Math.exp(-rate * seconds);
895
+ const drift = (velocity + rate * offset) * seconds;
896
+ return [(offset + drift) * decay, (velocity - rate * drift) * decay];
897
+ }
898
+ function readStored(key, allowed, fallback) {
899
+ try {
900
+ const value = globalThis.localStorage?.getItem(key);
901
+ return allowed.find((entry) => entry.id === value)?.id ?? fallback;
902
+ } catch {
903
+ return fallback;
904
+ }
905
+ }
906
+ function writeStored(key, value) {
907
+ try {
908
+ globalThis.localStorage?.setItem(key, value);
909
+ } catch {
910
+ }
911
+ }
912
+
913
+ // src/react/board/board-engine.ts
914
+ var TEXT_FONT = 6;
915
+ var NOTE_FONT_SIZE = 18;
916
+ var BOUND_TEXT_PADDING = 5;
917
+ function engineTool(tool) {
918
+ if (tool === "pen" || tool === "highlighter") return { type: "freedraw", locked: true };
919
+ if (tool === "note" || tool === "selection") return { type: "selection" };
920
+ return { type: tool, locked: true };
921
+ }
922
+ function penStyle(tool, pen) {
923
+ if (tool === "highlighter") return highlighterStyle(pen.color, pen.width);
924
+ return { strokeColor: penHex(pen.color, pen.surface, pen.theme), strokeWidth: widthUnits(pen.width), opacity: 100 };
925
+ }
926
+ function applyTool(engine, tool, pen) {
927
+ const style = penStyle(tool, pen);
928
+ engine.api.updateScene({
929
+ appState: {
930
+ currentItemStrokeColor: style.strokeColor,
931
+ currentItemStrokeWidth: engineStrokeWidth(style.strokeWidth, tool === "pen" || tool === "highlighter"),
932
+ currentItemOpacity: style.opacity,
933
+ currentItemRoughness: 0,
934
+ currentItemFontFamily: TEXT_FONT,
935
+ currentItemBackgroundColor: "transparent"
936
+ }
937
+ });
938
+ engine.api.setActiveTool(engineTool(tool));
939
+ }
940
+ function selectionStyle(element, pen, changes) {
941
+ if (element.type === "rectangle" && element.customData?.chalkNote) {
942
+ if (!changes.color) return {};
943
+ return { strokeColor: NOTE_FILL[changes.color], backgroundColor: NOTE_FILL[changes.color] };
944
+ }
945
+ const style = penStyle(element.type === "freedraw" && element.opacity === 40 ? "highlighter" : "pen", pen);
946
+ return {
947
+ ...changes.color ? { strokeColor: style.strokeColor } : {},
948
+ ...changes.width ? { strokeWidth: engineStrokeWidth(style.strokeWidth, element.type === "freedraw") } : {}
949
+ };
950
+ }
951
+ function styleChanged(element, updates) {
952
+ return updates.strokeColor !== void 0 && updates.strokeColor !== element.strokeColor || updates.strokeWidth !== void 0 && updates.strokeWidth !== element.strokeWidth || updates.backgroundColor !== void 0 && updates.backgroundColor !== element.backgroundColor;
953
+ }
954
+ function restyleSelection(engine, pen, changes) {
955
+ const { api, excalidraw } = engine;
956
+ const selected = api.getAppState().selectedElementIds;
957
+ let changed = false;
958
+ const elements = api.getSceneElementsIncludingDeleted().map((element) => {
959
+ if (!selected[element.id] || element.isDeleted) return element;
960
+ const updates = selectionStyle(element, pen, changes);
961
+ if (!styleChanged(element, updates)) return element;
962
+ changed = true;
963
+ return excalidraw.newElementWith(element, updates);
964
+ });
965
+ if (changed) api.updateScene({ elements, captureUpdate: excalidraw.CaptureUpdateAction.IMMEDIATELY });
966
+ return changed;
967
+ }
968
+ function placeNote(engine, container, point, color) {
969
+ const { api, excalidraw } = engine;
970
+ const fill = NOTE_FILL[color];
971
+ const [note] = excalidraw.convertToExcalidrawElements([
972
+ { type: "rectangle", x: point.x - NOTE_SIZE.width / 2, y: point.y - NOTE_SIZE.height / 2, width: NOTE_SIZE.width, height: NOTE_SIZE.height, backgroundColor: fill, strokeColor: fill, fillStyle: "solid", strokeWidth: 1, roughness: 0, roundness: NOTE_ROUNDNESS, customData: { chalkNote: true } }
973
+ ]);
974
+ if (!note) return;
975
+ api.updateScene({
976
+ elements: [...api.getSceneElementsIncludingDeleted(), note],
977
+ appState: { selectedElementIds: { [note.id]: true }, currentItemStrokeColor: NOTE_TEXT, currentItemFontSize: NOTE_FONT_SIZE, currentItemTextAlign: "left", currentItemFontFamily: TEXT_FONT },
978
+ captureUpdate: excalidraw.CaptureUpdateAction.IMMEDIATELY
979
+ });
980
+ api.setActiveTool({ type: "selection" });
981
+ requestAnimationFrame(() => container.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true })));
982
+ }
983
+ function settleNoteText(engine) {
984
+ const { api, excalidraw } = engine;
985
+ const elements = api.getSceneElementsIncludingDeleted();
986
+ const notes = new Map(elements.filter((element) => element.type === "rectangle" && element.customData?.chalkNote).map((element) => [element.id, element]));
987
+ let changed = false;
988
+ const next = elements.map((element) => {
989
+ if (element.type !== "text" || !element.containerId || element.isDeleted) return element;
990
+ const note = notes.get(element.containerId);
991
+ if (!note) return element;
992
+ const x = note.x + BOUND_TEXT_PADDING;
993
+ const y = note.y + BOUND_TEXT_PADDING;
994
+ if (element.verticalAlign === "top" && element.textAlign === "left" && element.strokeColor === NOTE_TEXT && element.x === x && element.y === y) return element;
995
+ changed = true;
996
+ return excalidraw.newElementWith(element, { verticalAlign: "top", textAlign: "left", strokeColor: NOTE_TEXT, x, y });
997
+ });
998
+ if (changed) api.updateScene({ elements: next, captureUpdate: excalidraw.CaptureUpdateAction.NEVER });
999
+ }
1000
+ var isApple = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform);
1001
+ function pressHistory(container, redo) {
1002
+ const apple = isApple();
1003
+ container.dispatchEvent(new KeyboardEvent("keydown", { key: redo ? "Z" : "z", code: "KeyZ", shiftKey: redo, metaKey: apple, ctrlKey: !apple, bubbles: true }));
1004
+ }
1005
+ function readCamera(engine) {
1006
+ const state = engine.api.getAppState();
1007
+ return { scrollX: state.scrollX, scrollY: state.scrollY, zoom: state.zoom.value };
1008
+ }
1009
+ function setCamera(engine, camera) {
1010
+ engine.api.updateScene({ appState: { scrollX: camera.scrollX, scrollY: camera.scrollY, zoom: { value: camera.zoom } } });
1011
+ }
1012
+ async function boardImage(engine, background) {
1013
+ const { api, excalidraw } = engine;
1014
+ return excalidraw.exportToBlob({ elements: api.getSceneElements(), files: api.getFiles(), appState: { exportBackground: true, viewBackgroundColor: background }, mimeType: "image/png", exportPadding: 24 });
1015
+ }
1016
+ function sceneSignature(engine) {
1017
+ return engine.excalidraw.getSceneVersion(engine.api.getSceneElementsIncludingDeleted());
1018
+ }
1019
+
1020
+ // src/react/board/BoardCursors.tsx
1021
+ import { useEffect as useEffect4, useRef as useRef2, useState as useState5 } from "react";
1022
+
1023
+ // src/react/board/BoardIcon.tsx
1024
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1025
+ var GLYPHS = {
1026
+ select: /* @__PURE__ */ jsx3("path", { d: "M5 3l14 7-6.5 2L10 19z" }),
1027
+ hand: /* @__PURE__ */ jsx3("path", { d: "M18 11V6a2 2 0 0 0-4 0M14 10V4a2 2 0 0 0-4 0v2M10 10.5V6a2 2 0 0 0-4 0v8M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.9-6-2.3l-3.6-3.6a2 2 0 0 1 2.8-2.8L7 15" }),
1028
+ pen: /* @__PURE__ */ jsx3("path", { d: "M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z" }),
1029
+ highlighter: /* @__PURE__ */ jsxs3(Fragment, { children: [
1030
+ /* @__PURE__ */ jsx3("path", { d: "M9 11l-6 6v3h9l3-3" }),
1031
+ /* @__PURE__ */ jsx3("path", { d: "M22 12l-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" })
1032
+ ] }),
1033
+ rectangle: /* @__PURE__ */ jsx3("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
1034
+ ellipse: /* @__PURE__ */ jsx3("ellipse", { cx: "12", cy: "12", rx: "9", ry: "6.5" }),
1035
+ diamond: /* @__PURE__ */ jsx3("path", { d: "M12 3l9 9-9 9-9-9z" }),
1036
+ arrow: /* @__PURE__ */ jsx3("path", { d: "M5 19L19 5M9 5h10v10" }),
1037
+ line: /* @__PURE__ */ jsx3("path", { d: "M4 20L20 4" }),
1038
+ text: /* @__PURE__ */ jsx3("path", { d: "M5 6V4h14v2M12 4v16M9 20h6" }),
1039
+ note: /* @__PURE__ */ jsxs3(Fragment, { children: [
1040
+ /* @__PURE__ */ jsx3("path", { d: "M5 3h14a2 2 0 0 1 2 2v9l-7 7H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z" }),
1041
+ /* @__PURE__ */ jsx3("path", { d: "M14 21v-5a2 2 0 0 1 2-2h5" })
1042
+ ] }),
1043
+ eraser: /* @__PURE__ */ jsx3("path", { d: "M7 21h10M5.5 13.5l7-7a2 2 0 0 1 2.8 0l3.2 3.2a2 2 0 0 1 0 2.8L11 19H8l-2.5-2.5a2 2 0 0 1 0-3z" }),
1044
+ laser: /* @__PURE__ */ jsxs3(Fragment, { children: [
1045
+ /* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "2.5", fill: "currentColor" }),
1046
+ /* @__PURE__ */ jsx3("path", { d: "M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1" })
1047
+ ] }),
1048
+ undo: /* @__PURE__ */ jsxs3(Fragment, { children: [
1049
+ /* @__PURE__ */ jsx3("path", { d: "M9 14L4 9l5-5" }),
1050
+ /* @__PURE__ */ jsx3("path", { d: "M4 9h10.5a5.5 5.5 0 0 1 0 11H11" })
1051
+ ] }),
1052
+ redo: /* @__PURE__ */ jsxs3(Fragment, { children: [
1053
+ /* @__PURE__ */ jsx3("path", { d: "M15 14l5-5-5-5" }),
1054
+ /* @__PURE__ */ jsx3("path", { d: "M20 9H9.5a5.5 5.5 0 0 0 0 11H13" })
1055
+ ] }),
1056
+ minus: /* @__PURE__ */ jsx3("path", { d: "M5 12h14" }),
1057
+ plus: /* @__PURE__ */ jsx3("path", { d: "M12 5v14M5 12h14" }),
1058
+ fit: /* @__PURE__ */ jsx3("path", { d: "M4 9V5a1 1 0 0 1 1-1h4M20 9V5a1 1 0 0 0-1-1h-4M4 15v4a1 1 0 0 0 1 1h4M20 15v4a1 1 0 0 1-1 1h-4" }),
1059
+ download: /* @__PURE__ */ jsx3("path", { d: "M12 3v12M7 10l5 5 5-5M4 21h16" }),
1060
+ copy: /* @__PURE__ */ jsxs3(Fragment, { children: [
1061
+ /* @__PURE__ */ jsx3("rect", { x: "9", y: "9", width: "12", height: "12", rx: "2" }),
1062
+ /* @__PURE__ */ jsx3("path", { d: "M5 15V5a2 2 0 0 1 2-2h10" })
1063
+ ] }),
1064
+ trash: /* @__PURE__ */ jsx3("path", { d: "M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3" }),
1065
+ close: /* @__PURE__ */ jsx3("path", { d: "M6 6l12 12M18 6L6 18" }),
1066
+ check: /* @__PURE__ */ jsx3("path", { d: "M5 12l5 5 9-10" }),
1067
+ target: /* @__PURE__ */ jsxs3(Fragment, { children: [
1068
+ /* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "9" }),
1069
+ /* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "4" })
1070
+ ] }),
1071
+ eye: /* @__PURE__ */ jsxs3(Fragment, { children: [
1072
+ /* @__PURE__ */ jsx3("path", { d: "M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z" }),
1073
+ /* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "3" })
1074
+ ] }),
1075
+ wifiOff: /* @__PURE__ */ jsx3("path", { d: "M2 2l20 20M8.5 16.5a5 5 0 0 1 7 0M5 12.9a10 10 0 0 1 5-2.7M10.7 5.1A16 16 0 0 1 22 9M2 8.8a15 15 0 0 1 4.2-2.5M12 20h.01" }),
1076
+ lock: /* @__PURE__ */ jsxs3(Fragment, { children: [
1077
+ /* @__PURE__ */ jsx3("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }),
1078
+ /* @__PURE__ */ jsx3("path", { d: "M8 11V8a4 4 0 0 1 8 0v3" })
1079
+ ] }),
1080
+ more: /* @__PURE__ */ jsxs3(Fragment, { children: [
1081
+ /* @__PURE__ */ jsx3("circle", { cx: "5", cy: "12", r: "1.2" }),
1082
+ /* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "1.2" }),
1083
+ /* @__PURE__ */ jsx3("circle", { cx: "19", cy: "12", r: "1.2" })
1084
+ ] }),
1085
+ chevron: /* @__PURE__ */ jsx3("path", { d: "M6 9l6 6 6-6" })
1086
+ };
1087
+ function BoardIcon({ name, size = 20, className }) {
1088
+ return /* @__PURE__ */ jsx3("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", className, "aria-hidden": "true", children: GLYPHS[name] });
1089
+ }
1090
+ function GripIcon({ className }) {
1091
+ return /* @__PURE__ */ jsx3("svg", { width: "10", height: "18", viewBox: "0 0 10 18", fill: "currentColor", className, "aria-hidden": "true", children: [3, 9, 15].flatMap((y) => [/* @__PURE__ */ jsx3("circle", { cx: "2.5", cy: y, r: "1.5" }, `l${y}`), /* @__PURE__ */ jsx3("circle", { cx: "7.5", cy: y, r: "1.5" }, `r${y}`)]) });
1092
+ }
1093
+ function CursorArrow({ color }) {
1094
+ const path = "M3.2 2.6V20.2L8 15.7L11.3 22.6L14.6 21L11.4 14.2L17.9 13.6Z";
1095
+ return /* @__PURE__ */ jsxs3("svg", { className: "chalk-board-cursor-arrow", width: "22", height: "26", viewBox: "0 0 22 26", fill: "none", "aria-hidden": "true", children: [
1096
+ /* @__PURE__ */ jsx3("path", { d: path, fill: "#fff", stroke: "#fff", strokeWidth: "5", strokeLinejoin: "round" }),
1097
+ /* @__PURE__ */ jsx3("path", { d: path, fill: color, stroke: color, strokeWidth: "2.2", strokeLinejoin: "round" })
1098
+ ] });
1099
+ }
1100
+
1101
+ // src/react/board/BoardCursors.tsx
1102
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1103
+ var REST_RATE = 16;
1104
+ var STALE_MS = 1e4;
1105
+ var NAME_HOLD_MS = 700;
1106
+ var MOVING_PX_PER_SECOND = 25;
1107
+ var HOVER_PX = 26;
1108
+ function rememberCursor(cursors, cursor) {
1109
+ const now = Date.now();
1110
+ for (const [id, previous] of cursors) if (now - Date.parse(previous.occurredAt) > STALE_MS) cursors.delete(id);
1111
+ cursors.delete(cursor.participantId);
1112
+ if (now - Date.parse(cursor.occurredAt) <= STALE_MS) cursors.set(cursor.participantId, cursor);
1113
+ if (cursors.size > 256) {
1114
+ const oldest = cursors.keys().next().value;
1115
+ if (oldest !== void 0) cursors.delete(oldest);
1116
+ }
1117
+ }
1118
+ function BoardCursors({
1119
+ cursorsRef,
1120
+ cameraRef,
1121
+ colorFor,
1122
+ hidden,
1123
+ renderAvatar
1124
+ }) {
1125
+ const layer = useRef2(null);
1126
+ const [people, setPeople] = useState5([]);
1127
+ const tracked = useRef2(/* @__PURE__ */ new Map());
1128
+ const pointer = useRef2(null);
1129
+ useEffect4(() => {
1130
+ let frame = 0;
1131
+ let last = performance.now();
1132
+ const step = (now) => {
1133
+ const seconds = Math.min(0.1, (now - last) / 1e3);
1134
+ last = now;
1135
+ const camera = cameraRef.current;
1136
+ const roster = cursorsRef.current;
1137
+ let rosterChanged = false;
1138
+ for (const [id, cursor] of roster) {
1139
+ if (Date.now() - Date.parse(cursor.occurredAt) > STALE_MS) {
1140
+ roster.delete(id);
1141
+ continue;
1142
+ }
1143
+ let state = tracked.current.get(id);
1144
+ if (!state) {
1145
+ state = { name: cursor.displayName, x: cursor.x, y: cursor.y, vx: 0, vy: 0, tx: cursor.x, ty: cursor.y, movingUntil: 0, lastX: cursor.x, lastY: cursor.y };
1146
+ tracked.current.set(id, state);
1147
+ rosterChanged = true;
1148
+ }
1149
+ if (state.name !== cursor.displayName) {
1150
+ state.name = cursor.displayName;
1151
+ rosterChanged = true;
1152
+ }
1153
+ if (cursor.x !== state.tx || cursor.y !== state.ty) {
1154
+ state.tx = cursor.x;
1155
+ state.ty = cursor.y;
1156
+ }
1157
+ }
1158
+ for (const [id, state] of tracked.current) {
1159
+ if (!roster.has(id)) {
1160
+ tracked.current.delete(id);
1161
+ roster.delete(id);
1162
+ rosterChanged = true;
1163
+ continue;
1164
+ }
1165
+ const [dx, vx] = springStep(state.x - state.tx, state.vx, REST_RATE, seconds);
1166
+ const [dy, vy] = springStep(state.y - state.ty, state.vy, REST_RATE, seconds);
1167
+ state.x = state.tx + dx;
1168
+ state.y = state.ty + dy;
1169
+ state.vx = vx;
1170
+ state.vy = vy;
1171
+ const screenX = (state.x + camera.scrollX) * camera.zoom;
1172
+ const screenY = (state.y + camera.scrollY) * camera.zoom;
1173
+ const speed = Math.hypot(screenX - state.lastX, screenY - state.lastY) / Math.max(seconds, 1e-3);
1174
+ state.lastX = screenX;
1175
+ state.lastY = screenY;
1176
+ if (speed > MOVING_PX_PER_SECOND) state.movingUntil = now + NAME_HOLD_MS;
1177
+ const element = layer.current?.querySelector(`[data-cursor="${CSS.escape(id)}"]`);
1178
+ if (!element) continue;
1179
+ element.style.transform = `translate3d(${screenX}px, ${screenY}px, 0)`;
1180
+ const near = pointer.current !== null && Math.hypot(pointer.current.x - screenX, pointer.current.y - screenY) < HOVER_PX;
1181
+ element.dataset.named = String(near || now < state.movingUntil);
1182
+ }
1183
+ if (rosterChanged) setPeople([...roster.values()].filter((cursor) => tracked.current.has(cursor.participantId)).map((cursor) => ({ id: cursor.participantId, name: cursor.displayName })));
1184
+ frame = requestAnimationFrame(step);
1185
+ };
1186
+ frame = requestAnimationFrame(step);
1187
+ return () => cancelAnimationFrame(frame);
1188
+ }, [cameraRef, cursorsRef]);
1189
+ useEffect4(() => {
1190
+ const board = layer.current?.parentElement;
1191
+ if (!board) return;
1192
+ const move = (event) => {
1193
+ const rect = board.getBoundingClientRect();
1194
+ pointer.current = { x: event.clientX - rect.left, y: event.clientY - rect.top };
1195
+ };
1196
+ const leave = () => pointer.current = null;
1197
+ board.addEventListener("pointermove", move);
1198
+ board.addEventListener("pointerleave", leave);
1199
+ return () => {
1200
+ board.removeEventListener("pointermove", move);
1201
+ board.removeEventListener("pointerleave", leave);
1202
+ };
1203
+ }, []);
1204
+ return /* @__PURE__ */ jsx4("div", { ref: layer, className: "chalk-board-cursors", "aria-hidden": "true", "data-hidden": hidden || void 0, children: people.map((person) => /* @__PURE__ */ jsxs4("div", { className: "chalk-board-cursor", "data-cursor": person.id, children: [
1205
+ /* @__PURE__ */ jsx4(CursorArrow, { color: colorFor(person.id) }),
1206
+ /* @__PURE__ */ jsxs4("span", { className: "chalk-board-cursor-tag", children: [
1207
+ /* @__PURE__ */ jsx4("span", { className: "chalk-board-cursor-name", style: { background: colorFor(person.id) }, children: person.name.split(" ")[0] }),
1208
+ renderAvatar?.(person.name)
1209
+ ] })
1210
+ ] }, person.id)) });
1211
+ }
1212
+
1213
+ // src/react/board/BoardIsland.tsx
1214
+ import { useLayoutEffect, useRef as useRef5, useState as useState7 } from "react";
1215
+
1216
+ // src/react/board/controls/ChalkMenu.tsx
1217
+ import { Menu } from "@base-ui/react/menu";
1218
+ import { useRef as useRef3 } from "react";
1219
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1220
+ function ChalkMenu({ trigger, children, side = "bottom", className, onOpenChange }) {
1221
+ const container = useRef3(null);
1222
+ return /* @__PURE__ */ jsx5(
1223
+ "span",
1224
+ {
1225
+ ref: (element) => {
1226
+ container.current = element?.closest("[data-chalk]") ?? element;
1227
+ },
1228
+ style: { display: "contents" },
1229
+ children: /* @__PURE__ */ jsxs5(Menu.Root, { onOpenChange, children: [
1230
+ /* @__PURE__ */ jsx5(Menu.Trigger, { render: trigger }),
1231
+ /* @__PURE__ */ jsx5(Menu.Portal, { container, children: /* @__PURE__ */ jsx5(Menu.Positioner, { side, align: side === "top" ? "center" : "end", sideOffset: side === "top" ? 10 : 6, collisionPadding: 8, className: "chalk-menu-positioner", children: /* @__PURE__ */ jsx5(Menu.Popup, { "data-part": "menu", className: ["chalk-menu", className].filter(Boolean).join(" "), style: { maxHeight: "var(--available-height)", maxWidth: "var(--available-width)", overflowY: "auto", overscrollBehavior: "contain" }, children }) }) })
1232
+ ] })
1233
+ }
1234
+ );
1235
+ }
1236
+ function ChalkMenuItem({ children, checked, disabled, onClick, closeOnClick }) {
1237
+ const content = /* @__PURE__ */ jsxs5(Fragment2, { children: [
1238
+ checked !== void 0 ? /* @__PURE__ */ jsx5("span", { "data-part": "menu-check", className: "chalk-menu-check", "aria-hidden": "true", children: checked ? /* @__PURE__ */ jsx5("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx5("path", { d: "M20 6L9 17l-5-5" }) }) : null }) : null,
1239
+ children
1240
+ ] });
1241
+ if (checked !== void 0)
1242
+ return /* @__PURE__ */ jsx5(Menu.Item, { "data-part": "menu-item", className: "chalk-menu-item", role: "menuitemradio", "aria-checked": checked, disabled, onClick, closeOnClick, children: content });
1243
+ return /* @__PURE__ */ jsx5(Menu.Item, { "data-part": "menu-item", className: "chalk-menu-item", disabled, onClick, closeOnClick, children: content });
1244
+ }
1245
+ function ChalkMenuLabel({ children }) {
1246
+ return /* @__PURE__ */ jsx5("div", { "data-part": "menu-label", className: "chalk-menu-label", children });
1247
+ }
1248
+
1249
+ // src/react/board/controls/ChalkPopover.tsx
1250
+ import { Popover } from "@base-ui/react/popover";
1251
+ import { useState as useState6 } from "react";
1252
+
1253
+ // src/react/board/controls/ChalkTooltip.tsx
1254
+ import { Tooltip } from "@base-ui/react/tooltip";
1255
+ import { useRef as useRef4 } from "react";
1256
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1257
+ function ChalkTooltip({ trigger, children }) {
1258
+ const container = useRef4(null);
1259
+ const fine = useMediaQuery("(pointer:fine)");
1260
+ if (!fine) return trigger;
1261
+ return /* @__PURE__ */ jsx6(
1262
+ "span",
1263
+ {
1264
+ ref: (element) => {
1265
+ container.current = element?.closest("[data-chalk]") ?? element;
1266
+ },
1267
+ style: { display: "contents" },
1268
+ children: /* @__PURE__ */ jsx6(Tooltip.Provider, { delay: 400, children: /* @__PURE__ */ jsxs6(Tooltip.Root, { children: [
1269
+ /* @__PURE__ */ jsx6(Tooltip.Trigger, { render: trigger }),
1270
+ /* @__PURE__ */ jsx6(Tooltip.Portal, { container, children: /* @__PURE__ */ jsx6(Tooltip.Positioner, { side: "top", sideOffset: 8, collisionPadding: 8, style: { zIndex: 95 }, children: /* @__PURE__ */ jsx6(Tooltip.Popup, { "data-part": "tooltip", className: "chalk-tooltip", children }) }) })
1271
+ ] }) })
1272
+ }
1273
+ );
1274
+ }
1275
+
1276
+ // src/react/board/controls/ChalkPopover.tsx
1277
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1278
+ function ChalkPopover({
1279
+ trigger,
1280
+ children,
1281
+ label,
1282
+ className,
1283
+ alignOffset = 0,
1284
+ tooltip,
1285
+ open,
1286
+ onOpenChange
1287
+ }) {
1288
+ const [holder, setHolder] = useState6(null);
1289
+ const root = holder?.closest("[data-chalk]") ?? null;
1290
+ return /* @__PURE__ */ jsx7("span", { ref: setHolder, style: { display: "contents" }, children: /* @__PURE__ */ jsxs7(Popover.Root, { open, onOpenChange, children: [
1291
+ tooltip ? /* @__PURE__ */ jsx7(ChalkTooltip, { trigger: /* @__PURE__ */ jsx7(Popover.Trigger, { render: trigger }), children: tooltip }) : /* @__PURE__ */ jsx7(Popover.Trigger, { render: trigger }),
1292
+ /* @__PURE__ */ jsx7(Popover.Portal, { container: root ?? holder, children: /* @__PURE__ */ jsx7(Popover.Positioner, { side: "bottom", align: "start", alignOffset, sideOffset: 8, collisionPadding: 8, collisionBoundary: root ?? void 0, className: "chalk-popover-positioner", children: /* @__PURE__ */ jsx7(Popover.Popup, { "aria-label": label, "data-part": "popover", className: ["chalk-menu chalk-popover", className].filter(Boolean).join(" "), style: { maxHeight: "var(--available-height)", maxWidth: "var(--available-width)", overflowY: "auto", overscrollBehavior: "contain" }, children }) }) })
1293
+ ] }) });
1294
+ }
1295
+
1296
+ // src/react/board/BoardIsland.tsx
1297
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1298
+ var TOOLS = [
1299
+ [
1300
+ { tool: "selection", label: "Select", key: "V", glyph: "select" },
1301
+ { tool: "hand", label: "Hand", key: "H", glyph: "hand" }
1302
+ ],
1303
+ [
1304
+ { tool: "pen", label: "Pen", key: "P", glyph: "pen" },
1305
+ { tool: "highlighter", label: "Highlighter", key: "U", glyph: "highlighter" }
1306
+ ],
1307
+ [
1308
+ { tool: "text", label: "Text", key: "T", glyph: "text" },
1309
+ { tool: "note", label: "Sticky note", key: "N", glyph: "note" }
1310
+ ],
1311
+ [
1312
+ { tool: "eraser", label: "Eraser", key: "E", glyph: "eraser" },
1313
+ { tool: "laser", label: "Laser pointer", key: "K", glyph: "laser" }
1314
+ ]
1315
+ ];
1316
+ var LONG_PRESS_MS = 500;
1317
+ var DRAG_START_PX = 4;
1318
+ function Tip({ label, keyName, children }) {
1319
+ return /* @__PURE__ */ jsxs8(ChalkTooltip, { trigger: children, children: [
1320
+ label,
1321
+ keyName ? /* @__PURE__ */ jsx8("kbd", { className: "chalk-board-kbd", children: keyName }) : null
1322
+ ] });
1323
+ }
1324
+ function ToolButton({ entry, active, onTool }) {
1325
+ return /* @__PURE__ */ jsx8(Tip, { label: entry.label, keyName: entry.key, children: /* @__PURE__ */ jsx8("button", { type: "button", className: "chalk-board-tool", "aria-label": entry.label, "aria-keyshortcuts": entry.key, "aria-pressed": active, onClick: () => onTool(entry.tool), children: /* @__PURE__ */ jsx8(BoardIcon, { name: entry.glyph }) }) });
1326
+ }
1327
+ function ShapesButton({ tool, shape, onTool, side }) {
1328
+ const active = SHAPES.some((entry) => entry.id === tool);
1329
+ return /* @__PURE__ */ jsx8(
1330
+ ChalkMenu,
1331
+ {
1332
+ side,
1333
+ trigger: /* @__PURE__ */ jsxs8("button", { type: "button", className: "chalk-board-tool", "aria-label": "Shapes", "aria-keyshortcuts": "R", "aria-pressed": active, children: [
1334
+ /* @__PURE__ */ jsx8(BoardIcon, { name: shape }),
1335
+ /* @__PURE__ */ jsx8(BoardIcon, { name: "chevron", size: 9, className: "chalk-board-tool-caret" })
1336
+ ] }),
1337
+ children: SHAPES.map((entry) => /* @__PURE__ */ jsxs8(ChalkMenuItem, { checked: tool === entry.id, onClick: () => onTool(entry.id), children: [
1338
+ /* @__PURE__ */ jsx8(BoardIcon, { name: entry.id, size: 18 }),
1339
+ entry.label,
1340
+ /* @__PURE__ */ jsx8("kbd", { className: "chalk-board-menu-kbd", children: entry.key })
1341
+ ] }, entry.id))
1342
+ }
1343
+ );
1344
+ }
1345
+ function ColorPanel({ color, width, surface, theme, onColor, onWidth }) {
1346
+ return /* @__PURE__ */ jsxs8("div", { className: "chalk-board-colors", children: [
1347
+ /* @__PURE__ */ jsx8("div", { role: "radiogroup", "aria-label": "Colour", className: "chalk-board-swatches", children: PEN_COLORS.map((entry) => /* @__PURE__ */ jsx8("button", { type: "button", role: "radio", "aria-checked": color === entry.id, "aria-label": entry.label, className: "chalk-board-swatch", onClick: () => onColor(entry.id), children: /* @__PURE__ */ jsx8("i", { style: { background: penHex(entry.id, surface, theme) } }) }, entry.id)) }),
1348
+ /* @__PURE__ */ jsx8("div", { role: "radiogroup", "aria-label": "Stroke width", className: "chalk-board-swatches", children: PEN_WIDTHS.map((entry) => /* @__PURE__ */ jsx8("button", { type: "button", role: "radio", "aria-checked": width === entry.id, "aria-label": entry.label, className: "chalk-board-width", onClick: () => onWidth(entry.id), children: /* @__PURE__ */ jsx8("i", { style: { height: entry.units } }) }, entry.id)) })
1349
+ ] });
1350
+ }
1351
+ function ColorButton(props) {
1352
+ const swatch = penHex(props.color, props.surface, props.theme);
1353
+ return /* @__PURE__ */ jsx8(
1354
+ ChalkPopover,
1355
+ {
1356
+ label: "Colour and width",
1357
+ tooltip: "Colour and width",
1358
+ open: props.open,
1359
+ onOpenChange: props.onOpenChange,
1360
+ className: "chalk-board-color-popover",
1361
+ trigger: props.trigger ?? /* @__PURE__ */ jsx8("button", { type: "button", className: "chalk-board-tool", "aria-label": `Colour: ${PEN_COLORS.find((entry) => entry.id === props.color).label}`, children: /* @__PURE__ */ jsx8("span", { className: "chalk-board-tool-swatch", style: { background: swatch } }) }),
1362
+ children: /* @__PURE__ */ jsx8(ColorPanel, { ...props })
1363
+ }
1364
+ );
1365
+ }
1366
+ function roveFocus(event, vertical) {
1367
+ const back = vertical ? "ArrowUp" : "ArrowLeft";
1368
+ const forward = vertical ? "ArrowDown" : "ArrowRight";
1369
+ if (event.key !== back && event.key !== forward && event.key !== "Home" && event.key !== "End") return;
1370
+ const buttons = [...event.currentTarget.querySelectorAll("button:not(:disabled)")];
1371
+ const index = buttons.findIndex((button) => button === document.activeElement);
1372
+ if (index < 0) return;
1373
+ event.preventDefault();
1374
+ event.stopPropagation();
1375
+ const next = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : (index + (event.key === forward ? 1 : -1) + buttons.length) % buttons.length;
1376
+ buttons[next]?.focus();
1377
+ }
1378
+ function Separator() {
1379
+ return /* @__PURE__ */ jsx8("span", { className: "chalk-board-separator", "aria-hidden": "true" });
1380
+ }
1381
+ function ToolRow({ tool, shape, onTool, place, canUndo, canRedo, onUndo, onRedo, primaryToolsFirst, ...colors }) {
1382
+ const primaryTools = ["pen", "text", "note", "eraser"];
1383
+ const entries = TOOLS.flat();
1384
+ const shapes = /* @__PURE__ */ jsx8(ShapesButton, { tool, shape, onTool, side: place === "top" ? "bottom" : "top" });
1385
+ const button = (entry) => /* @__PURE__ */ jsx8(ToolButton, { entry, active: tool === entry.tool, onTool }, entry.tool);
1386
+ const groups = TOOLS.map((group, index) => /* @__PURE__ */ jsxs8("span", { className: "chalk-board-group", children: [
1387
+ group.map((entry) => /* @__PURE__ */ jsx8(ToolButton, { entry, active: tool === entry.tool, onTool }, entry.tool)),
1388
+ index === 1 ? shapes : null
1389
+ ] }, index));
1390
+ return /* @__PURE__ */ jsxs8(Fragment3, { children: [
1391
+ primaryToolsFirst ? entries.filter((entry) => primaryTools.includes(entry.tool)).map((entry) => /* @__PURE__ */ jsxs8("span", { className: "chalk-board-group", children: [
1392
+ button(entry),
1393
+ entry.tool === "pen" ? shapes : null
1394
+ ] }, entry.tool)) : groups.flatMap((group, index) => [index > 0 ? /* @__PURE__ */ jsx8(Separator, {}, `s${index}`) : null, group]),
1395
+ /* @__PURE__ */ jsx8(Separator, {}),
1396
+ primaryToolsFirst ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
1397
+ /* @__PURE__ */ jsx8(ColorButton, { ...colors }),
1398
+ /* @__PURE__ */ jsx8(Separator, {})
1399
+ ] }) : null,
1400
+ /* @__PURE__ */ jsx8(Tip, { label: "Undo", keyName: "\u2318Z", children: /* @__PURE__ */ jsx8("button", { type: "button", className: "chalk-board-tool", "aria-label": "Undo", disabled: !canUndo, onClick: onUndo, children: /* @__PURE__ */ jsx8(BoardIcon, { name: "undo" }) }) }),
1401
+ /* @__PURE__ */ jsx8(Tip, { label: "Redo", keyName: "\u21E7\u2318Z", children: /* @__PURE__ */ jsx8("button", { type: "button", className: "chalk-board-tool", "aria-label": "Redo", disabled: !canRedo, onClick: onRedo, children: /* @__PURE__ */ jsx8(BoardIcon, { name: "redo" }) }) }),
1402
+ primaryToolsFirst ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
1403
+ /* @__PURE__ */ jsx8(Separator, {}),
1404
+ entries.filter((entry) => !primaryTools.includes(entry.tool)).map(button)
1405
+ ] }) : /* @__PURE__ */ jsxs8(Fragment3, { children: [
1406
+ /* @__PURE__ */ jsx8(Separator, {}),
1407
+ /* @__PURE__ */ jsx8(ColorButton, { ...colors })
1408
+ ] })
1409
+ ] });
1410
+ }
1411
+ function nearestPlace(board, x, y) {
1412
+ const distances = { left: x - board.left, bottom: board.bottom - y, top: y - board.top };
1413
+ return ISLAND_PLACES.map((entry) => entry.id).reduce((best, place) => distances[place] < distances[best] ? place : best);
1414
+ }
1415
+ function BoardIsland(props) {
1416
+ const { place, onPlace, compact } = props;
1417
+ const island = useRef5(null);
1418
+ const tabStop = useRef5(null);
1419
+ const reduced = usePrefersReducedMotion();
1420
+ const [drag, setDrag] = useState7(null);
1421
+ const dropRect = useRef5(null);
1422
+ const [expanded, setExpanded] = useState7(false);
1423
+ const [colorsOpen, setColorsOpen] = useState7(false);
1424
+ const press = useRef5({ long: false });
1425
+ useLayoutEffect(() => {
1426
+ const buttons = [...island.current?.querySelectorAll("button") ?? []];
1427
+ const stop = buttons.find((button) => button === tabStop.current && !button.disabled) ?? buttons.find((button) => !button.disabled);
1428
+ tabStop.current = stop ?? null;
1429
+ for (const button of buttons) button.tabIndex = button === stop ? 0 : -1;
1430
+ });
1431
+ useLayoutEffect(() => {
1432
+ const element = island.current;
1433
+ const from = dropRect.current;
1434
+ dropRect.current = null;
1435
+ if (!element || !from || reduced) return;
1436
+ const to = element.getBoundingClientRect();
1437
+ element.animate(
1438
+ [
1439
+ { transform: `translate(${from.left - to.left}px, ${from.top - to.top}px)`, opacity: 0.85 },
1440
+ { transform: "none", opacity: 1 }
1441
+ ],
1442
+ { duration: 300, easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
1443
+ );
1444
+ }, [place, reduced]);
1445
+ const startDrag = (event) => {
1446
+ const board = island.current?.parentElement;
1447
+ if (!board || event.button !== 0) return;
1448
+ const start = { x: event.clientX, y: event.clientY };
1449
+ let moved = false;
1450
+ const grip = event.currentTarget;
1451
+ grip.setPointerCapture(event.pointerId);
1452
+ const move = (next) => {
1453
+ const dx = next.clientX - start.x;
1454
+ const dy = next.clientY - start.y;
1455
+ if (!moved && Math.hypot(dx, dy) < DRAG_START_PX) return;
1456
+ moved = true;
1457
+ setDrag({ x: dx, y: dy, near: nearestPlace(board.getBoundingClientRect(), next.clientX, next.clientY) });
1458
+ };
1459
+ const end = (next) => {
1460
+ grip.removeEventListener("pointermove", move);
1461
+ grip.removeEventListener("pointerup", end);
1462
+ grip.removeEventListener("pointercancel", end);
1463
+ if (moved && next.type === "pointerup") {
1464
+ dropRect.current = island.current?.getBoundingClientRect() ?? null;
1465
+ onPlace(nearestPlace(board.getBoundingClientRect(), next.clientX, next.clientY), true);
1466
+ }
1467
+ setDrag(null);
1468
+ };
1469
+ grip.addEventListener("pointermove", move);
1470
+ grip.addEventListener("pointerup", end);
1471
+ grip.addEventListener("pointercancel", end);
1472
+ };
1473
+ const gripKeys = (event) => {
1474
+ if (!event.altKey) return;
1475
+ const target = { ArrowLeft: "left", ArrowDown: "bottom", ArrowUp: "top" };
1476
+ const next = target[event.key];
1477
+ if (!next) return;
1478
+ event.preventDefault();
1479
+ event.stopPropagation();
1480
+ if (next !== place) {
1481
+ dropRect.current = island.current?.getBoundingClientRect() ?? null;
1482
+ onPlace(next, true);
1483
+ }
1484
+ };
1485
+ if (compact && !expanded) {
1486
+ const cancelPress = () => clearTimeout(press.current.timer);
1487
+ return /* @__PURE__ */ jsx8("div", { ref: island, className: "chalk-board-island", "data-place": "compact", children: /* @__PURE__ */ jsx8(
1488
+ ColorButton,
1489
+ {
1490
+ ...props,
1491
+ open: colorsOpen,
1492
+ onOpenChange: (open) => {
1493
+ if (!open) setColorsOpen(false);
1494
+ },
1495
+ trigger: /* @__PURE__ */ jsxs8(
1496
+ "button",
1497
+ {
1498
+ type: "button",
1499
+ className: "chalk-board-tool",
1500
+ "aria-label": "Drawing tools",
1501
+ "aria-expanded": false,
1502
+ onPointerDown: () => {
1503
+ press.current.long = false;
1504
+ press.current.timer = setTimeout(() => {
1505
+ press.current.long = true;
1506
+ setColorsOpen(true);
1507
+ }, LONG_PRESS_MS);
1508
+ },
1509
+ onPointerUp: cancelPress,
1510
+ onPointerLeave: cancelPress,
1511
+ onContextMenu: (event) => event.preventDefault(),
1512
+ onClick: () => {
1513
+ if (!press.current.long) setExpanded(true);
1514
+ },
1515
+ children: [
1516
+ /* @__PURE__ */ jsx8(BoardIcon, { name: "pen" }),
1517
+ /* @__PURE__ */ jsx8("span", { className: "chalk-board-tool-dot", style: { background: penHex(props.color, props.surface, props.theme) } })
1518
+ ]
1519
+ }
1520
+ )
1521
+ }
1522
+ ) });
1523
+ }
1524
+ const vertical = place === "left" && !compact;
1525
+ return /* @__PURE__ */ jsxs8(Fragment3, { children: [
1526
+ drag ? ISLAND_PLACES.map((entry) => /* @__PURE__ */ jsx8("div", { className: "chalk-board-dropzone", "data-zone": entry.id, "data-near": drag.near === entry.id || void 0, "aria-hidden": "true" }, entry.id)) : null,
1527
+ /* @__PURE__ */ jsxs8(
1528
+ "div",
1529
+ {
1530
+ ref: island,
1531
+ role: "toolbar",
1532
+ "aria-label": "Drawing tools",
1533
+ "aria-orientation": vertical ? "vertical" : "horizontal",
1534
+ className: "chalk-board-island",
1535
+ "data-place": compact ? "expanded" : place,
1536
+ "data-dragging": drag ? true : void 0,
1537
+ style: drag ? { transform: `translate(${drag.x}px, ${drag.y}px)` } : void 0,
1538
+ onKeyDownCapture: (event) => {
1539
+ if (!event.altKey) roveFocus(event, vertical);
1540
+ },
1541
+ onFocusCapture: (event) => {
1542
+ if (!(event.target instanceof HTMLButtonElement) || !event.currentTarget.contains(event.target)) return;
1543
+ tabStop.current = event.target;
1544
+ for (const button of event.currentTarget.querySelectorAll("button")) button.tabIndex = button === event.target ? 0 : -1;
1545
+ },
1546
+ children: [
1547
+ compact ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
1548
+ /* @__PURE__ */ jsx8("button", { type: "button", className: "chalk-board-tool", "aria-label": "Close drawing tools", onClick: () => setExpanded(false), children: /* @__PURE__ */ jsx8(BoardIcon, { name: "close" }) }),
1549
+ /* @__PURE__ */ jsx8(Separator, {})
1550
+ ] }) : !props.primaryToolsFirst ? /* @__PURE__ */ jsx8(Tip, { label: "Move the tools", children: /* @__PURE__ */ jsx8("button", { type: "button", className: "chalk-board-grip", "aria-label": "Move the tools. Use Alt and the arrow keys to dock them left, bottom or top.", onPointerDown: startDrag, onKeyDown: gripKeys, children: /* @__PURE__ */ jsx8(GripIcon, {}) }) }) : null,
1551
+ /* @__PURE__ */ jsx8(ToolRow, { ...props, place: compact ? "bottom" : place })
1552
+ ]
1553
+ }
1554
+ )
1555
+ ] });
1556
+ }
1557
+
1558
+ // src/react/board/BoardMenu.tsx
1559
+ import { Menu as Menu2 } from "@base-ui/react/menu";
1560
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1561
+ var MOBILE_ISLAND_PLACES = [...ISLAND_PLACES].reverse();
1562
+ function SurfacePreview({ surface }) {
1563
+ return /* @__PURE__ */ jsx9("span", { className: "chalk-board-tile-preview", "data-surface": surface, "aria-hidden": "true", children: surface === "dots" ? null : /* @__PURE__ */ jsx9("svg", { viewBox: "0 0 60 38", width: "100%", height: "100%", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ jsx9("path", { d: "M14 22c5-8 9-8 12 0s8 6 12-2 6-6 8 0" }) }) });
1564
+ }
1565
+ function IslandPreview({ place }) {
1566
+ return /* @__PURE__ */ jsx9("span", { className: "chalk-board-tile-preview", "data-island": place, "aria-hidden": "true", children: /* @__PURE__ */ jsx9("i", {}) });
1567
+ }
1568
+ function Tiles({
1569
+ label,
1570
+ value,
1571
+ options,
1572
+ onChange,
1573
+ preview,
1574
+ list
1575
+ }) {
1576
+ return /* @__PURE__ */ jsxs9(Fragment4, { children: [
1577
+ /* @__PURE__ */ jsx9("div", { className: "chalk-board-menu-sublabel", children: label }),
1578
+ /* @__PURE__ */ jsx9(Menu2.RadioGroup, { value, onValueChange: (next) => onChange(next), className: list ? "chalk-board-options" : "chalk-board-tiles", "aria-label": label, children: options.map((option) => /* @__PURE__ */ jsxs9(Menu2.RadioItem, { value: option.id, closeOnClick: false, className: "chalk-board-tile", children: [
1579
+ preview(option.id),
1580
+ /* @__PURE__ */ jsx9("span", { children: option.label }),
1581
+ list ? /* @__PURE__ */ jsx9("span", { className: "chalk-board-option-check", "aria-hidden": "true", children: value === option.id ? "\u2713" : null }) : null
1582
+ ] }, option.id)) })
1583
+ ] });
1584
+ }
1585
+ function Check({ on }) {
1586
+ return /* @__PURE__ */ jsx9("span", { "data-part": "menu-check", className: "chalk-menu-check", "aria-hidden": "true", children: on ? /* @__PURE__ */ jsx9("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx9("path", { d: "M20 6L9 17l-5-5" }) }) : null });
1587
+ }
1588
+ function PickedPeople({ people, onPick }) {
1589
+ if (people.length === 0) return /* @__PURE__ */ jsx9("p", { className: "chalk-board-picked-note", children: "No one else is here yet. Hosts can always draw." });
1590
+ return /* @__PURE__ */ jsxs9(Fragment4, { children: [
1591
+ /* @__PURE__ */ jsx9("div", { className: "chalk-board-picked", role: "group", "aria-label": "Picked people", children: people.map((person) => /* @__PURE__ */ jsxs9(Menu2.CheckboxItem, { className: "chalk-menu-item", checked: person.picked, onCheckedChange: (picked) => onPick(person.id, picked), closeOnClick: false, children: [
1592
+ /* @__PURE__ */ jsx9(Check, { on: person.picked }),
1593
+ /* @__PURE__ */ jsx9("span", { className: "chalk-board-picked-name", children: person.name })
1594
+ ] }, person.id)) }),
1595
+ people.some((person) => person.picked) ? null : /* @__PURE__ */ jsx9("p", { className: "chalk-board-picked-note", children: "Only hosts can draw until you pick someone." })
1596
+ ] });
1597
+ }
1598
+ function WhoCanDrawSection({ policy, people, onPolicy, onPick }) {
1599
+ return /* @__PURE__ */ jsxs9(Fragment4, { children: [
1600
+ /* @__PURE__ */ jsx9(Menu2.Separator, { className: "chalk-board-menu-rule" }),
1601
+ /* @__PURE__ */ jsx9(ChalkMenuLabel, { children: "Who can draw" }),
1602
+ DRAW_POLICIES.map((option) => /* @__PURE__ */ jsx9(ChalkMenuItem, { checked: policy === option.id, closeOnClick: false, onClick: () => onPolicy(option.id), children: option.label }, option.id)),
1603
+ policy === "picked" ? /* @__PURE__ */ jsx9(PickedPeople, { people, onPick }) : null
1604
+ ] });
1605
+ }
1606
+ function BoardMenu({ surface, onSurface, place, onPlace, onDownload, onCopy, whoCanDraw, onBring, onClear, onClose, downloadLabel = "Download as PNG", presentation = "web" }) {
1607
+ const list = presentation === "mobile";
1608
+ const appearance = /* @__PURE__ */ jsxs9(Fragment4, { children: [
1609
+ !list ? /* @__PURE__ */ jsx9(ChalkMenuLabel, { children: "Board appearance" }) : null,
1610
+ /* @__PURE__ */ jsx9(Tiles, { list, label: list ? "Surface, just for you" : "Surface", value: surface, options: SURFACES, onChange: onSurface, preview: (id) => /* @__PURE__ */ jsx9(SurfacePreview, { surface: id }) }),
1611
+ place && list ? /* @__PURE__ */ jsx9(Menu2.Separator, { className: "chalk-board-menu-rule" }) : null,
1612
+ place ? /* @__PURE__ */ jsx9(Tiles, { list, label: "Tool island", value: place, options: list ? MOBILE_ISLAND_PLACES : ISLAND_PLACES, onChange: onPlace, preview: (id) => list ? null : /* @__PURE__ */ jsx9(IslandPreview, { place: id }) }) : null
1613
+ ] });
1614
+ return /* @__PURE__ */ jsxs9(
1615
+ ChalkMenu,
1616
+ {
1617
+ className: list ? "chalk-board-menu chalk-board-menu-mobile" : "chalk-board-menu",
1618
+ trigger: /* @__PURE__ */ jsx9("button", { type: "button", className: "chalk-board-menu-button", "aria-label": "Board menu", children: /* @__PURE__ */ jsx9(BoardIcon, { name: "more" }) }),
1619
+ children: [
1620
+ list ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
1621
+ appearance,
1622
+ /* @__PURE__ */ jsx9(Menu2.Separator, { className: "chalk-board-menu-rule" })
1623
+ ] }) : null,
1624
+ /* @__PURE__ */ jsxs9(ChalkMenuItem, { onClick: onDownload, children: [
1625
+ /* @__PURE__ */ jsx9(BoardIcon, { name: "download", size: 18 }),
1626
+ downloadLabel
1627
+ ] }),
1628
+ /* @__PURE__ */ jsxs9(ChalkMenuItem, { onClick: onCopy, children: [
1629
+ /* @__PURE__ */ jsx9(BoardIcon, { name: "copy", size: 18 }),
1630
+ "Copy as image"
1631
+ ] }),
1632
+ !list || onBring || onClear ? /* @__PURE__ */ jsx9(Menu2.Separator, { className: "chalk-board-menu-rule" }) : null,
1633
+ !list ? appearance : null,
1634
+ whoCanDraw ? /* @__PURE__ */ jsx9(WhoCanDrawSection, { ...whoCanDraw }) : null,
1635
+ !list && (onBring || onClear) ? /* @__PURE__ */ jsx9(Menu2.Separator, { className: "chalk-board-menu-rule" }) : null,
1636
+ onBring ? /* @__PURE__ */ jsxs9(ChalkMenuItem, { onClick: onBring, children: [
1637
+ /* @__PURE__ */ jsx9(BoardIcon, { name: "target", size: 18 }),
1638
+ "Bring everyone here"
1639
+ ] }) : null,
1640
+ onClear ? /* @__PURE__ */ jsx9(ChalkMenuItem, { onClick: onClear, children: /* @__PURE__ */ jsxs9("span", { className: "chalk-board-danger", children: [
1641
+ /* @__PURE__ */ jsx9(BoardIcon, { name: "trash", size: 18 }),
1642
+ "Clear board"
1643
+ ] }) }) : null,
1644
+ onClose ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
1645
+ /* @__PURE__ */ jsx9(Menu2.Separator, { className: "chalk-board-menu-rule" }),
1646
+ /* @__PURE__ */ jsxs9(ChalkMenuItem, { onClick: onClose, children: [
1647
+ /* @__PURE__ */ jsx9(BoardIcon, { name: "close", size: 18 }),
1648
+ "Close board"
1649
+ ] })
1650
+ ] }) : null
1651
+ ]
1652
+ }
1653
+ );
1654
+ }
1655
+
1656
+ // src/collab/view-limits.ts
1657
+ var WHITEBOARD_VIEW_PEOPLE_MAX = 500;
1658
+
1659
+ // src/react/board/BoardStyles.tsx
1660
+ import { jsx as jsx10 } from "react/jsx-runtime";
1661
+ function BoardStyles() {
1662
+ return /* @__PURE__ */ jsx10("style", { href: "chalk-board", precedence: "default", children: BOARD_CSS });
1663
+ }
1664
+ var BOARD_CSS = `.chalk-board {
1665
+ --board-shadow: 0 0 0 1px var(--chalk-line-strong), 0 8px 24px rgba(0,0,0,.35);
1666
+ --board-dot-gap: 24px;
1667
+ --board-dot-x: 0px;
1668
+ --board-dot-y: 0px;
1669
+ position: relative;
1670
+ height: 100%;
1671
+ overflow: clip;
1672
+ border-radius: inherit;
1673
+ background-color: var(--board-fill);
1674
+ color: var(--board-ink);
1675
+ font-family: var(--chalk-font-family, Figtree, system-ui, sans-serif);
1676
+ isolation: isolate;
1677
+ }
1678
+ .chalk-board[hidden] {display:none;}
1679
+ .chalk-board[data-theme="light"] {--board-shadow: 0 0 0 1px var(--chalk-line-strong), 0 8px 24px rgba(0,0,0,.12);}
1680
+ .chalk-board[data-surface="chalkboard"] {
1681
+ --board-fill: #1c2622;
1682
+ --board-ink: #f1f0ea;
1683
+ --board-muted: #aeb9b4;
1684
+ background-image: radial-gradient(120% 90% at 50% 35%, rgba(255,255,255,.05), transparent 70%), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .05 0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
1685
+ }
1686
+ .chalk-board[data-surface="paper"] {--board-fill: #eae6db; --board-ink: #1b1c1f; --board-muted: #5d5a52;}
1687
+ .chalk-board[data-surface="paper"][data-theme="light"] {--board-fill: #f5f2ea;}
1688
+ .chalk-board[data-surface="dots"] {
1689
+ --board-fill: var(--chalk-surface);
1690
+ --board-ink: var(--chalk-text);
1691
+ --board-muted: var(--chalk-muted-text);
1692
+ --board-dot: rgba(255,255,255,.11);
1693
+ background-image: radial-gradient(var(--board-dot) 1.3px, transparent 1.7px);
1694
+ background-size: var(--board-dot-gap) var(--board-dot-gap);
1695
+ background-position: var(--board-dot-x) var(--board-dot-y);
1696
+ }
1697
+ .chalk-board[data-surface="dots"][data-theme="light"] {--board-dot: rgba(0,0,0,.16);}
1698
+
1699
+ /* Excalidraw draws; none of its own interface shows. */
1700
+ .chalk-board-engine {position:absolute; inset:0; transition:opacity .2s ease-out;}
1701
+ .chalk-board:not([data-ready]) .chalk-board-engine:not(:has([role="alert"])) {opacity:0; pointer-events:none;}
1702
+ .chalk-board-engine > [role="alert"] {background:var(--board-fill); color:var(--board-ink);}
1703
+ .chalk-board:has(.chalk-board-engine > [role="alert"]) .chalk-board-loading {display:none;}
1704
+ .chalk-board .excalidraw {--color-selection: var(--board-selection); background: transparent;}
1705
+ .chalk-board .excalidraw :is(.layer-ui__wrapper, .context-menu, .HintViewer, .Modal, .welcome-screen-center, .library-menu, .sidebar, .App-toolbar, .App-bottom-bar, .FixedSideContainer, .popover, .scroll-back-to-content, .excalidraw-tooltip, .ToolIcon__hidden, .App-menu, .layer-ui__wrapper__footer) {display:none !important;}
1706
+ body:has(.chalk-board) :is(.excalidraw-modal-container, .excalidraw-tooltip) {display:none !important;}
1707
+ .chalk-board .excalidraw-wysiwyg {font-family: Nunito, var(--chalk-font-family, system-ui), sans-serif;}
1708
+
1709
+ .chalk-board-loading {position:absolute; inset:0; display:grid; place-content:center; justify-items:center; gap:12px; color:var(--board-muted); font-size:14px;}
1710
+ .chalk-board-spinner {width:22px; height:22px; border-radius:50%; border:2px solid var(--board-muted); border-top-color:transparent; animation:chalk-board-spin .8s linear infinite; opacity:.8;}
1711
+ @keyframes chalk-board-spin {to {transform:rotate(360deg);}}
1712
+
1713
+ .chalk-board-hint {position:absolute; inset:0; display:grid; place-content:center; justify-items:center; gap:14px; color:var(--board-muted); text-align:center; pointer-events:none; z-index:1;}
1714
+ .chalk-board-hint p {margin:0; font-size:15px; max-width:280px; line-height:1.4; text-wrap:balance;}
1715
+ .chalk-board-hint ul {list-style:none; margin:0; padding:0; display:flex; gap:18px; flex-wrap:wrap; justify-content:center; font-size:13px;}
1716
+ .chalk-board-hint li {display:flex; align-items:center; gap:6px;}
1717
+ .chalk-board-hint kbd {font:600 11px ui-monospace, SFMono-Regular, Menlo, monospace; padding:2px 6px; border-radius:5px; border:1px solid var(--board-muted); color:var(--board-muted);}
1718
+
1719
+ .chalk-board-note-layer {position:absolute; inset:0; z-index:2; cursor:crosshair; touch-action:none;}
1720
+
1721
+ .chalk-board-cursors {position:absolute; inset:0; z-index:3; pointer-events:none; overflow:hidden;}
1722
+ .chalk-board-cursors[data-hidden] {display:none;}
1723
+ .chalk-board-cursor {position:absolute; left:0; top:0; will-change:transform;}
1724
+ .chalk-board-cursor-arrow {position:absolute; left:-3px; top:-3px; width:22px; height:26px; max-width:none; display:block; filter:drop-shadow(0 2px 3px rgba(0,0,0,.35));}
1725
+ .chalk-board-cursor-tag {position:absolute; left:12px; top:18px; display:flex; align-items:center; height:28px;}
1726
+ .chalk-board-cursor-avatar {position:relative; z-index:1; box-shadow:0 0 0 2px rgba(255,255,255,.92); border-radius:50%;}
1727
+ .chalk-board-cursor-name {position:absolute; left:14px; top:0; height:28px; display:flex; align-items:center; padding:0 11px 0 18px; border-radius:14px; white-space:nowrap; color:#fff; font:600 13px/1 var(--chalk-font-family, Figtree, system-ui, sans-serif); box-shadow:0 0 0 1.5px rgba(255,255,255,.92), 0 3px 10px rgba(0,0,0,.28); opacity:0; transform:translateX(-10px); transition:opacity .3s ease-out, transform .3s ease-out;}
1728
+ .chalk-board-cursor[data-named="true"] .chalk-board-cursor-name {opacity:1; transform:none;}
1729
+
1730
+ .chalk-board :focus-visible {outline:2px solid var(--chalk-focus); outline-offset:2px;}
1731
+ .chalk-board button {font:inherit; color:inherit; background:none; border:0; padding:0; cursor:pointer;}
1732
+
1733
+ .chalk-board-island, .chalk-board-zoom, .chalk-board .chalk-board-menu-button, .chalk-board-chip, .chalk-board-view-only {background:var(--chalk-surface); color:var(--chalk-text); box-shadow:var(--board-shadow);}
1734
+ .chalk-board-chips {position:absolute; left:0; right:0; top:12px; z-index:5; display:flex; flex-direction:column; align-items:center; gap:8px; pointer-events:none;}
1735
+ .chalk-board-chips[data-low] {top:76px;}
1736
+ .chalk-board-chip {display:flex; align-items:center; gap:8px; height:36px; padding:0 6px 0 14px; border-radius:999px; font-size:13px; font-weight:600; pointer-events:auto;}
1737
+ .chalk-board-chip[data-quiet] {padding:0 14px; font-weight:500;}
1738
+ .chalk-board-chip svg {color:var(--chalk-muted-text);}
1739
+ .chalk-board-chip-button {height:28px !important; padding:0 12px !important; border-radius:999px; background:var(--chalk-hover) !important; font-weight:600; font-size:13px;}
1740
+ .chalk-board-view-only {position:absolute; left:12px; top:12px; z-index:5; height:30px; padding:0 12px; border-radius:999px; color:var(--chalk-soft-text); font-size:12px; font-weight:600; display:flex; align-items:center; gap:6px; box-shadow:0 0 0 1px var(--chalk-line-strong);}
1741
+
1742
+ .chalk-board-corner {position:absolute; right:12px; top:12px; z-index:5;}
1743
+ .chalk-board-menu-button {width:40px; height:40px; border-radius:50%; display:grid; place-items:center; transition:transform .15s ease-out;}
1744
+ .chalk-board-menu-button:hover {background:var(--chalk-raised);}
1745
+ .chalk-board-menu-button:active {transform:scale(.94);}
1746
+ .chalk-board-menu-button[aria-expanded="true"], .chalk-board-menu-button[data-popup-open] {background:var(--chalk-text); color:var(--chalk-canvas);}
1747
+ .chalk-menu.chalk-board-menu {width:304px;}
1748
+ .chalk-menu.chalk-board-menu-mobile {width:264px;}
1749
+ .chalk-board-menu-mobile .chalk-menu-item {min-height:44px;}
1750
+ .chalk-board-options {display:grid;}
1751
+ .chalk-board-options .chalk-board-tile {display:flex; align-items:center; gap:10px; min-height:44px; padding:8px 10px; border:0; font-size:14px; font-weight:500; text-align:left;}
1752
+ .chalk-board-options .chalk-board-tile-preview {width:22px; height:22px; flex:none;}
1753
+ .chalk-board-options .chalk-board-tile-preview svg {display:none;}
1754
+ .chalk-board-options .chalk-board-tile[data-checked] {border:0;}
1755
+ .chalk-board-option-check {margin-left:auto;}
1756
+ .chalk-board-menu-rule {border:0; border-top:1px solid var(--chalk-line); margin:4px 0;}
1757
+ .chalk-board-menu-sublabel {padding:6px 10px 4px; font-size:12px; font-weight:600; color:var(--chalk-muted-text);}
1758
+ .chalk-board-tiles {display:grid; grid-template-columns:repeat(3, 1fr); gap:6px; padding:2px 4px 8px;}
1759
+ .chalk-board-tile {display:grid; gap:6px; justify-items:center; align-content:start; min-height:44px; padding:6px 4px 8px; border-radius:10px; border:2px solid transparent; font-size:12px; font-weight:600; color:var(--chalk-text); text-align:center; cursor:pointer; outline:none;}
1760
+ .chalk-board-tile[data-highlighted], .chalk-board-tile:hover {background:var(--chalk-hover);}
1761
+ .chalk-board-tile[data-checked] {border-color:var(--chalk-text);}
1762
+ .chalk-board-tile-preview {display:block; position:relative; width:100%; height:38px; border-radius:6px; overflow:hidden; box-shadow:0 0 0 1px var(--chalk-line-strong);}
1763
+ .chalk-board-tile-preview[data-surface="chalkboard"] {background:#1c2622; color:#f1f0ea;}
1764
+ .chalk-board-tile-preview[data-surface="paper"] {background:#f5f2ea; color:#1b1c1f;}
1765
+ .chalk-board-tile-preview[data-surface="dots"] {background:var(--chalk-surface) radial-gradient(var(--chalk-muted-text) 1px, transparent 1.5px) 0 0/8px 8px;}
1766
+ .chalk-board-tile-preview[data-island] {background:var(--chalk-raised);}
1767
+ .chalk-board-tile-preview[data-island] i {position:absolute; background:var(--chalk-text); border-radius:3px;}
1768
+ .chalk-board-tile-preview[data-island="bottom"] i {left:30%; right:30%; bottom:5px; height:5px;}
1769
+ .chalk-board-tile-preview[data-island="top"] i {left:30%; right:30%; top:5px; height:5px;}
1770
+ .chalk-board-tile-preview[data-island="left"] i {left:5px; top:20%; bottom:20%; width:5px;}
1771
+ .chalk-board-menu-kbd {margin-left:auto; padding-left:18px; font:600 11px ui-monospace, SFMono-Regular, Menlo, monospace; color:var(--chalk-muted-text);}
1772
+ .chalk-board-danger {display:contents; color:var(--chalk-danger-text);}
1773
+ .chalk-board-picked {max-height:220px; overflow-y:auto; padding-left:16px;}
1774
+ .chalk-board-picked-name {min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
1775
+ .chalk-board-picked-note {margin:0; padding:4px 10px 8px 26px; font-size:12px; line-height:1.4; color:var(--chalk-muted-text);}
1776
+
1777
+ .chalk-board-zoom {position:absolute; right:12px; bottom:12px; z-index:5; height:40px; display:flex; align-items:center; border-radius:999px; padding:0 4px; transition:translate .2s ease-out;}
1778
+ .chalk-board-zoom button {height:32px; min-width:32px; border-radius:999px; display:grid; place-items:center; padding:0 6px !important; font-size:13px; font-weight:600; font-variant-numeric:tabular-nums;}
1779
+ .chalk-board-zoom button:hover {background:var(--chalk-hover);}
1780
+ .chalk-board-zoom-value {min-width:52px !important;}
1781
+ .chalk-board-zoom-rule {width:1px; height:18px; background:var(--chalk-line-strong); margin:0 2px;}
1782
+
1783
+ .chalk-board-island {position:absolute; z-index:5; display:flex; align-items:center; gap:2px; padding:6px; border-radius:26px; max-width:calc(100% - 24px);}
1784
+ .chalk-board-island[data-place="bottom"] {left:50%; bottom:12px; translate:-50% 0;}
1785
+ .chalk-board-island[data-place="top"] {left:50%; top:12px; translate:-50% 0;}
1786
+ .chalk-board-island[data-place="left"] {left:12px; top:50%; translate:0 -50%; flex-direction:column; max-height:calc(100% - 24px); overflow-y:auto; scrollbar-width:none;}
1787
+ .chalk-board-island[data-place="compact"] {left:12px; bottom:12px; padding:4px;}
1788
+ .chalk-board-island[data-place="expanded"] {left:12px; right:12px; bottom:12px; padding:4px; border-radius:28px; overflow-x:auto; scrollbar-width:none;}
1789
+ .chalk-board-island[data-dragging] {z-index:6; cursor:grabbing; box-shadow:0 0 0 1px var(--chalk-line-strong), 0 14px 36px rgba(0,0,0,.45);}
1790
+ .chalk-board:has(.chalk-board-island[data-place="expanded"]) .chalk-board-zoom {translate:0 -60px;}
1791
+ .chalk-board[data-presentation="mobile"] .chalk-board-island {padding:4px; border-radius:28px;}
1792
+ .chalk-board[data-presentation="mobile"]:has(.chalk-board-island[data-place="expanded"]) .chalk-board-zoom {translate:0 -64px;}
1793
+ .chalk-board[data-presentation="mobile"][data-call-controls] :is(.chalk-board-island[data-place="bottom"], .chalk-board-island[data-place="compact"], .chalk-board-island[data-place="expanded"], .chalk-board-zoom) {bottom:76px;}
1794
+ .chalk-board-group {display:contents;}
1795
+ .chalk-board-separator {flex:none; width:1px; height:24px; margin:0 4px; background:var(--chalk-line-strong);}
1796
+ .chalk-board-island[data-place="left"] .chalk-board-separator {width:24px; height:1px; margin:4px 0;}
1797
+ .chalk-board-tool {width:40px; height:40px; flex:none; border-radius:50%; display:grid !important; place-items:center; position:relative; transition:transform .15s ease-out;}
1798
+ .chalk-board-tool:hover {background:var(--chalk-hover);}
1799
+ .chalk-board-tool:active {transform:scale(.92);}
1800
+ .chalk-board-tool[aria-pressed="true"] {background:var(--chalk-text); color:var(--chalk-canvas);}
1801
+ .chalk-board-tool[aria-pressed="true"]:hover {background:var(--chalk-text-strong);}
1802
+ .chalk-board-tool:disabled {opacity:.35; cursor:default; transform:none; background:none;}
1803
+ .chalk-board[data-compact] .chalk-board-tool {width:44px; height:44px;}
1804
+ .chalk-board[data-presentation="mobile"] .chalk-board-tool {width:44px; height:44px;}
1805
+ .chalk-board[data-presentation="mobile"] .chalk-board-zoom {height:52px;}
1806
+ .chalk-board[data-presentation="mobile"] .chalk-board-zoom button {height:44px; min-width:44px;}
1807
+ .chalk-board[data-presentation="mobile"] .chalk-board-menu-button {width:44px; height:44px;}
1808
+ @media (pointer:coarse) {.chalk-board-tool {width:44px; height:44px;}}
1809
+ .chalk-board-tool-caret {position:absolute; right:3px; bottom:3px;}
1810
+ .chalk-board-tool-swatch {width:22px; height:22px; border-radius:50%; box-shadow:0 0 0 2px var(--chalk-surface), 0 0 0 3.5px var(--chalk-muted-text);}
1811
+ .chalk-board-tool-dot {position:absolute; right:5px; bottom:5px; width:11px; height:11px; border-radius:50%; box-shadow:0 0 0 2px var(--chalk-surface);}
1812
+ .chalk-board-grip {width:22px; height:40px; flex:none; display:grid !important; place-items:center; border-radius:12px; color:var(--chalk-muted-text) !important; cursor:grab !important; touch-action:none;}
1813
+ .chalk-board-grip:hover {background:var(--chalk-hover); color:var(--chalk-text) !important;}
1814
+ .chalk-board-island[data-place="left"] .chalk-board-grip {width:40px; height:22px;}
1815
+ .chalk-board-island[data-place="left"] .chalk-board-grip svg {transform:rotate(90deg);}
1816
+ @media (pointer:coarse) {.chalk-board-grip {width:26px; height:44px;} .chalk-board-island[data-place="left"] .chalk-board-grip {width:44px; height:26px;}}
1817
+ .chalk-board-kbd {font:600 11px ui-monospace, monospace; margin-left:6px; padding:1px 5px; border-radius:4px; background:rgba(127,127,127,.28);}
1818
+
1819
+ .chalk-board-dropzone {position:absolute; z-index:4; border:2px dashed var(--board-selection); border-radius:28px; background:rgba(125,184,255,.08); opacity:.45; pointer-events:none; transition:opacity .15s ease-out;}
1820
+ .chalk-board-dropzone[data-near] {opacity:1;}
1821
+ .chalk-board-dropzone[data-zone="left"] {left:6px; top:12px; bottom:12px; width:70px;}
1822
+ .chalk-board-dropzone[data-zone="bottom"] {left:12px; right:12px; bottom:6px; height:70px;}
1823
+ .chalk-board-dropzone[data-zone="top"] {left:12px; right:12px; top:6px; height:70px;}
1824
+
1825
+ .chalk-board-colors {display:grid; gap:10px; padding:4px;}
1826
+ .chalk-board-swatches {display:flex; gap:2px;}
1827
+ .chalk-board-swatch, .chalk-board-width {width:40px; height:40px; border-radius:50%; display:grid; place-items:center; flex:none; color:var(--chalk-text); background:none; border:0; cursor:pointer;}
1828
+ @media (pointer:coarse) {.chalk-board-swatch, .chalk-board-width {width:44px; height:44px;}}
1829
+ .chalk-board-swatch:hover, .chalk-board-width:hover {background:var(--chalk-hover);}
1830
+ .chalk-board-swatch i {width:24px; height:24px; border-radius:50%; box-shadow:0 0 0 1px var(--chalk-muted-text); display:grid; place-items:center;}
1831
+ .chalk-board-swatch[aria-checked="true"] i {box-shadow:0 0 0 2px var(--chalk-popover), 0 0 0 4px var(--chalk-text);}
1832
+ .chalk-board-width i {display:block; width:22px; border-radius:99px; background:var(--chalk-text);}
1833
+ .chalk-board-width[aria-checked="true"] {background:var(--chalk-text);}
1834
+ .chalk-board-width[aria-checked="true"] i {background:var(--chalk-canvas);}
1835
+
1836
+ .chalk-board-announcer {position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap;}
1837
+
1838
+ @media (max-width: 640px), (pointer: coarse) {
1839
+ .chalk-board-zoom {height:52px;}
1840
+ .chalk-board-zoom button {height:44px; min-width:44px;}
1841
+ .chalk-board-menu-button {width:44px; height:44px;}
1842
+ }
1843
+ @media (prefers-reduced-motion: reduce) {
1844
+ .chalk-board-spinner {animation:none;}
1845
+ .chalk-board-engine, .chalk-board-cursor-name, .chalk-board-zoom {transition:none;}
1846
+ .chalk-board-cursor-name {transform:none !important;}
1847
+ }
1848
+ `;
1849
+
1850
+ // src/react/board/board-toasts.ts
1851
+ import { createContext, useCallback as useCallback2, useContext, useEffect as useEffect5, useMemo as useMemo2 } from "react";
1852
+ var BoardToastContext = createContext(() => void 0);
1853
+ function useBoardToast(lifetime) {
1854
+ const publish = useContext(BoardToastContext);
1855
+ const scope = useMemo2(() => ({ active: true }), [lifetime]);
1856
+ useEffect5(() => {
1857
+ scope.active = true;
1858
+ return () => {
1859
+ scope.active = false;
1860
+ publish(null);
1861
+ };
1862
+ }, [publish, scope]);
1863
+ return useCallback2(
1864
+ (toast) => {
1865
+ if (scope.active) publish(toast);
1866
+ },
1867
+ [publish, scope]
1868
+ );
1869
+ }
1870
+
1871
+ // src/react/board/Board.tsx
1872
+ import { Fragment as Fragment5, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1873
+ var SURFACE_KEY = "chalk.board.surface";
1874
+ var ISLAND_KEY = "chalk.board.island";
1875
+ var COMPACT_BELOW_PX = 1060;
1876
+ var VIEW_SEND_MS = 150;
1877
+ var VIEW_HEARTBEAT_MS = 2e3;
1878
+ var GLIDE_MS = 300;
1879
+ var ZOOM_STEP = 1.25;
1880
+ var NOTE_FONT_PX = 18;
1881
+ var EDITING_KEYS = /* @__PURE__ */ new Set(["delete", "backspace", "arrowup", "arrowdown", "arrowleft", "arrowright", "escape", " ", "enter", "shift", "alt", "control", "meta", "tab"]);
1882
+ var MODIFIED_ENGINE_KEYS = /* @__PURE__ */ new Set(["c", "v", "x", "a", "d", "=", "+", "-", "0"]);
1883
+ var first = (name) => name.split(" ")[0] ?? name;
1884
+ function fitContent(engine, animate) {
1885
+ engine.api.scrollToContent(void 0, { fitToViewport: true, viewportZoomFactor: 0.85, maxZoom: 1, animate });
1886
+ }
1887
+ function isTyping(target) {
1888
+ return target instanceof HTMLElement && (target.isContentEditable || target.tagName === "TEXTAREA" || target.tagName === "INPUT");
1889
+ }
1890
+ function useCamera(engine, root, onLeadingMove, onMoved, onViewportReady) {
1891
+ const cameraRef = useRef6({ scrollX: 0, scrollY: 0, zoom: 1 });
1892
+ const viewportRef = useRef6({ width: 1, height: 1 });
1893
+ const expected = useRef6(null);
1894
+ const glide = useRef6(0);
1895
+ const [zoom, setZoom] = useState8(1);
1896
+ const reduced = usePrefersReducedMotion();
1897
+ const leading = useRef6(onLeadingMove);
1898
+ leading.current = onLeadingMove;
1899
+ const moved = useRef6(onMoved);
1900
+ moved.current = onMoved;
1901
+ const resized = useRef6(onViewportReady);
1902
+ resized.current = onViewportReady;
1903
+ useEffect6(() => {
1904
+ if (!engine) return;
1905
+ const paint = (camera) => {
1906
+ cameraRef.current = camera;
1907
+ const element = root.current;
1908
+ if (element) {
1909
+ let gap = 24 * camera.zoom;
1910
+ while (gap < 14) gap *= 2;
1911
+ element.style.setProperty("--board-dot-gap", `${gap}px`);
1912
+ element.style.setProperty("--board-dot-x", `${camera.scrollX * camera.zoom % gap}px`);
1913
+ element.style.setProperty("--board-dot-y", `${camera.scrollY * camera.zoom % gap}px`);
1914
+ }
1915
+ setZoom(camera.zoom);
1916
+ };
1917
+ paint(readCamera(engine));
1918
+ return engine.api.onScrollChange((scrollX, scrollY, next) => {
1919
+ const camera = { scrollX, scrollY, zoom: next.value };
1920
+ if (camera.zoom !== clampZoom(camera.zoom)) {
1921
+ leading.current();
1922
+ expected.current = { ...camera, zoom: clampZoom(camera.zoom) };
1923
+ setCamera(engine, expected.current);
1924
+ return;
1925
+ }
1926
+ paint(camera);
1927
+ if (expected.current && sameCamera(camera, expected.current)) return;
1928
+ expected.current = null;
1929
+ cancelAnimationFrame(glide.current);
1930
+ leading.current();
1931
+ moved.current();
1932
+ });
1933
+ }, [engine, root]);
1934
+ useEffect6(() => {
1935
+ const element = root.current;
1936
+ if (!element) return;
1937
+ viewportRef.current = { width: element.clientWidth || 1, height: element.clientHeight || 1 };
1938
+ const observer = new ResizeObserver(([entry]) => {
1939
+ if (!entry) return;
1940
+ viewportRef.current = { width: entry.contentRect.width, height: entry.contentRect.height };
1941
+ if (entry.contentRect.width > 0 && entry.contentRect.height > 0) resized.current();
1942
+ });
1943
+ observer.observe(element);
1944
+ return () => observer.disconnect();
1945
+ }, [root]);
1946
+ useEffect6(() => () => cancelAnimationFrame(glide.current), []);
1947
+ const move = useCallback3(
1948
+ (target, animate, source) => {
1949
+ if (!engine) return;
1950
+ if (source !== "follow") leading.current();
1951
+ cancelAnimationFrame(glide.current);
1952
+ if (sameCamera(target, cameraRef.current)) {
1953
+ expected.current = target;
1954
+ moved.current();
1955
+ return;
1956
+ }
1957
+ const set = (camera) => {
1958
+ expected.current = camera;
1959
+ setCamera(engine, camera);
1960
+ };
1961
+ if (!animate || reduced) {
1962
+ set(target);
1963
+ moved.current();
1964
+ return;
1965
+ }
1966
+ const from = cameraRef.current;
1967
+ const started = performance.now();
1968
+ const step = (now) => {
1969
+ const progress = Math.max(0, Math.min(1, (now - started) / GLIDE_MS));
1970
+ set(mixCamera(from, target, 1 - (1 - progress) ** 3));
1971
+ if (progress < 1) glide.current = requestAnimationFrame(step);
1972
+ else {
1973
+ if (source !== "follow") leading.current();
1974
+ moved.current();
1975
+ }
1976
+ };
1977
+ glide.current = requestAnimationFrame(step);
1978
+ },
1979
+ [engine, reduced]
1980
+ );
1981
+ const fit = useCallback3(() => {
1982
+ if (engine) fitContent(engine, false);
1983
+ }, [engine]);
1984
+ return useMemo3(() => ({ cameraRef, viewportRef, zoom, move, fit }), [move, zoom, fit]);
1985
+ }
1986
+ function useBoardKeys(root, handlers) {
1987
+ const latest = useRef6(handlers);
1988
+ latest.current = handlers;
1989
+ useEffect6(() => {
1990
+ const element = root.current;
1991
+ if (!element) return;
1992
+ const keydown = (event) => {
1993
+ if (isTyping(event.target)) return;
1994
+ const key = event.key.toLowerCase();
1995
+ if (event.altKey && key.startsWith("arrow") && event.target instanceof Element && event.target.closest(".chalk-board-grip")) return;
1996
+ const modified = event.metaKey || event.ctrlKey;
1997
+ const { onTool, onZoom, canDraw } = latest.current;
1998
+ if (modified && !event.altKey && key === "z") return;
1999
+ if (modified && !event.altKey && key === "y") return;
2000
+ if (modified && !event.altKey && !event.shiftKey && MODIFIED_ENGINE_KEYS.has(key)) return;
2001
+ if (!modified && !event.altKey) {
2002
+ const tool = TOOL_KEYS[key];
2003
+ if (tool && canDraw) {
2004
+ event.preventDefault();
2005
+ event.stopPropagation();
2006
+ onTool(tool);
2007
+ return;
2008
+ }
2009
+ if (key === "+" || key === "=" || key === "-") {
2010
+ event.preventDefault();
2011
+ event.stopPropagation();
2012
+ onZoom(key === "-" ? 1 / ZOOM_STEP : ZOOM_STEP);
2013
+ return;
2014
+ }
2015
+ if (key === "escape" && canDraw) onTool("selection");
2016
+ if (EDITING_KEYS.has(key)) return;
2017
+ }
2018
+ event.stopPropagation();
2019
+ };
2020
+ const blockMenu = (event) => {
2021
+ event.preventDefault();
2022
+ event.stopPropagation();
2023
+ };
2024
+ element.addEventListener("keydown", keydown, true);
2025
+ element.addEventListener("contextmenu", blockMenu, true);
2026
+ return () => {
2027
+ element.removeEventListener("keydown", keydown, true);
2028
+ element.removeEventListener("contextmenu", blockMenu, true);
2029
+ };
2030
+ }, [root]);
2031
+ }
2032
+ function useImageRefusal(root, toast) {
2033
+ const latest = useRef6(toast);
2034
+ latest.current = toast;
2035
+ useEffect6(() => {
2036
+ const element = root.current;
2037
+ if (!element) return;
2038
+ const refuse = (files, event) => {
2039
+ if (!files || ![...files].some((file) => file.type.startsWith("image/"))) return;
2040
+ event.preventDefault();
2041
+ event.stopPropagation();
2042
+ latest.current({ type: "error", message: "The board can't take images yet.", duration: 5e3 });
2043
+ };
2044
+ const paste = (event) => refuse(event.clipboardData?.files, event);
2045
+ const drop = (event) => refuse(event.dataTransfer?.files, event);
2046
+ element.addEventListener("paste", paste, true);
2047
+ element.addEventListener("drop", drop, true);
2048
+ return () => {
2049
+ element.removeEventListener("paste", paste, true);
2050
+ element.removeEventListener("drop", drop, true);
2051
+ };
2052
+ }, [root]);
2053
+ }
2054
+ function Board({
2055
+ board,
2056
+ primary,
2057
+ visible,
2058
+ colorScheme,
2059
+ people,
2060
+ following,
2061
+ onFollow,
2062
+ sendView,
2063
+ onClose,
2064
+ isHost,
2065
+ connectionStatus,
2066
+ sceneId,
2067
+ sceneStatus,
2068
+ ownParticipantId,
2069
+ participantIds,
2070
+ whoCanDraw,
2071
+ viewBounds,
2072
+ renderCursorAvatar,
2073
+ onExport,
2074
+ phone: phoneOverride,
2075
+ downloadLabel,
2076
+ presentation = "web",
2077
+ landscapeControlsVisible = false
2078
+ }) {
2079
+ const root = useRef6(null);
2080
+ const offline = connectionStatus === "reconnecting";
2081
+ const smallViewport = useMediaQuery("(max-width: 640px)");
2082
+ const phone = phoneOverride ?? smallViewport;
2083
+ const [engine, setEngine] = useState8(null);
2084
+ const toast = useBoardToast(engine);
2085
+ const [surface, setSurface] = useState8(() => readStored(SURFACE_KEY, SURFACES, "chalkboard"));
2086
+ const [place, setPlace] = useState8(() => readStored(ISLAND_KEY, ISLAND_PLACES, "bottom"));
2087
+ const [tool, setTool] = useState8("pen");
2088
+ const [shape, setShape] = useState8("rectangle");
2089
+ const [color, setColor] = useState8("ink");
2090
+ const [width, setWidth] = useState8("medium");
2091
+ const [elementCount, setElementCount] = useState8(0);
2092
+ const empty = elementCount === 0;
2093
+ const [narrow, setNarrow] = useState8(false);
2094
+ const [announcement, setAnnouncement] = useState8("");
2095
+ const canDraw = primary && (board.collab?.canDraw ?? board.canDraw ?? true);
2096
+ const compact = phone || narrow;
2097
+ const history = useEngineHistory(root, engine);
2098
+ const views = useRef6(/* @__PURE__ */ new Map());
2099
+ const cursors = useRef6(/* @__PURE__ */ new Map());
2100
+ const pen = useMemo3(() => ({ color, width, surface, theme: colorScheme }), [color, width, surface, colorScheme]);
2101
+ const followingRef = useRef6(following);
2102
+ followingRef.current = following;
2103
+ const nameOf = useCallback3((id) => first(people.find((person) => person.id === id)?.displayName ?? "Someone"), [people]);
2104
+ const colorFor = useCallback3(
2105
+ (id) => personColor(
2106
+ Math.max(
2107
+ 0,
2108
+ people.findIndex((person) => person.id === id)
2109
+ )
2110
+ ),
2111
+ [people]
2112
+ );
2113
+ const stopFollowing = useCallback3(() => {
2114
+ const current = followingRef.current;
2115
+ if (!current) return;
2116
+ followingRef.current = null;
2117
+ onFollow(null);
2118
+ toast({ message: `Stopped following ${nameOf(current)}` });
2119
+ }, [nameOf, onFollow, toast]);
2120
+ const shareView = useRef6(async () => false);
2121
+ const viewSender = useRef6(sendView);
2122
+ viewSender.current = sendView;
2123
+ const selectedFollow = useRef6(following);
2124
+ const appliedVersions = useRef6(new ChalkWhiteboardViewVersions());
2125
+ const previousRoster = useRef6(new Set(participantIds ?? people.map((person) => person.id)));
2126
+ useEffect6(() => {
2127
+ const next = new Set(participantIds ?? people.map((person) => person.id));
2128
+ for (const id of previousRoster.current) {
2129
+ if (next.has(id)) continue;
2130
+ appliedVersions.current.forgetParticipant(id);
2131
+ for (const [publisher, view] of views.current) {
2132
+ if (publisher === id || view.originParticipantId === id) views.current.delete(publisher);
2133
+ }
2134
+ cursors.current.delete(id);
2135
+ }
2136
+ previousRoster.current = next;
2137
+ }, [participantIds, people]);
2138
+ const viewsAvailable = sendView !== void 0;
2139
+ const reapplyView = useRef6(() => void 0);
2140
+ const camera = useCamera(
2141
+ engine,
2142
+ root,
2143
+ () => {
2144
+ stopFollowing();
2145
+ shareView.current(false, true);
2146
+ },
2147
+ () => shareView.current(false),
2148
+ () => reapplyView.current()
2149
+ );
2150
+ useEffect6(() => {
2151
+ if (!visible || !viewsAvailable || !engine) return;
2152
+ let last = 0;
2153
+ let pending;
2154
+ let pendingLead = false;
2155
+ const send = async (bring2) => {
2156
+ const lead = pendingLead || bring2;
2157
+ pendingLead = false;
2158
+ const followedView = followingRef.current ? views.current.get(followingRef.current) : void 0;
2159
+ const selected = followedView !== void 0 && selectedFollow.current === followingRef.current;
2160
+ if (followedView && !followedView.originParticipantId && !followedView.ownerId && !bring2) return true;
2161
+ const view = followedView ?? visibleView(camera.cameraRef.current, camera.viewportRef.current, viewBounds);
2162
+ last = performance.now();
2163
+ const rectangle = { x: view.x, y: view.y, width: view.width, height: view.height, bring: bring2, lead: lead || selected };
2164
+ let outgoing = rectangle;
2165
+ if (followedView && !lead) {
2166
+ if (followedView.ownerId) {
2167
+ outgoing = { ...rectangle, originParticipantId: followedView.participantId, originSequence: followedView.ownerSequence };
2168
+ } else if (followedView.originParticipantId && followedView.originSequence !== void 0) {
2169
+ outgoing = { ...rectangle, originParticipantId: followedView.originParticipantId, originSequence: followedView.originSequence, ...followedView.originEpoch ? { originEpoch: followedView.originEpoch } : {} };
2170
+ }
2171
+ }
2172
+ try {
2173
+ const sent = await viewSender.current?.(outgoing) ?? false;
2174
+ if (sent && selected) selectedFollow.current = null;
2175
+ return sent;
2176
+ } catch (error) {
2177
+ console.warn("Board view dropped.", { operation: "sendView", error });
2178
+ return false;
2179
+ }
2180
+ };
2181
+ shareView.current = (bring2, lead = false) => {
2182
+ pendingLead ||= lead;
2183
+ if (bring2) return send(true);
2184
+ clearTimeout(pending);
2185
+ pending = setTimeout(() => send(false), Math.max(0, VIEW_SEND_MS - (performance.now() - last)));
2186
+ return Promise.resolve(true);
2187
+ };
2188
+ const heartbeat = setInterval(() => {
2189
+ if (!followingRef.current) send(false);
2190
+ }, VIEW_HEARTBEAT_MS);
2191
+ send(false);
2192
+ return () => {
2193
+ clearInterval(heartbeat);
2194
+ clearTimeout(pending);
2195
+ shareView.current = async () => false;
2196
+ };
2197
+ }, [sceneId, sceneStatus, camera.cameraRef, camera.viewportRef, connectionStatus, engine, visible, viewsAvailable, viewBounds]);
2198
+ const { move, viewportRef } = camera;
2199
+ const goTo = useCallback3(
2200
+ (participantId, animate, selected = false) => {
2201
+ const view = views.current.get(participantId);
2202
+ if (!view || viewportRef.current.width <= 0 || viewportRef.current.height <= 0) return;
2203
+ if (!selected && !appliedVersions.current.isNew(view, participantId)) return;
2204
+ appliedVersions.current.remember(view, participantId);
2205
+ const cursor = cursors.current.get(participantId);
2206
+ const focus = cursor && Date.now() - Date.parse(cursor.occurredAt) <= 1e4 && cursor.x >= view.x && cursor.x <= view.x + view.width && cursor.y >= view.y && cursor.y <= view.y + view.height ? { x: cursor.x, y: cursor.y, width: 0, height: 0 } : view;
2207
+ move(cameraForView(focus, viewportRef.current, camera.cameraRef.current.zoom), animate, "follow");
2208
+ },
2209
+ [move, viewportRef, camera.cameraRef]
2210
+ );
2211
+ reapplyView.current = () => {
2212
+ if (followingRef.current) goTo(followingRef.current, false, true);
2213
+ };
2214
+ const subscribe = board.collab?.subscribe;
2215
+ const latestView = useRef6(() => void 0);
2216
+ useEffect6(
2217
+ () => subscribe?.((event) => {
2218
+ if (event.type !== "snapshot") return;
2219
+ views.current.clear();
2220
+ appliedVersions.current.clear();
2221
+ }),
2222
+ [subscribe]
2223
+ );
2224
+ latestView.current = (event) => {
2225
+ const previous = views.current.get(event.participantId);
2226
+ if (!event.bring && (event.participantId === ownParticipantId || !event.ownerId && event.originParticipantId === ownParticipantId)) return;
2227
+ if (!event.bring && !appliedVersions.current.isNew(event, event.participantId)) return;
2228
+ if (!event.ownerId && !event.bring && event.originParticipantId && previous?.originParticipantId === event.originParticipantId && event.originEpoch === previous.originEpoch && event.originSequence <= previous.originSequence) return;
2229
+ const newPeer = !previous;
2230
+ views.current.delete(event.participantId);
2231
+ views.current.set(event.participantId, { ...event, bring: false });
2232
+ if (views.current.size > WHITEBOARD_VIEW_PEOPLE_MAX) {
2233
+ const oldest = views.current.keys().next().value;
2234
+ if (oldest !== void 0) views.current.delete(oldest);
2235
+ }
2236
+ if (newPeer) {
2237
+ shareView.current(false);
2238
+ }
2239
+ if (event.bring) {
2240
+ appliedVersions.current.remember(event, event.participantId);
2241
+ followingRef.current = event.participantId;
2242
+ onFollow(event.participantId);
2243
+ toast({ message: `${nameOf(event.participantId)} brought everyone to this view` });
2244
+ goTo(event.participantId, true, true);
2245
+ return;
2246
+ }
2247
+ if (followingRef.current === event.participantId) goTo(event.participantId, true);
2248
+ };
2249
+ useEffect6(() => {
2250
+ if (!subscribe) return;
2251
+ return subscribe((event) => {
2252
+ if (event.type === "cursor") {
2253
+ rememberCursor(cursors.current, event);
2254
+ if (followingRef.current === event.participantId && Date.now() - Date.parse(event.occurredAt) <= 1e4) {
2255
+ camera.move(cameraForView({ x: event.x, y: event.y, width: 0, height: 0 }, camera.viewportRef.current, camera.cameraRef.current.zoom), false, "follow");
2256
+ }
2257
+ } else if (event.type === "view") latestView.current(event);
2258
+ });
2259
+ }, [subscribe, camera.move, camera.viewportRef, camera.cameraRef]);
2260
+ useEffect6(() => {
2261
+ selectedFollow.current = following;
2262
+ if (following) {
2263
+ goTo(following, true, true);
2264
+ shareView.current(false);
2265
+ }
2266
+ }, [following, goTo]);
2267
+ useEffect6(() => {
2268
+ if (engine && canDraw) applyTool(engine, tool, pen);
2269
+ }, [canDraw, engine, pen, tool]);
2270
+ useEffect6(() => {
2271
+ if (engine) void document.fonts.load(`${NOTE_FONT_PX}px Nunito`);
2272
+ }, [engine]);
2273
+ useEffect6(() => {
2274
+ if (!engine) return;
2275
+ let beforeEditing = sceneSignature(engine);
2276
+ let wasEditing = false;
2277
+ setElementCount(engine.api.getSceneElements().length);
2278
+ const offChange = engine.api.onChange((elements, appState) => {
2279
+ const count = elements.filter((element) => !element.isDeleted).length;
2280
+ setElementCount(count);
2281
+ const editing = appState.editingTextElement !== null;
2282
+ if (!wasEditing && editing) beforeEditing = sceneSignature(engine);
2283
+ const finishedEditing = wasEditing && !editing;
2284
+ wasEditing = editing;
2285
+ if (finishedEditing && sceneSignature(engine) !== beforeEditing) {
2286
+ settleNoteText(engine);
2287
+ }
2288
+ });
2289
+ return () => {
2290
+ offChange();
2291
+ };
2292
+ }, [engine]);
2293
+ useEffect6(() => {
2294
+ const element = root.current;
2295
+ if (!element) return;
2296
+ const observer = new ResizeObserver(([entry]) => setNarrow((entry?.contentRect.width ?? COMPACT_BELOW_PX) < COMPACT_BELOW_PX));
2297
+ observer.observe(element);
2298
+ return () => observer.disconnect();
2299
+ }, []);
2300
+ const chooseTool = useCallback3((next) => {
2301
+ if (next === "rectangle" || next === "ellipse" || next === "diamond" || next === "arrow" || next === "line") setShape(next);
2302
+ setTool(next);
2303
+ }, []);
2304
+ const undo = useCallback3(() => {
2305
+ const container = root.current?.querySelector(".excalidraw");
2306
+ if (container) pressHistory(container, false);
2307
+ }, []);
2308
+ const redo = useCallback3(() => {
2309
+ const container = root.current?.querySelector(".excalidraw");
2310
+ if (container) pressHistory(container, true);
2311
+ }, []);
2312
+ const zoomBy = useCallback3(
2313
+ (factor) => {
2314
+ stopFollowing();
2315
+ camera.move(zoomAroundCentre(camera.cameraRef.current, camera.viewportRef.current, camera.cameraRef.current.zoom * factor), true);
2316
+ },
2317
+ [camera, stopFollowing]
2318
+ );
2319
+ useBoardKeys(root, { onTool: chooseTool, onZoom: zoomBy, canDraw });
2320
+ useImageRefusal(root, toast);
2321
+ const restyle = (next) => {
2322
+ if (next.color) setColor(next.color);
2323
+ if (next.width) setWidth(next.width);
2324
+ if (engine) restyleSelection(engine, { ...pen, ...next }, next);
2325
+ };
2326
+ const choosePlace = (next, announce) => {
2327
+ setPlace(next);
2328
+ writeStored(ISLAND_KEY, next);
2329
+ if (announce) setAnnouncement(`Tool island moved to ${next}`);
2330
+ };
2331
+ const chooseSurface = (next) => {
2332
+ setSurface(next);
2333
+ writeStored(SURFACE_KEY, next);
2334
+ };
2335
+ const fit = () => {
2336
+ stopFollowing();
2337
+ if (engine) fitContent(engine, true);
2338
+ };
2339
+ const download = async () => {
2340
+ if (!engine) return;
2341
+ try {
2342
+ const image = await boardImage(engine, surfaceFill(surface, colorScheme));
2343
+ if (onExport) {
2344
+ await onExport(image, "download");
2345
+ return;
2346
+ }
2347
+ const url = URL.createObjectURL(image);
2348
+ const link = document.createElement("a");
2349
+ link.href = url;
2350
+ link.download = "chalk-board.png";
2351
+ link.click();
2352
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
2353
+ toast({ message: "Saved chalk-board.png" });
2354
+ } catch {
2355
+ toast({ type: "error", message: "Could not save the image. Try again." });
2356
+ }
2357
+ };
2358
+ const copy = async () => {
2359
+ if (!engine) return;
2360
+ try {
2361
+ const image = await boardImage(engine, surfaceFill(surface, colorScheme));
2362
+ if (onExport) await onExport(image, "copy");
2363
+ else await navigator.clipboard.write([new ClipboardItem({ "image/png": image })]);
2364
+ toast({ message: "Copied the board as an image" });
2365
+ } catch {
2366
+ toast({ type: "error", message: "Copy failed. Try saving the image instead." });
2367
+ }
2368
+ };
2369
+ const bring = async () => {
2370
+ if (followingRef.current) {
2371
+ followingRef.current = null;
2372
+ onFollow(null);
2373
+ }
2374
+ if (await shareView.current(true)) toast({ message: "Sharing your view with everyone" });
2375
+ else toast({ type: "error", message: "Could not share your view. Try again." });
2376
+ };
2377
+ const clear = async () => {
2378
+ if (!engine) return;
2379
+ try {
2380
+ const restore = await engine.clearWithUndo();
2381
+ toast({ message: "Board cleared", ...canDraw ? { action: { label: "Undo", onClick: () => void restore() } } : {} });
2382
+ } catch {
2383
+ toast({ type: "error", message: "Could not clear the board. Try again." });
2384
+ }
2385
+ };
2386
+ const placeNoteAt = (event) => {
2387
+ if (event.button !== 0) return;
2388
+ const container = root.current?.querySelector(".excalidraw");
2389
+ if (!engine || !container || !root.current) return;
2390
+ const rect = root.current.getBoundingClientRect();
2391
+ const { scrollX, scrollY, zoom } = camera.cameraRef.current;
2392
+ placeNote(engine, container, { x: (event.clientX - rect.left) / zoom - scrollX, y: (event.clientY - rect.top) / zoom - scrollY }, color);
2393
+ setTool("selection");
2394
+ };
2395
+ const ink = surfaceInk(surface, colorScheme);
2396
+ const surfaceVars = { "--board-selection": selectionColor(surface, colorScheme) };
2397
+ const followed = following ? nameOf(following) : null;
2398
+ const ready = engine !== null;
2399
+ return /* @__PURE__ */ jsxs10(
2400
+ "div",
2401
+ {
2402
+ ref: root,
2403
+ className: "chalk-board",
2404
+ "data-presentation": presentation,
2405
+ "data-call-controls": landscapeControlsVisible || void 0,
2406
+ "data-surface": surface,
2407
+ "data-theme": colorScheme,
2408
+ "data-ready": ready || void 0,
2409
+ "data-compact": compact || void 0,
2410
+ "data-primary": primary || void 0,
2411
+ "data-elements": elementCount,
2412
+ style: surfaceVars,
2413
+ hidden: !visible,
2414
+ children: [
2415
+ /* @__PURE__ */ jsx11(BoardStyles, {}),
2416
+ /* @__PURE__ */ jsx11(WhiteboardCanvas, { ...board, headless: true, canDraw, collab: board.collab ? { ...board.collab, canDraw, canManage: isHost } : void 0, inkColor: ink, theme: "light", isVisible: visible, className: "chalk-board-engine", onEngineReady: setEngine }),
2417
+ !ready ? /* @__PURE__ */ jsxs10("div", { className: "chalk-board-loading", role: "status", children: [
2418
+ /* @__PURE__ */ jsx11("span", { className: "chalk-board-spinner", "aria-hidden": "true" }),
2419
+ "Opening board"
2420
+ ] }) : null,
2421
+ primary && ready && empty ? /* @__PURE__ */ jsx11(BoardHint, { canDraw, mobile: presentation === "mobile" }) : null,
2422
+ primary && tool === "note" && canDraw ? /* @__PURE__ */ jsx11("div", { className: "chalk-board-note-layer", "aria-hidden": "true", onPointerDown: placeNoteAt }) : null,
2423
+ primary ? /* @__PURE__ */ jsx11(BoardCursors, { cursorsRef: cursors, cameraRef: camera.cameraRef, colorFor, hidden: offline, renderAvatar: renderCursorAvatar }) : null,
2424
+ primary && ready ? /* @__PURE__ */ jsxs10(Fragment5, { children: [
2425
+ /* @__PURE__ */ jsxs10("div", { className: "chalk-board-chips", "data-low": !compact && place === "top" && canDraw || void 0, children: [
2426
+ followed ? /* @__PURE__ */ jsxs10("div", { className: "chalk-board-chip", role: "status", children: [
2427
+ /* @__PURE__ */ jsx11(BoardIcon, { name: "eye", size: 16 }),
2428
+ "Following ",
2429
+ followed,
2430
+ /* @__PURE__ */ jsx11("button", { type: "button", className: "chalk-board-chip-button", onClick: stopFollowing, children: "Stop" })
2431
+ ] }) : null,
2432
+ offline ? /* @__PURE__ */ jsxs10("div", { className: "chalk-board-chip", "data-quiet": true, role: "status", children: [
2433
+ /* @__PURE__ */ jsx11(BoardIcon, { name: "wifiOff", size: 16 }),
2434
+ "Offline. Your drawing will sync."
2435
+ ] }) : null
2436
+ ] }),
2437
+ !canDraw ? /* @__PURE__ */ jsxs10("div", { className: "chalk-board-view-only", children: [
2438
+ /* @__PURE__ */ jsx11(BoardIcon, { name: "lock", size: 14 }),
2439
+ "View only"
2440
+ ] }) : null,
2441
+ /* @__PURE__ */ jsx11("div", { className: "chalk-board-corner", children: /* @__PURE__ */ jsx11(
2442
+ BoardMenu,
2443
+ {
2444
+ presentation,
2445
+ surface,
2446
+ onSurface: chooseSurface,
2447
+ place: !compact && canDraw ? place : void 0,
2448
+ onPlace: (next) => choosePlace(next, true),
2449
+ downloadLabel,
2450
+ onDownload: () => void download(),
2451
+ onCopy: () => void copy(),
2452
+ whoCanDraw,
2453
+ onBring: isHost && sendView ? bring : void 0,
2454
+ onClear: isHost ? () => void clear() : void 0,
2455
+ onClose
2456
+ }
2457
+ ) }),
2458
+ /* @__PURE__ */ jsxs10("div", { className: "chalk-board-zoom", role: "group", "aria-label": "Zoom", children: [
2459
+ /* @__PURE__ */ jsx11("button", { type: "button", "aria-label": "Zoom out", onClick: () => zoomBy(1 / ZOOM_STEP), children: /* @__PURE__ */ jsx11(BoardIcon, { name: "minus", size: 16 }) }),
2460
+ /* @__PURE__ */ jsxs10("button", { type: "button", "aria-label": `Zoom ${Math.round(camera.zoom * 100)}%, reset to 100%`, className: "chalk-board-zoom-value", onClick: () => zoomBy(1 / camera.cameraRef.current.zoom), children: [
2461
+ Math.round(camera.zoom * 100),
2462
+ "%"
2463
+ ] }),
2464
+ /* @__PURE__ */ jsx11("button", { type: "button", "aria-label": "Zoom in", onClick: () => zoomBy(ZOOM_STEP), children: /* @__PURE__ */ jsx11(BoardIcon, { name: "plus", size: 16 }) }),
2465
+ presentation === "web" ? /* @__PURE__ */ jsxs10(Fragment5, { children: [
2466
+ /* @__PURE__ */ jsx11("span", { className: "chalk-board-zoom-rule", "aria-hidden": "true" }),
2467
+ /* @__PURE__ */ jsx11("button", { type: "button", "aria-label": "Fit to content", onClick: fit, children: /* @__PURE__ */ jsx11(BoardIcon, { name: "fit", size: 16 }) })
2468
+ ] }) : null
2469
+ ] }),
2470
+ canDraw ? /* @__PURE__ */ jsx11(
2471
+ BoardIsland,
2472
+ {
2473
+ tool,
2474
+ shape,
2475
+ onTool: chooseTool,
2476
+ color,
2477
+ width,
2478
+ onColor: (next) => restyle({ color: next }),
2479
+ onWidth: (next) => restyle({ width: next }),
2480
+ surface,
2481
+ theme: colorScheme,
2482
+ canUndo: history.canUndo,
2483
+ canRedo: history.canRedo,
2484
+ onUndo: undo,
2485
+ onRedo: redo,
2486
+ place,
2487
+ onPlace: choosePlace,
2488
+ compact,
2489
+ primaryToolsFirst: presentation === "mobile"
2490
+ }
2491
+ ) : null,
2492
+ /* @__PURE__ */ jsx11("div", { className: "chalk-board-announcer", "aria-live": "polite", children: announcement })
2493
+ ] }) : null
2494
+ ]
2495
+ }
2496
+ );
2497
+ }
2498
+ function BoardHint({ canDraw, mobile }) {
2499
+ if (!canDraw)
2500
+ return /* @__PURE__ */ jsx11("div", { className: "chalk-board-hint", children: /* @__PURE__ */ jsx11("p", { children: "Nothing here yet. When someone draws, it shows up here." }) });
2501
+ return /* @__PURE__ */ jsxs10("div", { className: "chalk-board-hint", children: [
2502
+ /* @__PURE__ */ jsx11("p", { children: "An empty board. Draw, type or add a sticky note." }),
2503
+ mobile ? /* @__PURE__ */ jsx11("p", { children: "Draw with one finger. Use two fingers to move or zoom." }) : /* @__PURE__ */ jsxs10("ul", { children: [
2504
+ /* @__PURE__ */ jsxs10("li", { children: [
2505
+ /* @__PURE__ */ jsx11("kbd", { children: "P" }),
2506
+ " Pen"
2507
+ ] }),
2508
+ /* @__PURE__ */ jsxs10("li", { children: [
2509
+ /* @__PURE__ */ jsx11("kbd", { children: "T" }),
2510
+ " Text"
2511
+ ] }),
2512
+ /* @__PURE__ */ jsxs10("li", { children: [
2513
+ /* @__PURE__ */ jsx11("kbd", { children: "N" }),
2514
+ " Sticky note"
2515
+ ] })
2516
+ ] })
2517
+ ] });
2518
+ }
735
2519
  export {
2520
+ Board,
2521
+ BoardToastContext,
736
2522
  CANONICAL_INK,
737
2523
  CHALK_MATH_CUSTOM_DATA_KEY,
738
2524
  CHALK_MATH_RENDERER,
2525
+ ChalkMenu,
2526
+ ChalkMenuItem,
2527
+ ChalkMenuLabel,
2528
+ Menu as ChalkMenuPrimitive,
2529
+ ChalkPopover,
2530
+ Popover as ChalkPopoverPrimitive,
2531
+ ChalkTooltip,
739
2532
  WhiteboardCanvas,
740
2533
  appendOrReplaceMathElement,
741
2534
  createMathFileId,