@scrawl-board/board 0.1.0-beta.10 → 0.1.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js CHANGED
@@ -5810,12 +5810,35 @@ function toDataUri(bytes, mediaType) {
5810
5810
  return `data:${mediaType};base64,${btoa(binary)}`;
5811
5811
  }
5812
5812
  //#endregion
5813
+ //#region src/persistence/adapters/imageDecode.ts
5814
+ var ORIENTED = { imageOrientation: "from-image" };
5815
+ /**
5816
+ * Decodes image bytes the way a browser displays them: EXIF orientation
5817
+ * applied, so a phone photo comes out upright and its width/height are the
5818
+ * displayed ones.
5819
+ *
5820
+ * `"from-image"` is already the default in every browser this SDK supports
5821
+ * (Chrome/Edge 112+, Firefox 111+, Safari 16+); passing it just makes that
5822
+ * explicit. Engines from before `"none"` was renamed to `"from-image"` reject
5823
+ * the value with a TypeError — they get their own default decode instead of
5824
+ * no image at all.
5825
+ */
5826
+ async function decodeImageBitmap(blob) {
5827
+ try {
5828
+ return await createImageBitmap(blob, ORIENTED);
5829
+ } catch (error) {
5830
+ if (error instanceof TypeError) return createImageBitmap(blob);
5831
+ throw error;
5832
+ }
5833
+ }
5834
+ //#endregion
5813
5835
  //#region src/persistence/adapters/assetIngestion.ts
5836
+ /** Displayed dimensions — EXIF orientation applied, so a portrait phone photo measures as portrait. */
5814
5837
  async function decodeDimensionsWithImageBitmap(bytes, mediaType) {
5815
5838
  const blob = new Blob([bytes], { type: mediaType });
5816
5839
  let bitmap;
5817
5840
  try {
5818
- bitmap = await createImageBitmap(blob);
5841
+ bitmap = await decodeImageBitmap(blob);
5819
5842
  } catch (cause) {
5820
5843
  throw new AssetResolutionError("decode-failed", false, cause instanceof Error ? cause.message : String(cause));
5821
5844
  }
@@ -6005,10 +6028,7 @@ var AssetTextureCache = class {
6005
6028
  }
6006
6029
  let texture;
6007
6030
  try {
6008
- const bitmap = await createImageBitmap(new Blob([outcome.bytes], { type: outcome.mediaType }));
6009
- texture = new THREE.Texture(bitmap);
6010
- texture.colorSpace = THREE.SRGBColorSpace;
6011
- texture.needsUpdate = true;
6031
+ texture = uprightBitmapTexture(await decodeImageBitmap(new Blob([outcome.bytes], { type: outcome.mediaType })));
6012
6032
  } catch (cause) {
6013
6033
  onError(toDecodeError(cause, ref));
6014
6034
  return;
@@ -6063,6 +6083,23 @@ var AssetTextureCache = class {
6063
6083
  function toDecodeError(cause, ref) {
6064
6084
  return new AssetResolutionError("decode-failed", false, cause instanceof Error ? cause.message : String(cause), ref);
6065
6085
  }
6086
+ /**
6087
+ * A texture that shows `bitmap` the right way up. Three.js normally flips an
6088
+ * image vertically on upload (`flipY`) to match WebGL's bottom-up texture
6089
+ * rows, but WebGL ignores that flag for ImageBitmap sources — so every
6090
+ * bitmap-backed image drew upside-down. The flip happens in the texture's UV
6091
+ * transform instead, which every material using this shared texture (built-in
6092
+ * images and Custom SceneImages alike) picks up.
6093
+ */
6094
+ function uprightBitmapTexture(bitmap) {
6095
+ const texture = new THREE.Texture(bitmap);
6096
+ texture.colorSpace = THREE.SRGBColorSpace;
6097
+ texture.flipY = false;
6098
+ texture.repeat.set(1, -1);
6099
+ texture.offset.set(0, 1);
6100
+ texture.needsUpdate = true;
6101
+ return texture;
6102
+ }
6066
6103
  //#endregion
6067
6104
  //#region src/renderer/canvas/raycast.ts
6068
6105
  var origin$1 = new THREE.Vector3();
@@ -19340,7 +19377,7 @@ var ScrawlEngine = class {
19340
19377
  this.eraserRadius = 2;
19341
19378
  this.tableRows = 3;
19342
19379
  this.tableCols = 3;
19343
- this.tool = "marker";
19380
+ this.tool = "select";
19344
19381
  this.voter = null;
19345
19382
  this.stampKind = "emoji:grinning";
19346
19383
  this.timerDurationMs = TIMER_DEFAULT_DURATION_MS;
@@ -21173,7 +21210,8 @@ function createBoardControllerWithEngine(options, existingEngine) {
21173
21210
  const storeListeners = /* @__PURE__ */ new Set();
21174
21211
  const eventListeners = /* @__PURE__ */ new Map();
21175
21212
  const documentListeners = /* @__PURE__ */ new Set();
21176
- let tool = "marker";
21213
+ let tool = "select";
21214
+ let inlineEditing = null;
21177
21215
  let style = Object.freeze({
21178
21216
  inkColor: "#1C1C1E",
21179
21217
  highlightColor: HIGHLIGHT_COLORS.yellow,
@@ -21748,6 +21786,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
21748
21786
  readOnly,
21749
21787
  selection: Object.freeze([...selection]),
21750
21788
  focusedItem: computeFocusedItem(),
21789
+ inlineEditing,
21751
21790
  strokeCount: [...document.all()].length,
21752
21791
  objectCount: allObjects().length,
21753
21792
  canUndo: history.canUndo,
@@ -22374,6 +22413,8 @@ function createBoardControllerWithEngine(options, existingEngine) {
22374
22413
  setInlineEditing(target) {
22375
22414
  assertActive();
22376
22415
  engine?.setInlineEditing(target);
22416
+ inlineEditing = target ? { ...target } : null;
22417
+ changed();
22377
22418
  },
22378
22419
  centerOn(point) {
22379
22420
  assertActive();
@@ -22819,6 +22860,15 @@ function createBoardControllerWithEngine(options, existingEngine) {
22819
22860
  query: {
22820
22861
  get: readObject,
22821
22862
  all: () => allObjects(),
22863
+ bounds: (id) => {
22864
+ const box = document.bbox(id);
22865
+ return box ? {
22866
+ minX: box.minX,
22867
+ minY: box.minY,
22868
+ maxX: box.maxX,
22869
+ maxY: box.maxY
22870
+ } : void 0;
22871
+ },
22822
22872
  search: (query) => searchBoard(query, {
22823
22873
  notes: document.allNotes(),
22824
22874
  texts: document.allTexts(),
@@ -22829,6 +22879,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
22829
22879
  })
22830
22880
  },
22831
22881
  comments: {
22882
+ list: () => comments,
22832
22883
  sync(next) {
22833
22884
  comments = Object.freeze([...next]);
22834
22885
  if (engine) engine.setComments(comments);
@@ -24022,9 +24073,17 @@ var PATHS = {
24022
24073
  "shape:curved-connector": "M5 18C5 8 19 16 19 6M3 18h4v4H3ZM17 2h4v4h-4Z",
24023
24074
  "shape:polyline-connector": "m5 12 7-7 7 5-4 9-10-7M3 10h4v4H3ZM10 3h4v4h-4ZM17 8h4v4h-4ZM13 17h4v4h-4Z",
24024
24075
  chevronDown: "m6 9 6 6 6-6",
24076
+ chevronRight: "m9 6 6 6-6 6",
24077
+ notebook: "M4 4.5A1.5 1.5 0 0 1 5.5 3H18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H6a2 2 0 0 1-2-2ZM4 19a2 2 0 0 1 2-2h13",
24078
+ section: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z",
24079
+ page: "M7 3h7l4 4v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1ZM14 3v4h4",
24025
24080
  pages: "M7 3h13v16H7ZM4 7H2v15h13v-1M10 7h7M10 11h7M10 15h4",
24026
24081
  close: "M5 5l14 14M19 5 5 19",
24027
24082
  check: "m5 12 5 5L20 7",
24083
+ activity: "M3 12h4l3-8 4 16 3-8h4",
24084
+ shield: "M12 3 4 6v6c0 5 3.5 8 8 9 4.5-1 8-4 8-9V6Z",
24085
+ moreVertical: "M12 5h.01M12 12h.01M12 19h.01",
24086
+ share: "M18 2a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM6 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM18 16a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM8.6 10.6l6.8-3.2M8.6 13.4l6.8 3.2",
24028
24087
  lock: "M5 10h14v11H5ZM8 10V6a4 4 0 0 1 8 0v4",
24029
24088
  unlock: "M5 10h14v11H5ZM8 10V6a4 4 0 0 1 8 0",
24030
24089
  duplicate: "M8 8h13v13H8ZM16 8V3H3v13h5",
@@ -24034,8 +24093,8 @@ var PATHS = {
24034
24093
  color: "M12 3C9 8 5 11 5 15a7 7 0 0 0 14 0c0-4-4-7-7-12Z",
24035
24094
  none: "M4 4l16 16M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z",
24036
24095
  search: "M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14ZM15 15l6 6",
24037
- import: "M12 3v12M7 10l5 5 5-5M4 16v5h16v-5",
24038
- export: "M12 16V3M7 8l5-5 5 5M4 16v5h16v-5",
24096
+ import: "M12 16V3M7 8l5-5 5 5M4 16v5h16v-5",
24097
+ export: "M12 3v12M7 10l5 5 5-5M4 16v5h16v-5",
24039
24098
  fit: "M3 9V3h6M15 3h6v6M21 15v6h-6M9 21H3v-6M8 8h8v8H8Z",
24040
24099
  select: "m5 3 14 9-7 1-3 7Z",
24041
24100
  marker: "m4 20 2-6L16 4a2 2 0 0 1 3 3L9 17Zm2-6 3 3M14 6l3 3",
@@ -24064,7 +24123,8 @@ var PATHS = {
24064
24123
  undo: "M9 5 4 10l5 5M4 10h10a6 6 0 0 1 6 6v3",
24065
24124
  redo: "m15 5 5 5-5 5M20 10H10a6 6 0 0 0-6 6v3",
24066
24125
  addRow: "M4 6h16M4 12h16M4 18h10M17 16v5M14.5 18.5h5",
24067
- addColumn: "M6 4v16M12 4v16M18 4v10M16 16.5h5M18.5 14v5"
24126
+ addColumn: "M6 4v16M12 4v16M18 4v10M16 16.5h5M18.5 14v5",
24127
+ deleteRow: "M4 6h16M4 12h16M4 18h10M15 16.5l5 5M20 16.5l-5 5"
24068
24128
  };
24069
24129
  function ToolIcon({ tool }) {
24070
24130
  return /* @__PURE__ */ jsx("svg", {
@@ -24121,127 +24181,197 @@ function NavigatorName({ name, kind, onRename }) {
24121
24181
  children: /* @__PURE__ */ jsx(ToolIcon, { tool: "marker" })
24122
24182
  });
24123
24183
  }
24124
- /**
24125
- * The notebook header across the top of the board (notebook switcher on the
24126
- * left) and a breadcrumb on the canvas, just past the sidebar and tool rail.
24127
- * Both stay visible whether or not the sidebar is open.
24128
- */
24129
- function NotebookNavigation({ controller, snapshot, onOpen }) {
24184
+ /** A crumb (or the switcher) with its menu: pick an item, rename one, or add one. */
24185
+ function NavigatorMenu({ label, items, activeId, kind, mutable, onSelect, onAdd, onRename, children, className, triggerLabel, triggerProps }) {
24130
24186
  const [open, setOpen] = useState(false);
24131
24187
  const root = useRef(null);
24132
24188
  const trigger = useRef(null);
24133
24189
  const menuId = useId();
24134
- const notebook = snapshot.notebooks.find((item) => item.id === snapshot.activeNotebookId);
24135
- const section = snapshot.sections.find((item) => item.id === snapshot.activeSectionId);
24136
- const page = snapshot.pages.find((item) => item.id === snapshot.activePageId);
24137
- const mutable = snapshot.status === "ready" && !snapshot.readOnly;
24138
24190
  useEffect(() => {
24139
24191
  if (!open) return;
24140
24192
  const outside = (event) => {
24141
24193
  if (!root.current?.contains(event.target)) setOpen(false);
24142
24194
  };
24195
+ const escape = (event) => {
24196
+ if (event.key !== "Escape") return;
24197
+ setOpen(false);
24198
+ trigger.current?.focus();
24199
+ };
24143
24200
  document.addEventListener("pointerdown", outside);
24144
- return () => document.removeEventListener("pointerdown", outside);
24201
+ document.addEventListener("keydown", escape);
24202
+ return () => {
24203
+ document.removeEventListener("pointerdown", outside);
24204
+ document.removeEventListener("keydown", escape);
24205
+ };
24145
24206
  }, [open]);
24146
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
24147
- className: "scrawl-board__header",
24148
- children: /* @__PURE__ */ jsxs("div", {
24149
- className: "scrawl-board__notebook-switcher",
24150
- ref: root,
24151
- onKeyDown: (event) => {
24152
- event.stopPropagation();
24153
- if (event.key === "Escape") {
24154
- event.preventDefault();
24155
- setOpen(false);
24156
- trigger.current?.focus();
24157
- }
24158
- },
24159
- children: [/* @__PURE__ */ jsxs("button", {
24160
- className: "scrawl-board__notebook-trigger",
24161
- ref: trigger,
24162
- type: "button",
24163
- "aria-label": "Choose notebook",
24164
- "aria-expanded": open,
24165
- "aria-controls": menuId,
24166
- onClick: () => setOpen((value) => !value),
24167
- children: [
24168
- /* @__PURE__ */ jsx("span", {
24169
- className: "scrawl-board__notebook-color",
24170
- style: { background: notebook?.color }
24171
- }),
24172
- /* @__PURE__ */ jsx("span", { children: notebook?.name ?? "Notebooks" }),
24173
- /* @__PURE__ */ jsx(ToolIcon, { tool: "chevronDown" })
24174
- ]
24175
- }), open && /* @__PURE__ */ jsxs("div", {
24176
- className: "scrawl-board__notebook-dropdown",
24177
- id: menuId,
24178
- role: "group",
24179
- "aria-label": "Notebooks",
24180
- children: [/* @__PURE__ */ jsx("div", {
24181
- className: "scrawl-board__notebook-options",
24182
- children: snapshot.notebooks.map((item) => /* @__PURE__ */ jsxs("div", {
24183
- className: "scrawl-board__page-row",
24184
- "data-active": item.id === snapshot.activeNotebookId,
24185
- children: [/* @__PURE__ */ jsxs("button", {
24186
- className: "scrawl-board__page-select",
24187
- type: "button",
24188
- "aria-pressed": item.id === snapshot.activeNotebookId,
24189
- onClick: () => {
24190
- controller.notebooks.select(item.id);
24191
- setOpen(false);
24192
- onOpen();
24193
- trigger.current?.focus();
24194
- },
24195
- children: [/* @__PURE__ */ jsx("span", {
24207
+ const close = () => {
24208
+ setOpen(false);
24209
+ trigger.current?.focus();
24210
+ };
24211
+ return /* @__PURE__ */ jsxs("div", {
24212
+ className,
24213
+ "data-open": open ? "" : void 0,
24214
+ ref: root,
24215
+ onKeyDown: (event) => {
24216
+ event.stopPropagation();
24217
+ if (event.key === "Escape") {
24218
+ event.preventDefault();
24219
+ close();
24220
+ }
24221
+ },
24222
+ children: [/* @__PURE__ */ jsx("button", {
24223
+ ...triggerProps,
24224
+ ref: trigger,
24225
+ type: "button",
24226
+ "aria-label": triggerLabel,
24227
+ "aria-expanded": open,
24228
+ "aria-controls": menuId,
24229
+ "aria-haspopup": "true",
24230
+ onClick: () => setOpen((value) => !value),
24231
+ children
24232
+ }), open && /* @__PURE__ */ jsxs("div", {
24233
+ className: "scrawl-board__navigator-menu",
24234
+ id: menuId,
24235
+ role: "group",
24236
+ "aria-label": label,
24237
+ children: [/* @__PURE__ */ jsx("div", {
24238
+ className: "scrawl-board__navigator-options",
24239
+ children: items.map((item) => /* @__PURE__ */ jsxs("div", {
24240
+ className: "scrawl-board__page-row",
24241
+ "data-active": item.id === activeId,
24242
+ children: [/* @__PURE__ */ jsxs("button", {
24243
+ className: "scrawl-board__page-select",
24244
+ type: "button",
24245
+ "aria-pressed": item.id === activeId,
24246
+ onClick: () => {
24247
+ onSelect(item.id);
24248
+ close();
24249
+ },
24250
+ children: [
24251
+ item.color !== void 0 ? /* @__PURE__ */ jsx("span", {
24196
24252
  className: "scrawl-board__notebook-color",
24197
24253
  style: { background: item.color }
24198
- }), /* @__PURE__ */ jsx("span", { children: item.name })]
24199
- }), mutable && /* @__PURE__ */ jsx(NavigatorName, {
24200
- name: item.name,
24201
- kind: "Notebook",
24202
- onRename: (name) => controller.notebooks.rename(item.id, name)
24203
- })]
24204
- }, item.id))
24205
- }), /* @__PURE__ */ jsxs("button", {
24206
- className: "scrawl-board__add-notebook",
24207
- type: "button",
24208
- disabled: !mutable,
24209
- onClick: () => {
24210
- controller.notebooks.add();
24211
- setOpen(false);
24212
- onOpen();
24213
- trigger.current?.focus();
24214
- },
24215
- children: [/* @__PURE__ */ jsx(ToolIcon, { tool: "more" }), /* @__PURE__ */ jsx("span", { children: "New notebook" })]
24216
- })]
24254
+ }) : /* @__PURE__ */ jsx(ToolIcon, { tool: kind === "Section" ? "section" : "page" }),
24255
+ /* @__PURE__ */ jsx("span", {
24256
+ className: "scrawl-board__navigator-label",
24257
+ children: item.name
24258
+ }),
24259
+ item.id === activeId && /* @__PURE__ */ jsx(ToolIcon, { tool: "check" })
24260
+ ]
24261
+ }), mutable && /* @__PURE__ */ jsx(NavigatorName, {
24262
+ name: item.name,
24263
+ kind,
24264
+ onRename: (name) => onRename(item.id, name)
24265
+ })]
24266
+ }, item.id))
24267
+ }), /* @__PURE__ */ jsxs("button", {
24268
+ className: "scrawl-board__navigator-add",
24269
+ type: "button",
24270
+ disabled: !mutable,
24271
+ onClick: () => {
24272
+ onAdd();
24273
+ close();
24274
+ },
24275
+ children: [/* @__PURE__ */ jsx(ToolIcon, { tool: "more" }), /* @__PURE__ */ jsxs("span", { children: ["New ", kind.toLowerCase()] })]
24217
24276
  })]
24218
- })
24277
+ })]
24278
+ });
24279
+ }
24280
+ /**
24281
+ * The floating navigator: a notebook switcher in the top-left corner, and a
24282
+ * breadcrumb across the top whose notebook, section and page crumbs each open
24283
+ * their own menu (pick, rename, or add one).
24284
+ */
24285
+ function NotebookNavigation({ controller, snapshot }) {
24286
+ const notebook = snapshot.notebooks.find((item) => item.id === snapshot.activeNotebookId);
24287
+ const section = snapshot.sections.find((item) => item.id === snapshot.activeSectionId);
24288
+ const page = snapshot.pages.find((item) => item.id === snapshot.activePageId);
24289
+ const mutable = snapshot.status === "ready" && !snapshot.readOnly;
24290
+ const sections = snapshot.sections.filter((item) => item.notebookId === snapshot.activeNotebookId);
24291
+ const pages = snapshot.pages.filter((item) => item.sectionId === snapshot.activeSectionId);
24292
+ const notebookMenu = {
24293
+ items: snapshot.notebooks,
24294
+ activeId: snapshot.activeNotebookId,
24295
+ kind: "Notebook",
24296
+ mutable,
24297
+ onSelect: (id) => controller.notebooks.select(id),
24298
+ onAdd: () => controller.notebooks.add(),
24299
+ onRename: (id, name) => controller.notebooks.rename(id, name)
24300
+ };
24301
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(NavigatorMenu, {
24302
+ ...notebookMenu,
24303
+ label: "Notebooks",
24304
+ className: "scrawl-board__notebook-switcher",
24305
+ triggerLabel: "Choose notebook",
24306
+ triggerProps: { className: "scrawl-board__notebook-trigger" },
24307
+ children: [
24308
+ /* @__PURE__ */ jsx("span", {
24309
+ className: "scrawl-board__notebook-color",
24310
+ style: { background: notebook?.color }
24311
+ }),
24312
+ /* @__PURE__ */ jsx("span", {
24313
+ className: "scrawl-board__navigator-label",
24314
+ children: notebook?.name ?? "Notebooks"
24315
+ }),
24316
+ /* @__PURE__ */ jsx(ToolIcon, { tool: "chevronDown" })
24317
+ ]
24219
24318
  }), /* @__PURE__ */ jsx("nav", {
24220
24319
  className: "scrawl-board__breadcrumb",
24221
24320
  "aria-label": "Breadcrumb",
24222
24321
  children: /* @__PURE__ */ jsxs("ol", { children: [
24223
- /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
24224
- type: "button",
24225
- onClick: () => {
24226
- setOpen(true);
24227
- trigger.current?.focus();
24228
- },
24229
- children: notebook?.name ?? "Notebook"
24322
+ /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(NavigatorMenu, {
24323
+ ...notebookMenu,
24324
+ label: "Notebooks",
24325
+ className: "scrawl-board__crumb-group",
24326
+ triggerLabel: `Notebook: ${notebook?.name ?? "Notebook"}`,
24327
+ triggerProps: { className: "scrawl-board__crumb" },
24328
+ children: [/* @__PURE__ */ jsx(ToolIcon, { tool: "notebook" }), /* @__PURE__ */ jsx("span", {
24329
+ className: "scrawl-board__navigator-label",
24330
+ children: notebook?.name ?? "Notebook"
24331
+ })]
24230
24332
  }) }),
24231
- /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("span", {
24232
- "aria-hidden": "true",
24233
- children: "›"
24234
- }), /* @__PURE__ */ jsx("button", {
24235
- type: "button",
24236
- onClick: onOpen,
24237
- children: section?.name ?? "Section"
24333
+ /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx(ToolIcon, { tool: "chevronRight" }), /* @__PURE__ */ jsxs(NavigatorMenu, {
24334
+ label: "Sections",
24335
+ items: sections,
24336
+ activeId: snapshot.activeSectionId,
24337
+ kind: "Section",
24338
+ mutable: true,
24339
+ onSelect: (id) => controller.sections.select(id),
24340
+ onAdd: () => controller.sections.add(),
24341
+ onRename: (id, name) => controller.sections.rename(id, name),
24342
+ className: "scrawl-board__crumb-group",
24343
+ triggerLabel: `Section: ${section?.name ?? "Section"}`,
24344
+ triggerProps: { className: "scrawl-board__crumb" },
24345
+ children: [
24346
+ /* @__PURE__ */ jsx(ToolIcon, { tool: "section" }),
24347
+ /* @__PURE__ */ jsx("span", {
24348
+ className: "scrawl-board__navigator-label",
24349
+ children: section?.name ?? "Section"
24350
+ }),
24351
+ /* @__PURE__ */ jsx(ToolIcon, { tool: "chevronDown" })
24352
+ ]
24238
24353
  })] }),
24239
- /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("span", {
24240
- "aria-hidden": "true",
24241
- children: "›"
24242
- }), /* @__PURE__ */ jsx("span", {
24243
- "aria-current": "page",
24244
- children: page?.name ?? "Page"
24354
+ /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx(ToolIcon, { tool: "chevronRight" }), /* @__PURE__ */ jsxs(NavigatorMenu, {
24355
+ label: "Pages",
24356
+ items: pages,
24357
+ activeId: snapshot.activePageId,
24358
+ kind: "Page",
24359
+ mutable: true,
24360
+ onSelect: (id) => controller.pages.select(id),
24361
+ onAdd: () => controller.pages.add(),
24362
+ onRename: (id, name) => controller.pages.rename(id, name),
24363
+ className: "scrawl-board__crumb-group",
24364
+ triggerLabel: `Page: ${page?.name ?? "Page"}`,
24365
+ triggerProps: { className: "scrawl-board__crumb scrawl-board__crumb--current" },
24366
+ children: [
24367
+ /* @__PURE__ */ jsx(ToolIcon, { tool: "page" }),
24368
+ /* @__PURE__ */ jsx("span", {
24369
+ className: "scrawl-board__navigator-label",
24370
+ "aria-current": "page",
24371
+ children: page?.name ?? "Page"
24372
+ }),
24373
+ /* @__PURE__ */ jsx(ToolIcon, { tool: "chevronDown" })
24374
+ ]
24245
24375
  })] })
24246
24376
  ] })
24247
24377
  })] });
@@ -24330,14 +24460,6 @@ function PagesPanel({ controller, snapshot, open, onClose }) {
24330
24460
  onClick: () => controller.sections.add(),
24331
24461
  children: /* @__PURE__ */ jsx(ToolIcon, { tool: "more" })
24332
24462
  }),
24333
- /* @__PURE__ */ jsx("button", {
24334
- "aria-label": "New page",
24335
- "data-tooltip": "New page",
24336
- type: "button",
24337
- disabled: !mutable,
24338
- onClick: addPage,
24339
- children: /* @__PURE__ */ jsx(ToolIcon, { tool: "more" })
24340
- }),
24341
24463
  /* @__PURE__ */ jsx("button", {
24342
24464
  "aria-label": "Close pages",
24343
24465
  "data-tooltip": "Close pages",
@@ -24758,298 +24880,425 @@ async function insertImageFile(controller, file) {
24758
24880
  return id;
24759
24881
  }
24760
24882
  //#endregion
24761
- //#region src/ui/dialogs/inline-editing.tsx
24762
- /** Inline note, text, and table-cell editors — driven entirely by public controller events and commands. */
24763
- function InlineEditors({ controller, renderPortal }) {
24764
- const [editor, setEditor] = useState(null);
24765
- const anchorRef = useRef(null);
24766
- const textAreaRef = useRef(null);
24767
- const measureRef = useRef(null);
24768
- const coveredRow = editor?.kind === "table-cell" ? editor.row : void 0;
24769
- const coveredCol = editor?.kind === "table-cell" ? editor.col : void 0;
24770
- const coveredId = editor && editor.kind !== "note" ? editor.id : null;
24883
+ //#region src/ui/panels/board-panels.tsx
24884
+ /** A floating drawer under the header (a bottom sheet on narrow boards). */
24885
+ function BoardDrawer({ label, icon, subtitle, onClose, onMarkAllRead, markAllDisabled, footer, children }) {
24886
+ const headingId = useId();
24887
+ const ref = useRef(null);
24888
+ const closeRef = useRef(onClose);
24771
24889
  useEffect(() => {
24772
- if (!coveredId) return;
24773
- controller.view.setInlineEditing(coveredRow === void 0 ? { id: coveredId } : {
24774
- id: coveredId,
24775
- row: coveredRow,
24776
- col: coveredCol
24777
- });
24778
- return () => {
24779
- if (controller.getSnapshot().status !== "disposed") controller.view.setInlineEditing(null);
24890
+ closeRef.current = onClose;
24891
+ });
24892
+ useEffect(() => {
24893
+ ref.current?.querySelector("button:not(:disabled), [tabindex='0']")?.focus({ preventScroll: true });
24894
+ const outside = (event) => {
24895
+ const target = event.target;
24896
+ if (!ref.current?.contains(target) && !target.closest?.(".scrawl-board__actions")) closeRef.current();
24780
24897
  };
24781
- }, [
24782
- controller,
24783
- coveredId,
24784
- coveredRow,
24785
- coveredCol
24786
- ]);
24787
- const textValue = editor?.kind === "text" ? editor.text : null;
24788
- useLayoutEffect(() => {
24789
- const el = textAreaRef.current;
24790
- if (textValue === null || !el) return;
24791
- const cs = getComputedStyle(el);
24792
- measureRef.current ??= document.createElement("canvas").getContext("2d");
24793
- const measure = measureRef.current;
24794
- if (measure) {
24795
- measure.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
24796
- const widest = Math.max(...textValue.split("\n").map((line) => measure.measureText(line || " ").width));
24797
- const chrome = parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight) + parseFloat(cs.borderLeftWidth) + parseFloat(cs.borderRightWidth);
24798
- const cap = Math.min(window.innerWidth * .6, 640);
24799
- el.style.width = `${Math.ceil(Math.min(cap, widest + chrome + 4))}px`;
24800
- }
24801
- el.style.height = "auto";
24802
- el.style.height = `${el.scrollHeight + parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth)}px`;
24803
- }, [textValue, editor?.kind === "text" ? editor.fontSizePx : null]);
24804
- /** Portal children are positioned relative to the Board root, not the viewport. */
24805
- const rootOffset = () => {
24806
- const rootRect = (anchorRef.current?.closest("[data-scrawl-root]"))?.getBoundingClientRect();
24807
- return {
24808
- left: rootRect?.left ?? 0,
24809
- top: rootRect?.top ?? 0
24898
+ const escape = (event) => {
24899
+ if (event.key !== "Escape" || event.defaultPrevented || document.activeElement && document.activeElement !== document.body) return;
24900
+ event.preventDefault();
24901
+ closeRef.current();
24902
+ };
24903
+ document.addEventListener("pointerdown", outside);
24904
+ document.addEventListener("keydown", escape);
24905
+ return () => {
24906
+ document.removeEventListener("pointerdown", outside);
24907
+ document.removeEventListener("keydown", escape);
24810
24908
  };
24909
+ }, []);
24910
+ return /* @__PURE__ */ jsxs("section", {
24911
+ "aria-labelledby": headingId,
24912
+ className: "scrawl-board__drawer",
24913
+ ref,
24914
+ role: "dialog",
24915
+ tabIndex: -1,
24916
+ onKeyDown: (event) => {
24917
+ event.stopPropagation();
24918
+ if (event.key === "Escape") {
24919
+ event.preventDefault();
24920
+ onClose();
24921
+ }
24922
+ },
24923
+ children: [
24924
+ /* @__PURE__ */ jsxs("header", {
24925
+ className: "scrawl-board__drawer-header",
24926
+ children: [
24927
+ icon && /* @__PURE__ */ jsx("span", {
24928
+ "aria-hidden": "true",
24929
+ className: "scrawl-board__drawer-icon",
24930
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: icon })
24931
+ }),
24932
+ /* @__PURE__ */ jsxs("div", {
24933
+ className: "scrawl-board__drawer-titles",
24934
+ children: [/* @__PURE__ */ jsx("h2", {
24935
+ id: headingId,
24936
+ children: label
24937
+ }), subtitle && /* @__PURE__ */ jsx("p", { children: subtitle })]
24938
+ }),
24939
+ onMarkAllRead && /* @__PURE__ */ jsx("button", {
24940
+ className: "scrawl-board__drawer-link",
24941
+ disabled: markAllDisabled,
24942
+ onClick: () => {
24943
+ onMarkAllRead();
24944
+ ref.current?.focus();
24945
+ },
24946
+ type: "button",
24947
+ children: "Mark all as read"
24948
+ }),
24949
+ /* @__PURE__ */ jsx("button", {
24950
+ "aria-label": `Close ${label}`,
24951
+ className: "scrawl-board__drawer-close",
24952
+ onClick: onClose,
24953
+ type: "button",
24954
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "close" })
24955
+ })
24956
+ ]
24957
+ }),
24958
+ /* @__PURE__ */ jsx("div", {
24959
+ className: "scrawl-board__drawer-body",
24960
+ children
24961
+ }),
24962
+ footer && /* @__PURE__ */ jsx("footer", {
24963
+ className: "scrawl-board__drawer-footer",
24964
+ children: footer
24965
+ })
24966
+ ]
24967
+ });
24968
+ }
24969
+ function DrawerEmpty({ icon, title, hint }) {
24970
+ return /* @__PURE__ */ jsxs("div", {
24971
+ className: "scrawl-board__drawer-empty",
24972
+ children: [
24973
+ /* @__PURE__ */ jsx("span", {
24974
+ "aria-hidden": "true",
24975
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: icon })
24976
+ }),
24977
+ /* @__PURE__ */ jsx("p", {
24978
+ className: "scrawl-board__drawer-empty-title",
24979
+ children: title
24980
+ }),
24981
+ /* @__PURE__ */ jsx("p", { children: hint })
24982
+ ]
24983
+ });
24984
+ }
24985
+ function CommentsDrawer({ controller, comments, unread, onMarkAllRead, onClose }) {
24986
+ return /* @__PURE__ */ jsx(BoardDrawer, {
24987
+ icon: "comment",
24988
+ label: "Comments",
24989
+ markAllDisabled: unread === 0,
24990
+ onClose,
24991
+ onMarkAllRead,
24992
+ children: comments.length === 0 ? /* @__PURE__ */ jsx(DrawerEmpty, {
24993
+ hint: "Pick the Comment tool and click the board to start a conversation.",
24994
+ icon: "comment",
24995
+ title: "No comments yet"
24996
+ }) : /* @__PURE__ */ jsx("ul", {
24997
+ className: "scrawl-board__drawer-list",
24998
+ children: comments.map((comment) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
24999
+ "aria-label": `Open comment by ${comment.authorName}: ${comment.text}`,
25000
+ className: "scrawl-board__drawer-row",
25001
+ type: "button",
25002
+ onClick: () => {
25003
+ onClose();
25004
+ controller.view.centerOn({
25005
+ x: comment.x,
25006
+ y: comment.y
25007
+ });
25008
+ controller.comments.open(comment.id);
25009
+ },
25010
+ children: [/* @__PURE__ */ jsx("span", {
25011
+ "aria-hidden": "true",
25012
+ className: "scrawl-board__comment-avatar",
25013
+ style: { background: comment.authorColor },
25014
+ children: comment.authorName.slice(0, 1).toUpperCase()
25015
+ }), /* @__PURE__ */ jsxs("span", {
25016
+ className: "scrawl-board__drawer-row-text",
25017
+ children: [
25018
+ /* @__PURE__ */ jsxs("strong", { children: [comment.authorName, comment.resolved ? " · Resolved" : ""] }),
25019
+ /* @__PURE__ */ jsx("span", { children: comment.text }),
25020
+ comment.replies.length > 0 && /* @__PURE__ */ jsxs("small", { children: [
25021
+ comment.replies.length,
25022
+ " ",
25023
+ comment.replies.length === 1 ? "reply" : "replies"
25024
+ ] })
25025
+ ]
25026
+ })]
25027
+ }) }, comment.id))
25028
+ })
25029
+ });
25030
+ }
25031
+ var KIND_ICON = {
25032
+ added: "more",
25033
+ removed: "delete",
25034
+ updated: "marker",
25035
+ commented: "comment",
25036
+ shared: "share",
25037
+ exported: "export"
25038
+ };
25039
+ function timeLabels(at, now) {
25040
+ const date = new Date(at);
25041
+ const seconds = Math.max(0, Math.round((now - date.getTime()) / 1e3));
25042
+ return {
25043
+ relative: seconds < 45 ? "just now" : seconds < 3600 ? `${Math.round(seconds / 60)} min ago` : seconds < 86400 ? `${Math.round(seconds / 3600)} h ago` : `${Math.round(seconds / 86400)} d ago`,
25044
+ absolute: date.toLocaleString(void 0, {
25045
+ dateStyle: "medium",
25046
+ timeStyle: "short"
25047
+ })
24811
25048
  };
24812
- useEffect(() => controller.on("edit-request", (event) => {
24813
- const offset = rootOffset();
24814
- if (event.kind === "text" && event.id === null) {
24815
- const newId = controller.content.add({
24816
- type: "text",
24817
- x: event.boardX,
24818
- y: event.boardY,
24819
- text: "",
24820
- color: event.color,
24821
- fontSize: TEXT_DEFAULT_SIZE
24822
- });
24823
- controller.content.select([newId]);
24824
- setEditor({
24825
- ...event,
24826
- id: newId,
24827
- left: event.screenRect.left - offset.left,
24828
- top: event.screenRect.top - offset.top
24829
- });
24830
- return;
24831
- }
24832
- setEditor({
24833
- ...event,
24834
- left: event.screenRect.left - offset.left,
24835
- top: event.screenRect.top - offset.top
24836
- });
24837
- }), [controller]);
24838
- const commitNote = () => {
24839
- if (editor?.kind === "note") controller.content.update(editor.id, { text: editor.text });
24840
- setEditor(null);
24841
- };
24842
- const commitText = () => {
24843
- if (editor?.kind === "text" && editor.id !== null) {
24844
- const text = editor.text;
24845
- const existing = controller.query.get(editor.id);
24846
- if (!text.trim()) controller.content.remove([editor.id]);
24847
- else if (existing?.type !== "text" || existing.text !== text) controller.content.update(editor.id, { text });
24848
- }
24849
- setEditor(null);
24850
- };
24851
- const commitTableCell = () => {
24852
- if (editor?.kind === "table-cell") controller.content.table.commitCell(editor.id, editor.row, editor.col, editor.text);
24853
- setEditor(null);
24854
- };
24855
- useEffect(() => controller.on("page-changing", () => {
24856
- if (editor?.kind === "note") commitNote();
24857
- else if (editor?.kind === "text") commitText();
24858
- else if (editor?.kind === "table-cell") commitTableCell();
25049
+ }
25050
+ function ActivityDrawer({ entries, unread, onMarkAllRead, onJump, onClose }) {
25051
+ const [now] = useState(() => Date.now());
25052
+ return /* @__PURE__ */ jsx(BoardDrawer, {
25053
+ footer: /* @__PURE__ */ jsxs("span", { children: [
25054
+ entries.length,
25055
+ " ",
25056
+ entries.length === 1 ? "entry" : "entries"
25057
+ ] }),
25058
+ icon: "activity",
25059
+ label: "Recent Activities",
25060
+ markAllDisabled: unread === 0,
25061
+ onClose,
25062
+ onMarkAllRead,
25063
+ subtitle: "Track board actions",
25064
+ children: entries.length === 0 ? /* @__PURE__ */ jsx(DrawerEmpty, {
25065
+ hint: "Changes to this board will show up here.",
25066
+ icon: "activity",
25067
+ title: "No activity yet"
25068
+ }) : /* @__PURE__ */ jsx("ol", {
25069
+ className: "scrawl-board__drawer-list scrawl-board__timeline",
25070
+ children: entries.map((entry) => {
25071
+ const time = timeLabels(entry.at, now);
25072
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
25073
+ "aria-hidden": "true",
25074
+ className: "scrawl-board__timeline-icon",
25075
+ "data-kind": entry.kind,
25076
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: KIND_ICON[entry.kind] })
25077
+ }), /* @__PURE__ */ jsxs("span", {
25078
+ className: "scrawl-board__drawer-row-text",
25079
+ children: [
25080
+ /* @__PURE__ */ jsx("strong", { children: entry.title }),
25081
+ entry.actorName && /* @__PURE__ */ jsxs("span", { children: ["by ", entry.actorName] }),
25082
+ /* @__PURE__ */ jsx("small", { children: /* @__PURE__ */ jsxs("time", {
25083
+ dateTime: new Date(entry.at).toISOString(),
25084
+ children: [
25085
+ time.relative,
25086
+ " · ",
25087
+ time.absolute
25088
+ ]
25089
+ }) })
25090
+ ]
25091
+ })] });
25092
+ return /* @__PURE__ */ jsx("li", { children: entry.location ? /* @__PURE__ */ jsx("button", {
25093
+ "aria-label": `${entry.title}${entry.actorName ? ` by ${entry.actorName}` : ""} — show on board`,
25094
+ className: "scrawl-board__drawer-row",
25095
+ type: "button",
25096
+ onClick: () => {
25097
+ onClose();
25098
+ onJump(entry.location);
25099
+ },
25100
+ children: content
25101
+ }) : /* @__PURE__ */ jsx("div", {
25102
+ className: "scrawl-board__drawer-row",
25103
+ children: content
25104
+ }) }, entry.id);
25105
+ })
25106
+ })
25107
+ });
25108
+ }
25109
+ var ROLE_LABEL = {
25110
+ viewer: "View",
25111
+ editor: "Edit",
25112
+ admin: "Admin"
25113
+ };
25114
+ var ROLES = [
25115
+ "viewer",
25116
+ "editor",
25117
+ "admin"
25118
+ ];
25119
+ function PermissionsDrawer({ permissions, onClose }) {
25120
+ const [pending, setPending] = useState({});
25121
+ const saveRef = useRef(null);
25122
+ const [status, setStatus] = useState({ kind: "idle" });
25123
+ const changes = permissions.members.filter((member) => pending[member.id] && pending[member.id] !== member.role).map((member) => ({
25124
+ memberId: member.id,
25125
+ role: pending[member.id]
24859
25126
  }));
24860
- const navigateTableCell = (dRow, dCol) => {
24861
- if (editor?.kind !== "table-cell") return;
24862
- controller.content.table.commitCell(editor.id, editor.row, editor.col, editor.text);
24863
- const table = controller.query.get(editor.id);
24864
- if (!table || table.type !== "table") {
24865
- setEditor(null);
24866
- return;
24867
- }
24868
- let nextCol = editor.col + dCol;
24869
- let nextRow = editor.row + dRow;
24870
- if (nextCol >= table.cols) {
24871
- nextCol = 0;
24872
- nextRow += 1;
24873
- } else if (nextCol < 0) {
24874
- nextCol = table.cols - 1;
24875
- nextRow -= 1;
25127
+ useEffect(() => {
25128
+ if (status.kind !== "saved") return;
25129
+ const timer = setTimeout(() => setStatus({ kind: "idle" }), 1800);
25130
+ return () => clearTimeout(timer);
25131
+ }, [status]);
25132
+ const save = async () => {
25133
+ setStatus({ kind: "saving" });
25134
+ try {
25135
+ await permissions.onSave(changes);
25136
+ setPending({});
25137
+ setStatus({ kind: "saved" });
25138
+ saveRef.current?.closest(".scrawl-board__drawer")?.focus();
25139
+ } catch (error) {
25140
+ setStatus({
25141
+ kind: "error",
25142
+ message: error instanceof Error ? error.message : "Your changes weren't saved."
25143
+ });
24876
25144
  }
24877
- if (nextRow >= table.rows) nextRow = 0;
24878
- if (nextRow < 0) nextRow = table.rows - 1;
24879
- let leftBoard = table.x;
24880
- for (let c = 0; c < nextCol; c++) leftBoard += table.colWidths[c] ?? 8;
24881
- let topBoard = table.y;
24882
- for (let r = 0; r < nextRow; r++) topBoard -= table.rowHeights[r] ?? 3.6;
24883
- const colWidth = table.colWidths[nextCol] ?? 8;
24884
- const rowHeight = table.rowHeights[nextRow] ?? 3.6;
24885
- const topLeft = controller.view.boardToScreen({
24886
- x: leftBoard,
24887
- y: topBoard
24888
- });
24889
- const bottomRight = controller.view.boardToScreen({
24890
- x: leftBoard + colWidth,
24891
- y: topBoard - rowHeight
24892
- });
24893
- const offset = rootOffset();
24894
- setEditor({
24895
- kind: "table-cell",
24896
- id: editor.id,
24897
- row: nextRow,
24898
- col: nextCol,
24899
- text: table.cells[`${nextRow},${nextCol}`] ?? "",
24900
- fontSizePx: editor.fontSizePx,
24901
- screenRect: {
24902
- left: topLeft.x,
24903
- top: topLeft.y,
24904
- width: Math.max(40, bottomRight.x - topLeft.x),
24905
- height: Math.max(24, bottomRight.y - topLeft.y)
24906
- },
24907
- left: topLeft.x - offset.left,
24908
- top: topLeft.y - offset.top
24909
- });
24910
25145
  };
24911
- const focusEditor = useCallback((el) => {
24912
- textAreaRef.current = el;
24913
- if (!el) return;
24914
- const doFocus = () => {
24915
- try {
24916
- el.focus({ preventScroll: true });
24917
- el.setSelectionRange(el.value.length, el.value.length);
24918
- } catch {}
25146
+ return /* @__PURE__ */ jsxs(BoardDrawer, {
25147
+ icon: "shield",
25148
+ label: "Pod Member Permissions",
25149
+ onClose,
25150
+ subtitle: "Manage whiteboard access",
25151
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
25152
+ "aria-live": "polite",
25153
+ className: "scrawl-board__drawer-status",
25154
+ "data-kind": status.kind,
25155
+ children: status.kind === "saved" ? "Saved" : status.kind === "error" ? status.message : status.kind === "saving" ? "Saving…" : ""
25156
+ }), /* @__PURE__ */ jsx("button", {
25157
+ className: "scrawl-board__drawer-primary",
25158
+ ref: saveRef,
25159
+ disabled: changes.length === 0 || status.kind === "saving",
25160
+ onClick: () => void save(),
25161
+ type: "button",
25162
+ children: "Save Permissions"
25163
+ })] }),
25164
+ children: [/* @__PURE__ */ jsxs("dl", {
25165
+ className: "scrawl-board__role-legend",
25166
+ children: [
25167
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "View Only" }), /* @__PURE__ */ jsx("dd", { children: "Can see the board but not change it." })] }),
25168
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Edit Access" }), /* @__PURE__ */ jsx("dd", { children: "Can draw, add and change content." })] }),
25169
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Admin Access" }), /* @__PURE__ */ jsx("dd", { children: "Can edit and manage who has access." })] })
25170
+ ]
25171
+ }), /* @__PURE__ */ jsx("ul", {
25172
+ className: "scrawl-board__member-list",
25173
+ children: permissions.members.map((member) => {
25174
+ const role = pending[member.id] ?? member.role;
25175
+ const isYou = member.id === permissions.currentUserId;
25176
+ return /* @__PURE__ */ jsxs("li", {
25177
+ className: "scrawl-board__member",
25178
+ children: [
25179
+ member.avatarUrl ? /* @__PURE__ */ jsx("img", {
25180
+ alt: "",
25181
+ className: "scrawl-board__member-avatar",
25182
+ src: member.avatarUrl
25183
+ }) : /* @__PURE__ */ jsx("span", {
25184
+ "aria-hidden": "true",
25185
+ className: "scrawl-board__member-avatar",
25186
+ children: initials(member.name)
25187
+ }),
25188
+ /* @__PURE__ */ jsxs("div", {
25189
+ className: "scrawl-board__member-info",
25190
+ children: [/* @__PURE__ */ jsxs("p", { children: [
25191
+ /* @__PURE__ */ jsx("strong", { children: member.name }),
25192
+ isYou && /* @__PURE__ */ jsx("span", {
25193
+ className: "scrawl-board__member-you",
25194
+ children: " (you)"
25195
+ }),
25196
+ member.online !== void 0 && /* @__PURE__ */ jsx("span", {
25197
+ className: "scrawl-board__member-status",
25198
+ "data-online": member.online,
25199
+ children: member.online ? "Online" : "Offline"
25200
+ })
25201
+ ] }), member.email && /* @__PURE__ */ jsx("p", {
25202
+ className: "scrawl-board__member-email",
25203
+ children: member.email
25204
+ })]
25205
+ }),
25206
+ /* @__PURE__ */ jsx("div", {
25207
+ "aria-label": `Access for ${member.name}`,
25208
+ className: "scrawl-board__segmented",
25209
+ role: "radiogroup",
25210
+ children: ROLES.map((option) => /* @__PURE__ */ jsx("button", {
25211
+ "aria-checked": role === option,
25212
+ disabled: isYou,
25213
+ role: "radio",
25214
+ type: "button",
25215
+ onClick: () => {
25216
+ setStatus({ kind: "idle" });
25217
+ setPending((current) => ({
25218
+ ...current,
25219
+ [member.id]: option
25220
+ }));
25221
+ },
25222
+ children: ROLE_LABEL[option]
25223
+ }, option))
25224
+ })
25225
+ ]
25226
+ }, member.id);
25227
+ })
25228
+ })]
25229
+ });
25230
+ }
25231
+ function initials(name) {
25232
+ return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0].toUpperCase()).join("");
25233
+ }
25234
+ /** The ⋮ overflow menu in the board actions. */
25235
+ function BoardMoreMenu({ items, onSelect, onClose }) {
25236
+ const ref = useRef(null);
25237
+ const closeRef = useRef(onClose);
25238
+ useEffect(() => {
25239
+ closeRef.current = onClose;
25240
+ });
25241
+ useEffect(() => {
25242
+ ref.current?.querySelector("button")?.focus({ preventScroll: true });
25243
+ const outside = (event) => {
25244
+ const target = event.target;
25245
+ if (!ref.current?.contains(target) && !target.closest?.("[aria-label=\"More board actions\"]")) closeRef.current();
24919
25246
  };
24920
- doFocus();
24921
- requestAnimationFrame(doFocus);
25247
+ document.addEventListener("pointerdown", outside);
25248
+ return () => document.removeEventListener("pointerdown", outside);
24922
25249
  }, []);
24923
- if (!editor) return /* @__PURE__ */ jsx("div", {
24924
- ref: anchorRef,
24925
- style: { display: "none" }
24926
- });
24927
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
24928
- ref: anchorRef,
24929
- style: { display: "none" }
24930
- }), renderPortal(editor.kind === "note" ? /* @__PURE__ */ jsx("textarea", {
24931
- "aria-label": "Edit note",
24932
- autoFocus: true,
24933
- className: "scrawl-board__inline-editor scrawl-board__inline-editor--note",
24934
- onBlur: commitNote,
24935
- onChange: (event) => setEditor({
24936
- ...editor,
24937
- text: event.currentTarget.value
24938
- }),
24939
- onKeyDown: (event) => {
24940
- event.stopPropagation();
24941
- if (event.key === "Escape") commitNote();
24942
- if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) commitNote();
24943
- },
24944
- ref: focusEditor,
24945
- style: {
24946
- left: editor.left,
24947
- top: editor.top,
24948
- width: editor.screenRect.width,
24949
- height: editor.screenRect.height,
24950
- backgroundColor: editor.color,
24951
- fontSize: Math.max(12, editor.screenRect.width * 42 / 512),
24952
- lineHeight: 1.3,
24953
- padding: editor.screenRect.width * 40 / 512
24954
- },
24955
- value: editor.text
24956
- }) : editor.kind === "text" ? /* @__PURE__ */ jsx("textarea", {
24957
- "aria-label": "Edit text",
24958
- autoFocus: true,
24959
- className: "scrawl-board__inline-editor scrawl-board__inline-editor--text",
24960
- onBlur: commitText,
24961
- onChange: (event) => setEditor({
24962
- ...editor,
24963
- text: event.currentTarget.value
24964
- }),
25250
+ return /* @__PURE__ */ jsx("div", {
25251
+ "aria-label": "More board actions",
25252
+ className: "scrawl-board__more-menu",
25253
+ ref,
25254
+ role: "menu",
24965
25255
  onKeyDown: (event) => {
24966
25256
  event.stopPropagation();
24967
- if (event.key === "Escape") commitText();
24968
- if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) commitText();
24969
- },
24970
- ref: focusEditor,
24971
- rows: Math.max(1, editor.text.split("\n").length),
24972
- style: {
24973
- left: editor.left,
24974
- top: editor.top,
24975
- color: editor.color,
24976
- fontSize: Math.max(12, editor.fontSizePx)
24977
- },
24978
- value: editor.text
24979
- }) : /* @__PURE__ */ jsxs("div", {
24980
- className: "scrawl-board__inline-editor--table-cell",
24981
- style: {
24982
- left: editor.left,
24983
- top: editor.top,
24984
- width: editor.screenRect.width,
24985
- minHeight: editor.screenRect.height
25257
+ if (event.key === "Escape") {
25258
+ event.preventDefault();
25259
+ onClose();
25260
+ }
25261
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
25262
+ event.preventDefault();
25263
+ const buttons = [...ref.current?.querySelectorAll("button") ?? []];
25264
+ buttons[(buttons.indexOf(document.activeElement) + (event.key === "ArrowDown" ? 1 : -1) + buttons.length) % buttons.length]?.focus();
25265
+ }
24986
25266
  },
24987
- children: [/* @__PURE__ */ jsx("textarea", {
24988
- "aria-label": `Edit table cell ${editor.row + 1}, ${editor.col + 1}`,
24989
- autoFocus: true,
24990
- className: "scrawl-board__inline-editor",
24991
- onBlur: commitTableCell,
24992
- onChange: (event) => setEditor({
24993
- ...editor,
24994
- text: event.currentTarget.value
24995
- }),
24996
- onKeyDown: (event) => {
24997
- event.stopPropagation();
24998
- if (event.key === "Tab") {
24999
- event.preventDefault();
25000
- navigateTableCell(0, event.shiftKey ? -1 : 1);
25001
- } else if (event.key === "Enter" && !event.shiftKey) {
25002
- event.preventDefault();
25003
- navigateTableCell(1, 0);
25004
- } else if (event.key === "Escape") commitTableCell();
25267
+ children: items.map((item) => /* @__PURE__ */ jsxs("button", {
25268
+ onClick: () => {
25269
+ onClose();
25270
+ onSelect(item.id);
25005
25271
  },
25006
- ref: focusEditor,
25007
- style: { fontSize: Math.max(11, editor.fontSizePx) },
25008
- value: editor.text
25009
- }), /* @__PURE__ */ jsxs("div", {
25010
- className: "scrawl-board__inline-editor-actions",
25011
- children: [
25012
- /* @__PURE__ */ jsxs("span", { children: [
25013
- "Cell (",
25014
- editor.row + 1,
25015
- ", ",
25016
- editor.col + 1,
25017
- ")"
25018
- ] }),
25019
- /* @__PURE__ */ jsx("button", {
25020
- "aria-label": "Add row",
25021
- onMouseDown: (event) => {
25022
- event.preventDefault();
25023
- controller.content.table.addRow(editor.id);
25024
- },
25025
- "data-tooltip": "Add row",
25026
- type: "button",
25027
- children: /* @__PURE__ */ jsx(ToolIcon, { tool: "addRow" })
25028
- }),
25029
- /* @__PURE__ */ jsx("button", {
25030
- "aria-label": "Add column",
25031
- onMouseDown: (event) => {
25032
- event.preventDefault();
25033
- controller.content.table.addCol(editor.id);
25034
- },
25035
- "data-tooltip": "Add column",
25036
- type: "button",
25037
- children: /* @__PURE__ */ jsx(ToolIcon, { tool: "addColumn" })
25038
- }),
25039
- /* @__PURE__ */ jsx("button", {
25040
- "aria-label": "Delete row",
25041
- onMouseDown: (event) => {
25042
- event.preventDefault();
25043
- controller.content.table.deleteRow(editor.id, editor.row);
25044
- setEditor(null);
25045
- },
25046
- "data-tooltip": "Delete row",
25047
- type: "button",
25048
- children: /* @__PURE__ */ jsx(ToolIcon, { tool: "delete" })
25049
- })
25050
- ]
25051
- })]
25052
- }))] });
25272
+ role: "menuitem",
25273
+ type: "button",
25274
+ children: [item.icon ?? /* @__PURE__ */ jsx("span", {
25275
+ "aria-hidden": "true",
25276
+ className: "scrawl-board__menu-icon-space"
25277
+ }), /* @__PURE__ */ jsx("span", { children: item.label })]
25278
+ }, item.id))
25279
+ });
25280
+ }
25281
+ /** A persistent pin where the last activity jump landed, with a pulse on arrival. */
25282
+ function JumpPin({ controller, location, arrival }) {
25283
+ const ref = useRef(null);
25284
+ useLayoutEffect(() => {
25285
+ const place = () => {
25286
+ const pin = ref.current;
25287
+ if (!pin) return;
25288
+ const root = pin.closest("[data-scrawl-root]")?.getBoundingClientRect();
25289
+ const screen = controller.view.boardToScreen(location);
25290
+ pin.style.left = `${screen.x - (root?.left ?? 0)}px`;
25291
+ pin.style.top = `${screen.y - (root?.top ?? 0)}px`;
25292
+ };
25293
+ place();
25294
+ return controller.on("view-change", place);
25295
+ }, [controller, location]);
25296
+ return /* @__PURE__ */ jsx("span", {
25297
+ "aria-hidden": "true",
25298
+ className: "scrawl-board__jump-pin",
25299
+ ref,
25300
+ children: /* @__PURE__ */ jsx("span", { className: "scrawl-board__jump-pulse" }, arrival)
25301
+ });
25053
25302
  }
25054
25303
  //#endregion
25055
25304
  //#region src/ui/toolbar/compact-picker.tsx
@@ -25190,9 +25439,11 @@ var HANDLE_CORNERS = [
25190
25439
  * Floating toolbar above the focused note, shape, text block, or semantic
25191
25440
  * Rectangle/Ellipse — Colour, Size (note/text) or Width (shape), Fill/Stroke
25192
25441
  * (Rectangle/Ellipse), Lock/Unlock, Duplicate, Delete. Images (photos and
25193
- * placed emoji/icons) get Lock/Duplicate/Delete only — the native gizmo
25194
- * already resizes them. Table/timer/custom objects and plain (non-shape) ink
25195
- * strokes never get a toolbar here — out of scope for this destination.
25442
+ * placed emoji/icons) and tables get Lock/Duplicate/Delete only — each
25443
+ * already has its own resize affordance (the native gizmo for images, edge
25444
+ * handles for tables), and a table's row/column actions live in its cell
25445
+ * editor. Timer/custom objects and plain (non-shape) ink strokes never get a
25446
+ * toolbar here — out of scope for this destination.
25196
25447
  *
25197
25448
  * Rectangle/Ellipse (Phase 2) are a second, parallel "shape" concept from
25198
25449
  * the legacy ink-stroke shape below: both draw from the same toolbar
@@ -25256,7 +25507,8 @@ function FocusedItemToolbar({ controller, snapshot }) {
25256
25507
  locked: focused.locked,
25257
25508
  anchor: focused.screenPosition
25258
25509
  }, obj.id);
25259
- if (obj.type === "image") return /* @__PURE__ */ jsx(ToolbarShell, {
25510
+ if (obj.type === "table" && snapshot.inlineEditing?.id === obj.id) return null;
25511
+ if (obj.type === "image" || obj.type === "table") return /* @__PURE__ */ jsx(ToolbarShell, {
25260
25512
  anchor: focused.screenPosition,
25261
25513
  children: /* @__PURE__ */ jsx(LockDuplicateDelete, {
25262
25514
  controller,
@@ -25277,8 +25529,8 @@ function ToolbarShell({ anchor, children }) {
25277
25529
  const board = root?.getBoundingClientRect();
25278
25530
  const left = Math.max(8, board?.left ?? 0);
25279
25531
  const right = Math.min(window.innerWidth, board?.right ?? window.innerWidth) - 8;
25280
- const header = root?.querySelector(".scrawl-board__header")?.getBoundingClientRect();
25281
- const top = Math.max(8, header ? header.bottom + 8 : board?.top ?? 0);
25532
+ const navigator = root?.querySelector(".scrawl-board__breadcrumb ol")?.getBoundingClientRect();
25533
+ const top = Math.max(8, navigator ? navigator.bottom + 8 : board?.top ?? 0);
25282
25534
  toolbar.style.left = `${Math.max(left, Math.min(anchor.x - toolbar.offsetWidth / 2, right - toolbar.offsetWidth))}px`;
25283
25535
  toolbar.style.top = `${Math.max(top, anchor.y - toolbar.offsetHeight - 12)}px`;
25284
25536
  };
@@ -25637,140 +25889,451 @@ function ShapeObjectToolbar({ controller, snapshot, shape, locked, anchor }) {
25637
25889
  /* @__PURE__ */ jsxs(ToolbarShell, {
25638
25890
  anchor,
25639
25891
  children: [
25640
- /* @__PURE__ */ jsx(ColorPicker, {
25641
- label: "Shape fill",
25642
- color: shape.fill,
25643
- colors: INK_COLORS,
25644
- disabled: locked_,
25645
- itemLabel: "fill",
25646
- allowNone: true,
25647
- onChange: (value) => controller.content.update(shape.id, { fill: value })
25892
+ /* @__PURE__ */ jsx(ColorPicker, {
25893
+ label: "Shape fill",
25894
+ color: shape.fill,
25895
+ colors: INK_COLORS,
25896
+ disabled: locked_,
25897
+ itemLabel: "fill",
25898
+ allowNone: true,
25899
+ onChange: (value) => controller.content.update(shape.id, { fill: value })
25900
+ }),
25901
+ /* @__PURE__ */ jsx(ColorPicker, {
25902
+ label: "Shape stroke",
25903
+ color: shape.stroke,
25904
+ colors: INK_COLORS,
25905
+ disabled: locked_,
25906
+ itemLabel: "stroke",
25907
+ onChange: (value) => controller.content.update(shape.id, { stroke: value })
25908
+ }),
25909
+ /* @__PURE__ */ jsx(LockDuplicateDelete, {
25910
+ controller,
25911
+ mutable,
25912
+ locked,
25913
+ ids: [shape.id]
25914
+ })
25915
+ ]
25916
+ })
25917
+ ] });
25918
+ }
25919
+ /** One corner: drags the board point directly under the pointer, keeping the opposite corner fixed — independent width/height, no proportional lock. */
25920
+ function ShapeResizeHandle({ controller, shape, corner, x, y }) {
25921
+ const [dragging, setDragging] = useState(false);
25922
+ const fixedCorner = () => {
25923
+ return {
25924
+ x: corner === "nw" || corner === "sw" ? shape.x + shape.width : shape.x,
25925
+ y: corner === "nw" || corner === "ne" ? shape.y - shape.height : shape.y
25926
+ };
25927
+ };
25928
+ const resizeTo = (boardX, boardY) => {
25929
+ const fixed = fixedCorner();
25930
+ const width = Math.max(SHAPE_MIN_SIZE, Math.abs(boardX - fixed.x));
25931
+ const height = Math.max(SHAPE_MIN_SIZE, Math.abs(boardY - fixed.y));
25932
+ const nextX = Math.min(boardX, fixed.x);
25933
+ const nextY = Math.max(boardY, fixed.y);
25934
+ controller.content.update(shape.id, {
25935
+ x: nextX,
25936
+ y: nextY,
25937
+ width,
25938
+ height
25939
+ });
25940
+ };
25941
+ const nudge = (dx, dy) => {
25942
+ resizeTo((corner === "ne" || corner === "se" ? shape.x + shape.width : shape.x) + dx, (corner === "nw" || corner === "ne" ? shape.y : shape.y - shape.height) + dy);
25943
+ };
25944
+ return /* @__PURE__ */ jsx("span", {
25945
+ "aria-label": `Resize shape (${corner})`,
25946
+ role: "slider",
25947
+ "aria-valuemin": SHAPE_MIN_SIZE,
25948
+ "aria-valuemax": 9999,
25949
+ "aria-valuenow": Math.round(shape.width),
25950
+ tabIndex: 0,
25951
+ className: "scrawl-board__focus-handle",
25952
+ "data-orientation": corner === "nw" || corner === "se" ? "nwse" : "nesw",
25953
+ style: {
25954
+ left: x,
25955
+ top: y
25956
+ },
25957
+ onKeyDown: (event) => {
25958
+ if (event.key === "ArrowUp") {
25959
+ event.preventDefault();
25960
+ nudge(0, .5);
25961
+ } else if (event.key === "ArrowDown") {
25962
+ event.preventDefault();
25963
+ nudge(0, -.5);
25964
+ } else if (event.key === "ArrowRight") {
25965
+ event.preventDefault();
25966
+ nudge(.5, 0);
25967
+ } else if (event.key === "ArrowLeft") {
25968
+ event.preventDefault();
25969
+ nudge(-.5, 0);
25970
+ }
25971
+ },
25972
+ onPointerDown: (event) => {
25973
+ event.stopPropagation();
25974
+ event.currentTarget.setPointerCapture(event.pointerId);
25975
+ setDragging(true);
25976
+ },
25977
+ onPointerMove: (event) => {
25978
+ if (!dragging) return;
25979
+ const board = controller.view.screenToBoard({
25980
+ x: event.clientX,
25981
+ y: event.clientY
25982
+ });
25983
+ resizeTo(board.x, board.y);
25984
+ },
25985
+ onPointerUp: (event) => {
25986
+ event.currentTarget.releasePointerCapture(event.pointerId);
25987
+ setDragging(false);
25988
+ }
25989
+ });
25990
+ }
25991
+ function LockDuplicateDelete({ controller, mutable, locked, ids }) {
25992
+ return /* @__PURE__ */ jsxs("div", {
25993
+ className: "scrawl-board__style-group",
25994
+ children: [
25995
+ /* @__PURE__ */ jsx("button", {
25996
+ "aria-label": locked ? "Unlock" : "Lock",
25997
+ disabled: !mutable,
25998
+ onClick: () => {
25999
+ for (const id of ids) controller.content.update(id, locked ? { locked: false } : { locked: true });
26000
+ },
26001
+ "data-tooltip": locked ? "Unlock" : "Lock",
26002
+ type: "button",
26003
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: locked ? "lock" : "unlock" })
26004
+ }),
26005
+ /* @__PURE__ */ jsx("button", {
26006
+ "aria-label": "Duplicate",
26007
+ disabled: !mutable || locked,
26008
+ onClick: () => {
26009
+ const newIds = controller.content.duplicate(ids);
26010
+ if (newIds.length) controller.content.select(newIds);
26011
+ },
26012
+ "data-tooltip": "Duplicate",
26013
+ type: "button",
26014
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "duplicate" })
26015
+ }),
26016
+ /* @__PURE__ */ jsx("button", {
26017
+ "aria-label": "Delete",
26018
+ disabled: !mutable || locked,
26019
+ onClick: () => controller.content.remove(ids),
26020
+ "data-tooltip": "Delete",
26021
+ type: "button",
26022
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "delete" })
26023
+ })
26024
+ ]
26025
+ });
26026
+ }
26027
+ //#endregion
26028
+ //#region src/ui/dialogs/inline-editing.tsx
26029
+ /** Inline note, text, and table-cell editors — driven entirely by public controller events and commands. */
26030
+ function InlineEditors({ controller, renderPortal }) {
26031
+ const [editor, setEditor] = useState(null);
26032
+ const [snapshot, setSnapshot] = useState(() => controller.getSnapshot());
26033
+ useEffect(() => controller.subscribe(() => setSnapshot(controller.getSnapshot())), [controller]);
26034
+ const mutable = snapshot.status === "ready" && !snapshot.readOnly;
26035
+ const editedTable = editor?.kind === "table-cell" ? controller.query.get(editor.id) : void 0;
26036
+ const locked = editedTable?.type === "table" && Boolean(editedTable.locked);
26037
+ const anchorRef = useRef(null);
26038
+ const textAreaRef = useRef(null);
26039
+ const measureRef = useRef(null);
26040
+ const coveredRow = editor?.kind === "table-cell" ? editor.row : void 0;
26041
+ const coveredCol = editor?.kind === "table-cell" ? editor.col : void 0;
26042
+ const coveredId = editor && editor.kind !== "note" ? editor.id : null;
26043
+ useEffect(() => {
26044
+ if (!coveredId) return;
26045
+ controller.view.setInlineEditing(coveredRow === void 0 ? { id: coveredId } : {
26046
+ id: coveredId,
26047
+ row: coveredRow,
26048
+ col: coveredCol
26049
+ });
26050
+ return () => {
26051
+ if (controller.getSnapshot().status !== "disposed") controller.view.setInlineEditing(null);
26052
+ };
26053
+ }, [
26054
+ controller,
26055
+ coveredId,
26056
+ coveredRow,
26057
+ coveredCol
26058
+ ]);
26059
+ const textValue = editor?.kind === "text" ? editor.text : null;
26060
+ useLayoutEffect(() => {
26061
+ const el = textAreaRef.current;
26062
+ if (textValue === null || !el) return;
26063
+ const cs = getComputedStyle(el);
26064
+ measureRef.current ??= document.createElement("canvas").getContext("2d");
26065
+ const measure = measureRef.current;
26066
+ if (measure) {
26067
+ measure.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
26068
+ const widest = Math.max(...textValue.split("\n").map((line) => measure.measureText(line || " ").width));
26069
+ const chrome = parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight) + parseFloat(cs.borderLeftWidth) + parseFloat(cs.borderRightWidth);
26070
+ const cap = Math.min(window.innerWidth * .6, 640);
26071
+ el.style.width = `${Math.ceil(Math.min(cap, widest + chrome + 4))}px`;
26072
+ }
26073
+ el.style.height = "auto";
26074
+ el.style.height = `${el.scrollHeight + parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth)}px`;
26075
+ }, [textValue, editor?.kind === "text" ? editor.fontSizePx : null]);
26076
+ /** Portal children are positioned relative to the Board root, not the viewport. */
26077
+ const rootOffset = () => {
26078
+ const rootRect = (anchorRef.current?.closest("[data-scrawl-root]"))?.getBoundingClientRect();
26079
+ return {
26080
+ left: rootRect?.left ?? 0,
26081
+ top: rootRect?.top ?? 0
26082
+ };
26083
+ };
26084
+ useEffect(() => controller.on("edit-request", (event) => {
26085
+ const offset = rootOffset();
26086
+ if (event.kind === "text" && event.id === null) {
26087
+ const newId = controller.content.add({
26088
+ type: "text",
26089
+ x: event.boardX,
26090
+ y: event.boardY,
26091
+ text: "",
26092
+ color: event.color,
26093
+ fontSize: TEXT_DEFAULT_SIZE
26094
+ });
26095
+ controller.content.select([newId]);
26096
+ setEditor({
26097
+ ...event,
26098
+ id: newId,
26099
+ left: event.screenRect.left - offset.left,
26100
+ top: event.screenRect.top - offset.top
26101
+ });
26102
+ return;
26103
+ }
26104
+ setEditor({
26105
+ ...event,
26106
+ left: event.screenRect.left - offset.left,
26107
+ top: event.screenRect.top - offset.top
26108
+ });
26109
+ }), [controller]);
26110
+ const commitNote = () => {
26111
+ if (editor?.kind === "note") controller.content.update(editor.id, { text: editor.text });
26112
+ setEditor(null);
26113
+ };
26114
+ const commitText = () => {
26115
+ if (editor?.kind === "text" && editor.id !== null) {
26116
+ const text = editor.text;
26117
+ const existing = controller.query.get(editor.id);
26118
+ if (!text.trim()) controller.content.remove([editor.id]);
26119
+ else if (existing?.type !== "text" || existing.text !== text) controller.content.update(editor.id, { text });
26120
+ }
26121
+ setEditor(null);
26122
+ };
26123
+ const commitTableCell = () => {
26124
+ if (editor?.kind === "table-cell") controller.content.table.commitCell(editor.id, editor.row, editor.col, editor.text);
26125
+ setEditor(null);
26126
+ };
26127
+ useEffect(() => controller.on("page-changing", () => {
26128
+ if (editor?.kind === "note") commitNote();
26129
+ else if (editor?.kind === "text") commitText();
26130
+ else if (editor?.kind === "table-cell") commitTableCell();
26131
+ }));
26132
+ const navigateTableCell = (dRow, dCol) => {
26133
+ if (editor?.kind !== "table-cell") return;
26134
+ controller.content.table.commitCell(editor.id, editor.row, editor.col, editor.text);
26135
+ const table = controller.query.get(editor.id);
26136
+ if (!table || table.type !== "table") {
26137
+ setEditor(null);
26138
+ return;
26139
+ }
26140
+ let nextCol = editor.col + dCol;
26141
+ let nextRow = editor.row + dRow;
26142
+ if (nextCol >= table.cols) {
26143
+ nextCol = 0;
26144
+ nextRow += 1;
26145
+ } else if (nextCol < 0) {
26146
+ nextCol = table.cols - 1;
26147
+ nextRow -= 1;
26148
+ }
26149
+ if (nextRow >= table.rows) nextRow = 0;
26150
+ if (nextRow < 0) nextRow = table.rows - 1;
26151
+ let leftBoard = table.x;
26152
+ for (let c = 0; c < nextCol; c++) leftBoard += table.colWidths[c] ?? 8;
26153
+ let topBoard = table.y;
26154
+ for (let r = 0; r < nextRow; r++) topBoard -= table.rowHeights[r] ?? 3.6;
26155
+ const colWidth = table.colWidths[nextCol] ?? 8;
26156
+ const rowHeight = table.rowHeights[nextRow] ?? 3.6;
26157
+ const topLeft = controller.view.boardToScreen({
26158
+ x: leftBoard,
26159
+ y: topBoard
26160
+ });
26161
+ const bottomRight = controller.view.boardToScreen({
26162
+ x: leftBoard + colWidth,
26163
+ y: topBoard - rowHeight
26164
+ });
26165
+ const offset = rootOffset();
26166
+ setEditor({
26167
+ kind: "table-cell",
26168
+ id: editor.id,
26169
+ row: nextRow,
26170
+ col: nextCol,
26171
+ text: table.cells[`${nextRow},${nextCol}`] ?? "",
26172
+ fontSizePx: editor.fontSizePx,
26173
+ screenRect: {
26174
+ left: topLeft.x,
26175
+ top: topLeft.y,
26176
+ width: Math.max(40, bottomRight.x - topLeft.x),
26177
+ height: Math.max(24, bottomRight.y - topLeft.y)
26178
+ },
26179
+ left: topLeft.x - offset.left,
26180
+ top: topLeft.y - offset.top
26181
+ });
26182
+ };
26183
+ const focusEditor = useCallback((el) => {
26184
+ textAreaRef.current = el;
26185
+ if (!el) return;
26186
+ const doFocus = () => {
26187
+ try {
26188
+ el.focus({ preventScroll: true });
26189
+ el.setSelectionRange(el.value.length, el.value.length);
26190
+ } catch {}
26191
+ };
26192
+ doFocus();
26193
+ requestAnimationFrame(doFocus);
26194
+ }, []);
26195
+ if (!editor) return /* @__PURE__ */ jsx("div", {
26196
+ ref: anchorRef,
26197
+ style: { display: "none" }
26198
+ });
26199
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
26200
+ ref: anchorRef,
26201
+ style: { display: "none" }
26202
+ }), renderPortal(editor.kind === "note" ? /* @__PURE__ */ jsx("textarea", {
26203
+ "aria-label": "Edit note",
26204
+ autoFocus: true,
26205
+ className: "scrawl-board__inline-editor scrawl-board__inline-editor--note",
26206
+ onBlur: commitNote,
26207
+ onChange: (event) => setEditor({
26208
+ ...editor,
26209
+ text: event.currentTarget.value
26210
+ }),
26211
+ onKeyDown: (event) => {
26212
+ event.stopPropagation();
26213
+ if (event.key === "Escape") commitNote();
26214
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) commitNote();
26215
+ },
26216
+ ref: focusEditor,
26217
+ style: {
26218
+ left: editor.left,
26219
+ top: editor.top,
26220
+ width: editor.screenRect.width,
26221
+ height: editor.screenRect.height,
26222
+ backgroundColor: editor.color,
26223
+ fontSize: Math.max(12, editor.screenRect.width * 42 / 512),
26224
+ lineHeight: 1.3,
26225
+ padding: editor.screenRect.width * 40 / 512
26226
+ },
26227
+ value: editor.text
26228
+ }) : editor.kind === "text" ? /* @__PURE__ */ jsx("textarea", {
26229
+ "aria-label": "Edit text",
26230
+ autoFocus: true,
26231
+ className: "scrawl-board__inline-editor scrawl-board__inline-editor--text",
26232
+ onBlur: commitText,
26233
+ onChange: (event) => setEditor({
26234
+ ...editor,
26235
+ text: event.currentTarget.value
26236
+ }),
26237
+ onKeyDown: (event) => {
26238
+ event.stopPropagation();
26239
+ if (event.key === "Escape") commitText();
26240
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) commitText();
26241
+ },
26242
+ ref: focusEditor,
26243
+ rows: Math.max(1, editor.text.split("\n").length),
26244
+ style: {
26245
+ left: editor.left,
26246
+ top: editor.top,
26247
+ color: editor.color,
26248
+ fontSize: Math.max(12, editor.fontSizePx)
26249
+ },
26250
+ value: editor.text
26251
+ }) : /* @__PURE__ */ jsxs("div", {
26252
+ className: "scrawl-board__inline-editor--table-cell",
26253
+ style: {
26254
+ left: editor.left,
26255
+ top: editor.top,
26256
+ width: editor.screenRect.width,
26257
+ minHeight: editor.screenRect.height
26258
+ },
26259
+ children: [/* @__PURE__ */ jsx("textarea", {
26260
+ "aria-label": `Edit table cell ${editor.row + 1}, ${editor.col + 1}`,
26261
+ autoFocus: true,
26262
+ className: "scrawl-board__inline-editor",
26263
+ onBlur: commitTableCell,
26264
+ onChange: (event) => setEditor({
26265
+ ...editor,
26266
+ text: event.currentTarget.value
26267
+ }),
26268
+ onKeyDown: (event) => {
26269
+ event.stopPropagation();
26270
+ if (event.key === "Tab") {
26271
+ event.preventDefault();
26272
+ navigateTableCell(0, event.shiftKey ? -1 : 1);
26273
+ } else if (event.key === "Enter" && !event.shiftKey) {
26274
+ event.preventDefault();
26275
+ navigateTableCell(1, 0);
26276
+ } else if (event.key === "Escape") commitTableCell();
26277
+ },
26278
+ ref: focusEditor,
26279
+ style: { fontSize: Math.max(11, editor.fontSizePx) },
26280
+ value: editor.text
26281
+ }), /* @__PURE__ */ jsxs("div", {
26282
+ "aria-label": "Selected object",
26283
+ className: "scrawl-board__inline-editor-actions",
26284
+ role: "toolbar",
26285
+ children: [
26286
+ /* @__PURE__ */ jsxs("span", { children: [
26287
+ "Cell (",
26288
+ editor.row + 1,
26289
+ ", ",
26290
+ editor.col + 1,
26291
+ ")"
26292
+ ] }),
26293
+ /* @__PURE__ */ jsx("button", {
26294
+ "aria-label": "Add row",
26295
+ onMouseDown: (event) => {
26296
+ event.preventDefault();
26297
+ controller.content.table.addRow(editor.id);
26298
+ },
26299
+ "data-tooltip": "Add row",
26300
+ type: "button",
26301
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "addRow" })
25648
26302
  }),
25649
- /* @__PURE__ */ jsx(ColorPicker, {
25650
- label: "Shape stroke",
25651
- color: shape.stroke,
25652
- colors: INK_COLORS,
25653
- disabled: locked_,
25654
- itemLabel: "stroke",
25655
- onChange: (value) => controller.content.update(shape.id, { stroke: value })
26303
+ /* @__PURE__ */ jsx("button", {
26304
+ "aria-label": "Add column",
26305
+ onMouseDown: (event) => {
26306
+ event.preventDefault();
26307
+ controller.content.table.addCol(editor.id);
26308
+ },
26309
+ "data-tooltip": "Add column",
26310
+ type: "button",
26311
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "addColumn" })
26312
+ }),
26313
+ /* @__PURE__ */ jsx("button", {
26314
+ "aria-label": "Delete row",
26315
+ onMouseDown: (event) => {
26316
+ event.preventDefault();
26317
+ controller.content.table.deleteRow(editor.id, editor.row);
26318
+ setEditor(null);
26319
+ },
26320
+ "data-tooltip": "Delete row",
26321
+ type: "button",
26322
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "deleteRow" })
26323
+ }),
26324
+ /* @__PURE__ */ jsx("span", {
26325
+ "aria-hidden": "true",
26326
+ className: "scrawl-board__toolbar-divider"
25656
26327
  }),
25657
26328
  /* @__PURE__ */ jsx(LockDuplicateDelete, {
25658
26329
  controller,
25659
- mutable,
26330
+ ids: [editor.id],
25660
26331
  locked,
25661
- ids: [shape.id]
26332
+ mutable
25662
26333
  })
25663
26334
  ]
25664
- })
25665
- ] });
25666
- }
25667
- /** One corner: drags the board point directly under the pointer, keeping the opposite corner fixed — independent width/height, no proportional lock. */
25668
- function ShapeResizeHandle({ controller, shape, corner, x, y }) {
25669
- const [dragging, setDragging] = useState(false);
25670
- const fixedCorner = () => {
25671
- return {
25672
- x: corner === "nw" || corner === "sw" ? shape.x + shape.width : shape.x,
25673
- y: corner === "nw" || corner === "ne" ? shape.y - shape.height : shape.y
25674
- };
25675
- };
25676
- const resizeTo = (boardX, boardY) => {
25677
- const fixed = fixedCorner();
25678
- const width = Math.max(SHAPE_MIN_SIZE, Math.abs(boardX - fixed.x));
25679
- const height = Math.max(SHAPE_MIN_SIZE, Math.abs(boardY - fixed.y));
25680
- const nextX = Math.min(boardX, fixed.x);
25681
- const nextY = Math.max(boardY, fixed.y);
25682
- controller.content.update(shape.id, {
25683
- x: nextX,
25684
- y: nextY,
25685
- width,
25686
- height
25687
- });
25688
- };
25689
- const nudge = (dx, dy) => {
25690
- resizeTo((corner === "ne" || corner === "se" ? shape.x + shape.width : shape.x) + dx, (corner === "nw" || corner === "ne" ? shape.y : shape.y - shape.height) + dy);
25691
- };
25692
- return /* @__PURE__ */ jsx("span", {
25693
- "aria-label": `Resize shape (${corner})`,
25694
- role: "slider",
25695
- "aria-valuemin": SHAPE_MIN_SIZE,
25696
- "aria-valuemax": 9999,
25697
- "aria-valuenow": Math.round(shape.width),
25698
- tabIndex: 0,
25699
- className: "scrawl-board__focus-handle",
25700
- "data-orientation": corner === "nw" || corner === "se" ? "nwse" : "nesw",
25701
- style: {
25702
- left: x,
25703
- top: y
25704
- },
25705
- onKeyDown: (event) => {
25706
- if (event.key === "ArrowUp") {
25707
- event.preventDefault();
25708
- nudge(0, .5);
25709
- } else if (event.key === "ArrowDown") {
25710
- event.preventDefault();
25711
- nudge(0, -.5);
25712
- } else if (event.key === "ArrowRight") {
25713
- event.preventDefault();
25714
- nudge(.5, 0);
25715
- } else if (event.key === "ArrowLeft") {
25716
- event.preventDefault();
25717
- nudge(-.5, 0);
25718
- }
25719
- },
25720
- onPointerDown: (event) => {
25721
- event.stopPropagation();
25722
- event.currentTarget.setPointerCapture(event.pointerId);
25723
- setDragging(true);
25724
- },
25725
- onPointerMove: (event) => {
25726
- if (!dragging) return;
25727
- const board = controller.view.screenToBoard({
25728
- x: event.clientX,
25729
- y: event.clientY
25730
- });
25731
- resizeTo(board.x, board.y);
25732
- },
25733
- onPointerUp: (event) => {
25734
- event.currentTarget.releasePointerCapture(event.pointerId);
25735
- setDragging(false);
25736
- }
25737
- });
25738
- }
25739
- function LockDuplicateDelete({ controller, mutable, locked, ids }) {
25740
- return /* @__PURE__ */ jsxs("div", {
25741
- className: "scrawl-board__style-group",
25742
- children: [
25743
- /* @__PURE__ */ jsx("button", {
25744
- "aria-label": locked ? "Unlock" : "Lock",
25745
- disabled: !mutable,
25746
- onClick: () => {
25747
- for (const id of ids) controller.content.update(id, locked ? { locked: false } : { locked: true });
25748
- },
25749
- "data-tooltip": locked ? "Unlock" : "Lock",
25750
- type: "button",
25751
- children: /* @__PURE__ */ jsx(ToolIcon, { tool: locked ? "lock" : "unlock" })
25752
- }),
25753
- /* @__PURE__ */ jsx("button", {
25754
- "aria-label": "Duplicate",
25755
- disabled: !mutable || locked,
25756
- onClick: () => {
25757
- const newIds = controller.content.duplicate(ids);
25758
- if (newIds.length) controller.content.select(newIds);
25759
- },
25760
- "data-tooltip": "Duplicate",
25761
- type: "button",
25762
- children: /* @__PURE__ */ jsx(ToolIcon, { tool: "duplicate" })
25763
- }),
25764
- /* @__PURE__ */ jsx("button", {
25765
- "aria-label": "Delete",
25766
- disabled: !mutable || locked,
25767
- onClick: () => controller.content.remove(ids),
25768
- "data-tooltip": "Delete",
25769
- type: "button",
25770
- children: /* @__PURE__ */ jsx(ToolIcon, { tool: "delete" })
25771
- })
25772
- ]
25773
- });
26335
+ })]
26336
+ }))] });
25774
26337
  }
25775
26338
  //#endregion
25776
26339
  //#region src/ui/toolbar/board-tooltips.tsx
@@ -25832,7 +26395,7 @@ function BoardTooltips({ renderPortal }) {
25832
26395
  document.addEventListener("focusin", show);
25833
26396
  document.addEventListener("focusout", leave);
25834
26397
  document.addEventListener("pointerdown", hide);
25835
- document.addEventListener("keydown", key);
26398
+ document.addEventListener("keydown", key, true);
25836
26399
  window.addEventListener("scroll", hide, true);
25837
26400
  window.addEventListener("resize", hide);
25838
26401
  return () => {
@@ -25842,11 +26405,22 @@ function BoardTooltips({ renderPortal }) {
25842
26405
  document.removeEventListener("focusin", show);
25843
26406
  document.removeEventListener("focusout", leave);
25844
26407
  document.removeEventListener("pointerdown", hide);
25845
- document.removeEventListener("keydown", key);
26408
+ document.removeEventListener("keydown", key, true);
25846
26409
  window.removeEventListener("scroll", hide, true);
25847
26410
  window.removeEventListener("resize", hide);
25848
26411
  };
25849
26412
  }, []);
26413
+ useEffect(() => {
26414
+ if (!active) return;
26415
+ const observer = new MutationObserver(() => {
26416
+ if (!active.element.isConnected) setActive(null);
26417
+ });
26418
+ observer.observe(document.body, {
26419
+ childList: true,
26420
+ subtree: true
26421
+ });
26422
+ return () => observer.disconnect();
26423
+ }, [active]);
25850
26424
  useLayoutEffect(() => {
25851
26425
  if (!active || !tooltip.current) return;
25852
26426
  const box = active.element.getBoundingClientRect();
@@ -26365,9 +26939,18 @@ var INKS = [
26365
26939
  ["Forest", "#315d46"],
26366
26940
  ["Oxblood", "#773f3b"]
26367
26941
  ];
26368
- function DefaultBoardChrome({ controller, snapshot, renderPortal, className, style, regions, slots, icons, commentAuthor }) {
26942
+ var ACTIVITY_KIND = {
26943
+ create: "added",
26944
+ update: "updated",
26945
+ delete: "removed",
26946
+ comment: "commented",
26947
+ share: "shared",
26948
+ export: "exported"
26949
+ };
26950
+ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, style, regions, slots, icons, commentAuthor, onShare, activity, commentFeed, permissions, menuItems }) {
26369
26951
  const [welcomeDismissed, setWelcomeDismissed] = useState(false);
26370
26952
  const [panel, setPanel] = useState(null);
26953
+ const [searchOpen, setSearchOpen] = useState(false);
26371
26954
  const [pagesOpen, setPagesOpen] = useState(false);
26372
26955
  const [moreOpen, setMoreOpen] = useState(false);
26373
26956
  const [query, setQuery] = useState("");
@@ -26388,8 +26971,17 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
26388
26971
  const enabled = (region) => regions?.[region] !== false;
26389
26972
  const mutable = snapshot.status === "ready" && !snapshot.readOnly;
26390
26973
  useEffect(() => {
26391
- if (panel === "search") searchRef.current?.focus();
26392
- }, [panel]);
26974
+ if (searchOpen) searchRef.current?.focus();
26975
+ }, [searchOpen]);
26976
+ useEffect(() => {
26977
+ if (!searchOpen) return;
26978
+ const dismiss = (event) => {
26979
+ if (searchRef.current?.value) return;
26980
+ if (!event.target.closest?.(".scrawl-board__actions")) setSearchOpen(false);
26981
+ };
26982
+ document.addEventListener("pointerdown", dismiss);
26983
+ return () => document.removeEventListener("pointerdown", dismiss);
26984
+ }, [searchOpen]);
26393
26985
  useEffect(() => {
26394
26986
  const root = chromeRef.current;
26395
26987
  if (!root) return;
@@ -26486,10 +27078,90 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
26486
27078
  root.removeEventListener("drop", drop);
26487
27079
  };
26488
27080
  }, [imagesEnabled]);
27081
+ const [localActivity, setLocalActivity] = useState([]);
27082
+ const [localActivityUnread, setLocalActivityUnread] = useState(0);
27083
+ const hostActivity = activity?.entries;
27084
+ const actorName = commentAuthor?.name ?? "You";
27085
+ useEffect(() => {
27086
+ if (hostActivity) return;
27087
+ let sequence = 0;
27088
+ return controller.on("audit", (payload) => {
27089
+ const event = payload;
27090
+ queueMicrotask(() => {
27091
+ const selected = controller.getSnapshot().selection[0];
27092
+ const box = selected && event.action !== "delete" ? controller.query.bounds(selected) : void 0;
27093
+ const entry = {
27094
+ id: `session-${Date.now()}-${sequence++}`,
27095
+ kind: ACTIVITY_KIND[event.action] ?? "updated",
27096
+ title: event.summary.charAt(0).toUpperCase() + event.summary.slice(1),
27097
+ actorName,
27098
+ at: Date.now(),
27099
+ ...box ? { location: {
27100
+ x: (box.minX + box.maxX) / 2,
27101
+ y: (box.minY + box.maxY) / 2
27102
+ } } : {}
27103
+ };
27104
+ setLocalActivity((list) => [entry, ...list].slice(0, 200));
27105
+ setLocalActivityUnread((count) => count + 1);
27106
+ });
27107
+ });
27108
+ }, [
27109
+ controller,
27110
+ hostActivity,
27111
+ actorName
27112
+ ]);
27113
+ const activityEntries = hostActivity ?? localActivity;
27114
+ const activityUnread = activity?.unreadCount ?? (hostActivity ? 0 : localActivityUnread);
27115
+ const commentsUnread = commentFeed?.unreadCount ?? 0;
27116
+ const markActivityRead = () => {
27117
+ setLocalActivityUnread(0);
27118
+ if (activityUnread > 0) activity?.onMarkAllRead?.();
27119
+ };
27120
+ const markCommentsRead = () => {
27121
+ if (commentsUnread > 0) commentFeed?.onMarkAllRead?.();
27122
+ };
27123
+ const showComments = enabled("comments");
27124
+ const showActivity = enabled("activity");
27125
+ const [jumpPin, setJumpPin] = useState(null);
27126
+ const jumpTo = (location) => {
27127
+ controller.view.centerOn(location);
27128
+ setJumpPin({
27129
+ location,
27130
+ arrival: Date.now()
27131
+ });
27132
+ };
27133
+ const moreItems = [
27134
+ ...enabled("import") && mutable ? [{
27135
+ id: "scrawl:import",
27136
+ label: "Import board JSON",
27137
+ icon: /* @__PURE__ */ jsx(ToolIcon, { tool: "import" })
27138
+ }] : [],
27139
+ ...permissions?.canManage ? [{
27140
+ id: "scrawl:permissions",
27141
+ label: "Manage permissions",
27142
+ icon: /* @__PURE__ */ jsx(ToolIcon, { tool: "shield" })
27143
+ }] : [],
27144
+ ...menuItems ?? []
27145
+ ];
27146
+ const selectMoreItem = (id) => {
27147
+ if (id === "scrawl:import") importRef.current?.click();
27148
+ else if (id === "scrawl:permissions") {
27149
+ const more = chromeRef.current?.querySelector("[aria-label=\"More board actions\"]");
27150
+ if (more) open("permissions", more);
27151
+ } else menuItems?.find((item) => item.id === id)?.onSelect();
27152
+ };
26489
27153
  const open = (next, opener) => {
26490
27154
  setMoreOpen(false);
27155
+ if (!searchRef.current?.value) setSearchOpen(false);
26491
27156
  openerRef.current = opener;
26492
27157
  setPanel(next);
27158
+ if (next === "comments") markCommentsRead();
27159
+ if (next === "activity") markActivityRead();
27160
+ };
27161
+ /** A header button opens its own panel, or closes it if it's already open. */
27162
+ const toggle = (next, opener) => {
27163
+ if (panel === next) close();
27164
+ else open(next, opener);
26493
27165
  };
26494
27166
  const close = useCallback(() => {
26495
27167
  setPanel(null);
@@ -26534,12 +27206,9 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
26534
27206
  return;
26535
27207
  }
26536
27208
  if (modifier && event.key.toLowerCase() === "f" && enabled("search")) {
26537
- const opener = chromeRef.current?.querySelector("[aria-label=\"Search Board\"]");
26538
- if (opener) {
26539
- event.preventDefault();
26540
- event.stopPropagation();
26541
- open("search", opener);
26542
- }
27209
+ event.preventDefault();
27210
+ event.stopPropagation();
27211
+ setSearchOpen(true);
26543
27212
  return;
26544
27213
  }
26545
27214
  if (editing) return;
@@ -26601,14 +27270,149 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
26601
27270
  ref: chromeRef,
26602
27271
  style,
26603
27272
  children: [
26604
- enabled("pages") && /* @__PURE__ */ jsx(NotebookNavigation, {
26605
- controller,
26606
- snapshot,
26607
- onOpen: () => {
26608
- setPagesOpen(true);
26609
- setMoreOpen(false);
26610
- setPanel(null);
26611
- }
27273
+ /* @__PURE__ */ jsxs("div", {
27274
+ className: "scrawl-board__topbar",
27275
+ children: [enabled("pages") && /* @__PURE__ */ jsx("div", {
27276
+ className: "scrawl-board__navigator",
27277
+ children: /* @__PURE__ */ jsx(NotebookNavigation, {
27278
+ controller,
27279
+ snapshot
27280
+ })
27281
+ }), renderSlot(slots?.topBar, (enabled("search") || enabled("export") || showComments || showActivity || moreItems.length > 0 || onShare) && /* @__PURE__ */ jsxs("div", {
27282
+ className: "scrawl-board__header-actions",
27283
+ children: [
27284
+ /* @__PURE__ */ jsxs("div", {
27285
+ "aria-label": "Board actions",
27286
+ className: "scrawl-board__actions",
27287
+ role: "toolbar",
27288
+ children: [
27289
+ enabled("search") && /* @__PURE__ */ jsxs("div", {
27290
+ className: "scrawl-board__search",
27291
+ "data-open": searchOpen ? "" : void 0,
27292
+ "data-scrawl-search": "",
27293
+ children: [
27294
+ /* @__PURE__ */ jsx("button", {
27295
+ "aria-label": "Search Board",
27296
+ "aria-expanded": searchOpen,
27297
+ "data-tooltip": "Search",
27298
+ type: "button",
27299
+ onClick: () => {
27300
+ if (searchOpen && !query) setSearchOpen(false);
27301
+ else setSearchOpen(true);
27302
+ searchRef.current?.focus();
27303
+ },
27304
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "search" })
27305
+ }),
27306
+ /* @__PURE__ */ jsx("input", {
27307
+ "aria-label": "Search Board",
27308
+ className: "scrawl-board__search-input",
27309
+ placeholder: "Search this board…",
27310
+ ref: searchRef,
27311
+ tabIndex: searchOpen ? void 0 : -1,
27312
+ type: "search",
27313
+ value: query,
27314
+ onChange: (event) => setQuery(event.currentTarget.value),
27315
+ onKeyDown: (event) => {
27316
+ event.stopPropagation();
27317
+ if (event.key !== "Escape") return;
27318
+ event.preventDefault();
27319
+ setQuery("");
27320
+ setSearchOpen(false);
27321
+ chromeRef.current?.querySelector("[aria-label=\"Search Board\"]")?.focus();
27322
+ }
27323
+ }),
27324
+ searchOpen && query && /* @__PURE__ */ jsx("div", {
27325
+ "aria-live": "polite",
27326
+ className: "scrawl-board__search-results",
27327
+ "data-scrawl-search-results": "",
27328
+ children: results.length === 0 ? /* @__PURE__ */ jsx("p", { children: "No matching Board content." }) : results.map((result) => /* @__PURE__ */ jsxs("button", {
27329
+ type: "button",
27330
+ onClick: () => {
27331
+ if (result.kind === "comment") controller.comments.open(result.id);
27332
+ else controller.content.select([result.id]);
27333
+ controller.view.centerOn({
27334
+ x: result.x,
27335
+ y: result.y
27336
+ });
27337
+ setAnnouncement(`${result.title} selected`);
27338
+ setQuery("");
27339
+ setSearchOpen(false);
27340
+ },
27341
+ children: [/* @__PURE__ */ jsx("strong", { children: result.title }), /* @__PURE__ */ jsx("span", { children: result.snippet })]
27342
+ }, `${result.kind}:${result.id}`))
27343
+ })
27344
+ ]
27345
+ }),
27346
+ enabled("export") && /* @__PURE__ */ jsx("button", {
27347
+ "aria-label": "Export Board",
27348
+ "aria-expanded": panel === "export",
27349
+ onClick: (event) => toggle("export", event.currentTarget),
27350
+ "data-tooltip": "Export",
27351
+ type: "button",
27352
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "export" })
27353
+ }),
27354
+ showComments && /* @__PURE__ */ jsxs("button", {
27355
+ "aria-expanded": panel === "comments",
27356
+ "aria-label": commentsUnread > 0 ? `Comments (${commentsUnread} unread)` : "Comments",
27357
+ "data-tooltip": "Comments",
27358
+ type: "button",
27359
+ onClick: (event) => toggle("comments", event.currentTarget),
27360
+ children: [/* @__PURE__ */ jsx(ToolIcon, { tool: "comment" }), commentsUnread > 0 && /* @__PURE__ */ jsx("span", {
27361
+ "aria-hidden": "true",
27362
+ className: "scrawl-board__badge",
27363
+ children: commentsUnread > 99 ? "99+" : commentsUnread
27364
+ })]
27365
+ }),
27366
+ showActivity && /* @__PURE__ */ jsxs("button", {
27367
+ "aria-expanded": panel === "activity",
27368
+ "aria-label": activityUnread > 0 ? `Recent activity (${activityUnread} unread)` : "Recent activity",
27369
+ "data-tooltip": "Recent activity",
27370
+ type: "button",
27371
+ onClick: (event) => toggle("activity", event.currentTarget),
27372
+ children: [/* @__PURE__ */ jsx(ToolIcon, { tool: "activity" }), activityUnread > 0 && /* @__PURE__ */ jsx("span", {
27373
+ "aria-hidden": "true",
27374
+ className: "scrawl-board__badge",
27375
+ children: activityUnread > 99 ? "99+" : activityUnread
27376
+ })]
27377
+ }),
27378
+ moreItems.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
27379
+ "aria-hidden": "true",
27380
+ className: "scrawl-board__actions-divider"
27381
+ }), /* @__PURE__ */ jsx("button", {
27382
+ "aria-expanded": panel === "more",
27383
+ "aria-haspopup": "menu",
27384
+ "aria-label": "More board actions",
27385
+ "data-tooltip": "More",
27386
+ type: "button",
27387
+ onClick: (event) => toggle("more", event.currentTarget),
27388
+ children: /* @__PURE__ */ jsx(ToolIcon, { tool: "moreVertical" })
27389
+ })] })
27390
+ ]
27391
+ }),
27392
+ onShare && /* @__PURE__ */ jsxs("button", {
27393
+ className: "scrawl-board__share",
27394
+ onClick: onShare,
27395
+ type: "button",
27396
+ children: [/* @__PURE__ */ jsx(ToolIcon, { tool: "share" }), /* @__PURE__ */ jsx("span", { children: "Share" })]
27397
+ }),
27398
+ enabled("import") && /* @__PURE__ */ jsx("input", {
27399
+ accept: "application/json,.json",
27400
+ "aria-label": "Choose Board JSON",
27401
+ className: "scrawl-board__file-input",
27402
+ onChange: (event) => {
27403
+ importDocument(event.currentTarget.files?.[0]);
27404
+ event.currentTarget.value = "";
27405
+ },
27406
+ ref: importRef,
27407
+ type: "file"
27408
+ }),
27409
+ panel === "more" && /* @__PURE__ */ jsx(BoardMoreMenu, {
27410
+ items: moreItems,
27411
+ onClose: close,
27412
+ onSelect: selectMoreItem
27413
+ })
27414
+ ]
27415
+ }))]
26612
27416
  }),
26613
27417
  enabled("pages") && /* @__PURE__ */ jsx(PagesPanel, {
26614
27418
  controller,
@@ -26710,45 +27514,29 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
26710
27514
  })]
26711
27515
  })]
26712
27516
  }),
26713
- renderSlot(slots?.topBar, (enabled("search") || enabled("import") || enabled("export")) && /* @__PURE__ */ jsxs("div", {
26714
- "aria-label": "Board actions",
26715
- className: "scrawl-board__actions",
26716
- role: "toolbar",
26717
- children: [
26718
- enabled("search") && /* @__PURE__ */ jsx("button", {
26719
- "aria-label": "Search Board",
26720
- onClick: (event) => open("search", event.currentTarget),
26721
- "data-tooltip": "Search",
26722
- type: "button",
26723
- children: /* @__PURE__ */ jsx(ToolIcon, { tool: "search" })
26724
- }),
26725
- enabled("import") && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
26726
- "aria-label": "Import Board JSON",
26727
- disabled: !mutable,
26728
- onClick: () => importRef.current?.click(),
26729
- "data-tooltip": "Import",
26730
- type: "button",
26731
- children: /* @__PURE__ */ jsx(ToolIcon, { tool: "import" })
26732
- }), /* @__PURE__ */ jsx("input", {
26733
- accept: "application/json,.json",
26734
- "aria-label": "Choose Board JSON",
26735
- className: "scrawl-board__file-input",
26736
- onChange: (event) => {
26737
- importDocument(event.currentTarget.files?.[0]);
26738
- event.currentTarget.value = "";
26739
- },
26740
- ref: importRef,
26741
- type: "file"
26742
- })] }),
26743
- enabled("export") && /* @__PURE__ */ jsx("button", {
26744
- "aria-label": "Export Board",
26745
- onClick: (event) => open("export", event.currentTarget),
26746
- "data-tooltip": "Export",
26747
- type: "button",
26748
- children: /* @__PURE__ */ jsx(ToolIcon, { tool: "export" })
26749
- })
26750
- ]
26751
- })),
27517
+ panel === "comments" && /* @__PURE__ */ jsx(CommentsDrawer, {
27518
+ comments: controller.comments.list(),
27519
+ controller,
27520
+ onClose: close,
27521
+ onMarkAllRead: markCommentsRead,
27522
+ unread: commentsUnread
27523
+ }),
27524
+ panel === "activity" && /* @__PURE__ */ jsx(ActivityDrawer, {
27525
+ entries: activityEntries,
27526
+ onClose: close,
27527
+ onJump: jumpTo,
27528
+ onMarkAllRead: markActivityRead,
27529
+ unread: activityUnread
27530
+ }),
27531
+ panel === "permissions" && permissions && /* @__PURE__ */ jsx(PermissionsDrawer, {
27532
+ onClose: close,
27533
+ permissions
27534
+ }),
27535
+ jumpPin && /* @__PURE__ */ jsx(JumpPin, {
27536
+ arrival: jumpPin.arrival,
27537
+ controller,
27538
+ location: jumpPin.location
27539
+ }),
26752
27540
  renderSlot(slots?.toolbar, (enabled("tools") || enabled("style") || enabled("history") || enabled("pages")) && /* @__PURE__ */ jsxs("div", {
26753
27541
  "aria-label": "Board tools",
26754
27542
  className: "scrawl-board__tools",
@@ -26954,47 +27742,16 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
26954
27742
  }, color))
26955
27743
  })
26956
27744
  })),
26957
- panel === "search" && renderPortal(/* @__PURE__ */ jsx(ChromePanel, {
26958
- anchor: "actions",
26959
- label: "Search Board",
26960
- onClose: close,
26961
- slot: slots?.dialogs,
26962
- children: /* @__PURE__ */ jsxs("div", {
26963
- "data-scrawl-search": "",
26964
- children: [/* @__PURE__ */ jsxs("label", { children: ["Search Board", /* @__PURE__ */ jsx("input", {
26965
- onChange: (event) => setQuery(event.currentTarget.value),
26966
- ref: searchRef,
26967
- type: "search",
26968
- value: query
26969
- })] }), /* @__PURE__ */ jsx("div", {
26970
- "aria-live": "polite",
26971
- "data-scrawl-search-results": "",
26972
- children: query && results.length === 0 ? /* @__PURE__ */ jsx("p", { children: "No matching Board content." }) : results.map((result) => /* @__PURE__ */ jsxs("button", {
26973
- onClick: () => {
26974
- if (result.kind === "comment") controller.comments.open(result.id);
26975
- else controller.content.select([result.id]);
26976
- controller.view.centerOn({
26977
- x: result.x,
26978
- y: result.y
26979
- });
26980
- setAnnouncement(`${result.title} selected`);
26981
- close();
26982
- },
26983
- type: "button",
26984
- children: [/* @__PURE__ */ jsx("strong", { children: result.title }), /* @__PURE__ */ jsx("span", { children: result.snippet })]
26985
- }, `${result.kind}:${result.id}`))
26986
- })]
26987
- })
26988
- })),
26989
27745
  panel === "export" && renderPortal(/* @__PURE__ */ jsx(ChromePanel, {
26990
27746
  anchor: "actions",
27747
+ headless: true,
26991
27748
  label: "Export Board",
26992
27749
  onClose: close,
26993
27750
  slot: slots?.dialogs,
26994
27751
  children: /* @__PURE__ */ jsxs("div", {
26995
27752
  className: "scrawl-board__export-options",
26996
27753
  children: [
26997
- /* @__PURE__ */ jsx("button", {
27754
+ /* @__PURE__ */ jsxs("button", {
26998
27755
  "aria-label": "Export PNG",
26999
27756
  onClick: () => {
27000
27757
  controller.export.png().then((blob) => blob && download(blob, `${snapshot.documentId}.png`));
@@ -27002,9 +27759,9 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
27002
27759
  close();
27003
27760
  },
27004
27761
  type: "button",
27005
- children: "PNG image"
27762
+ children: [/* @__PURE__ */ jsx(ToolIcon, { tool: "image" }), /* @__PURE__ */ jsx("span", { children: "Export as PNG" })]
27006
27763
  }),
27007
- /* @__PURE__ */ jsx("button", {
27764
+ /* @__PURE__ */ jsxs("button", {
27008
27765
  "aria-label": "Export SVG",
27009
27766
  onClick: () => {
27010
27767
  download(new Blob([controller.export.svg()], { type: "image/svg+xml" }), `${snapshot.documentId}.svg`);
@@ -27012,9 +27769,9 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
27012
27769
  close();
27013
27770
  },
27014
27771
  type: "button",
27015
- children: "SVG vector"
27772
+ children: [/* @__PURE__ */ jsx(ToolIcon, { tool: "shape:triangle" }), /* @__PURE__ */ jsx("span", { children: "Export as SVG" })]
27016
27773
  }),
27017
- /* @__PURE__ */ jsx("button", {
27774
+ /* @__PURE__ */ jsxs("button", {
27018
27775
  "aria-label": "Export JSON",
27019
27776
  onClick: () => {
27020
27777
  download(new Blob([JSON.stringify(controller.export.json(), null, 2)], { type: "application/json" }), `${snapshot.documentId}.json`);
@@ -27022,7 +27779,7 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
27022
27779
  close();
27023
27780
  },
27024
27781
  type: "button",
27025
- children: "Board JSON"
27782
+ children: [/* @__PURE__ */ jsx(ToolIcon, { tool: "page" }), /* @__PURE__ */ jsx("span", { children: "Export as JSON" })]
27026
27783
  })
27027
27784
  ]
27028
27785
  })
@@ -27339,7 +28096,7 @@ function ScrawlProvider({ controller, children, preset = "light", theme, portalC
27339
28096
  * field except `canvas` (pass `canvas={null}` for a headless board, or an
27340
28097
  * existing `<canvas>` element to control its identity yourself).
27341
28098
  */
27342
- function Scrawl({ children, preset, theme, portalContainer, className, style, onReady, onError, onThemeDiagnostic, canvas: suppliedCanvas, icons, ...options }) {
28099
+ function Scrawl({ children, preset, theme, portalContainer, className, style, onReady, onError, onThemeDiagnostic, canvas: suppliedCanvas, icons, onShare, activity, commentFeed, permissions, menuItems, ...options }) {
27343
28100
  const [runtime, setRuntime] = useState(null);
27344
28101
  const runtimeRef = useRef(null);
27345
28102
  const ownedCanvasRef = useRef(null);
@@ -27436,8 +28193,13 @@ function Scrawl({ children, preset, theme, portalContainer, className, style, on
27436
28193
  className,
27437
28194
  style,
27438
28195
  children: [runtime.canvas && /* @__PURE__ */ jsx(ScrawlCanvas, { element: runtime.canvas }), children ?? /* @__PURE__ */ jsx(ScrawlDefaultUI, {
28196
+ activity,
27439
28197
  commentAuthor: options.identity,
27440
- icons
28198
+ commentFeed,
28199
+ icons,
28200
+ menuItems,
28201
+ onShare,
28202
+ permissions
27441
28203
  })]
27442
28204
  });
27443
28205
  }
@@ -27474,12 +28236,17 @@ function ScrawlCanvas({ element, className, style, "aria-label": ariaLabel = "Sc
27474
28236
  style
27475
28237
  });
27476
28238
  }
27477
- function ScrawlDefaultUI({ className, style, regions, slots, icons, commentAuthor }) {
28239
+ function ScrawlDefaultUI({ className, style, regions, slots, icons, commentAuthor, onShare, activity, commentFeed, permissions, menuItems }) {
27478
28240
  return /* @__PURE__ */ jsx(DefaultBoardChrome, {
28241
+ activity,
27479
28242
  className,
27480
28243
  commentAuthor,
28244
+ commentFeed,
27481
28245
  controller: useScrawlController(),
27482
28246
  icons,
28247
+ menuItems,
28248
+ onShare,
28249
+ permissions,
27483
28250
  regions,
27484
28251
  renderPortal: (children) => /* @__PURE__ */ jsx(ScrawlPortal, { children }),
27485
28252
  slots,