@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/browser.d.ts +24 -1
- package/dist/browser.js +58 -7
- package/dist/index.d.ts +130 -9
- package/dist/index.js +1397 -630
- package/dist/react.d.ts +130 -9
- package/dist/react.js +1397 -630
- package/dist/styles.css +383 -94
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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 = "
|
|
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 = "
|
|
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
|
|
24038
|
-
export: "M12
|
|
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
|
-
|
|
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
|
-
|
|
24201
|
+
document.addEventListener("keydown", escape);
|
|
24202
|
+
return () => {
|
|
24203
|
+
document.removeEventListener("pointerdown", outside);
|
|
24204
|
+
document.removeEventListener("keydown", escape);
|
|
24205
|
+
};
|
|
24145
24206
|
}, [open]);
|
|
24146
|
-
|
|
24147
|
-
|
|
24148
|
-
|
|
24149
|
-
|
|
24150
|
-
|
|
24151
|
-
|
|
24152
|
-
|
|
24153
|
-
|
|
24154
|
-
|
|
24155
|
-
|
|
24156
|
-
|
|
24157
|
-
|
|
24158
|
-
|
|
24159
|
-
|
|
24160
|
-
|
|
24161
|
-
|
|
24162
|
-
|
|
24163
|
-
|
|
24164
|
-
|
|
24165
|
-
|
|
24166
|
-
|
|
24167
|
-
|
|
24168
|
-
|
|
24169
|
-
|
|
24170
|
-
|
|
24171
|
-
|
|
24172
|
-
|
|
24173
|
-
|
|
24174
|
-
|
|
24175
|
-
|
|
24176
|
-
|
|
24177
|
-
|
|
24178
|
-
|
|
24179
|
-
|
|
24180
|
-
|
|
24181
|
-
|
|
24182
|
-
|
|
24183
|
-
|
|
24184
|
-
"
|
|
24185
|
-
|
|
24186
|
-
|
|
24187
|
-
|
|
24188
|
-
|
|
24189
|
-
|
|
24190
|
-
|
|
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
|
-
})
|
|
24199
|
-
|
|
24200
|
-
|
|
24201
|
-
|
|
24202
|
-
|
|
24203
|
-
|
|
24204
|
-
|
|
24205
|
-
|
|
24206
|
-
|
|
24207
|
-
|
|
24208
|
-
|
|
24209
|
-
|
|
24210
|
-
|
|
24211
|
-
|
|
24212
|
-
|
|
24213
|
-
|
|
24214
|
-
|
|
24215
|
-
|
|
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__ */
|
|
24224
|
-
|
|
24225
|
-
|
|
24226
|
-
|
|
24227
|
-
|
|
24228
|
-
},
|
|
24229
|
-
children: 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("
|
|
24232
|
-
|
|
24233
|
-
|
|
24234
|
-
|
|
24235
|
-
|
|
24236
|
-
|
|
24237
|
-
|
|
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("
|
|
24240
|
-
|
|
24241
|
-
|
|
24242
|
-
|
|
24243
|
-
|
|
24244
|
-
|
|
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/
|
|
24762
|
-
/**
|
|
24763
|
-
function
|
|
24764
|
-
const
|
|
24765
|
-
const
|
|
24766
|
-
const
|
|
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
|
-
|
|
24773
|
-
|
|
24774
|
-
|
|
24775
|
-
|
|
24776
|
-
|
|
24777
|
-
|
|
24778
|
-
|
|
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
|
-
|
|
24783
|
-
|
|
24784
|
-
|
|
24785
|
-
|
|
24786
|
-
|
|
24787
|
-
|
|
24788
|
-
|
|
24789
|
-
|
|
24790
|
-
|
|
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
|
-
|
|
24813
|
-
|
|
24814
|
-
|
|
24815
|
-
|
|
24816
|
-
|
|
24817
|
-
|
|
24818
|
-
|
|
24819
|
-
|
|
24820
|
-
|
|
24821
|
-
|
|
24822
|
-
|
|
24823
|
-
|
|
24824
|
-
|
|
24825
|
-
|
|
24826
|
-
|
|
24827
|
-
|
|
24828
|
-
|
|
24829
|
-
|
|
24830
|
-
|
|
24831
|
-
}
|
|
24832
|
-
|
|
24833
|
-
|
|
24834
|
-
|
|
24835
|
-
|
|
24836
|
-
|
|
24837
|
-
|
|
24838
|
-
|
|
24839
|
-
|
|
24840
|
-
|
|
24841
|
-
|
|
24842
|
-
|
|
24843
|
-
|
|
24844
|
-
|
|
24845
|
-
|
|
24846
|
-
|
|
24847
|
-
|
|
24848
|
-
|
|
24849
|
-
|
|
24850
|
-
|
|
24851
|
-
|
|
24852
|
-
|
|
24853
|
-
|
|
24854
|
-
|
|
24855
|
-
|
|
24856
|
-
|
|
24857
|
-
|
|
24858
|
-
|
|
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
|
-
|
|
24861
|
-
if (
|
|
24862
|
-
|
|
24863
|
-
|
|
24864
|
-
|
|
24865
|
-
|
|
24866
|
-
|
|
24867
|
-
|
|
24868
|
-
|
|
24869
|
-
|
|
24870
|
-
|
|
24871
|
-
|
|
24872
|
-
|
|
24873
|
-
|
|
24874
|
-
|
|
24875
|
-
|
|
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
|
-
|
|
24912
|
-
|
|
24913
|
-
|
|
24914
|
-
|
|
24915
|
-
|
|
24916
|
-
|
|
24917
|
-
|
|
24918
|
-
|
|
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
|
-
|
|
24921
|
-
|
|
25247
|
+
document.addEventListener("pointerdown", outside);
|
|
25248
|
+
return () => document.removeEventListener("pointerdown", outside);
|
|
24922
25249
|
}, []);
|
|
24923
|
-
|
|
24924
|
-
|
|
24925
|
-
|
|
24926
|
-
|
|
24927
|
-
|
|
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")
|
|
24968
|
-
|
|
24969
|
-
|
|
24970
|
-
|
|
24971
|
-
|
|
24972
|
-
|
|
24973
|
-
|
|
24974
|
-
|
|
24975
|
-
|
|
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:
|
|
24988
|
-
|
|
24989
|
-
|
|
24990
|
-
|
|
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
|
-
|
|
25007
|
-
|
|
25008
|
-
|
|
25009
|
-
|
|
25010
|
-
|
|
25011
|
-
children:
|
|
25012
|
-
|
|
25013
|
-
|
|
25014
|
-
|
|
25015
|
-
|
|
25016
|
-
|
|
25017
|
-
|
|
25018
|
-
|
|
25019
|
-
|
|
25020
|
-
|
|
25021
|
-
|
|
25022
|
-
|
|
25023
|
-
|
|
25024
|
-
|
|
25025
|
-
|
|
25026
|
-
|
|
25027
|
-
|
|
25028
|
-
|
|
25029
|
-
|
|
25030
|
-
|
|
25031
|
-
|
|
25032
|
-
|
|
25033
|
-
|
|
25034
|
-
|
|
25035
|
-
|
|
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 —
|
|
25194
|
-
* already
|
|
25195
|
-
*
|
|
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 === "
|
|
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
|
|
25281
|
-
const top = Math.max(8,
|
|
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(
|
|
25650
|
-
label: "
|
|
25651
|
-
|
|
25652
|
-
|
|
25653
|
-
|
|
25654
|
-
|
|
25655
|
-
|
|
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
|
-
|
|
26330
|
+
ids: [editor.id],
|
|
25660
26331
|
locked,
|
|
25661
|
-
|
|
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
|
-
|
|
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 (
|
|
26392
|
-
}, [
|
|
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
|
-
|
|
26538
|
-
|
|
26539
|
-
|
|
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
|
-
|
|
26605
|
-
|
|
26606
|
-
|
|
26607
|
-
|
|
26608
|
-
|
|
26609
|
-
|
|
26610
|
-
|
|
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
|
-
|
|
26714
|
-
|
|
26715
|
-
|
|
26716
|
-
|
|
26717
|
-
|
|
26718
|
-
|
|
26719
|
-
|
|
26720
|
-
|
|
26721
|
-
|
|
26722
|
-
|
|
26723
|
-
|
|
26724
|
-
|
|
26725
|
-
|
|
26726
|
-
|
|
26727
|
-
|
|
26728
|
-
|
|
26729
|
-
|
|
26730
|
-
|
|
26731
|
-
|
|
26732
|
-
|
|
26733
|
-
|
|
26734
|
-
|
|
26735
|
-
|
|
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__ */
|
|
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
|
|
27762
|
+
children: [/* @__PURE__ */ jsx(ToolIcon, { tool: "image" }), /* @__PURE__ */ jsx("span", { children: "Export as PNG" })]
|
|
27006
27763
|
}),
|
|
27007
|
-
/* @__PURE__ */
|
|
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
|
|
27772
|
+
children: [/* @__PURE__ */ jsx(ToolIcon, { tool: "shape:triangle" }), /* @__PURE__ */ jsx("span", { children: "Export as SVG" })]
|
|
27016
27773
|
}),
|
|
27017
|
-
/* @__PURE__ */
|
|
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: "
|
|
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
|
-
|
|
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,
|