@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/index.js
CHANGED
|
@@ -151,7 +151,11 @@ function validateStrokes(raw) {
|
|
|
151
151
|
for (let index = 0; index < raw.length; index += 1) {
|
|
152
152
|
const value = raw[index];
|
|
153
153
|
const path = `strokes[${index}]`;
|
|
154
|
-
if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, [
|
|
154
|
+
if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, [
|
|
155
|
+
"marker",
|
|
156
|
+
"highlighter",
|
|
157
|
+
"shape"
|
|
158
|
+
]) || !validLock(value) || !optionalString(value.clusterId) || !validMatrix(value.matrix) || !Array.isArray(value.points)) return validation("invalid stroke", path);
|
|
155
159
|
const points = [];
|
|
156
160
|
for (let pointIndex = 0; pointIndex < value.points.length; pointIndex += 1) {
|
|
157
161
|
const point = normalizePoint(value.points[pointIndex]);
|
|
@@ -432,9 +436,21 @@ var HIGHLIGHT_COLORS = {
|
|
|
432
436
|
};
|
|
433
437
|
var NOTE_COLORS = {
|
|
434
438
|
yellow: "#FDE68A",
|
|
439
|
+
amber: "#FCD34D",
|
|
440
|
+
orange: "#FDBA74",
|
|
441
|
+
peach: "#FED7AA",
|
|
442
|
+
red: "#FCA5A5",
|
|
443
|
+
rose: "#FECACA",
|
|
435
444
|
pink: "#FBCFE8",
|
|
445
|
+
magenta: "#F9A8D4",
|
|
446
|
+
purple: "#D8B4FE",
|
|
447
|
+
lavender: "#E9D5FF",
|
|
436
448
|
blue: "#BFDBFE",
|
|
437
|
-
|
|
449
|
+
cornflower: "#93C5FD",
|
|
450
|
+
sky: "#7DD3FC",
|
|
451
|
+
cyan: "#BAE6FD",
|
|
452
|
+
green: "#BBF7D0",
|
|
453
|
+
mint: "#86EFAC"
|
|
438
454
|
};
|
|
439
455
|
var TEXT_DEFAULT_SIZE = 1.8;
|
|
440
456
|
function cloneText(block) {
|
|
@@ -36550,6 +36566,7 @@ var StrokeRenderer = class {
|
|
|
36550
36566
|
if (mesh) {
|
|
36551
36567
|
mesh.geometry.dispose();
|
|
36552
36568
|
mesh.geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke));
|
|
36569
|
+
mesh.material = stroke.tool === "highlighter" ? this.materials.highlight(stroke.color) : this.materials.get(stroke.color);
|
|
36553
36570
|
}
|
|
36554
36571
|
}
|
|
36555
36572
|
for (const stroke of change.transformed) {
|
|
@@ -44729,7 +44746,7 @@ function distToSegmentSq(p, a, b) {
|
|
|
44729
44746
|
}
|
|
44730
44747
|
//#endregion
|
|
44731
44748
|
//#region src/interaction/tools/eraserTool.ts
|
|
44732
|
-
var DECAY_PER_PASS =
|
|
44749
|
+
var DECAY_PER_PASS = 1;
|
|
44733
44750
|
/** A point can take another decay pass after this long — rubbing works. */
|
|
44734
44751
|
var REARM_MS = 250;
|
|
44735
44752
|
var EraserTool = class {
|
|
@@ -44823,19 +44840,11 @@ var EraserTool = class {
|
|
|
44823
44840
|
const runs = partitionByErasure(current.points);
|
|
44824
44841
|
const survives = runs.filter((r) => r.survives && r.points.length >= 2);
|
|
44825
44842
|
const keepId = survives.length === 1 && survives[0].points.length === current.points.length;
|
|
44826
|
-
const m = current.matrix;
|
|
44827
44843
|
for (const run of runs) if (run.survives && run.points.length >= 2) after.push({
|
|
44828
44844
|
...current,
|
|
44829
44845
|
id: keepId ? current.id : crypto.randomUUID(),
|
|
44830
44846
|
points: run.points
|
|
44831
44847
|
});
|
|
44832
|
-
else if (!run.survives) {
|
|
44833
|
-
const world = m ? run.points.map((p) => ({
|
|
44834
|
-
...p,
|
|
44835
|
-
...apply(m, p)
|
|
44836
|
-
})) : run.points;
|
|
44837
|
-
this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1), current.tool !== "shape");
|
|
44838
|
-
}
|
|
44839
44848
|
}
|
|
44840
44849
|
doc.removeStrokes(before.map((s) => s.id));
|
|
44841
44850
|
doc.addStrokes(after.map(cloneStroke));
|
|
@@ -45043,10 +45052,10 @@ function buildShape(kind, a, b, step, constrain) {
|
|
|
45043
45052
|
case "ellipse": return [ellipse(a, targetB, step)];
|
|
45044
45053
|
case "line": return [sampleSegment(a, constrain ? snappedEnd(a, b) : b, step)];
|
|
45045
45054
|
case "arrow": return arrow(a, constrain ? snappedEnd(a, b) : b, step);
|
|
45046
|
-
case "triangle": return [polygon(3, a, targetB, step,
|
|
45055
|
+
case "triangle": return [polygon(3, a, targetB, step, Math.PI / 2)];
|
|
45047
45056
|
case "diamond": return [polygon(4, a, targetB, step, 0)];
|
|
45048
|
-
case "pentagon": return [polygon(5, a, targetB, step,
|
|
45049
|
-
case "hexagon": return [polygon(6, a, targetB, step,
|
|
45057
|
+
case "pentagon": return [polygon(5, a, targetB, step, Math.PI / 2)];
|
|
45058
|
+
case "hexagon": return [polygon(6, a, targetB, step, Math.PI / 2)];
|
|
45050
45059
|
case "octagon": return [polygon(8, a, targetB, step, Math.PI / 8)];
|
|
45051
45060
|
case "star": return [star(5, a, targetB, step)];
|
|
45052
45061
|
case "heart": return [heart(a, targetB, step)];
|
|
@@ -45245,6 +45254,7 @@ var ShapeTool = class {
|
|
|
45245
45254
|
this.ctx.clusters.assign(strokes[0]);
|
|
45246
45255
|
for (const s of strokes) s.clusterId = strokes[0].clusterId;
|
|
45247
45256
|
this.ctx.history.execute(new AddStrokesCommand(strokes));
|
|
45257
|
+
this.ctx.selection.set(strokes.map((s) => s.id));
|
|
45248
45258
|
this.ctx.setTool?.("select");
|
|
45249
45259
|
}
|
|
45250
45260
|
this.ctx.transition("idle");
|
|
@@ -45826,6 +45836,13 @@ var HANDLE_HIT_PX = 12;
|
|
|
45826
45836
|
var ROTATE_OFFSET_PX = 26;
|
|
45827
45837
|
var ROTATE_HIT_PX = 14;
|
|
45828
45838
|
var COLOR = 2450411;
|
|
45839
|
+
/**
|
|
45840
|
+
* Visual-only outset between the selected object and the gizmo's outline/
|
|
45841
|
+
* handles — the object's actual bounding box (used for resize math, see
|
|
45842
|
+
* `anchorPoint`/`handlePoint`'s callers in `selectTool.ts`) never changes;
|
|
45843
|
+
* only where the outline and handle meshes are *drawn* and hit-tested does.
|
|
45844
|
+
*/
|
|
45845
|
+
var GAP_PX = 8;
|
|
45829
45846
|
var SelectionGizmo = class {
|
|
45830
45847
|
constructor(scene) {
|
|
45831
45848
|
this.group = new Group();
|
|
@@ -45840,7 +45857,7 @@ var SelectionGizmo = class {
|
|
|
45840
45857
|
this.outline = new LineLoop(new BufferGeometry(), this.lineMaterial);
|
|
45841
45858
|
this.outline.position.z = GIZMO_Z;
|
|
45842
45859
|
this.outline.frustumCulled = false;
|
|
45843
|
-
const handleGeometry = new
|
|
45860
|
+
const handleGeometry = new CircleGeometry(.5, 20);
|
|
45844
45861
|
const handleMaterial = new MeshBasicMaterial({ color: COLOR });
|
|
45845
45862
|
const makeHandle = () => new Mesh(handleGeometry, handleMaterial);
|
|
45846
45863
|
this.handles = {
|
|
@@ -45868,18 +45885,19 @@ var SelectionGizmo = class {
|
|
|
45868
45885
|
if (!box) return;
|
|
45869
45886
|
this.lineMaterial.color.setHex(locked ? 16096779 : COLOR);
|
|
45870
45887
|
this.rotateHandle.visible = !locked;
|
|
45888
|
+
const visual = this.visualBox(box, wpp);
|
|
45871
45889
|
const positions = new Float32Array([
|
|
45872
|
-
|
|
45873
|
-
|
|
45890
|
+
visual.minX,
|
|
45891
|
+
visual.minY,
|
|
45874
45892
|
GIZMO_Z,
|
|
45875
|
-
|
|
45876
|
-
|
|
45893
|
+
visual.maxX,
|
|
45894
|
+
visual.minY,
|
|
45877
45895
|
GIZMO_Z,
|
|
45878
|
-
|
|
45879
|
-
|
|
45896
|
+
visual.maxX,
|
|
45897
|
+
visual.maxY,
|
|
45880
45898
|
GIZMO_Z,
|
|
45881
|
-
|
|
45882
|
-
|
|
45899
|
+
visual.minX,
|
|
45900
|
+
visual.maxY,
|
|
45883
45901
|
GIZMO_Z
|
|
45884
45902
|
]);
|
|
45885
45903
|
this.outline.geometry.dispose();
|
|
@@ -45891,17 +45909,27 @@ var SelectionGizmo = class {
|
|
|
45891
45909
|
for (const [handle, mesh] of Object.entries(this.handles)) {
|
|
45892
45910
|
mesh.visible = !locked;
|
|
45893
45911
|
if (!locked) {
|
|
45894
|
-
const p = this.handlePoint(handle,
|
|
45912
|
+
const p = this.handlePoint(handle, visual);
|
|
45895
45913
|
mesh.position.set(p.x, p.y, GIZMO_Z);
|
|
45896
45914
|
mesh.scale.set(size, size, 1);
|
|
45897
45915
|
}
|
|
45898
45916
|
}
|
|
45899
45917
|
if (!locked) {
|
|
45900
|
-
const rotate = this.rotatePoint(
|
|
45918
|
+
const rotate = this.rotatePoint(visual, wpp);
|
|
45901
45919
|
this.rotateHandle.position.set(rotate.x, rotate.y, GIZMO_Z);
|
|
45902
45920
|
this.rotateHandle.scale.set(size, size, 1);
|
|
45903
45921
|
}
|
|
45904
45922
|
}
|
|
45923
|
+
/** 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`). */
|
|
45924
|
+
visualBox(box, wpp) {
|
|
45925
|
+
const gap = GAP_PX * wpp;
|
|
45926
|
+
return {
|
|
45927
|
+
minX: box.minX - gap,
|
|
45928
|
+
minY: box.minY - gap,
|
|
45929
|
+
maxX: box.maxX + gap,
|
|
45930
|
+
maxY: box.maxY + gap
|
|
45931
|
+
};
|
|
45932
|
+
}
|
|
45905
45933
|
handlePoint(handle, box) {
|
|
45906
45934
|
const midX = (box.minX + box.maxX) / 2;
|
|
45907
45935
|
const midY = (box.minY + box.maxY) / 2;
|
|
@@ -45989,7 +46017,8 @@ var SelectionGizmo = class {
|
|
|
45989
46017
|
if (!this.box || this.isLocked) return null;
|
|
45990
46018
|
const handleR = HANDLE_HIT_PX * this.wpp;
|
|
45991
46019
|
const edgeSlop = 6 * this.wpp;
|
|
45992
|
-
const
|
|
46020
|
+
const visual = this.visualBox(this.box, this.wpp);
|
|
46021
|
+
const rotate = this.rotatePoint(visual, this.wpp);
|
|
45993
46022
|
if (Math.hypot(p.x - rotate.x, p.y - rotate.y) < ROTATE_HIT_PX * this.wpp) return { kind: "rotate" };
|
|
45994
46023
|
for (const handle of [
|
|
45995
46024
|
"nw",
|
|
@@ -46001,13 +46030,13 @@ var SelectionGizmo = class {
|
|
|
46001
46030
|
"e",
|
|
46002
46031
|
"w"
|
|
46003
46032
|
]) {
|
|
46004
|
-
const hp = this.handlePoint(handle,
|
|
46033
|
+
const hp = this.handlePoint(handle, visual);
|
|
46005
46034
|
if (Math.abs(p.x - hp.x) < handleR && Math.abs(p.y - hp.y) < handleR) return {
|
|
46006
46035
|
kind: "scale",
|
|
46007
46036
|
handle
|
|
46008
46037
|
};
|
|
46009
46038
|
}
|
|
46010
|
-
const { minX, maxX, minY, maxY } =
|
|
46039
|
+
const { minX, maxX, minY, maxY } = visual;
|
|
46011
46040
|
const inX = p.x >= minX - edgeSlop && p.x <= maxX + edgeSlop;
|
|
46012
46041
|
const inY = p.y >= minY - edgeSlop && p.y <= maxY + edgeSlop;
|
|
46013
46042
|
if (inX && Math.abs(p.y - maxY) < edgeSlop) return {
|
|
@@ -46027,7 +46056,7 @@ var SelectionGizmo = class {
|
|
|
46027
46056
|
handle: "w"
|
|
46028
46057
|
};
|
|
46029
46058
|
const pad = 2 * this.wpp;
|
|
46030
|
-
if (p.x >= minX - pad && p.x <= maxX + pad && p.y >= minY - pad && p.y <= maxY + pad) return { kind: "inside" };
|
|
46059
|
+
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" };
|
|
46031
46060
|
return null;
|
|
46032
46061
|
}
|
|
46033
46062
|
rotatePoint(box, wpp) {
|
|
@@ -48260,6 +48289,112 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48260
48289
|
})),
|
|
48261
48290
|
...[...document.allCustomObjects()].map((value) => clone(toBoardObject(value)))
|
|
48262
48291
|
];
|
|
48292
|
+
const projectToScreen = (point) => engine ? engine.boardToScreen(point) : {
|
|
48293
|
+
x: (point.x - view.x) * view.zoom,
|
|
48294
|
+
y: (view.y - point.y) * view.zoom
|
|
48295
|
+
};
|
|
48296
|
+
const lockFields = (item) => ({
|
|
48297
|
+
locked: !!item.locked,
|
|
48298
|
+
...item.lockedBy ? { lockedBy: item.lockedBy } : {},
|
|
48299
|
+
...item.lockedByName ? { lockedByName: item.lockedByName } : {}
|
|
48300
|
+
});
|
|
48301
|
+
const lockFieldsMany = (items) => {
|
|
48302
|
+
const locked = items.filter((i) => i.locked);
|
|
48303
|
+
if (locked.length === 0) return { locked: false };
|
|
48304
|
+
const lockedBys = [...new Set(locked.map((i) => i.lockedBy).filter((v) => !!v))];
|
|
48305
|
+
const names = [...new Set(locked.map((i) => i.lockedByName).filter((v) => !!v))];
|
|
48306
|
+
return {
|
|
48307
|
+
locked: true,
|
|
48308
|
+
...lockedBys.length === 1 ? { lockedBy: lockedBys[0] } : {},
|
|
48309
|
+
...names.length === 1 ? { lockedByName: names[0] } : {}
|
|
48310
|
+
};
|
|
48311
|
+
};
|
|
48312
|
+
const strokeClusterAnchor = (strokes) => {
|
|
48313
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
48314
|
+
for (const s of strokes) for (const p of s.points) {
|
|
48315
|
+
const world = s.matrix ? apply(s.matrix, p) : p;
|
|
48316
|
+
minX = Math.min(minX, world.x);
|
|
48317
|
+
maxX = Math.max(maxX, world.x);
|
|
48318
|
+
minY = Math.min(minY, world.y);
|
|
48319
|
+
maxY = Math.max(maxY, world.y);
|
|
48320
|
+
}
|
|
48321
|
+
if (!Number.isFinite(minX)) return projectToScreen({
|
|
48322
|
+
x: 0,
|
|
48323
|
+
y: 0
|
|
48324
|
+
});
|
|
48325
|
+
return projectToScreen({
|
|
48326
|
+
x: (minX + maxX) / 2,
|
|
48327
|
+
y: maxY + 1.4
|
|
48328
|
+
});
|
|
48329
|
+
};
|
|
48330
|
+
/** Per-type anchor formulas mirror `ScrawlEngine.getSelectedItemInfo` exactly, so a Host-built toolbar lands in the same spot the SDK's own would. */
|
|
48331
|
+
const anchorFor = (obj) => {
|
|
48332
|
+
switch (obj.type) {
|
|
48333
|
+
case "note": return projectToScreen({
|
|
48334
|
+
x: obj.x,
|
|
48335
|
+
y: obj.y + obj.size / 2 + 1
|
|
48336
|
+
});
|
|
48337
|
+
case "text": {
|
|
48338
|
+
const { width } = measureTextBlock(obj.text, obj.fontSize);
|
|
48339
|
+
return projectToScreen({
|
|
48340
|
+
x: obj.x + width / 2,
|
|
48341
|
+
y: obj.y + 1
|
|
48342
|
+
});
|
|
48343
|
+
}
|
|
48344
|
+
case "table": {
|
|
48345
|
+
const { width } = measureTable(obj);
|
|
48346
|
+
return projectToScreen({
|
|
48347
|
+
x: obj.x + width / 2,
|
|
48348
|
+
y: obj.y + 1.2
|
|
48349
|
+
});
|
|
48350
|
+
}
|
|
48351
|
+
case "image": return projectToScreen({
|
|
48352
|
+
x: obj.x,
|
|
48353
|
+
y: obj.y + obj.height / 2 + 1.2
|
|
48354
|
+
});
|
|
48355
|
+
case "timer": return projectToScreen({
|
|
48356
|
+
x: obj.x,
|
|
48357
|
+
y: obj.y + obj.size / 2 + 1.2
|
|
48358
|
+
});
|
|
48359
|
+
case "custom": {
|
|
48360
|
+
const b = obj.fallback.bounds;
|
|
48361
|
+
return projectToScreen(apply(obj.transform, {
|
|
48362
|
+
x: b.x + b.width / 2,
|
|
48363
|
+
y: b.y + b.height + 1
|
|
48364
|
+
}));
|
|
48365
|
+
}
|
|
48366
|
+
case "stroke": return strokeClusterAnchor([obj]);
|
|
48367
|
+
}
|
|
48368
|
+
};
|
|
48369
|
+
const computeFocusedItem = () => {
|
|
48370
|
+
if (selection.length === 0) return null;
|
|
48371
|
+
if (selection.length === 1) {
|
|
48372
|
+
const obj = readObject(selection[0]);
|
|
48373
|
+
if (!obj) return null;
|
|
48374
|
+
if (obj.type === "custom") return {
|
|
48375
|
+
type: "custom",
|
|
48376
|
+
id: obj.id,
|
|
48377
|
+
locked: false,
|
|
48378
|
+
screenPosition: anchorFor(obj)
|
|
48379
|
+
};
|
|
48380
|
+
return {
|
|
48381
|
+
type: obj.type,
|
|
48382
|
+
id: obj.id,
|
|
48383
|
+
...lockFields(obj),
|
|
48384
|
+
screenPosition: anchorFor(obj)
|
|
48385
|
+
};
|
|
48386
|
+
}
|
|
48387
|
+
const objects = selection.map(readObject);
|
|
48388
|
+
if (!objects.every((o) => o?.type === "stroke")) return null;
|
|
48389
|
+
const clusterId = objects[0].clusterId;
|
|
48390
|
+
if (!clusterId || !objects.every((o) => o.clusterId === clusterId)) return null;
|
|
48391
|
+
return {
|
|
48392
|
+
type: "stroke",
|
|
48393
|
+
id: objects[0].id,
|
|
48394
|
+
...lockFieldsMany(objects),
|
|
48395
|
+
screenPosition: strokeClusterAnchor(objects)
|
|
48396
|
+
};
|
|
48397
|
+
};
|
|
48263
48398
|
const updateObject = (id, patch) => {
|
|
48264
48399
|
assertMutable();
|
|
48265
48400
|
const before = readObject(id);
|
|
@@ -48301,6 +48436,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48301
48436
|
zoom: view.zoom,
|
|
48302
48437
|
readOnly,
|
|
48303
48438
|
selection: Object.freeze([...selection]),
|
|
48439
|
+
focusedItem: computeFocusedItem(),
|
|
48304
48440
|
strokeCount: [...document.all()].length,
|
|
48305
48441
|
objectCount: allObjects().length,
|
|
48306
48442
|
canUndo: history.canUndo,
|
|
@@ -48715,10 +48851,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48715
48851
|
changed();
|
|
48716
48852
|
},
|
|
48717
48853
|
get: () => ({ ...view }),
|
|
48718
|
-
boardToScreen: (point) =>
|
|
48719
|
-
x: (point.x - view.x) * view.zoom,
|
|
48720
|
-
y: (view.y - point.y) * view.zoom
|
|
48721
|
-
},
|
|
48854
|
+
boardToScreen: (point) => projectToScreen(point),
|
|
48722
48855
|
screenToBoard: (point) => engine ? engine.screenToBoard(point.x, point.y) ?? {
|
|
48723
48856
|
x: point.x,
|
|
48724
48857
|
y: point.y
|
|
@@ -48755,6 +48888,81 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48755
48888
|
update(id, patch) {
|
|
48756
48889
|
updateObject(id, patch);
|
|
48757
48890
|
},
|
|
48891
|
+
duplicate(ids) {
|
|
48892
|
+
assertMutable();
|
|
48893
|
+
const objects = [...new Set(ids)].map(readObject).filter((o) => !!o);
|
|
48894
|
+
if (objects.length === 0) return [];
|
|
48895
|
+
const DX = 1.5;
|
|
48896
|
+
const DY = -1.5;
|
|
48897
|
+
const newClusterIds = /* @__PURE__ */ new Map();
|
|
48898
|
+
const clusterIdFor = (oldClusterId) => {
|
|
48899
|
+
let next = newClusterIds.get(oldClusterId);
|
|
48900
|
+
if (!next) {
|
|
48901
|
+
next = crypto.randomUUID();
|
|
48902
|
+
newClusterIds.set(oldClusterId, next);
|
|
48903
|
+
}
|
|
48904
|
+
return next;
|
|
48905
|
+
};
|
|
48906
|
+
const commands = [];
|
|
48907
|
+
const newIds = [];
|
|
48908
|
+
for (const obj of objects) {
|
|
48909
|
+
const id = createId();
|
|
48910
|
+
newIds.push(id);
|
|
48911
|
+
const rest = { ...obj };
|
|
48912
|
+
delete rest.locked;
|
|
48913
|
+
delete rest.lockedBy;
|
|
48914
|
+
delete rest.lockedByName;
|
|
48915
|
+
let duplicated;
|
|
48916
|
+
if (obj.type === "stroke") {
|
|
48917
|
+
const matrix = obj.matrix ?? [
|
|
48918
|
+
1,
|
|
48919
|
+
0,
|
|
48920
|
+
0,
|
|
48921
|
+
1,
|
|
48922
|
+
0,
|
|
48923
|
+
0
|
|
48924
|
+
];
|
|
48925
|
+
duplicated = {
|
|
48926
|
+
...rest,
|
|
48927
|
+
id,
|
|
48928
|
+
matrix: [
|
|
48929
|
+
matrix[0],
|
|
48930
|
+
matrix[1],
|
|
48931
|
+
matrix[2],
|
|
48932
|
+
matrix[3],
|
|
48933
|
+
matrix[4] + DX,
|
|
48934
|
+
matrix[5] + DY
|
|
48935
|
+
],
|
|
48936
|
+
...obj.clusterId ? { clusterId: clusterIdFor(obj.clusterId) } : {}
|
|
48937
|
+
};
|
|
48938
|
+
} else if (obj.type === "custom") {
|
|
48939
|
+
const t = obj.transform;
|
|
48940
|
+
duplicated = {
|
|
48941
|
+
...rest,
|
|
48942
|
+
id,
|
|
48943
|
+
transform: [
|
|
48944
|
+
t[0],
|
|
48945
|
+
t[1],
|
|
48946
|
+
t[2],
|
|
48947
|
+
t[3],
|
|
48948
|
+
t[4] + DX,
|
|
48949
|
+
t[5] + DY
|
|
48950
|
+
]
|
|
48951
|
+
};
|
|
48952
|
+
delete duplicated.lock;
|
|
48953
|
+
} else duplicated = {
|
|
48954
|
+
...rest,
|
|
48955
|
+
id,
|
|
48956
|
+
x: obj.x + DX,
|
|
48957
|
+
y: obj.y + DY
|
|
48958
|
+
};
|
|
48959
|
+
validateBoardObject(duplicated.type, duplicated);
|
|
48960
|
+
commands.push(addCommand(duplicated.type, duplicated));
|
|
48961
|
+
}
|
|
48962
|
+
executeHistory(() => history.execute(new CommandBatch("duplicate content", commands)));
|
|
48963
|
+
changed();
|
|
48964
|
+
return newIds;
|
|
48965
|
+
},
|
|
48758
48966
|
table: {
|
|
48759
48967
|
addRow(tableId) {
|
|
48760
48968
|
const table = document.getTable(tableId);
|
|
@@ -49702,14 +49910,17 @@ function InlineEditors({ controller, renderPortal }) {
|
|
|
49702
49910
|
if (editor?.kind === "text") {
|
|
49703
49911
|
const text = editor.text;
|
|
49704
49912
|
if (editor.id === null) {
|
|
49705
|
-
if (text.trim())
|
|
49706
|
-
|
|
49707
|
-
|
|
49708
|
-
|
|
49709
|
-
|
|
49710
|
-
|
|
49711
|
-
|
|
49712
|
-
|
|
49913
|
+
if (text.trim()) {
|
|
49914
|
+
const newId = controller.content.add({
|
|
49915
|
+
type: "text",
|
|
49916
|
+
x: editor.boardX,
|
|
49917
|
+
y: editor.boardY,
|
|
49918
|
+
text,
|
|
49919
|
+
color: editor.color,
|
|
49920
|
+
fontSize: TEXT_DEFAULT_SIZE
|
|
49921
|
+
});
|
|
49922
|
+
controller.content.select([newId]);
|
|
49923
|
+
}
|
|
49713
49924
|
} else if (!text.trim()) controller.content.remove([editor.id]);
|
|
49714
49925
|
else controller.content.update(editor.id, { text });
|
|
49715
49926
|
}
|
|
@@ -49904,6 +50115,366 @@ function InlineEditors({ controller, renderPortal }) {
|
|
|
49904
50115
|
}))] });
|
|
49905
50116
|
}
|
|
49906
50117
|
//#endregion
|
|
50118
|
+
//#region src/ui/toolbar/focused-item-toolbar.tsx
|
|
50119
|
+
var NOTE_SIZES = [
|
|
50120
|
+
["Small", 7],
|
|
50121
|
+
["Medium", 10],
|
|
50122
|
+
["Large", 14]
|
|
50123
|
+
];
|
|
50124
|
+
var TEXT_SIZES = [
|
|
50125
|
+
["Small", 1.2],
|
|
50126
|
+
["Medium", 1.8],
|
|
50127
|
+
["Large", 2.6]
|
|
50128
|
+
];
|
|
50129
|
+
var SHAPE_WIDTHS = [
|
|
50130
|
+
["Thin", .1],
|
|
50131
|
+
["Medium", .15],
|
|
50132
|
+
["Thick", .3]
|
|
50133
|
+
];
|
|
50134
|
+
/** Matches the native selection gizmo's own outset (renderer/selection/gizmo.ts's GAP_PX), for a consistent look. */
|
|
50135
|
+
var HANDLE_GAP_PX = 8;
|
|
50136
|
+
var MIN_SIZE = 4;
|
|
50137
|
+
var MAX_SIZE = 30;
|
|
50138
|
+
var HANDLE_CORNERS = [
|
|
50139
|
+
"nw",
|
|
50140
|
+
"ne",
|
|
50141
|
+
"sw",
|
|
50142
|
+
"se"
|
|
50143
|
+
];
|
|
50144
|
+
/**
|
|
50145
|
+
* Floating toolbar above the focused note, shape, or text block — Colour,
|
|
50146
|
+
* Size (note/text) or Width (shape), Lock/Unlock, Duplicate, Delete.
|
|
50147
|
+
* Table/image/timer/custom objects and plain (non-shape) ink strokes never
|
|
50148
|
+
* get a toolbar here — out of scope for this destination.
|
|
50149
|
+
*
|
|
50150
|
+
* Lock/Unlock carries no per-user ownership gating: nothing else in this
|
|
50151
|
+
* SDK enforces lock ownership either (`content.update` never checks
|
|
50152
|
+
* `locked`, and the engine's own internal unlock check has no way for a
|
|
50153
|
+
* Host to ever supply a real user id) — locking is advisory UI state
|
|
50154
|
+
* throughout, and this toolbar matches that rather than inventing an
|
|
50155
|
+
* enforcement story alone.
|
|
50156
|
+
*
|
|
50157
|
+
* A shape can be several strokes sharing one `clusterId` (e.g. an arrow's
|
|
50158
|
+
* shaft + head) — every action here applies to the whole cluster. Resolved
|
|
50159
|
+
* via `query.all()` + `clusterId`, not `snapshot.selection`: a canvas click
|
|
50160
|
+
* on one member selects every member internally, but the public selection
|
|
50161
|
+
* bridge only ever reports one id (`FocusedItem.id` is singular by design),
|
|
50162
|
+
* so reconstructing the cluster from the document is the reliable path
|
|
50163
|
+
* regardless of how the selection was made.
|
|
50164
|
+
*
|
|
50165
|
+
* A focused note also gets drag-to-resize corner handles — notes have no
|
|
50166
|
+
* gizmo of their own (the native selection gizmo is ink-stroke/shape-only),
|
|
50167
|
+
* so this is that capability's default-ui-owned equivalent. Proportional
|
|
50168
|
+
* (always-square) resize from the note's own centre, clamped to
|
|
50169
|
+
* `[MIN_SIZE, MAX_SIZE]`, independent of which corner is grabbed.
|
|
50170
|
+
*/
|
|
50171
|
+
function FocusedItemToolbar({ controller, snapshot }) {
|
|
50172
|
+
const focused = snapshot.focusedItem;
|
|
50173
|
+
if (!focused) return null;
|
|
50174
|
+
const obj = controller.query.get(focused.id);
|
|
50175
|
+
if (!obj) return null;
|
|
50176
|
+
if (obj.type === "note") return /* @__PURE__ */ jsx(NoteToolbar, {
|
|
50177
|
+
controller,
|
|
50178
|
+
snapshot,
|
|
50179
|
+
note: obj,
|
|
50180
|
+
locked: focused.locked,
|
|
50181
|
+
anchor: focused.screenPosition
|
|
50182
|
+
});
|
|
50183
|
+
if (obj.type === "text") return /* @__PURE__ */ jsx(TextToolbar, {
|
|
50184
|
+
controller,
|
|
50185
|
+
snapshot,
|
|
50186
|
+
text: obj,
|
|
50187
|
+
locked: focused.locked,
|
|
50188
|
+
anchor: focused.screenPosition
|
|
50189
|
+
});
|
|
50190
|
+
if (obj.type === "stroke" && obj.tool === "shape") return /* @__PURE__ */ jsx(ShapeToolbar, {
|
|
50191
|
+
controller,
|
|
50192
|
+
snapshot,
|
|
50193
|
+
stroke: obj,
|
|
50194
|
+
locked: focused.locked,
|
|
50195
|
+
anchor: focused.screenPosition
|
|
50196
|
+
});
|
|
50197
|
+
return null;
|
|
50198
|
+
}
|
|
50199
|
+
function ToolbarShell({ anchor, children }) {
|
|
50200
|
+
if (!anchor) return null;
|
|
50201
|
+
return /* @__PURE__ */ jsx("div", {
|
|
50202
|
+
"aria-label": "Selected object",
|
|
50203
|
+
className: "scrawl-board__focus-toolbar",
|
|
50204
|
+
role: "toolbar",
|
|
50205
|
+
style: {
|
|
50206
|
+
left: anchor.x,
|
|
50207
|
+
top: anchor.y
|
|
50208
|
+
},
|
|
50209
|
+
children
|
|
50210
|
+
});
|
|
50211
|
+
}
|
|
50212
|
+
function NoteToolbar({ controller, snapshot, note, locked, anchor }) {
|
|
50213
|
+
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
50214
|
+
const locked_ = !mutable || locked;
|
|
50215
|
+
const half = note.size / 2;
|
|
50216
|
+
const cornerA = controller.view.boardToScreen({
|
|
50217
|
+
x: note.x - half,
|
|
50218
|
+
y: note.y + half
|
|
50219
|
+
});
|
|
50220
|
+
const cornerB = controller.view.boardToScreen({
|
|
50221
|
+
x: note.x + half,
|
|
50222
|
+
y: note.y - half
|
|
50223
|
+
});
|
|
50224
|
+
const outline = {
|
|
50225
|
+
left: Math.min(cornerA.x, cornerB.x) - HANDLE_GAP_PX,
|
|
50226
|
+
top: Math.min(cornerA.y, cornerB.y) - HANDLE_GAP_PX,
|
|
50227
|
+
right: Math.max(cornerA.x, cornerB.x) + HANDLE_GAP_PX,
|
|
50228
|
+
bottom: Math.max(cornerA.y, cornerB.y) + HANDLE_GAP_PX
|
|
50229
|
+
};
|
|
50230
|
+
const handlePoints = {
|
|
50231
|
+
nw: {
|
|
50232
|
+
x: outline.left,
|
|
50233
|
+
y: outline.top
|
|
50234
|
+
},
|
|
50235
|
+
ne: {
|
|
50236
|
+
x: outline.right,
|
|
50237
|
+
y: outline.top
|
|
50238
|
+
},
|
|
50239
|
+
sw: {
|
|
50240
|
+
x: outline.left,
|
|
50241
|
+
y: outline.bottom
|
|
50242
|
+
},
|
|
50243
|
+
se: {
|
|
50244
|
+
x: outline.right,
|
|
50245
|
+
y: outline.bottom
|
|
50246
|
+
}
|
|
50247
|
+
};
|
|
50248
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
50249
|
+
/* @__PURE__ */ jsx("div", {
|
|
50250
|
+
"aria-hidden": "true",
|
|
50251
|
+
className: "scrawl-board__focus-outline",
|
|
50252
|
+
"data-locked": locked,
|
|
50253
|
+
style: {
|
|
50254
|
+
left: outline.left,
|
|
50255
|
+
top: outline.top,
|
|
50256
|
+
width: outline.right - outline.left,
|
|
50257
|
+
height: outline.bottom - outline.top
|
|
50258
|
+
}
|
|
50259
|
+
}),
|
|
50260
|
+
mutable && !locked && HANDLE_CORNERS.map((corner) => /* @__PURE__ */ jsx(ResizeHandle, {
|
|
50261
|
+
controller,
|
|
50262
|
+
note,
|
|
50263
|
+
corner,
|
|
50264
|
+
x: handlePoints[corner].x,
|
|
50265
|
+
y: handlePoints[corner].y
|
|
50266
|
+
}, corner)),
|
|
50267
|
+
/* @__PURE__ */ jsxs(ToolbarShell, {
|
|
50268
|
+
anchor,
|
|
50269
|
+
children: [
|
|
50270
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50271
|
+
className: "scrawl-board__style-group",
|
|
50272
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Size" }), NOTE_SIZES.map(([label, size]) => /* @__PURE__ */ jsx("button", {
|
|
50273
|
+
"aria-label": `${label} note`,
|
|
50274
|
+
"aria-pressed": note.size === size,
|
|
50275
|
+
disabled: locked_,
|
|
50276
|
+
onClick: () => controller.content.update(note.id, { size }),
|
|
50277
|
+
type: "button",
|
|
50278
|
+
children: label[0]
|
|
50279
|
+
}, label))]
|
|
50280
|
+
}),
|
|
50281
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50282
|
+
className: "scrawl-board__style-group",
|
|
50283
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(NOTE_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
|
|
50284
|
+
"aria-label": `${name} note`,
|
|
50285
|
+
"aria-pressed": note.color.toLowerCase() === value.toLowerCase(),
|
|
50286
|
+
disabled: locked_,
|
|
50287
|
+
onClick: () => controller.content.update(note.id, { color: value }),
|
|
50288
|
+
type: "button",
|
|
50289
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
50290
|
+
"aria-hidden": "true",
|
|
50291
|
+
style: { backgroundColor: value }
|
|
50292
|
+
})
|
|
50293
|
+
}, value))]
|
|
50294
|
+
}),
|
|
50295
|
+
/* @__PURE__ */ jsx(LockDuplicateDelete, {
|
|
50296
|
+
controller,
|
|
50297
|
+
mutable,
|
|
50298
|
+
locked,
|
|
50299
|
+
ids: [note.id]
|
|
50300
|
+
})
|
|
50301
|
+
]
|
|
50302
|
+
})
|
|
50303
|
+
] });
|
|
50304
|
+
}
|
|
50305
|
+
/**
|
|
50306
|
+
* One corner handle — drag distance from the note's own centre (its `x`/`y`
|
|
50307
|
+
* already *is* the centre) drives a proportional, always-square resize,
|
|
50308
|
+
* independent of which corner is grabbed. `note` is fresh every render (a
|
|
50309
|
+
* new `content.update` during drag triggers a new snapshot), so the
|
|
50310
|
+
* closure never goes stale.
|
|
50311
|
+
*/
|
|
50312
|
+
function ResizeHandle({ controller, note, corner, x, y }) {
|
|
50313
|
+
const [dragging, setDragging] = useState(false);
|
|
50314
|
+
const nudge = (delta) => {
|
|
50315
|
+
const half = Math.max(MIN_SIZE / 2, Math.min(MAX_SIZE / 2, note.size / 2 + delta));
|
|
50316
|
+
controller.content.update(note.id, { size: half * 2 });
|
|
50317
|
+
};
|
|
50318
|
+
return /* @__PURE__ */ jsx("span", {
|
|
50319
|
+
"aria-label": `Resize note (${corner})`,
|
|
50320
|
+
"aria-valuemax": MAX_SIZE,
|
|
50321
|
+
"aria-valuemin": MIN_SIZE,
|
|
50322
|
+
"aria-valuenow": note.size,
|
|
50323
|
+
role: "slider",
|
|
50324
|
+
tabIndex: 0,
|
|
50325
|
+
className: "scrawl-board__focus-handle",
|
|
50326
|
+
"data-orientation": corner === "nw" || corner === "se" ? "nwse" : "nesw",
|
|
50327
|
+
style: {
|
|
50328
|
+
left: x,
|
|
50329
|
+
top: y
|
|
50330
|
+
},
|
|
50331
|
+
onKeyDown: (event) => {
|
|
50332
|
+
if (event.key === "ArrowUp" || event.key === "ArrowRight") {
|
|
50333
|
+
event.preventDefault();
|
|
50334
|
+
nudge(.5);
|
|
50335
|
+
} else if (event.key === "ArrowDown" || event.key === "ArrowLeft") {
|
|
50336
|
+
event.preventDefault();
|
|
50337
|
+
nudge(-.5);
|
|
50338
|
+
}
|
|
50339
|
+
},
|
|
50340
|
+
onPointerDown: (event) => {
|
|
50341
|
+
event.stopPropagation();
|
|
50342
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
50343
|
+
setDragging(true);
|
|
50344
|
+
},
|
|
50345
|
+
onPointerMove: (event) => {
|
|
50346
|
+
if (!dragging) return;
|
|
50347
|
+
const board = controller.view.screenToBoard({
|
|
50348
|
+
x: event.clientX,
|
|
50349
|
+
y: event.clientY
|
|
50350
|
+
});
|
|
50351
|
+
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))));
|
|
50352
|
+
controller.content.update(note.id, { size: half * 2 });
|
|
50353
|
+
},
|
|
50354
|
+
onPointerUp: (event) => {
|
|
50355
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
50356
|
+
setDragging(false);
|
|
50357
|
+
}
|
|
50358
|
+
});
|
|
50359
|
+
}
|
|
50360
|
+
function TextToolbar({ controller, snapshot, text, locked, anchor }) {
|
|
50361
|
+
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
50362
|
+
const locked_ = !mutable || locked;
|
|
50363
|
+
return /* @__PURE__ */ jsxs(ToolbarShell, {
|
|
50364
|
+
anchor,
|
|
50365
|
+
children: [
|
|
50366
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50367
|
+
className: "scrawl-board__style-group",
|
|
50368
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Size" }), TEXT_SIZES.map(([label, fontSize]) => /* @__PURE__ */ jsx("button", {
|
|
50369
|
+
"aria-label": `${label} text`,
|
|
50370
|
+
"aria-pressed": Math.abs(text.fontSize - fontSize) < .001,
|
|
50371
|
+
disabled: locked_,
|
|
50372
|
+
onClick: () => controller.content.update(text.id, { fontSize }),
|
|
50373
|
+
type: "button",
|
|
50374
|
+
children: label[0]
|
|
50375
|
+
}, label))]
|
|
50376
|
+
}),
|
|
50377
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50378
|
+
className: "scrawl-board__style-group",
|
|
50379
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(INK_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
|
|
50380
|
+
"aria-label": `${name} text`,
|
|
50381
|
+
"aria-pressed": text.color.toLowerCase() === value.toLowerCase(),
|
|
50382
|
+
disabled: locked_,
|
|
50383
|
+
onClick: () => controller.content.update(text.id, { color: value }),
|
|
50384
|
+
type: "button",
|
|
50385
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
50386
|
+
"aria-hidden": "true",
|
|
50387
|
+
style: { backgroundColor: value }
|
|
50388
|
+
})
|
|
50389
|
+
}, value))]
|
|
50390
|
+
}),
|
|
50391
|
+
/* @__PURE__ */ jsx(LockDuplicateDelete, {
|
|
50392
|
+
controller,
|
|
50393
|
+
mutable,
|
|
50394
|
+
locked,
|
|
50395
|
+
ids: [text.id]
|
|
50396
|
+
})
|
|
50397
|
+
]
|
|
50398
|
+
});
|
|
50399
|
+
}
|
|
50400
|
+
function ShapeToolbar({ controller, snapshot, stroke, locked, anchor }) {
|
|
50401
|
+
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
50402
|
+
const locked_ = !mutable || locked;
|
|
50403
|
+
const ids = (stroke.clusterId ? controller.query.all().filter((o) => o.type === "stroke" && o.clusterId === stroke.clusterId) : [stroke]).map((m) => m.id);
|
|
50404
|
+
const applyToAll = (patch) => {
|
|
50405
|
+
for (const id of ids) controller.content.update(id, patch);
|
|
50406
|
+
};
|
|
50407
|
+
return /* @__PURE__ */ jsxs(ToolbarShell, {
|
|
50408
|
+
anchor,
|
|
50409
|
+
children: [
|
|
50410
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50411
|
+
className: "scrawl-board__style-group",
|
|
50412
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Width" }), SHAPE_WIDTHS.map(([label, width]) => /* @__PURE__ */ jsx("button", {
|
|
50413
|
+
"aria-label": `${label} stroke`,
|
|
50414
|
+
"aria-pressed": Math.abs(stroke.baseWidth - width) < .001,
|
|
50415
|
+
disabled: locked_,
|
|
50416
|
+
onClick: () => applyToAll({ baseWidth: width }),
|
|
50417
|
+
type: "button",
|
|
50418
|
+
children: label
|
|
50419
|
+
}, label))]
|
|
50420
|
+
}),
|
|
50421
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50422
|
+
className: "scrawl-board__style-group",
|
|
50423
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(INK_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
|
|
50424
|
+
"aria-label": `${name} shape`,
|
|
50425
|
+
"aria-pressed": stroke.color.toLowerCase() === value.toLowerCase(),
|
|
50426
|
+
disabled: locked_,
|
|
50427
|
+
onClick: () => applyToAll({ color: value }),
|
|
50428
|
+
type: "button",
|
|
50429
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
50430
|
+
"aria-hidden": "true",
|
|
50431
|
+
style: { backgroundColor: value }
|
|
50432
|
+
})
|
|
50433
|
+
}, value))]
|
|
50434
|
+
}),
|
|
50435
|
+
/* @__PURE__ */ jsx(LockDuplicateDelete, {
|
|
50436
|
+
controller,
|
|
50437
|
+
mutable,
|
|
50438
|
+
locked,
|
|
50439
|
+
ids
|
|
50440
|
+
})
|
|
50441
|
+
]
|
|
50442
|
+
});
|
|
50443
|
+
}
|
|
50444
|
+
function LockDuplicateDelete({ controller, mutable, locked, ids }) {
|
|
50445
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
50446
|
+
className: "scrawl-board__style-group",
|
|
50447
|
+
children: [
|
|
50448
|
+
/* @__PURE__ */ jsx("button", {
|
|
50449
|
+
"aria-label": locked ? "Unlock" : "Lock",
|
|
50450
|
+
disabled: !mutable,
|
|
50451
|
+
onClick: () => {
|
|
50452
|
+
for (const id of ids) controller.content.update(id, locked ? { locked: false } : { locked: true });
|
|
50453
|
+
},
|
|
50454
|
+
type: "button",
|
|
50455
|
+
children: locked ? "Unlock" : "Lock"
|
|
50456
|
+
}),
|
|
50457
|
+
/* @__PURE__ */ jsx("button", {
|
|
50458
|
+
"aria-label": "Duplicate",
|
|
50459
|
+
disabled: !mutable || locked,
|
|
50460
|
+
onClick: () => {
|
|
50461
|
+
const newIds = controller.content.duplicate(ids);
|
|
50462
|
+
if (newIds.length) controller.content.select(newIds);
|
|
50463
|
+
},
|
|
50464
|
+
type: "button",
|
|
50465
|
+
children: "Duplicate"
|
|
50466
|
+
}),
|
|
50467
|
+
/* @__PURE__ */ jsx("button", {
|
|
50468
|
+
"aria-label": "Delete",
|
|
50469
|
+
disabled: !mutable || locked,
|
|
50470
|
+
onClick: () => controller.content.remove(ids),
|
|
50471
|
+
type: "button",
|
|
50472
|
+
children: "Delete"
|
|
50473
|
+
})
|
|
50474
|
+
]
|
|
50475
|
+
});
|
|
50476
|
+
}
|
|
50477
|
+
//#endregion
|
|
49907
50478
|
//#region src/ui/toolbar/style-shelf.tsx
|
|
49908
50479
|
var ERASER_RADII = [
|
|
49909
50480
|
["Fine", 1],
|
|
@@ -49942,7 +50513,7 @@ var SHELF_TOOLS = /* @__PURE__ */ new Set([
|
|
|
49942
50513
|
"timer"
|
|
49943
50514
|
]);
|
|
49944
50515
|
/** Contextual per-tool style controls — visible while a styleable tool is active. */
|
|
49945
|
-
function StyleShelf({ controller, snapshot }) {
|
|
50516
|
+
function StyleShelf({ controller, snapshot, anchor }) {
|
|
49946
50517
|
if (!SHELF_TOOLS.has(snapshot.tool)) return null;
|
|
49947
50518
|
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
49948
50519
|
const { style } = snapshot;
|
|
@@ -49950,7 +50521,12 @@ function StyleShelf({ controller, snapshot }) {
|
|
|
49950
50521
|
return /* @__PURE__ */ jsxs("div", {
|
|
49951
50522
|
"aria-label": "Board style",
|
|
49952
50523
|
className: "scrawl-board__style-shelf",
|
|
50524
|
+
"data-anchored": !!anchor,
|
|
49953
50525
|
role: "toolbar",
|
|
50526
|
+
style: anchor ? {
|
|
50527
|
+
left: anchor.x,
|
|
50528
|
+
top: anchor.y
|
|
50529
|
+
} : void 0,
|
|
49954
50530
|
children: [
|
|
49955
50531
|
snapshot.tool === "highlighter" && /* @__PURE__ */ jsxs("div", {
|
|
49956
50532
|
className: "scrawl-board__style-group",
|
|
@@ -50180,6 +50756,7 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50180
50756
|
const [panel, setPanel] = useState(null);
|
|
50181
50757
|
const [query, setQuery] = useState("");
|
|
50182
50758
|
const [announcement, setAnnouncement] = useState("Board controls ready");
|
|
50759
|
+
const [styleShelfAnchor, setStyleShelfAnchor] = useState(void 0);
|
|
50183
50760
|
const importRef = useRef(null);
|
|
50184
50761
|
const openerRef = useRef(null);
|
|
50185
50762
|
const searchRef = useRef(null);
|
|
@@ -50190,6 +50767,25 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50190
50767
|
useEffect(() => {
|
|
50191
50768
|
if (panel === "search") searchRef.current?.focus();
|
|
50192
50769
|
}, [panel]);
|
|
50770
|
+
useEffect(() => {
|
|
50771
|
+
const recompute = () => {
|
|
50772
|
+
const root = chromeRef.current;
|
|
50773
|
+
const button = root?.querySelector(".scrawl-board__tools button[aria-pressed=\"true\"]");
|
|
50774
|
+
if (!root || !button) {
|
|
50775
|
+
setStyleShelfAnchor(void 0);
|
|
50776
|
+
return;
|
|
50777
|
+
}
|
|
50778
|
+
const buttonRect = button.getBoundingClientRect();
|
|
50779
|
+
const rootRect = root.getBoundingClientRect();
|
|
50780
|
+
setStyleShelfAnchor({
|
|
50781
|
+
x: buttonRect.left + buttonRect.width / 2 - rootRect.left,
|
|
50782
|
+
y: buttonRect.top - rootRect.top
|
|
50783
|
+
});
|
|
50784
|
+
};
|
|
50785
|
+
recompute();
|
|
50786
|
+
window.addEventListener("resize", recompute);
|
|
50787
|
+
return () => window.removeEventListener("resize", recompute);
|
|
50788
|
+
}, [snapshot.tool]);
|
|
50193
50789
|
const open = (next, opener) => {
|
|
50194
50790
|
openerRef.current = opener;
|
|
50195
50791
|
setPanel(next);
|
|
@@ -50418,9 +51014,14 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50418
51014
|
]
|
|
50419
51015
|
}),
|
|
50420
51016
|
renderSlot(slots?.stylePanel, enabled("styleShelf") && /* @__PURE__ */ jsx(StyleShelf, {
|
|
51017
|
+
anchor: styleShelfAnchor,
|
|
50421
51018
|
controller,
|
|
50422
51019
|
snapshot
|
|
50423
51020
|
})),
|
|
51021
|
+
enabled("focusedItemToolbar") && /* @__PURE__ */ jsx(FocusedItemToolbar, {
|
|
51022
|
+
controller,
|
|
51023
|
+
snapshot
|
|
51024
|
+
}),
|
|
50424
51025
|
ContextMenu && /* @__PURE__ */ jsx(ContextMenu, {
|
|
50425
51026
|
controller,
|
|
50426
51027
|
snapshot
|
|
@@ -51063,4 +51664,4 @@ function ScrawlBoard({ documentId, initialDocument, onReady, className, style })
|
|
|
51063
51664
|
});
|
|
51064
51665
|
}
|
|
51065
51666
|
//#endregion
|
|
51066
|
-
export { ASSET_CACHE_BYTES_DEFAULT, ASSET_CACHE_BYTES_MAX, ASSET_CACHE_BYTES_MIN, ASSET_EXPORT_MAX_DECODED_MEGAPIXELS, ASSET_EXPORT_MAX_ENCODED_BYTES, ASSET_MAX_CONCURRENT_RESOLUTIONS, ASSET_MAX_DECODED_MEGAPIXELS, ASSET_MAX_DIMENSION_PX, ASSET_MAX_ENCODED_BYTES, ASSET_REF_MAX_BYTES, ASSET_REF_PATTERN, AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, AssetResolutionError, BEACON_INSET, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, DefaultBoardChrome, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, InlineEditors, LockItemsCommand, MIN_WIDTH_FACTOR, MultiplayerCursors, NOTE_COLORS, NOTE_DEFAULT_SIZE, NOTE_DEFAULT_Z, NOTE_MAX_Z, NOTE_MIN_Z, NOTE_PEEL_STEP, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, STAMPS, STAMP_SIZE, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, SpatialIndex, StyleShelf, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, UpdateImageCommand, UpdateNoteCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, assetRef, avgScale, canUnlockItem, changeToOps, clampAssetCacheBytes, cloneCustomObject, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, createBoardController, createLocalBoard, documentId, documentToSVG, formatTimer, invert, isAssetRef, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, resolveScrawlTheme, ribbonEdges, rotationAbout, scalingAbout, scrawlThemePresets, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
|
|
51667
|
+
export { ASSET_CACHE_BYTES_DEFAULT, ASSET_CACHE_BYTES_MAX, ASSET_CACHE_BYTES_MIN, ASSET_EXPORT_MAX_DECODED_MEGAPIXELS, ASSET_EXPORT_MAX_ENCODED_BYTES, ASSET_MAX_CONCURRENT_RESOLUTIONS, ASSET_MAX_DECODED_MEGAPIXELS, ASSET_MAX_DIMENSION_PX, ASSET_MAX_ENCODED_BYTES, ASSET_REF_MAX_BYTES, ASSET_REF_PATTERN, AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, AssetResolutionError, BEACON_INSET, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, DefaultBoardChrome, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, FocusedItemToolbar, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, InlineEditors, LockItemsCommand, MIN_WIDTH_FACTOR, MultiplayerCursors, NOTE_COLORS, NOTE_DEFAULT_SIZE, NOTE_DEFAULT_Z, NOTE_MAX_Z, NOTE_MIN_Z, NOTE_PEEL_STEP, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, STAMPS, STAMP_SIZE, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, SpatialIndex, StyleShelf, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, UpdateImageCommand, UpdateNoteCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, assetRef, avgScale, canUnlockItem, changeToOps, clampAssetCacheBytes, cloneCustomObject, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, createBoardController, createLocalBoard, documentId, documentToSVG, formatTimer, invert, isAssetRef, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, resolveScrawlTheme, ribbonEdges, rotationAbout, scalingAbout, scrawlThemePresets, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
|