@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/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) {
|
|
@@ -35964,12 +35980,16 @@ var FRAGMENT = `
|
|
|
35964
35980
|
return max(lineAlpha2.x, lineAlpha2.y);
|
|
35965
35981
|
}
|
|
35966
35982
|
|
|
35967
|
-
// Distance from p's nearest grid intersection, in world units.
|
|
35968
|
-
//
|
|
35983
|
+
// Distance from p's nearest grid intersection, in world units. A line's
|
|
35984
|
+
// width reads as visible even at 1px because it spans the whole screen;
|
|
35985
|
+
// an isolated dot at that same 1px radius gets almost entirely eaten by
|
|
35986
|
+
// anti-aliasing and all but disappears. Scaling the radius up keeps a
|
|
35987
|
+
// dot's on-screen weight comparable to a line drawn with the same
|
|
35988
|
+
// uGridLineWidthPx, rather than using it literally as a radius.
|
|
35969
35989
|
float gridDotAlpha(vec2 p) {
|
|
35970
35990
|
vec2 cell = mod(p, uGridSpacing) - uGridSpacing * 0.5;
|
|
35971
35991
|
float dist = length(cell);
|
|
35972
|
-
float radius = uGridLineWidthPx * uWorldPerPixel;
|
|
35992
|
+
float radius = uGridLineWidthPx * uWorldPerPixel * 1.75;
|
|
35973
35993
|
float aa = uWorldPerPixel;
|
|
35974
35994
|
return 1.0 - smoothstep(radius - aa * 0.5, radius + aa * 0.5, dist);
|
|
35975
35995
|
}
|
|
@@ -36546,6 +36566,7 @@ var StrokeRenderer = class {
|
|
|
36546
36566
|
if (mesh) {
|
|
36547
36567
|
mesh.geometry.dispose();
|
|
36548
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);
|
|
36549
36570
|
}
|
|
36550
36571
|
}
|
|
36551
36572
|
for (const stroke of change.transformed) {
|
|
@@ -44725,7 +44746,7 @@ function distToSegmentSq(p, a, b) {
|
|
|
44725
44746
|
}
|
|
44726
44747
|
//#endregion
|
|
44727
44748
|
//#region src/interaction/tools/eraserTool.ts
|
|
44728
|
-
var DECAY_PER_PASS =
|
|
44749
|
+
var DECAY_PER_PASS = 1;
|
|
44729
44750
|
/** A point can take another decay pass after this long — rubbing works. */
|
|
44730
44751
|
var REARM_MS = 250;
|
|
44731
44752
|
var EraserTool = class {
|
|
@@ -44819,19 +44840,11 @@ var EraserTool = class {
|
|
|
44819
44840
|
const runs = partitionByErasure(current.points);
|
|
44820
44841
|
const survives = runs.filter((r) => r.survives && r.points.length >= 2);
|
|
44821
44842
|
const keepId = survives.length === 1 && survives[0].points.length === current.points.length;
|
|
44822
|
-
const m = current.matrix;
|
|
44823
44843
|
for (const run of runs) if (run.survives && run.points.length >= 2) after.push({
|
|
44824
44844
|
...current,
|
|
44825
44845
|
id: keepId ? current.id : crypto.randomUUID(),
|
|
44826
44846
|
points: run.points
|
|
44827
44847
|
});
|
|
44828
|
-
else if (!run.survives) {
|
|
44829
|
-
const world = m ? run.points.map((p) => ({
|
|
44830
|
-
...p,
|
|
44831
|
-
...apply(m, p)
|
|
44832
|
-
})) : run.points;
|
|
44833
|
-
this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1), current.tool !== "shape");
|
|
44834
|
-
}
|
|
44835
44848
|
}
|
|
44836
44849
|
doc.removeStrokes(before.map((s) => s.id));
|
|
44837
44850
|
doc.addStrokes(after.map(cloneStroke));
|
|
@@ -45039,10 +45052,10 @@ function buildShape(kind, a, b, step, constrain) {
|
|
|
45039
45052
|
case "ellipse": return [ellipse(a, targetB, step)];
|
|
45040
45053
|
case "line": return [sampleSegment(a, constrain ? snappedEnd(a, b) : b, step)];
|
|
45041
45054
|
case "arrow": return arrow(a, constrain ? snappedEnd(a, b) : b, step);
|
|
45042
|
-
case "triangle": return [polygon(3, a, targetB, step,
|
|
45055
|
+
case "triangle": return [polygon(3, a, targetB, step, Math.PI / 2)];
|
|
45043
45056
|
case "diamond": return [polygon(4, a, targetB, step, 0)];
|
|
45044
|
-
case "pentagon": return [polygon(5, a, targetB, step,
|
|
45045
|
-
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)];
|
|
45046
45059
|
case "octagon": return [polygon(8, a, targetB, step, Math.PI / 8)];
|
|
45047
45060
|
case "star": return [star(5, a, targetB, step)];
|
|
45048
45061
|
case "heart": return [heart(a, targetB, step)];
|
|
@@ -45241,6 +45254,7 @@ var ShapeTool = class {
|
|
|
45241
45254
|
this.ctx.clusters.assign(strokes[0]);
|
|
45242
45255
|
for (const s of strokes) s.clusterId = strokes[0].clusterId;
|
|
45243
45256
|
this.ctx.history.execute(new AddStrokesCommand(strokes));
|
|
45257
|
+
this.ctx.selection.set(strokes.map((s) => s.id));
|
|
45244
45258
|
this.ctx.setTool?.("select");
|
|
45245
45259
|
}
|
|
45246
45260
|
this.ctx.transition("idle");
|
|
@@ -45822,6 +45836,13 @@ var HANDLE_HIT_PX = 12;
|
|
|
45822
45836
|
var ROTATE_OFFSET_PX = 26;
|
|
45823
45837
|
var ROTATE_HIT_PX = 14;
|
|
45824
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;
|
|
45825
45846
|
var SelectionGizmo = class {
|
|
45826
45847
|
constructor(scene) {
|
|
45827
45848
|
this.group = new Group();
|
|
@@ -45836,7 +45857,7 @@ var SelectionGizmo = class {
|
|
|
45836
45857
|
this.outline = new LineLoop(new BufferGeometry(), this.lineMaterial);
|
|
45837
45858
|
this.outline.position.z = GIZMO_Z;
|
|
45838
45859
|
this.outline.frustumCulled = false;
|
|
45839
|
-
const handleGeometry = new
|
|
45860
|
+
const handleGeometry = new CircleGeometry(.5, 20);
|
|
45840
45861
|
const handleMaterial = new MeshBasicMaterial({ color: COLOR });
|
|
45841
45862
|
const makeHandle = () => new Mesh(handleGeometry, handleMaterial);
|
|
45842
45863
|
this.handles = {
|
|
@@ -45864,18 +45885,19 @@ var SelectionGizmo = class {
|
|
|
45864
45885
|
if (!box) return;
|
|
45865
45886
|
this.lineMaterial.color.setHex(locked ? 16096779 : COLOR);
|
|
45866
45887
|
this.rotateHandle.visible = !locked;
|
|
45888
|
+
const visual = this.visualBox(box, wpp);
|
|
45867
45889
|
const positions = new Float32Array([
|
|
45868
|
-
|
|
45869
|
-
|
|
45890
|
+
visual.minX,
|
|
45891
|
+
visual.minY,
|
|
45870
45892
|
GIZMO_Z,
|
|
45871
|
-
|
|
45872
|
-
|
|
45893
|
+
visual.maxX,
|
|
45894
|
+
visual.minY,
|
|
45873
45895
|
GIZMO_Z,
|
|
45874
|
-
|
|
45875
|
-
|
|
45896
|
+
visual.maxX,
|
|
45897
|
+
visual.maxY,
|
|
45876
45898
|
GIZMO_Z,
|
|
45877
|
-
|
|
45878
|
-
|
|
45899
|
+
visual.minX,
|
|
45900
|
+
visual.maxY,
|
|
45879
45901
|
GIZMO_Z
|
|
45880
45902
|
]);
|
|
45881
45903
|
this.outline.geometry.dispose();
|
|
@@ -45887,17 +45909,27 @@ var SelectionGizmo = class {
|
|
|
45887
45909
|
for (const [handle, mesh] of Object.entries(this.handles)) {
|
|
45888
45910
|
mesh.visible = !locked;
|
|
45889
45911
|
if (!locked) {
|
|
45890
|
-
const p = this.handlePoint(handle,
|
|
45912
|
+
const p = this.handlePoint(handle, visual);
|
|
45891
45913
|
mesh.position.set(p.x, p.y, GIZMO_Z);
|
|
45892
45914
|
mesh.scale.set(size, size, 1);
|
|
45893
45915
|
}
|
|
45894
45916
|
}
|
|
45895
45917
|
if (!locked) {
|
|
45896
|
-
const rotate = this.rotatePoint(
|
|
45918
|
+
const rotate = this.rotatePoint(visual, wpp);
|
|
45897
45919
|
this.rotateHandle.position.set(rotate.x, rotate.y, GIZMO_Z);
|
|
45898
45920
|
this.rotateHandle.scale.set(size, size, 1);
|
|
45899
45921
|
}
|
|
45900
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
|
+
}
|
|
45901
45933
|
handlePoint(handle, box) {
|
|
45902
45934
|
const midX = (box.minX + box.maxX) / 2;
|
|
45903
45935
|
const midY = (box.minY + box.maxY) / 2;
|
|
@@ -45985,7 +46017,8 @@ var SelectionGizmo = class {
|
|
|
45985
46017
|
if (!this.box || this.isLocked) return null;
|
|
45986
46018
|
const handleR = HANDLE_HIT_PX * this.wpp;
|
|
45987
46019
|
const edgeSlop = 6 * this.wpp;
|
|
45988
|
-
const
|
|
46020
|
+
const visual = this.visualBox(this.box, this.wpp);
|
|
46021
|
+
const rotate = this.rotatePoint(visual, this.wpp);
|
|
45989
46022
|
if (Math.hypot(p.x - rotate.x, p.y - rotate.y) < ROTATE_HIT_PX * this.wpp) return { kind: "rotate" };
|
|
45990
46023
|
for (const handle of [
|
|
45991
46024
|
"nw",
|
|
@@ -45997,13 +46030,13 @@ var SelectionGizmo = class {
|
|
|
45997
46030
|
"e",
|
|
45998
46031
|
"w"
|
|
45999
46032
|
]) {
|
|
46000
|
-
const hp = this.handlePoint(handle,
|
|
46033
|
+
const hp = this.handlePoint(handle, visual);
|
|
46001
46034
|
if (Math.abs(p.x - hp.x) < handleR && Math.abs(p.y - hp.y) < handleR) return {
|
|
46002
46035
|
kind: "scale",
|
|
46003
46036
|
handle
|
|
46004
46037
|
};
|
|
46005
46038
|
}
|
|
46006
|
-
const { minX, maxX, minY, maxY } =
|
|
46039
|
+
const { minX, maxX, minY, maxY } = visual;
|
|
46007
46040
|
const inX = p.x >= minX - edgeSlop && p.x <= maxX + edgeSlop;
|
|
46008
46041
|
const inY = p.y >= minY - edgeSlop && p.y <= maxY + edgeSlop;
|
|
46009
46042
|
if (inX && Math.abs(p.y - maxY) < edgeSlop) return {
|
|
@@ -46023,7 +46056,7 @@ var SelectionGizmo = class {
|
|
|
46023
46056
|
handle: "w"
|
|
46024
46057
|
};
|
|
46025
46058
|
const pad = 2 * this.wpp;
|
|
46026
|
-
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" };
|
|
46027
46060
|
return null;
|
|
46028
46061
|
}
|
|
46029
46062
|
rotatePoint(box, wpp) {
|
|
@@ -48256,6 +48289,112 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48256
48289
|
})),
|
|
48257
48290
|
...[...document.allCustomObjects()].map((value) => clone(toBoardObject(value)))
|
|
48258
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
|
+
};
|
|
48259
48398
|
const updateObject = (id, patch) => {
|
|
48260
48399
|
assertMutable();
|
|
48261
48400
|
const before = readObject(id);
|
|
@@ -48297,6 +48436,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48297
48436
|
zoom: view.zoom,
|
|
48298
48437
|
readOnly,
|
|
48299
48438
|
selection: Object.freeze([...selection]),
|
|
48439
|
+
focusedItem: computeFocusedItem(),
|
|
48300
48440
|
strokeCount: [...document.all()].length,
|
|
48301
48441
|
objectCount: allObjects().length,
|
|
48302
48442
|
canUndo: history.canUndo,
|
|
@@ -48711,10 +48851,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48711
48851
|
changed();
|
|
48712
48852
|
},
|
|
48713
48853
|
get: () => ({ ...view }),
|
|
48714
|
-
boardToScreen: (point) =>
|
|
48715
|
-
x: (point.x - view.x) * view.zoom,
|
|
48716
|
-
y: (view.y - point.y) * view.zoom
|
|
48717
|
-
},
|
|
48854
|
+
boardToScreen: (point) => projectToScreen(point),
|
|
48718
48855
|
screenToBoard: (point) => engine ? engine.screenToBoard(point.x, point.y) ?? {
|
|
48719
48856
|
x: point.x,
|
|
48720
48857
|
y: point.y
|
|
@@ -48751,6 +48888,81 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48751
48888
|
update(id, patch) {
|
|
48752
48889
|
updateObject(id, patch);
|
|
48753
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
|
+
},
|
|
48754
48966
|
table: {
|
|
48755
48967
|
addRow(tableId) {
|
|
48756
48968
|
const table = document.getTable(tableId);
|
|
@@ -49698,14 +49910,17 @@ function InlineEditors({ controller, renderPortal }) {
|
|
|
49698
49910
|
if (editor?.kind === "text") {
|
|
49699
49911
|
const text = editor.text;
|
|
49700
49912
|
if (editor.id === null) {
|
|
49701
|
-
if (text.trim())
|
|
49702
|
-
|
|
49703
|
-
|
|
49704
|
-
|
|
49705
|
-
|
|
49706
|
-
|
|
49707
|
-
|
|
49708
|
-
|
|
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
|
+
}
|
|
49709
49924
|
} else if (!text.trim()) controller.content.remove([editor.id]);
|
|
49710
49925
|
else controller.content.update(editor.id, { text });
|
|
49711
49926
|
}
|
|
@@ -49900,6 +50115,366 @@ function InlineEditors({ controller, renderPortal }) {
|
|
|
49900
50115
|
}))] });
|
|
49901
50116
|
}
|
|
49902
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
|
|
49903
50478
|
//#region src/ui/toolbar/style-shelf.tsx
|
|
49904
50479
|
var ERASER_RADII = [
|
|
49905
50480
|
["Fine", 1],
|
|
@@ -49938,7 +50513,7 @@ var SHELF_TOOLS = /* @__PURE__ */ new Set([
|
|
|
49938
50513
|
"timer"
|
|
49939
50514
|
]);
|
|
49940
50515
|
/** Contextual per-tool style controls — visible while a styleable tool is active. */
|
|
49941
|
-
function StyleShelf({ controller, snapshot }) {
|
|
50516
|
+
function StyleShelf({ controller, snapshot, anchor }) {
|
|
49942
50517
|
if (!SHELF_TOOLS.has(snapshot.tool)) return null;
|
|
49943
50518
|
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
49944
50519
|
const { style } = snapshot;
|
|
@@ -49946,7 +50521,12 @@ function StyleShelf({ controller, snapshot }) {
|
|
|
49946
50521
|
return /* @__PURE__ */ jsxs("div", {
|
|
49947
50522
|
"aria-label": "Board style",
|
|
49948
50523
|
className: "scrawl-board__style-shelf",
|
|
50524
|
+
"data-anchored": !!anchor,
|
|
49949
50525
|
role: "toolbar",
|
|
50526
|
+
style: anchor ? {
|
|
50527
|
+
left: anchor.x,
|
|
50528
|
+
top: anchor.y
|
|
50529
|
+
} : void 0,
|
|
49950
50530
|
children: [
|
|
49951
50531
|
snapshot.tool === "highlighter" && /* @__PURE__ */ jsxs("div", {
|
|
49952
50532
|
className: "scrawl-board__style-group",
|
|
@@ -50176,6 +50756,7 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50176
50756
|
const [panel, setPanel] = useState(null);
|
|
50177
50757
|
const [query, setQuery] = useState("");
|
|
50178
50758
|
const [announcement, setAnnouncement] = useState("Board controls ready");
|
|
50759
|
+
const [styleShelfAnchor, setStyleShelfAnchor] = useState(void 0);
|
|
50179
50760
|
const importRef = useRef(null);
|
|
50180
50761
|
const openerRef = useRef(null);
|
|
50181
50762
|
const searchRef = useRef(null);
|
|
@@ -50186,6 +50767,25 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50186
50767
|
useEffect(() => {
|
|
50187
50768
|
if (panel === "search") searchRef.current?.focus();
|
|
50188
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]);
|
|
50189
50789
|
const open = (next, opener) => {
|
|
50190
50790
|
openerRef.current = opener;
|
|
50191
50791
|
setPanel(next);
|
|
@@ -50414,9 +51014,14 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50414
51014
|
]
|
|
50415
51015
|
}),
|
|
50416
51016
|
renderSlot(slots?.stylePanel, enabled("styleShelf") && /* @__PURE__ */ jsx(StyleShelf, {
|
|
51017
|
+
anchor: styleShelfAnchor,
|
|
50417
51018
|
controller,
|
|
50418
51019
|
snapshot
|
|
50419
51020
|
})),
|
|
51021
|
+
enabled("focusedItemToolbar") && /* @__PURE__ */ jsx(FocusedItemToolbar, {
|
|
51022
|
+
controller,
|
|
51023
|
+
snapshot
|
|
51024
|
+
}),
|
|
50420
51025
|
ContextMenu && /* @__PURE__ */ jsx(ContextMenu, {
|
|
50421
51026
|
controller,
|
|
50422
51027
|
snapshot
|
|
@@ -51059,4 +51664,4 @@ function ScrawlBoard({ documentId, initialDocument, onReady, className, style })
|
|
|
51059
51664
|
});
|
|
51060
51665
|
}
|
|
51061
51666
|
//#endregion
|
|
51062
|
-
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 };
|