@scrawl-board/board 0.1.0-beta.4 → 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 +241 -33
- package/dist/core.d.ts +12 -0
- package/dist/core.js +18 -2
- package/dist/index.d.ts +93 -4
- package/dist/index.js +644 -43
- package/dist/react.d.ts +81 -4
- package/dist/react.js +644 -43
- 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) {
|
|
@@ -36512,6 +36528,7 @@ var StrokeRenderer = class {
|
|
|
36512
36528
|
if (mesh) {
|
|
36513
36529
|
mesh.geometry.dispose();
|
|
36514
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);
|
|
36515
36532
|
}
|
|
36516
36533
|
}
|
|
36517
36534
|
for (const stroke of change.transformed) {
|
|
@@ -44691,7 +44708,7 @@ function distToSegmentSq(p, a, b) {
|
|
|
44691
44708
|
}
|
|
44692
44709
|
//#endregion
|
|
44693
44710
|
//#region src/interaction/tools/eraserTool.ts
|
|
44694
|
-
var DECAY_PER_PASS =
|
|
44711
|
+
var DECAY_PER_PASS = 1;
|
|
44695
44712
|
/** A point can take another decay pass after this long — rubbing works. */
|
|
44696
44713
|
var REARM_MS = 250;
|
|
44697
44714
|
var EraserTool = class {
|
|
@@ -44785,19 +44802,11 @@ var EraserTool = class {
|
|
|
44785
44802
|
const runs = partitionByErasure(current.points);
|
|
44786
44803
|
const survives = runs.filter((r) => r.survives && r.points.length >= 2);
|
|
44787
44804
|
const keepId = survives.length === 1 && survives[0].points.length === current.points.length;
|
|
44788
|
-
const m = current.matrix;
|
|
44789
44805
|
for (const run of runs) if (run.survives && run.points.length >= 2) after.push({
|
|
44790
44806
|
...current,
|
|
44791
44807
|
id: keepId ? current.id : crypto.randomUUID(),
|
|
44792
44808
|
points: run.points
|
|
44793
44809
|
});
|
|
44794
|
-
else if (!run.survives) {
|
|
44795
|
-
const world = m ? run.points.map((p) => ({
|
|
44796
|
-
...p,
|
|
44797
|
-
...apply(m, p)
|
|
44798
|
-
})) : run.points;
|
|
44799
|
-
this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1), current.tool !== "shape");
|
|
44800
|
-
}
|
|
44801
44810
|
}
|
|
44802
44811
|
doc.removeStrokes(before.map((s) => s.id));
|
|
44803
44812
|
doc.addStrokes(after.map(cloneStroke));
|
|
@@ -45005,10 +45014,10 @@ function buildShape(kind, a, b, step, constrain) {
|
|
|
45005
45014
|
case "ellipse": return [ellipse(a, targetB, step)];
|
|
45006
45015
|
case "line": return [sampleSegment(a, constrain ? snappedEnd(a, b) : b, step)];
|
|
45007
45016
|
case "arrow": return arrow(a, constrain ? snappedEnd(a, b) : b, step);
|
|
45008
|
-
case "triangle": return [polygon(3, a, targetB, step,
|
|
45017
|
+
case "triangle": return [polygon(3, a, targetB, step, Math.PI / 2)];
|
|
45009
45018
|
case "diamond": return [polygon(4, a, targetB, step, 0)];
|
|
45010
|
-
case "pentagon": return [polygon(5, a, targetB, step,
|
|
45011
|
-
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)];
|
|
45012
45021
|
case "octagon": return [polygon(8, a, targetB, step, Math.PI / 8)];
|
|
45013
45022
|
case "star": return [star(5, a, targetB, step)];
|
|
45014
45023
|
case "heart": return [heart(a, targetB, step)];
|
|
@@ -45207,6 +45216,7 @@ var ShapeTool = class {
|
|
|
45207
45216
|
this.ctx.clusters.assign(strokes[0]);
|
|
45208
45217
|
for (const s of strokes) s.clusterId = strokes[0].clusterId;
|
|
45209
45218
|
this.ctx.history.execute(new AddStrokesCommand(strokes));
|
|
45219
|
+
this.ctx.selection.set(strokes.map((s) => s.id));
|
|
45210
45220
|
this.ctx.setTool?.("select");
|
|
45211
45221
|
}
|
|
45212
45222
|
this.ctx.transition("idle");
|
|
@@ -45788,6 +45798,13 @@ var HANDLE_HIT_PX = 12;
|
|
|
45788
45798
|
var ROTATE_OFFSET_PX = 26;
|
|
45789
45799
|
var ROTATE_HIT_PX = 14;
|
|
45790
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;
|
|
45791
45808
|
var SelectionGizmo = class {
|
|
45792
45809
|
constructor(scene) {
|
|
45793
45810
|
this.group = new Group();
|
|
@@ -45802,7 +45819,7 @@ var SelectionGizmo = class {
|
|
|
45802
45819
|
this.outline = new LineLoop(new BufferGeometry(), this.lineMaterial);
|
|
45803
45820
|
this.outline.position.z = GIZMO_Z;
|
|
45804
45821
|
this.outline.frustumCulled = false;
|
|
45805
|
-
const handleGeometry = new
|
|
45822
|
+
const handleGeometry = new CircleGeometry(.5, 20);
|
|
45806
45823
|
const handleMaterial = new MeshBasicMaterial({ color: COLOR });
|
|
45807
45824
|
const makeHandle = () => new Mesh(handleGeometry, handleMaterial);
|
|
45808
45825
|
this.handles = {
|
|
@@ -45830,18 +45847,19 @@ var SelectionGizmo = class {
|
|
|
45830
45847
|
if (!box) return;
|
|
45831
45848
|
this.lineMaterial.color.setHex(locked ? 16096779 : COLOR);
|
|
45832
45849
|
this.rotateHandle.visible = !locked;
|
|
45850
|
+
const visual = this.visualBox(box, wpp);
|
|
45833
45851
|
const positions = new Float32Array([
|
|
45834
|
-
|
|
45835
|
-
|
|
45852
|
+
visual.minX,
|
|
45853
|
+
visual.minY,
|
|
45836
45854
|
GIZMO_Z,
|
|
45837
|
-
|
|
45838
|
-
|
|
45855
|
+
visual.maxX,
|
|
45856
|
+
visual.minY,
|
|
45839
45857
|
GIZMO_Z,
|
|
45840
|
-
|
|
45841
|
-
|
|
45858
|
+
visual.maxX,
|
|
45859
|
+
visual.maxY,
|
|
45842
45860
|
GIZMO_Z,
|
|
45843
|
-
|
|
45844
|
-
|
|
45861
|
+
visual.minX,
|
|
45862
|
+
visual.maxY,
|
|
45845
45863
|
GIZMO_Z
|
|
45846
45864
|
]);
|
|
45847
45865
|
this.outline.geometry.dispose();
|
|
@@ -45853,17 +45871,27 @@ var SelectionGizmo = class {
|
|
|
45853
45871
|
for (const [handle, mesh] of Object.entries(this.handles)) {
|
|
45854
45872
|
mesh.visible = !locked;
|
|
45855
45873
|
if (!locked) {
|
|
45856
|
-
const p = this.handlePoint(handle,
|
|
45874
|
+
const p = this.handlePoint(handle, visual);
|
|
45857
45875
|
mesh.position.set(p.x, p.y, GIZMO_Z);
|
|
45858
45876
|
mesh.scale.set(size, size, 1);
|
|
45859
45877
|
}
|
|
45860
45878
|
}
|
|
45861
45879
|
if (!locked) {
|
|
45862
|
-
const rotate = this.rotatePoint(
|
|
45880
|
+
const rotate = this.rotatePoint(visual, wpp);
|
|
45863
45881
|
this.rotateHandle.position.set(rotate.x, rotate.y, GIZMO_Z);
|
|
45864
45882
|
this.rotateHandle.scale.set(size, size, 1);
|
|
45865
45883
|
}
|
|
45866
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
|
+
}
|
|
45867
45895
|
handlePoint(handle, box) {
|
|
45868
45896
|
const midX = (box.minX + box.maxX) / 2;
|
|
45869
45897
|
const midY = (box.minY + box.maxY) / 2;
|
|
@@ -45951,7 +45979,8 @@ var SelectionGizmo = class {
|
|
|
45951
45979
|
if (!this.box || this.isLocked) return null;
|
|
45952
45980
|
const handleR = HANDLE_HIT_PX * this.wpp;
|
|
45953
45981
|
const edgeSlop = 6 * this.wpp;
|
|
45954
|
-
const
|
|
45982
|
+
const visual = this.visualBox(this.box, this.wpp);
|
|
45983
|
+
const rotate = this.rotatePoint(visual, this.wpp);
|
|
45955
45984
|
if (Math.hypot(p.x - rotate.x, p.y - rotate.y) < ROTATE_HIT_PX * this.wpp) return { kind: "rotate" };
|
|
45956
45985
|
for (const handle of [
|
|
45957
45986
|
"nw",
|
|
@@ -45963,13 +45992,13 @@ var SelectionGizmo = class {
|
|
|
45963
45992
|
"e",
|
|
45964
45993
|
"w"
|
|
45965
45994
|
]) {
|
|
45966
|
-
const hp = this.handlePoint(handle,
|
|
45995
|
+
const hp = this.handlePoint(handle, visual);
|
|
45967
45996
|
if (Math.abs(p.x - hp.x) < handleR && Math.abs(p.y - hp.y) < handleR) return {
|
|
45968
45997
|
kind: "scale",
|
|
45969
45998
|
handle
|
|
45970
45999
|
};
|
|
45971
46000
|
}
|
|
45972
|
-
const { minX, maxX, minY, maxY } =
|
|
46001
|
+
const { minX, maxX, minY, maxY } = visual;
|
|
45973
46002
|
const inX = p.x >= minX - edgeSlop && p.x <= maxX + edgeSlop;
|
|
45974
46003
|
const inY = p.y >= minY - edgeSlop && p.y <= maxY + edgeSlop;
|
|
45975
46004
|
if (inX && Math.abs(p.y - maxY) < edgeSlop) return {
|
|
@@ -45989,7 +46018,7 @@ var SelectionGizmo = class {
|
|
|
45989
46018
|
handle: "w"
|
|
45990
46019
|
};
|
|
45991
46020
|
const pad = 2 * this.wpp;
|
|
45992
|
-
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" };
|
|
45993
46022
|
return null;
|
|
45994
46023
|
}
|
|
45995
46024
|
rotatePoint(box, wpp) {
|
|
@@ -48222,6 +48251,112 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48222
48251
|
})),
|
|
48223
48252
|
...[...document.allCustomObjects()].map((value) => clone(toBoardObject(value)))
|
|
48224
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
|
+
};
|
|
48225
48360
|
const updateObject = (id, patch) => {
|
|
48226
48361
|
assertMutable();
|
|
48227
48362
|
const before = readObject(id);
|
|
@@ -48263,6 +48398,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48263
48398
|
zoom: view.zoom,
|
|
48264
48399
|
readOnly,
|
|
48265
48400
|
selection: Object.freeze([...selection]),
|
|
48401
|
+
focusedItem: computeFocusedItem(),
|
|
48266
48402
|
strokeCount: [...document.all()].length,
|
|
48267
48403
|
objectCount: allObjects().length,
|
|
48268
48404
|
canUndo: history.canUndo,
|
|
@@ -48677,10 +48813,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48677
48813
|
changed();
|
|
48678
48814
|
},
|
|
48679
48815
|
get: () => ({ ...view }),
|
|
48680
|
-
boardToScreen: (point) =>
|
|
48681
|
-
x: (point.x - view.x) * view.zoom,
|
|
48682
|
-
y: (view.y - point.y) * view.zoom
|
|
48683
|
-
},
|
|
48816
|
+
boardToScreen: (point) => projectToScreen(point),
|
|
48684
48817
|
screenToBoard: (point) => engine ? engine.screenToBoard(point.x, point.y) ?? {
|
|
48685
48818
|
x: point.x,
|
|
48686
48819
|
y: point.y
|
|
@@ -48717,6 +48850,81 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48717
48850
|
update(id, patch) {
|
|
48718
48851
|
updateObject(id, patch);
|
|
48719
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
|
+
},
|
|
48720
48928
|
table: {
|
|
48721
48929
|
addRow(tableId) {
|
|
48722
48930
|
const table = document.getTable(tableId);
|
|
@@ -49664,14 +49872,17 @@ function InlineEditors({ controller, renderPortal }) {
|
|
|
49664
49872
|
if (editor?.kind === "text") {
|
|
49665
49873
|
const text = editor.text;
|
|
49666
49874
|
if (editor.id === null) {
|
|
49667
|
-
if (text.trim())
|
|
49668
|
-
|
|
49669
|
-
|
|
49670
|
-
|
|
49671
|
-
|
|
49672
|
-
|
|
49673
|
-
|
|
49674
|
-
|
|
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
|
+
}
|
|
49675
49886
|
} else if (!text.trim()) controller.content.remove([editor.id]);
|
|
49676
49887
|
else controller.content.update(editor.id, { text });
|
|
49677
49888
|
}
|
|
@@ -49866,6 +50077,366 @@ function InlineEditors({ controller, renderPortal }) {
|
|
|
49866
50077
|
}))] });
|
|
49867
50078
|
}
|
|
49868
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
|
|
49869
50440
|
//#region src/ui/toolbar/style-shelf.tsx
|
|
49870
50441
|
var ERASER_RADII = [
|
|
49871
50442
|
["Fine", 1],
|
|
@@ -49904,7 +50475,7 @@ var SHELF_TOOLS = /* @__PURE__ */ new Set([
|
|
|
49904
50475
|
"timer"
|
|
49905
50476
|
]);
|
|
49906
50477
|
/** Contextual per-tool style controls — visible while a styleable tool is active. */
|
|
49907
|
-
function StyleShelf({ controller, snapshot }) {
|
|
50478
|
+
function StyleShelf({ controller, snapshot, anchor }) {
|
|
49908
50479
|
if (!SHELF_TOOLS.has(snapshot.tool)) return null;
|
|
49909
50480
|
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
49910
50481
|
const { style } = snapshot;
|
|
@@ -49912,7 +50483,12 @@ function StyleShelf({ controller, snapshot }) {
|
|
|
49912
50483
|
return /* @__PURE__ */ jsxs("div", {
|
|
49913
50484
|
"aria-label": "Board style",
|
|
49914
50485
|
className: "scrawl-board__style-shelf",
|
|
50486
|
+
"data-anchored": !!anchor,
|
|
49915
50487
|
role: "toolbar",
|
|
50488
|
+
style: anchor ? {
|
|
50489
|
+
left: anchor.x,
|
|
50490
|
+
top: anchor.y
|
|
50491
|
+
} : void 0,
|
|
49916
50492
|
children: [
|
|
49917
50493
|
snapshot.tool === "highlighter" && /* @__PURE__ */ jsxs("div", {
|
|
49918
50494
|
className: "scrawl-board__style-group",
|
|
@@ -50142,6 +50718,7 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50142
50718
|
const [panel, setPanel] = useState(null);
|
|
50143
50719
|
const [query, setQuery] = useState("");
|
|
50144
50720
|
const [announcement, setAnnouncement] = useState("Board controls ready");
|
|
50721
|
+
const [styleShelfAnchor, setStyleShelfAnchor] = useState(void 0);
|
|
50145
50722
|
const importRef = useRef(null);
|
|
50146
50723
|
const openerRef = useRef(null);
|
|
50147
50724
|
const searchRef = useRef(null);
|
|
@@ -50152,6 +50729,25 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50152
50729
|
useEffect(() => {
|
|
50153
50730
|
if (panel === "search") searchRef.current?.focus();
|
|
50154
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]);
|
|
50155
50751
|
const open = (next, opener) => {
|
|
50156
50752
|
openerRef.current = opener;
|
|
50157
50753
|
setPanel(next);
|
|
@@ -50380,9 +50976,14 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50380
50976
|
]
|
|
50381
50977
|
}),
|
|
50382
50978
|
renderSlot(slots?.stylePanel, enabled("styleShelf") && /* @__PURE__ */ jsx(StyleShelf, {
|
|
50979
|
+
anchor: styleShelfAnchor,
|
|
50383
50980
|
controller,
|
|
50384
50981
|
snapshot
|
|
50385
50982
|
})),
|
|
50983
|
+
enabled("focusedItemToolbar") && /* @__PURE__ */ jsx(FocusedItemToolbar, {
|
|
50984
|
+
controller,
|
|
50985
|
+
snapshot
|
|
50986
|
+
}),
|
|
50386
50987
|
ContextMenu && /* @__PURE__ */ jsx(ContextMenu, {
|
|
50387
50988
|
controller,
|
|
50388
50989
|
snapshot
|
|
@@ -51025,4 +51626,4 @@ function ScrawlBoard({ documentId, initialDocument, onReady, className, style })
|
|
|
51025
51626
|
});
|
|
51026
51627
|
}
|
|
51027
51628
|
//#endregion
|
|
51028
|
-
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 };
|