@scrawl-board/board 0.1.0-beta.3 → 0.1.0-beta.5
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 +34 -1
- package/dist/browser.js +248 -36
- package/dist/core.d.ts +12 -0
- package/dist/core.js +18 -2
- package/dist/index.d.ts +93 -4
- package/dist/index.js +651 -46
- package/dist/react.d.ts +81 -4
- package/dist/react.js +651 -46
- package/dist/styles.css +62 -0
- package/package.json +1 -1
package/dist/react.js
CHANGED
|
@@ -126,7 +126,11 @@ function validateStrokes(raw) {
|
|
|
126
126
|
for (let index = 0; index < raw.length; index += 1) {
|
|
127
127
|
const value = raw[index];
|
|
128
128
|
const path = `strokes[${index}]`;
|
|
129
|
-
if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, [
|
|
129
|
+
if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, [
|
|
130
|
+
"marker",
|
|
131
|
+
"highlighter",
|
|
132
|
+
"shape"
|
|
133
|
+
]) || !validLock(value) || !optionalString(value.clusterId) || !validMatrix(value.matrix) || !Array.isArray(value.points)) return validation("invalid stroke", path);
|
|
130
134
|
const points = [];
|
|
131
135
|
for (let pointIndex = 0; pointIndex < value.points.length; pointIndex += 1) {
|
|
132
136
|
const point = normalizePoint(value.points[pointIndex]);
|
|
@@ -407,9 +411,21 @@ var HIGHLIGHT_COLORS = {
|
|
|
407
411
|
};
|
|
408
412
|
var NOTE_COLORS = {
|
|
409
413
|
yellow: "#FDE68A",
|
|
414
|
+
amber: "#FCD34D",
|
|
415
|
+
orange: "#FDBA74",
|
|
416
|
+
peach: "#FED7AA",
|
|
417
|
+
red: "#FCA5A5",
|
|
418
|
+
rose: "#FECACA",
|
|
410
419
|
pink: "#FBCFE8",
|
|
420
|
+
magenta: "#F9A8D4",
|
|
421
|
+
purple: "#D8B4FE",
|
|
422
|
+
lavender: "#E9D5FF",
|
|
411
423
|
blue: "#BFDBFE",
|
|
412
|
-
|
|
424
|
+
cornflower: "#93C5FD",
|
|
425
|
+
sky: "#7DD3FC",
|
|
426
|
+
cyan: "#BAE6FD",
|
|
427
|
+
green: "#BBF7D0",
|
|
428
|
+
mint: "#86EFAC"
|
|
413
429
|
};
|
|
414
430
|
var TEXT_DEFAULT_SIZE = 1.8;
|
|
415
431
|
function cloneText(block) {
|
|
@@ -35926,12 +35942,16 @@ var FRAGMENT = `
|
|
|
35926
35942
|
return max(lineAlpha2.x, lineAlpha2.y);
|
|
35927
35943
|
}
|
|
35928
35944
|
|
|
35929
|
-
// Distance from p's nearest grid intersection, in world units.
|
|
35930
|
-
//
|
|
35945
|
+
// Distance from p's nearest grid intersection, in world units. A line's
|
|
35946
|
+
// width reads as visible even at 1px because it spans the whole screen;
|
|
35947
|
+
// an isolated dot at that same 1px radius gets almost entirely eaten by
|
|
35948
|
+
// anti-aliasing and all but disappears. Scaling the radius up keeps a
|
|
35949
|
+
// dot's on-screen weight comparable to a line drawn with the same
|
|
35950
|
+
// uGridLineWidthPx, rather than using it literally as a radius.
|
|
35931
35951
|
float gridDotAlpha(vec2 p) {
|
|
35932
35952
|
vec2 cell = mod(p, uGridSpacing) - uGridSpacing * 0.5;
|
|
35933
35953
|
float dist = length(cell);
|
|
35934
|
-
float radius = uGridLineWidthPx * uWorldPerPixel;
|
|
35954
|
+
float radius = uGridLineWidthPx * uWorldPerPixel * 1.75;
|
|
35935
35955
|
float aa = uWorldPerPixel;
|
|
35936
35956
|
return 1.0 - smoothstep(radius - aa * 0.5, radius + aa * 0.5, dist);
|
|
35937
35957
|
}
|
|
@@ -36508,6 +36528,7 @@ var StrokeRenderer = class {
|
|
|
36508
36528
|
if (mesh) {
|
|
36509
36529
|
mesh.geometry.dispose();
|
|
36510
36530
|
mesh.geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke));
|
|
36531
|
+
mesh.material = stroke.tool === "highlighter" ? this.materials.highlight(stroke.color) : this.materials.get(stroke.color);
|
|
36511
36532
|
}
|
|
36512
36533
|
}
|
|
36513
36534
|
for (const stroke of change.transformed) {
|
|
@@ -44687,7 +44708,7 @@ function distToSegmentSq(p, a, b) {
|
|
|
44687
44708
|
}
|
|
44688
44709
|
//#endregion
|
|
44689
44710
|
//#region src/interaction/tools/eraserTool.ts
|
|
44690
|
-
var DECAY_PER_PASS =
|
|
44711
|
+
var DECAY_PER_PASS = 1;
|
|
44691
44712
|
/** A point can take another decay pass after this long — rubbing works. */
|
|
44692
44713
|
var REARM_MS = 250;
|
|
44693
44714
|
var EraserTool = class {
|
|
@@ -44781,19 +44802,11 @@ var EraserTool = class {
|
|
|
44781
44802
|
const runs = partitionByErasure(current.points);
|
|
44782
44803
|
const survives = runs.filter((r) => r.survives && r.points.length >= 2);
|
|
44783
44804
|
const keepId = survives.length === 1 && survives[0].points.length === current.points.length;
|
|
44784
|
-
const m = current.matrix;
|
|
44785
44805
|
for (const run of runs) if (run.survives && run.points.length >= 2) after.push({
|
|
44786
44806
|
...current,
|
|
44787
44807
|
id: keepId ? current.id : crypto.randomUUID(),
|
|
44788
44808
|
points: run.points
|
|
44789
44809
|
});
|
|
44790
|
-
else if (!run.survives) {
|
|
44791
|
-
const world = m ? run.points.map((p) => ({
|
|
44792
|
-
...p,
|
|
44793
|
-
...apply(m, p)
|
|
44794
|
-
})) : run.points;
|
|
44795
|
-
this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1), current.tool !== "shape");
|
|
44796
|
-
}
|
|
44797
44810
|
}
|
|
44798
44811
|
doc.removeStrokes(before.map((s) => s.id));
|
|
44799
44812
|
doc.addStrokes(after.map(cloneStroke));
|
|
@@ -45001,10 +45014,10 @@ function buildShape(kind, a, b, step, constrain) {
|
|
|
45001
45014
|
case "ellipse": return [ellipse(a, targetB, step)];
|
|
45002
45015
|
case "line": return [sampleSegment(a, constrain ? snappedEnd(a, b) : b, step)];
|
|
45003
45016
|
case "arrow": return arrow(a, constrain ? snappedEnd(a, b) : b, step);
|
|
45004
|
-
case "triangle": return [polygon(3, a, targetB, step,
|
|
45017
|
+
case "triangle": return [polygon(3, a, targetB, step, Math.PI / 2)];
|
|
45005
45018
|
case "diamond": return [polygon(4, a, targetB, step, 0)];
|
|
45006
|
-
case "pentagon": return [polygon(5, a, targetB, step,
|
|
45007
|
-
case "hexagon": return [polygon(6, a, targetB, step,
|
|
45019
|
+
case "pentagon": return [polygon(5, a, targetB, step, Math.PI / 2)];
|
|
45020
|
+
case "hexagon": return [polygon(6, a, targetB, step, Math.PI / 2)];
|
|
45008
45021
|
case "octagon": return [polygon(8, a, targetB, step, Math.PI / 8)];
|
|
45009
45022
|
case "star": return [star(5, a, targetB, step)];
|
|
45010
45023
|
case "heart": return [heart(a, targetB, step)];
|
|
@@ -45203,6 +45216,7 @@ var ShapeTool = class {
|
|
|
45203
45216
|
this.ctx.clusters.assign(strokes[0]);
|
|
45204
45217
|
for (const s of strokes) s.clusterId = strokes[0].clusterId;
|
|
45205
45218
|
this.ctx.history.execute(new AddStrokesCommand(strokes));
|
|
45219
|
+
this.ctx.selection.set(strokes.map((s) => s.id));
|
|
45206
45220
|
this.ctx.setTool?.("select");
|
|
45207
45221
|
}
|
|
45208
45222
|
this.ctx.transition("idle");
|
|
@@ -45784,6 +45798,13 @@ var HANDLE_HIT_PX = 12;
|
|
|
45784
45798
|
var ROTATE_OFFSET_PX = 26;
|
|
45785
45799
|
var ROTATE_HIT_PX = 14;
|
|
45786
45800
|
var COLOR = 2450411;
|
|
45801
|
+
/**
|
|
45802
|
+
* Visual-only outset between the selected object and the gizmo's outline/
|
|
45803
|
+
* handles — the object's actual bounding box (used for resize math, see
|
|
45804
|
+
* `anchorPoint`/`handlePoint`'s callers in `selectTool.ts`) never changes;
|
|
45805
|
+
* only where the outline and handle meshes are *drawn* and hit-tested does.
|
|
45806
|
+
*/
|
|
45807
|
+
var GAP_PX = 8;
|
|
45787
45808
|
var SelectionGizmo = class {
|
|
45788
45809
|
constructor(scene) {
|
|
45789
45810
|
this.group = new Group();
|
|
@@ -45798,7 +45819,7 @@ var SelectionGizmo = class {
|
|
|
45798
45819
|
this.outline = new LineLoop(new BufferGeometry(), this.lineMaterial);
|
|
45799
45820
|
this.outline.position.z = GIZMO_Z;
|
|
45800
45821
|
this.outline.frustumCulled = false;
|
|
45801
|
-
const handleGeometry = new
|
|
45822
|
+
const handleGeometry = new CircleGeometry(.5, 20);
|
|
45802
45823
|
const handleMaterial = new MeshBasicMaterial({ color: COLOR });
|
|
45803
45824
|
const makeHandle = () => new Mesh(handleGeometry, handleMaterial);
|
|
45804
45825
|
this.handles = {
|
|
@@ -45826,18 +45847,19 @@ var SelectionGizmo = class {
|
|
|
45826
45847
|
if (!box) return;
|
|
45827
45848
|
this.lineMaterial.color.setHex(locked ? 16096779 : COLOR);
|
|
45828
45849
|
this.rotateHandle.visible = !locked;
|
|
45850
|
+
const visual = this.visualBox(box, wpp);
|
|
45829
45851
|
const positions = new Float32Array([
|
|
45830
|
-
|
|
45831
|
-
|
|
45852
|
+
visual.minX,
|
|
45853
|
+
visual.minY,
|
|
45832
45854
|
GIZMO_Z,
|
|
45833
|
-
|
|
45834
|
-
|
|
45855
|
+
visual.maxX,
|
|
45856
|
+
visual.minY,
|
|
45835
45857
|
GIZMO_Z,
|
|
45836
|
-
|
|
45837
|
-
|
|
45858
|
+
visual.maxX,
|
|
45859
|
+
visual.maxY,
|
|
45838
45860
|
GIZMO_Z,
|
|
45839
|
-
|
|
45840
|
-
|
|
45861
|
+
visual.minX,
|
|
45862
|
+
visual.maxY,
|
|
45841
45863
|
GIZMO_Z
|
|
45842
45864
|
]);
|
|
45843
45865
|
this.outline.geometry.dispose();
|
|
@@ -45849,17 +45871,27 @@ var SelectionGizmo = class {
|
|
|
45849
45871
|
for (const [handle, mesh] of Object.entries(this.handles)) {
|
|
45850
45872
|
mesh.visible = !locked;
|
|
45851
45873
|
if (!locked) {
|
|
45852
|
-
const p = this.handlePoint(handle,
|
|
45874
|
+
const p = this.handlePoint(handle, visual);
|
|
45853
45875
|
mesh.position.set(p.x, p.y, GIZMO_Z);
|
|
45854
45876
|
mesh.scale.set(size, size, 1);
|
|
45855
45877
|
}
|
|
45856
45878
|
}
|
|
45857
45879
|
if (!locked) {
|
|
45858
|
-
const rotate = this.rotatePoint(
|
|
45880
|
+
const rotate = this.rotatePoint(visual, wpp);
|
|
45859
45881
|
this.rotateHandle.position.set(rotate.x, rotate.y, GIZMO_Z);
|
|
45860
45882
|
this.rotateHandle.scale.set(size, size, 1);
|
|
45861
45883
|
}
|
|
45862
45884
|
}
|
|
45885
|
+
/** The object's real bbox outset by `GAP_PX`, screen-constant — where the outline/handles are drawn and hit-tested. Never used for resize math (see `anchorPoint`). */
|
|
45886
|
+
visualBox(box, wpp) {
|
|
45887
|
+
const gap = GAP_PX * wpp;
|
|
45888
|
+
return {
|
|
45889
|
+
minX: box.minX - gap,
|
|
45890
|
+
minY: box.minY - gap,
|
|
45891
|
+
maxX: box.maxX + gap,
|
|
45892
|
+
maxY: box.maxY + gap
|
|
45893
|
+
};
|
|
45894
|
+
}
|
|
45863
45895
|
handlePoint(handle, box) {
|
|
45864
45896
|
const midX = (box.minX + box.maxX) / 2;
|
|
45865
45897
|
const midY = (box.minY + box.maxY) / 2;
|
|
@@ -45947,7 +45979,8 @@ var SelectionGizmo = class {
|
|
|
45947
45979
|
if (!this.box || this.isLocked) return null;
|
|
45948
45980
|
const handleR = HANDLE_HIT_PX * this.wpp;
|
|
45949
45981
|
const edgeSlop = 6 * this.wpp;
|
|
45950
|
-
const
|
|
45982
|
+
const visual = this.visualBox(this.box, this.wpp);
|
|
45983
|
+
const rotate = this.rotatePoint(visual, this.wpp);
|
|
45951
45984
|
if (Math.hypot(p.x - rotate.x, p.y - rotate.y) < ROTATE_HIT_PX * this.wpp) return { kind: "rotate" };
|
|
45952
45985
|
for (const handle of [
|
|
45953
45986
|
"nw",
|
|
@@ -45959,13 +45992,13 @@ var SelectionGizmo = class {
|
|
|
45959
45992
|
"e",
|
|
45960
45993
|
"w"
|
|
45961
45994
|
]) {
|
|
45962
|
-
const hp = this.handlePoint(handle,
|
|
45995
|
+
const hp = this.handlePoint(handle, visual);
|
|
45963
45996
|
if (Math.abs(p.x - hp.x) < handleR && Math.abs(p.y - hp.y) < handleR) return {
|
|
45964
45997
|
kind: "scale",
|
|
45965
45998
|
handle
|
|
45966
45999
|
};
|
|
45967
46000
|
}
|
|
45968
|
-
const { minX, maxX, minY, maxY } =
|
|
46001
|
+
const { minX, maxX, minY, maxY } = visual;
|
|
45969
46002
|
const inX = p.x >= minX - edgeSlop && p.x <= maxX + edgeSlop;
|
|
45970
46003
|
const inY = p.y >= minY - edgeSlop && p.y <= maxY + edgeSlop;
|
|
45971
46004
|
if (inX && Math.abs(p.y - maxY) < edgeSlop) return {
|
|
@@ -45985,7 +46018,7 @@ var SelectionGizmo = class {
|
|
|
45985
46018
|
handle: "w"
|
|
45986
46019
|
};
|
|
45987
46020
|
const pad = 2 * this.wpp;
|
|
45988
|
-
if (p.x >= minX - pad && p.x <= maxX + pad && p.y >= minY - pad && p.y <= maxY + pad) return { kind: "inside" };
|
|
46021
|
+
if (p.x >= this.box.minX - pad && p.x <= this.box.maxX + pad && p.y >= this.box.minY - pad && p.y <= this.box.maxY + pad) return { kind: "inside" };
|
|
45989
46022
|
return null;
|
|
45990
46023
|
}
|
|
45991
46024
|
rotatePoint(box, wpp) {
|
|
@@ -48218,6 +48251,112 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48218
48251
|
})),
|
|
48219
48252
|
...[...document.allCustomObjects()].map((value) => clone(toBoardObject(value)))
|
|
48220
48253
|
];
|
|
48254
|
+
const projectToScreen = (point) => engine ? engine.boardToScreen(point) : {
|
|
48255
|
+
x: (point.x - view.x) * view.zoom,
|
|
48256
|
+
y: (view.y - point.y) * view.zoom
|
|
48257
|
+
};
|
|
48258
|
+
const lockFields = (item) => ({
|
|
48259
|
+
locked: !!item.locked,
|
|
48260
|
+
...item.lockedBy ? { lockedBy: item.lockedBy } : {},
|
|
48261
|
+
...item.lockedByName ? { lockedByName: item.lockedByName } : {}
|
|
48262
|
+
});
|
|
48263
|
+
const lockFieldsMany = (items) => {
|
|
48264
|
+
const locked = items.filter((i) => i.locked);
|
|
48265
|
+
if (locked.length === 0) return { locked: false };
|
|
48266
|
+
const lockedBys = [...new Set(locked.map((i) => i.lockedBy).filter((v) => !!v))];
|
|
48267
|
+
const names = [...new Set(locked.map((i) => i.lockedByName).filter((v) => !!v))];
|
|
48268
|
+
return {
|
|
48269
|
+
locked: true,
|
|
48270
|
+
...lockedBys.length === 1 ? { lockedBy: lockedBys[0] } : {},
|
|
48271
|
+
...names.length === 1 ? { lockedByName: names[0] } : {}
|
|
48272
|
+
};
|
|
48273
|
+
};
|
|
48274
|
+
const strokeClusterAnchor = (strokes) => {
|
|
48275
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
48276
|
+
for (const s of strokes) for (const p of s.points) {
|
|
48277
|
+
const world = s.matrix ? apply(s.matrix, p) : p;
|
|
48278
|
+
minX = Math.min(minX, world.x);
|
|
48279
|
+
maxX = Math.max(maxX, world.x);
|
|
48280
|
+
minY = Math.min(minY, world.y);
|
|
48281
|
+
maxY = Math.max(maxY, world.y);
|
|
48282
|
+
}
|
|
48283
|
+
if (!Number.isFinite(minX)) return projectToScreen({
|
|
48284
|
+
x: 0,
|
|
48285
|
+
y: 0
|
|
48286
|
+
});
|
|
48287
|
+
return projectToScreen({
|
|
48288
|
+
x: (minX + maxX) / 2,
|
|
48289
|
+
y: maxY + 1.4
|
|
48290
|
+
});
|
|
48291
|
+
};
|
|
48292
|
+
/** Per-type anchor formulas mirror `ScrawlEngine.getSelectedItemInfo` exactly, so a Host-built toolbar lands in the same spot the SDK's own would. */
|
|
48293
|
+
const anchorFor = (obj) => {
|
|
48294
|
+
switch (obj.type) {
|
|
48295
|
+
case "note": return projectToScreen({
|
|
48296
|
+
x: obj.x,
|
|
48297
|
+
y: obj.y + obj.size / 2 + 1
|
|
48298
|
+
});
|
|
48299
|
+
case "text": {
|
|
48300
|
+
const { width } = measureTextBlock(obj.text, obj.fontSize);
|
|
48301
|
+
return projectToScreen({
|
|
48302
|
+
x: obj.x + width / 2,
|
|
48303
|
+
y: obj.y + 1
|
|
48304
|
+
});
|
|
48305
|
+
}
|
|
48306
|
+
case "table": {
|
|
48307
|
+
const { width } = measureTable(obj);
|
|
48308
|
+
return projectToScreen({
|
|
48309
|
+
x: obj.x + width / 2,
|
|
48310
|
+
y: obj.y + 1.2
|
|
48311
|
+
});
|
|
48312
|
+
}
|
|
48313
|
+
case "image": return projectToScreen({
|
|
48314
|
+
x: obj.x,
|
|
48315
|
+
y: obj.y + obj.height / 2 + 1.2
|
|
48316
|
+
});
|
|
48317
|
+
case "timer": return projectToScreen({
|
|
48318
|
+
x: obj.x,
|
|
48319
|
+
y: obj.y + obj.size / 2 + 1.2
|
|
48320
|
+
});
|
|
48321
|
+
case "custom": {
|
|
48322
|
+
const b = obj.fallback.bounds;
|
|
48323
|
+
return projectToScreen(apply(obj.transform, {
|
|
48324
|
+
x: b.x + b.width / 2,
|
|
48325
|
+
y: b.y + b.height + 1
|
|
48326
|
+
}));
|
|
48327
|
+
}
|
|
48328
|
+
case "stroke": return strokeClusterAnchor([obj]);
|
|
48329
|
+
}
|
|
48330
|
+
};
|
|
48331
|
+
const computeFocusedItem = () => {
|
|
48332
|
+
if (selection.length === 0) return null;
|
|
48333
|
+
if (selection.length === 1) {
|
|
48334
|
+
const obj = readObject(selection[0]);
|
|
48335
|
+
if (!obj) return null;
|
|
48336
|
+
if (obj.type === "custom") return {
|
|
48337
|
+
type: "custom",
|
|
48338
|
+
id: obj.id,
|
|
48339
|
+
locked: false,
|
|
48340
|
+
screenPosition: anchorFor(obj)
|
|
48341
|
+
};
|
|
48342
|
+
return {
|
|
48343
|
+
type: obj.type,
|
|
48344
|
+
id: obj.id,
|
|
48345
|
+
...lockFields(obj),
|
|
48346
|
+
screenPosition: anchorFor(obj)
|
|
48347
|
+
};
|
|
48348
|
+
}
|
|
48349
|
+
const objects = selection.map(readObject);
|
|
48350
|
+
if (!objects.every((o) => o?.type === "stroke")) return null;
|
|
48351
|
+
const clusterId = objects[0].clusterId;
|
|
48352
|
+
if (!clusterId || !objects.every((o) => o.clusterId === clusterId)) return null;
|
|
48353
|
+
return {
|
|
48354
|
+
type: "stroke",
|
|
48355
|
+
id: objects[0].id,
|
|
48356
|
+
...lockFieldsMany(objects),
|
|
48357
|
+
screenPosition: strokeClusterAnchor(objects)
|
|
48358
|
+
};
|
|
48359
|
+
};
|
|
48221
48360
|
const updateObject = (id, patch) => {
|
|
48222
48361
|
assertMutable();
|
|
48223
48362
|
const before = readObject(id);
|
|
@@ -48259,6 +48398,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48259
48398
|
zoom: view.zoom,
|
|
48260
48399
|
readOnly,
|
|
48261
48400
|
selection: Object.freeze([...selection]),
|
|
48401
|
+
focusedItem: computeFocusedItem(),
|
|
48262
48402
|
strokeCount: [...document.all()].length,
|
|
48263
48403
|
objectCount: allObjects().length,
|
|
48264
48404
|
canUndo: history.canUndo,
|
|
@@ -48673,10 +48813,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48673
48813
|
changed();
|
|
48674
48814
|
},
|
|
48675
48815
|
get: () => ({ ...view }),
|
|
48676
|
-
boardToScreen: (point) =>
|
|
48677
|
-
x: (point.x - view.x) * view.zoom,
|
|
48678
|
-
y: (view.y - point.y) * view.zoom
|
|
48679
|
-
},
|
|
48816
|
+
boardToScreen: (point) => projectToScreen(point),
|
|
48680
48817
|
screenToBoard: (point) => engine ? engine.screenToBoard(point.x, point.y) ?? {
|
|
48681
48818
|
x: point.x,
|
|
48682
48819
|
y: point.y
|
|
@@ -48713,6 +48850,81 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48713
48850
|
update(id, patch) {
|
|
48714
48851
|
updateObject(id, patch);
|
|
48715
48852
|
},
|
|
48853
|
+
duplicate(ids) {
|
|
48854
|
+
assertMutable();
|
|
48855
|
+
const objects = [...new Set(ids)].map(readObject).filter((o) => !!o);
|
|
48856
|
+
if (objects.length === 0) return [];
|
|
48857
|
+
const DX = 1.5;
|
|
48858
|
+
const DY = -1.5;
|
|
48859
|
+
const newClusterIds = /* @__PURE__ */ new Map();
|
|
48860
|
+
const clusterIdFor = (oldClusterId) => {
|
|
48861
|
+
let next = newClusterIds.get(oldClusterId);
|
|
48862
|
+
if (!next) {
|
|
48863
|
+
next = crypto.randomUUID();
|
|
48864
|
+
newClusterIds.set(oldClusterId, next);
|
|
48865
|
+
}
|
|
48866
|
+
return next;
|
|
48867
|
+
};
|
|
48868
|
+
const commands = [];
|
|
48869
|
+
const newIds = [];
|
|
48870
|
+
for (const obj of objects) {
|
|
48871
|
+
const id = createId();
|
|
48872
|
+
newIds.push(id);
|
|
48873
|
+
const rest = { ...obj };
|
|
48874
|
+
delete rest.locked;
|
|
48875
|
+
delete rest.lockedBy;
|
|
48876
|
+
delete rest.lockedByName;
|
|
48877
|
+
let duplicated;
|
|
48878
|
+
if (obj.type === "stroke") {
|
|
48879
|
+
const matrix = obj.matrix ?? [
|
|
48880
|
+
1,
|
|
48881
|
+
0,
|
|
48882
|
+
0,
|
|
48883
|
+
1,
|
|
48884
|
+
0,
|
|
48885
|
+
0
|
|
48886
|
+
];
|
|
48887
|
+
duplicated = {
|
|
48888
|
+
...rest,
|
|
48889
|
+
id,
|
|
48890
|
+
matrix: [
|
|
48891
|
+
matrix[0],
|
|
48892
|
+
matrix[1],
|
|
48893
|
+
matrix[2],
|
|
48894
|
+
matrix[3],
|
|
48895
|
+
matrix[4] + DX,
|
|
48896
|
+
matrix[5] + DY
|
|
48897
|
+
],
|
|
48898
|
+
...obj.clusterId ? { clusterId: clusterIdFor(obj.clusterId) } : {}
|
|
48899
|
+
};
|
|
48900
|
+
} else if (obj.type === "custom") {
|
|
48901
|
+
const t = obj.transform;
|
|
48902
|
+
duplicated = {
|
|
48903
|
+
...rest,
|
|
48904
|
+
id,
|
|
48905
|
+
transform: [
|
|
48906
|
+
t[0],
|
|
48907
|
+
t[1],
|
|
48908
|
+
t[2],
|
|
48909
|
+
t[3],
|
|
48910
|
+
t[4] + DX,
|
|
48911
|
+
t[5] + DY
|
|
48912
|
+
]
|
|
48913
|
+
};
|
|
48914
|
+
delete duplicated.lock;
|
|
48915
|
+
} else duplicated = {
|
|
48916
|
+
...rest,
|
|
48917
|
+
id,
|
|
48918
|
+
x: obj.x + DX,
|
|
48919
|
+
y: obj.y + DY
|
|
48920
|
+
};
|
|
48921
|
+
validateBoardObject(duplicated.type, duplicated);
|
|
48922
|
+
commands.push(addCommand(duplicated.type, duplicated));
|
|
48923
|
+
}
|
|
48924
|
+
executeHistory(() => history.execute(new CommandBatch("duplicate content", commands)));
|
|
48925
|
+
changed();
|
|
48926
|
+
return newIds;
|
|
48927
|
+
},
|
|
48716
48928
|
table: {
|
|
48717
48929
|
addRow(tableId) {
|
|
48718
48930
|
const table = document.getTable(tableId);
|
|
@@ -49660,14 +49872,17 @@ function InlineEditors({ controller, renderPortal }) {
|
|
|
49660
49872
|
if (editor?.kind === "text") {
|
|
49661
49873
|
const text = editor.text;
|
|
49662
49874
|
if (editor.id === null) {
|
|
49663
|
-
if (text.trim())
|
|
49664
|
-
|
|
49665
|
-
|
|
49666
|
-
|
|
49667
|
-
|
|
49668
|
-
|
|
49669
|
-
|
|
49670
|
-
|
|
49875
|
+
if (text.trim()) {
|
|
49876
|
+
const newId = controller.content.add({
|
|
49877
|
+
type: "text",
|
|
49878
|
+
x: editor.boardX,
|
|
49879
|
+
y: editor.boardY,
|
|
49880
|
+
text,
|
|
49881
|
+
color: editor.color,
|
|
49882
|
+
fontSize: TEXT_DEFAULT_SIZE
|
|
49883
|
+
});
|
|
49884
|
+
controller.content.select([newId]);
|
|
49885
|
+
}
|
|
49671
49886
|
} else if (!text.trim()) controller.content.remove([editor.id]);
|
|
49672
49887
|
else controller.content.update(editor.id, { text });
|
|
49673
49888
|
}
|
|
@@ -49862,6 +50077,366 @@ function InlineEditors({ controller, renderPortal }) {
|
|
|
49862
50077
|
}))] });
|
|
49863
50078
|
}
|
|
49864
50079
|
//#endregion
|
|
50080
|
+
//#region src/ui/toolbar/focused-item-toolbar.tsx
|
|
50081
|
+
var NOTE_SIZES = [
|
|
50082
|
+
["Small", 7],
|
|
50083
|
+
["Medium", 10],
|
|
50084
|
+
["Large", 14]
|
|
50085
|
+
];
|
|
50086
|
+
var TEXT_SIZES = [
|
|
50087
|
+
["Small", 1.2],
|
|
50088
|
+
["Medium", 1.8],
|
|
50089
|
+
["Large", 2.6]
|
|
50090
|
+
];
|
|
50091
|
+
var SHAPE_WIDTHS = [
|
|
50092
|
+
["Thin", .1],
|
|
50093
|
+
["Medium", .15],
|
|
50094
|
+
["Thick", .3]
|
|
50095
|
+
];
|
|
50096
|
+
/** Matches the native selection gizmo's own outset (renderer/selection/gizmo.ts's GAP_PX), for a consistent look. */
|
|
50097
|
+
var HANDLE_GAP_PX = 8;
|
|
50098
|
+
var MIN_SIZE = 4;
|
|
50099
|
+
var MAX_SIZE = 30;
|
|
50100
|
+
var HANDLE_CORNERS = [
|
|
50101
|
+
"nw",
|
|
50102
|
+
"ne",
|
|
50103
|
+
"sw",
|
|
50104
|
+
"se"
|
|
50105
|
+
];
|
|
50106
|
+
/**
|
|
50107
|
+
* Floating toolbar above the focused note, shape, or text block — Colour,
|
|
50108
|
+
* Size (note/text) or Width (shape), Lock/Unlock, Duplicate, Delete.
|
|
50109
|
+
* Table/image/timer/custom objects and plain (non-shape) ink strokes never
|
|
50110
|
+
* get a toolbar here — out of scope for this destination.
|
|
50111
|
+
*
|
|
50112
|
+
* Lock/Unlock carries no per-user ownership gating: nothing else in this
|
|
50113
|
+
* SDK enforces lock ownership either (`content.update` never checks
|
|
50114
|
+
* `locked`, and the engine's own internal unlock check has no way for a
|
|
50115
|
+
* Host to ever supply a real user id) — locking is advisory UI state
|
|
50116
|
+
* throughout, and this toolbar matches that rather than inventing an
|
|
50117
|
+
* enforcement story alone.
|
|
50118
|
+
*
|
|
50119
|
+
* A shape can be several strokes sharing one `clusterId` (e.g. an arrow's
|
|
50120
|
+
* shaft + head) — every action here applies to the whole cluster. Resolved
|
|
50121
|
+
* via `query.all()` + `clusterId`, not `snapshot.selection`: a canvas click
|
|
50122
|
+
* on one member selects every member internally, but the public selection
|
|
50123
|
+
* bridge only ever reports one id (`FocusedItem.id` is singular by design),
|
|
50124
|
+
* so reconstructing the cluster from the document is the reliable path
|
|
50125
|
+
* regardless of how the selection was made.
|
|
50126
|
+
*
|
|
50127
|
+
* A focused note also gets drag-to-resize corner handles — notes have no
|
|
50128
|
+
* gizmo of their own (the native selection gizmo is ink-stroke/shape-only),
|
|
50129
|
+
* so this is that capability's default-ui-owned equivalent. Proportional
|
|
50130
|
+
* (always-square) resize from the note's own centre, clamped to
|
|
50131
|
+
* `[MIN_SIZE, MAX_SIZE]`, independent of which corner is grabbed.
|
|
50132
|
+
*/
|
|
50133
|
+
function FocusedItemToolbar({ controller, snapshot }) {
|
|
50134
|
+
const focused = snapshot.focusedItem;
|
|
50135
|
+
if (!focused) return null;
|
|
50136
|
+
const obj = controller.query.get(focused.id);
|
|
50137
|
+
if (!obj) return null;
|
|
50138
|
+
if (obj.type === "note") return /* @__PURE__ */ jsx(NoteToolbar, {
|
|
50139
|
+
controller,
|
|
50140
|
+
snapshot,
|
|
50141
|
+
note: obj,
|
|
50142
|
+
locked: focused.locked,
|
|
50143
|
+
anchor: focused.screenPosition
|
|
50144
|
+
});
|
|
50145
|
+
if (obj.type === "text") return /* @__PURE__ */ jsx(TextToolbar, {
|
|
50146
|
+
controller,
|
|
50147
|
+
snapshot,
|
|
50148
|
+
text: obj,
|
|
50149
|
+
locked: focused.locked,
|
|
50150
|
+
anchor: focused.screenPosition
|
|
50151
|
+
});
|
|
50152
|
+
if (obj.type === "stroke" && obj.tool === "shape") return /* @__PURE__ */ jsx(ShapeToolbar, {
|
|
50153
|
+
controller,
|
|
50154
|
+
snapshot,
|
|
50155
|
+
stroke: obj,
|
|
50156
|
+
locked: focused.locked,
|
|
50157
|
+
anchor: focused.screenPosition
|
|
50158
|
+
});
|
|
50159
|
+
return null;
|
|
50160
|
+
}
|
|
50161
|
+
function ToolbarShell({ anchor, children }) {
|
|
50162
|
+
if (!anchor) return null;
|
|
50163
|
+
return /* @__PURE__ */ jsx("div", {
|
|
50164
|
+
"aria-label": "Selected object",
|
|
50165
|
+
className: "scrawl-board__focus-toolbar",
|
|
50166
|
+
role: "toolbar",
|
|
50167
|
+
style: {
|
|
50168
|
+
left: anchor.x,
|
|
50169
|
+
top: anchor.y
|
|
50170
|
+
},
|
|
50171
|
+
children
|
|
50172
|
+
});
|
|
50173
|
+
}
|
|
50174
|
+
function NoteToolbar({ controller, snapshot, note, locked, anchor }) {
|
|
50175
|
+
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
50176
|
+
const locked_ = !mutable || locked;
|
|
50177
|
+
const half = note.size / 2;
|
|
50178
|
+
const cornerA = controller.view.boardToScreen({
|
|
50179
|
+
x: note.x - half,
|
|
50180
|
+
y: note.y + half
|
|
50181
|
+
});
|
|
50182
|
+
const cornerB = controller.view.boardToScreen({
|
|
50183
|
+
x: note.x + half,
|
|
50184
|
+
y: note.y - half
|
|
50185
|
+
});
|
|
50186
|
+
const outline = {
|
|
50187
|
+
left: Math.min(cornerA.x, cornerB.x) - HANDLE_GAP_PX,
|
|
50188
|
+
top: Math.min(cornerA.y, cornerB.y) - HANDLE_GAP_PX,
|
|
50189
|
+
right: Math.max(cornerA.x, cornerB.x) + HANDLE_GAP_PX,
|
|
50190
|
+
bottom: Math.max(cornerA.y, cornerB.y) + HANDLE_GAP_PX
|
|
50191
|
+
};
|
|
50192
|
+
const handlePoints = {
|
|
50193
|
+
nw: {
|
|
50194
|
+
x: outline.left,
|
|
50195
|
+
y: outline.top
|
|
50196
|
+
},
|
|
50197
|
+
ne: {
|
|
50198
|
+
x: outline.right,
|
|
50199
|
+
y: outline.top
|
|
50200
|
+
},
|
|
50201
|
+
sw: {
|
|
50202
|
+
x: outline.left,
|
|
50203
|
+
y: outline.bottom
|
|
50204
|
+
},
|
|
50205
|
+
se: {
|
|
50206
|
+
x: outline.right,
|
|
50207
|
+
y: outline.bottom
|
|
50208
|
+
}
|
|
50209
|
+
};
|
|
50210
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
50211
|
+
/* @__PURE__ */ jsx("div", {
|
|
50212
|
+
"aria-hidden": "true",
|
|
50213
|
+
className: "scrawl-board__focus-outline",
|
|
50214
|
+
"data-locked": locked,
|
|
50215
|
+
style: {
|
|
50216
|
+
left: outline.left,
|
|
50217
|
+
top: outline.top,
|
|
50218
|
+
width: outline.right - outline.left,
|
|
50219
|
+
height: outline.bottom - outline.top
|
|
50220
|
+
}
|
|
50221
|
+
}),
|
|
50222
|
+
mutable && !locked && HANDLE_CORNERS.map((corner) => /* @__PURE__ */ jsx(ResizeHandle, {
|
|
50223
|
+
controller,
|
|
50224
|
+
note,
|
|
50225
|
+
corner,
|
|
50226
|
+
x: handlePoints[corner].x,
|
|
50227
|
+
y: handlePoints[corner].y
|
|
50228
|
+
}, corner)),
|
|
50229
|
+
/* @__PURE__ */ jsxs(ToolbarShell, {
|
|
50230
|
+
anchor,
|
|
50231
|
+
children: [
|
|
50232
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50233
|
+
className: "scrawl-board__style-group",
|
|
50234
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Size" }), NOTE_SIZES.map(([label, size]) => /* @__PURE__ */ jsx("button", {
|
|
50235
|
+
"aria-label": `${label} note`,
|
|
50236
|
+
"aria-pressed": note.size === size,
|
|
50237
|
+
disabled: locked_,
|
|
50238
|
+
onClick: () => controller.content.update(note.id, { size }),
|
|
50239
|
+
type: "button",
|
|
50240
|
+
children: label[0]
|
|
50241
|
+
}, label))]
|
|
50242
|
+
}),
|
|
50243
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50244
|
+
className: "scrawl-board__style-group",
|
|
50245
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(NOTE_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
|
|
50246
|
+
"aria-label": `${name} note`,
|
|
50247
|
+
"aria-pressed": note.color.toLowerCase() === value.toLowerCase(),
|
|
50248
|
+
disabled: locked_,
|
|
50249
|
+
onClick: () => controller.content.update(note.id, { color: value }),
|
|
50250
|
+
type: "button",
|
|
50251
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
50252
|
+
"aria-hidden": "true",
|
|
50253
|
+
style: { backgroundColor: value }
|
|
50254
|
+
})
|
|
50255
|
+
}, value))]
|
|
50256
|
+
}),
|
|
50257
|
+
/* @__PURE__ */ jsx(LockDuplicateDelete, {
|
|
50258
|
+
controller,
|
|
50259
|
+
mutable,
|
|
50260
|
+
locked,
|
|
50261
|
+
ids: [note.id]
|
|
50262
|
+
})
|
|
50263
|
+
]
|
|
50264
|
+
})
|
|
50265
|
+
] });
|
|
50266
|
+
}
|
|
50267
|
+
/**
|
|
50268
|
+
* One corner handle — drag distance from the note's own centre (its `x`/`y`
|
|
50269
|
+
* already *is* the centre) drives a proportional, always-square resize,
|
|
50270
|
+
* independent of which corner is grabbed. `note` is fresh every render (a
|
|
50271
|
+
* new `content.update` during drag triggers a new snapshot), so the
|
|
50272
|
+
* closure never goes stale.
|
|
50273
|
+
*/
|
|
50274
|
+
function ResizeHandle({ controller, note, corner, x, y }) {
|
|
50275
|
+
const [dragging, setDragging] = useState(false);
|
|
50276
|
+
const nudge = (delta) => {
|
|
50277
|
+
const half = Math.max(MIN_SIZE / 2, Math.min(MAX_SIZE / 2, note.size / 2 + delta));
|
|
50278
|
+
controller.content.update(note.id, { size: half * 2 });
|
|
50279
|
+
};
|
|
50280
|
+
return /* @__PURE__ */ jsx("span", {
|
|
50281
|
+
"aria-label": `Resize note (${corner})`,
|
|
50282
|
+
"aria-valuemax": MAX_SIZE,
|
|
50283
|
+
"aria-valuemin": MIN_SIZE,
|
|
50284
|
+
"aria-valuenow": note.size,
|
|
50285
|
+
role: "slider",
|
|
50286
|
+
tabIndex: 0,
|
|
50287
|
+
className: "scrawl-board__focus-handle",
|
|
50288
|
+
"data-orientation": corner === "nw" || corner === "se" ? "nwse" : "nesw",
|
|
50289
|
+
style: {
|
|
50290
|
+
left: x,
|
|
50291
|
+
top: y
|
|
50292
|
+
},
|
|
50293
|
+
onKeyDown: (event) => {
|
|
50294
|
+
if (event.key === "ArrowUp" || event.key === "ArrowRight") {
|
|
50295
|
+
event.preventDefault();
|
|
50296
|
+
nudge(.5);
|
|
50297
|
+
} else if (event.key === "ArrowDown" || event.key === "ArrowLeft") {
|
|
50298
|
+
event.preventDefault();
|
|
50299
|
+
nudge(-.5);
|
|
50300
|
+
}
|
|
50301
|
+
},
|
|
50302
|
+
onPointerDown: (event) => {
|
|
50303
|
+
event.stopPropagation();
|
|
50304
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
50305
|
+
setDragging(true);
|
|
50306
|
+
},
|
|
50307
|
+
onPointerMove: (event) => {
|
|
50308
|
+
if (!dragging) return;
|
|
50309
|
+
const board = controller.view.screenToBoard({
|
|
50310
|
+
x: event.clientX,
|
|
50311
|
+
y: event.clientY
|
|
50312
|
+
});
|
|
50313
|
+
const half = Math.max(MIN_SIZE / 2, Math.min(MAX_SIZE / 2, Math.max(Math.abs(board.x - note.x), Math.abs(board.y - note.y))));
|
|
50314
|
+
controller.content.update(note.id, { size: half * 2 });
|
|
50315
|
+
},
|
|
50316
|
+
onPointerUp: (event) => {
|
|
50317
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
50318
|
+
setDragging(false);
|
|
50319
|
+
}
|
|
50320
|
+
});
|
|
50321
|
+
}
|
|
50322
|
+
function TextToolbar({ controller, snapshot, text, locked, anchor }) {
|
|
50323
|
+
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
50324
|
+
const locked_ = !mutable || locked;
|
|
50325
|
+
return /* @__PURE__ */ jsxs(ToolbarShell, {
|
|
50326
|
+
anchor,
|
|
50327
|
+
children: [
|
|
50328
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50329
|
+
className: "scrawl-board__style-group",
|
|
50330
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Size" }), TEXT_SIZES.map(([label, fontSize]) => /* @__PURE__ */ jsx("button", {
|
|
50331
|
+
"aria-label": `${label} text`,
|
|
50332
|
+
"aria-pressed": Math.abs(text.fontSize - fontSize) < .001,
|
|
50333
|
+
disabled: locked_,
|
|
50334
|
+
onClick: () => controller.content.update(text.id, { fontSize }),
|
|
50335
|
+
type: "button",
|
|
50336
|
+
children: label[0]
|
|
50337
|
+
}, label))]
|
|
50338
|
+
}),
|
|
50339
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50340
|
+
className: "scrawl-board__style-group",
|
|
50341
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(INK_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
|
|
50342
|
+
"aria-label": `${name} text`,
|
|
50343
|
+
"aria-pressed": text.color.toLowerCase() === value.toLowerCase(),
|
|
50344
|
+
disabled: locked_,
|
|
50345
|
+
onClick: () => controller.content.update(text.id, { color: value }),
|
|
50346
|
+
type: "button",
|
|
50347
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
50348
|
+
"aria-hidden": "true",
|
|
50349
|
+
style: { backgroundColor: value }
|
|
50350
|
+
})
|
|
50351
|
+
}, value))]
|
|
50352
|
+
}),
|
|
50353
|
+
/* @__PURE__ */ jsx(LockDuplicateDelete, {
|
|
50354
|
+
controller,
|
|
50355
|
+
mutable,
|
|
50356
|
+
locked,
|
|
50357
|
+
ids: [text.id]
|
|
50358
|
+
})
|
|
50359
|
+
]
|
|
50360
|
+
});
|
|
50361
|
+
}
|
|
50362
|
+
function ShapeToolbar({ controller, snapshot, stroke, locked, anchor }) {
|
|
50363
|
+
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
50364
|
+
const locked_ = !mutable || locked;
|
|
50365
|
+
const ids = (stroke.clusterId ? controller.query.all().filter((o) => o.type === "stroke" && o.clusterId === stroke.clusterId) : [stroke]).map((m) => m.id);
|
|
50366
|
+
const applyToAll = (patch) => {
|
|
50367
|
+
for (const id of ids) controller.content.update(id, patch);
|
|
50368
|
+
};
|
|
50369
|
+
return /* @__PURE__ */ jsxs(ToolbarShell, {
|
|
50370
|
+
anchor,
|
|
50371
|
+
children: [
|
|
50372
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50373
|
+
className: "scrawl-board__style-group",
|
|
50374
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Width" }), SHAPE_WIDTHS.map(([label, width]) => /* @__PURE__ */ jsx("button", {
|
|
50375
|
+
"aria-label": `${label} stroke`,
|
|
50376
|
+
"aria-pressed": Math.abs(stroke.baseWidth - width) < .001,
|
|
50377
|
+
disabled: locked_,
|
|
50378
|
+
onClick: () => applyToAll({ baseWidth: width }),
|
|
50379
|
+
type: "button",
|
|
50380
|
+
children: label
|
|
50381
|
+
}, label))]
|
|
50382
|
+
}),
|
|
50383
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50384
|
+
className: "scrawl-board__style-group",
|
|
50385
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(INK_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
|
|
50386
|
+
"aria-label": `${name} shape`,
|
|
50387
|
+
"aria-pressed": stroke.color.toLowerCase() === value.toLowerCase(),
|
|
50388
|
+
disabled: locked_,
|
|
50389
|
+
onClick: () => applyToAll({ color: value }),
|
|
50390
|
+
type: "button",
|
|
50391
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
50392
|
+
"aria-hidden": "true",
|
|
50393
|
+
style: { backgroundColor: value }
|
|
50394
|
+
})
|
|
50395
|
+
}, value))]
|
|
50396
|
+
}),
|
|
50397
|
+
/* @__PURE__ */ jsx(LockDuplicateDelete, {
|
|
50398
|
+
controller,
|
|
50399
|
+
mutable,
|
|
50400
|
+
locked,
|
|
50401
|
+
ids
|
|
50402
|
+
})
|
|
50403
|
+
]
|
|
50404
|
+
});
|
|
50405
|
+
}
|
|
50406
|
+
function LockDuplicateDelete({ controller, mutable, locked, ids }) {
|
|
50407
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
50408
|
+
className: "scrawl-board__style-group",
|
|
50409
|
+
children: [
|
|
50410
|
+
/* @__PURE__ */ jsx("button", {
|
|
50411
|
+
"aria-label": locked ? "Unlock" : "Lock",
|
|
50412
|
+
disabled: !mutable,
|
|
50413
|
+
onClick: () => {
|
|
50414
|
+
for (const id of ids) controller.content.update(id, locked ? { locked: false } : { locked: true });
|
|
50415
|
+
},
|
|
50416
|
+
type: "button",
|
|
50417
|
+
children: locked ? "Unlock" : "Lock"
|
|
50418
|
+
}),
|
|
50419
|
+
/* @__PURE__ */ jsx("button", {
|
|
50420
|
+
"aria-label": "Duplicate",
|
|
50421
|
+
disabled: !mutable || locked,
|
|
50422
|
+
onClick: () => {
|
|
50423
|
+
const newIds = controller.content.duplicate(ids);
|
|
50424
|
+
if (newIds.length) controller.content.select(newIds);
|
|
50425
|
+
},
|
|
50426
|
+
type: "button",
|
|
50427
|
+
children: "Duplicate"
|
|
50428
|
+
}),
|
|
50429
|
+
/* @__PURE__ */ jsx("button", {
|
|
50430
|
+
"aria-label": "Delete",
|
|
50431
|
+
disabled: !mutable || locked,
|
|
50432
|
+
onClick: () => controller.content.remove(ids),
|
|
50433
|
+
type: "button",
|
|
50434
|
+
children: "Delete"
|
|
50435
|
+
})
|
|
50436
|
+
]
|
|
50437
|
+
});
|
|
50438
|
+
}
|
|
50439
|
+
//#endregion
|
|
49865
50440
|
//#region src/ui/toolbar/style-shelf.tsx
|
|
49866
50441
|
var ERASER_RADII = [
|
|
49867
50442
|
["Fine", 1],
|
|
@@ -49900,7 +50475,7 @@ var SHELF_TOOLS = /* @__PURE__ */ new Set([
|
|
|
49900
50475
|
"timer"
|
|
49901
50476
|
]);
|
|
49902
50477
|
/** Contextual per-tool style controls — visible while a styleable tool is active. */
|
|
49903
|
-
function StyleShelf({ controller, snapshot }) {
|
|
50478
|
+
function StyleShelf({ controller, snapshot, anchor }) {
|
|
49904
50479
|
if (!SHELF_TOOLS.has(snapshot.tool)) return null;
|
|
49905
50480
|
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
49906
50481
|
const { style } = snapshot;
|
|
@@ -49908,7 +50483,12 @@ function StyleShelf({ controller, snapshot }) {
|
|
|
49908
50483
|
return /* @__PURE__ */ jsxs("div", {
|
|
49909
50484
|
"aria-label": "Board style",
|
|
49910
50485
|
className: "scrawl-board__style-shelf",
|
|
50486
|
+
"data-anchored": !!anchor,
|
|
49911
50487
|
role: "toolbar",
|
|
50488
|
+
style: anchor ? {
|
|
50489
|
+
left: anchor.x,
|
|
50490
|
+
top: anchor.y
|
|
50491
|
+
} : void 0,
|
|
49912
50492
|
children: [
|
|
49913
50493
|
snapshot.tool === "highlighter" && /* @__PURE__ */ jsxs("div", {
|
|
49914
50494
|
className: "scrawl-board__style-group",
|
|
@@ -50138,6 +50718,7 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50138
50718
|
const [panel, setPanel] = useState(null);
|
|
50139
50719
|
const [query, setQuery] = useState("");
|
|
50140
50720
|
const [announcement, setAnnouncement] = useState("Board controls ready");
|
|
50721
|
+
const [styleShelfAnchor, setStyleShelfAnchor] = useState(void 0);
|
|
50141
50722
|
const importRef = useRef(null);
|
|
50142
50723
|
const openerRef = useRef(null);
|
|
50143
50724
|
const searchRef = useRef(null);
|
|
@@ -50148,6 +50729,25 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50148
50729
|
useEffect(() => {
|
|
50149
50730
|
if (panel === "search") searchRef.current?.focus();
|
|
50150
50731
|
}, [panel]);
|
|
50732
|
+
useEffect(() => {
|
|
50733
|
+
const recompute = () => {
|
|
50734
|
+
const root = chromeRef.current;
|
|
50735
|
+
const button = root?.querySelector(".scrawl-board__tools button[aria-pressed=\"true\"]");
|
|
50736
|
+
if (!root || !button) {
|
|
50737
|
+
setStyleShelfAnchor(void 0);
|
|
50738
|
+
return;
|
|
50739
|
+
}
|
|
50740
|
+
const buttonRect = button.getBoundingClientRect();
|
|
50741
|
+
const rootRect = root.getBoundingClientRect();
|
|
50742
|
+
setStyleShelfAnchor({
|
|
50743
|
+
x: buttonRect.left + buttonRect.width / 2 - rootRect.left,
|
|
50744
|
+
y: buttonRect.top - rootRect.top
|
|
50745
|
+
});
|
|
50746
|
+
};
|
|
50747
|
+
recompute();
|
|
50748
|
+
window.addEventListener("resize", recompute);
|
|
50749
|
+
return () => window.removeEventListener("resize", recompute);
|
|
50750
|
+
}, [snapshot.tool]);
|
|
50151
50751
|
const open = (next, opener) => {
|
|
50152
50752
|
openerRef.current = opener;
|
|
50153
50753
|
setPanel(next);
|
|
@@ -50376,9 +50976,14 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50376
50976
|
]
|
|
50377
50977
|
}),
|
|
50378
50978
|
renderSlot(slots?.stylePanel, enabled("styleShelf") && /* @__PURE__ */ jsx(StyleShelf, {
|
|
50979
|
+
anchor: styleShelfAnchor,
|
|
50379
50980
|
controller,
|
|
50380
50981
|
snapshot
|
|
50381
50982
|
})),
|
|
50983
|
+
enabled("focusedItemToolbar") && /* @__PURE__ */ jsx(FocusedItemToolbar, {
|
|
50984
|
+
controller,
|
|
50985
|
+
snapshot
|
|
50986
|
+
}),
|
|
50382
50987
|
ContextMenu && /* @__PURE__ */ jsx(ContextMenu, {
|
|
50383
50988
|
controller,
|
|
50384
50989
|
snapshot
|
|
@@ -51021,4 +51626,4 @@ function ScrawlBoard({ documentId, initialDocument, onReady, className, style })
|
|
|
51021
51626
|
});
|
|
51022
51627
|
}
|
|
51023
51628
|
//#endregion
|
|
51024
|
-
export { AssetResolutionError, DefaultBoardChrome, InlineEditors, MultiplayerCursors, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, StyleShelf, assetRef, clampAssetCacheBytes, cloneCustomObject, isAssetRef, resolveScrawlTheme, scrawlThemePresets, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
|
|
51629
|
+
export { AssetResolutionError, DefaultBoardChrome, FocusedItemToolbar, InlineEditors, MultiplayerCursors, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, StyleShelf, assetRef, clampAssetCacheBytes, cloneCustomObject, isAssetRef, resolveScrawlTheme, scrawlThemePresets, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
|