@fieldnotes/core 0.15.0 → 0.17.0

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/index.js CHANGED
@@ -811,9 +811,228 @@ function createId(prefix) {
811
811
  return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
812
812
  }
813
813
 
814
+ // src/canvas/keyboard-actions.ts
815
+ var KeyboardActions = class {
816
+ constructor(deps) {
817
+ this.deps = deps;
818
+ }
819
+ clipboard = [];
820
+ pasteCount = 0;
821
+ nudgeTimer = null;
822
+ dispose() {
823
+ this.flushPendingNudge();
824
+ }
825
+ selectTool() {
826
+ const tm = this.deps.getToolManager();
827
+ const ctx = this.deps.getToolContext();
828
+ if (!tm || !ctx) return null;
829
+ const tool = tm.activeTool;
830
+ if (tool?.name !== "select") return null;
831
+ return { tool, ctx };
832
+ }
833
+ nudge(dx, dy, byCell) {
834
+ if (this.deps.isToolActive()) return false;
835
+ const sel = this.selectTool();
836
+ if (!sel) return false;
837
+ if (sel.tool.selectedIds.length === 0) return false;
838
+ const step = byCell ? sel.ctx.gridSize ?? 10 : 1;
839
+ if (this.nudgeTimer === null) {
840
+ this.deps.getHistoryRecorder()?.begin();
841
+ } else {
842
+ clearTimeout(this.nudgeTimer);
843
+ }
844
+ const moved = sel.tool.nudgeSelection(dx * step, dy * step, sel.ctx);
845
+ this.nudgeTimer = setTimeout(() => this.flushPendingNudge(), 400);
846
+ return moved;
847
+ }
848
+ flushPendingNudge() {
849
+ if (this.nudgeTimer === null) return;
850
+ clearTimeout(this.nudgeTimer);
851
+ this.nudgeTimer = null;
852
+ this.deps.getHistoryRecorder()?.commit();
853
+ }
854
+ deleteSelected() {
855
+ this.flushPendingNudge();
856
+ const sel = this.selectTool();
857
+ if (!sel) return;
858
+ const ids = sel.tool.selectedIds;
859
+ if (ids.length === 0) return;
860
+ const recorder = this.deps.getHistoryRecorder();
861
+ recorder?.begin();
862
+ for (const id of ids) {
863
+ sel.ctx.store.remove(id);
864
+ }
865
+ recorder?.commit();
866
+ sel.ctx.requestRender();
867
+ }
868
+ undo() {
869
+ this.flushPendingNudge();
870
+ const ctx = this.deps.getToolContext();
871
+ const stack = this.deps.getHistoryStack();
872
+ if (!stack || !ctx) return;
873
+ const recorder = this.deps.getHistoryRecorder();
874
+ recorder?.pause();
875
+ stack.undo(ctx.store);
876
+ recorder?.resume();
877
+ ctx.requestRender();
878
+ }
879
+ redo() {
880
+ this.flushPendingNudge();
881
+ const ctx = this.deps.getToolContext();
882
+ const stack = this.deps.getHistoryStack();
883
+ if (!stack || !ctx) return;
884
+ const recorder = this.deps.getHistoryRecorder();
885
+ recorder?.pause();
886
+ stack.redo(ctx.store);
887
+ recorder?.resume();
888
+ ctx.requestRender();
889
+ }
890
+ copy() {
891
+ if (this.deps.isToolActive()) return;
892
+ const sel = this.selectTool();
893
+ if (!sel) return;
894
+ const ids = sel.tool.selectedIds;
895
+ if (ids.length === 0) return;
896
+ this.clipboard = [];
897
+ for (const id of ids) {
898
+ const el = sel.ctx.store.getById(id);
899
+ if (el) this.clipboard.push(structuredClone(el));
900
+ }
901
+ this.pasteCount = 0;
902
+ }
903
+ paste() {
904
+ this.flushPendingNudge();
905
+ if (this.clipboard.length === 0 || this.deps.isToolActive()) return;
906
+ const sel = this.selectTool();
907
+ if (!sel) return;
908
+ this.pasteCount++;
909
+ this.insertClones(this.clipboard, this.pasteCount * 20, sel);
910
+ }
911
+ duplicate() {
912
+ this.flushPendingNudge();
913
+ if (this.deps.isToolActive()) return;
914
+ const sel = this.selectTool();
915
+ if (!sel) return;
916
+ const source = [];
917
+ for (const id of sel.tool.selectedIds) {
918
+ const el = sel.ctx.store.getById(id);
919
+ if (el) source.push(el);
920
+ }
921
+ if (source.length === 0) return;
922
+ this.insertClones(source, 20, sel);
923
+ }
924
+ deselect() {
925
+ if (this.deps.isToolActive()) return;
926
+ const sel = this.selectTool();
927
+ if (!sel) return;
928
+ if (sel.tool.selectedIds.length === 0) return;
929
+ sel.tool.setSelection([]);
930
+ sel.ctx.requestRender();
931
+ }
932
+ selectAll() {
933
+ if (this.deps.isToolActive()) return;
934
+ const tm = this.deps.getToolManager();
935
+ const ctx = this.deps.getToolContext();
936
+ if (!tm || !ctx) return;
937
+ if (tm.activeTool?.name !== "select") {
938
+ ctx.switchTool?.("select");
939
+ }
940
+ const sel = this.selectTool();
941
+ if (!sel) return;
942
+ const ids = sel.ctx.store.getAll().filter(
943
+ (el) => !el.locked && (sel.ctx.isLayerVisible?.(el.layerId) ?? true) && !(sel.ctx.isLayerLocked?.(el.layerId) ?? false)
944
+ ).map((el) => el.id);
945
+ sel.tool.setSelection(ids);
946
+ sel.ctx.requestRender();
947
+ }
948
+ zoomToFit() {
949
+ if (this.deps.isToolActive()) return;
950
+ this.deps.fitToContent?.();
951
+ }
952
+ zOrder(operation) {
953
+ this.flushPendingNudge();
954
+ const sel = this.selectTool();
955
+ if (!sel) return;
956
+ const ids = sel.tool.selectedIds;
957
+ if (ids.length === 0) return;
958
+ const recorder = this.deps.getHistoryRecorder();
959
+ recorder?.begin();
960
+ for (const id of ids) {
961
+ switch (operation) {
962
+ case "forward":
963
+ sel.ctx.store.bringForward(id);
964
+ break;
965
+ case "backward":
966
+ sel.ctx.store.sendBackward(id);
967
+ break;
968
+ case "front":
969
+ sel.ctx.store.bringToFront(id);
970
+ break;
971
+ case "back":
972
+ sel.ctx.store.sendToBack(id);
973
+ break;
974
+ }
975
+ }
976
+ recorder?.commit();
977
+ sel.ctx.requestRender();
978
+ }
979
+ insertClones(source, offset, sel) {
980
+ const idMap = /* @__PURE__ */ new Map();
981
+ for (const el of source) {
982
+ idMap.set(el.id, createId(el.type));
983
+ }
984
+ const newIds = [];
985
+ const recorder = this.deps.getHistoryRecorder();
986
+ recorder?.begin();
987
+ for (const el of source) {
988
+ const clone = structuredClone(el);
989
+ const newId = idMap.get(el.id);
990
+ if (!newId) continue;
991
+ clone.id = newId;
992
+ clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
993
+ if (clone.type === "arrow") {
994
+ const arrow = clone;
995
+ arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
996
+ arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
997
+ delete arrow.cachedControlPoint;
998
+ if (arrow.fromBinding) {
999
+ const newTarget = idMap.get(arrow.fromBinding.elementId);
1000
+ if (newTarget) {
1001
+ arrow.fromBinding = { elementId: newTarget };
1002
+ } else {
1003
+ delete arrow.fromBinding;
1004
+ }
1005
+ }
1006
+ if (arrow.toBinding) {
1007
+ const newTarget = idMap.get(arrow.toBinding.elementId);
1008
+ if (newTarget) {
1009
+ arrow.toBinding = { elementId: newTarget };
1010
+ } else {
1011
+ delete arrow.toBinding;
1012
+ }
1013
+ }
1014
+ }
1015
+ if (sel.ctx.activeLayerId) {
1016
+ clone.layerId = sel.ctx.activeLayerId;
1017
+ }
1018
+ sel.ctx.store.add(clone);
1019
+ newIds.push(clone.id);
1020
+ }
1021
+ recorder?.commit();
1022
+ sel.tool.setSelection(newIds);
1023
+ sel.ctx.requestRender();
1024
+ }
1025
+ };
1026
+
814
1027
  // src/canvas/input-handler.ts
815
1028
  var ZOOM_SENSITIVITY = 1e-3;
816
1029
  var MIDDLE_BUTTON = 1;
1030
+ var NUDGE_KEYS = {
1031
+ ArrowLeft: [-1, 0],
1032
+ ArrowRight: [1, 0],
1033
+ ArrowUp: [0, -1],
1034
+ ArrowDown: [0, 1]
1035
+ };
817
1036
  var InputHandler = class {
818
1037
  constructor(element, camera, options = {}) {
819
1038
  this.element = element;
@@ -822,6 +1041,14 @@ var InputHandler = class {
822
1041
  this.toolContext = options.toolContext ?? null;
823
1042
  this.historyRecorder = options.historyRecorder ?? null;
824
1043
  this.historyStack = options.historyStack ?? null;
1044
+ this.actions = new KeyboardActions({
1045
+ getToolManager: () => this.toolManager,
1046
+ getToolContext: () => this.toolContext,
1047
+ getHistoryRecorder: () => this.historyRecorder,
1048
+ getHistoryStack: () => this.historyStack,
1049
+ isToolActive: () => this.isToolActive,
1050
+ fitToContent: options.fitToContent
1051
+ });
825
1052
  this.element.style.touchAction = "none";
826
1053
  this.bind();
827
1054
  }
@@ -840,13 +1067,16 @@ var InputHandler = class {
840
1067
  inputFilter = new InputFilter();
841
1068
  deferredDown = null;
842
1069
  abortController = new AbortController();
843
- clipboard = [];
844
- pasteCount = 0;
1070
+ actions;
845
1071
  setToolManager(toolManager, toolContext) {
846
1072
  this.toolManager = toolManager;
847
1073
  this.toolContext = toolContext;
848
1074
  }
1075
+ flushPendingHistory() {
1076
+ this.actions.flushPendingNudge();
1077
+ }
849
1078
  destroy() {
1079
+ this.actions.dispose();
850
1080
  this.abortController.abort();
851
1081
  this.inputFilter.reset();
852
1082
  this.deferredDown = null;
@@ -952,36 +1182,61 @@ var InputHandler = class {
952
1182
  }
953
1183
  };
954
1184
  onKeyDown = (e) => {
955
- if (e.target?.isContentEditable) return;
1185
+ const target = e.target;
1186
+ if (target?.isContentEditable) return;
1187
+ const tag = target?.tagName;
1188
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
956
1189
  if (e.key === " ") {
957
1190
  this.spaceHeld = true;
958
1191
  }
959
1192
  if (e.key === "Delete" || e.key === "Backspace") {
960
- this.deleteSelected();
1193
+ this.actions.deleteSelected();
1194
+ }
1195
+ if (e.key === "Escape") {
1196
+ this.actions.deselect();
961
1197
  }
962
1198
  if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
963
1199
  e.preventDefault();
964
- this.handleUndo();
1200
+ this.actions.undo();
965
1201
  }
966
1202
  if ((e.ctrlKey || e.metaKey) && (e.key === "y" || e.key === "z" && e.shiftKey)) {
967
1203
  e.preventDefault();
968
- this.handleRedo();
1204
+ this.actions.redo();
1205
+ }
1206
+ if ((e.ctrlKey || e.metaKey) && e.key === "a") {
1207
+ e.preventDefault();
1208
+ this.actions.selectAll();
969
1209
  }
970
1210
  if ((e.ctrlKey || e.metaKey) && e.key === "c") {
971
1211
  e.preventDefault();
972
- this.handleCopy();
1212
+ this.actions.copy();
973
1213
  }
974
1214
  if ((e.ctrlKey || e.metaKey) && e.key === "v") {
975
1215
  e.preventDefault();
976
- this.handlePaste();
1216
+ this.actions.paste();
1217
+ }
1218
+ if ((e.ctrlKey || e.metaKey) && e.key === "d") {
1219
+ e.preventDefault();
1220
+ this.actions.duplicate();
977
1221
  }
978
1222
  if (e.key === "]") {
979
1223
  e.preventDefault();
980
- this.handleZOrder(e.ctrlKey || e.metaKey ? "front" : "forward");
1224
+ this.actions.zOrder(e.ctrlKey || e.metaKey ? "front" : "forward");
981
1225
  }
982
1226
  if (e.key === "[") {
983
1227
  e.preventDefault();
984
- this.handleZOrder(e.ctrlKey || e.metaKey ? "back" : "backward");
1228
+ this.actions.zOrder(e.ctrlKey || e.metaKey ? "back" : "backward");
1229
+ }
1230
+ if (e.shiftKey && e.code === "Digit1" && !e.ctrlKey && !e.metaKey && !e.altKey) {
1231
+ e.preventDefault();
1232
+ this.actions.zoomToFit();
1233
+ }
1234
+ const nudgeDelta = NUDGE_KEYS[e.key];
1235
+ if (nudgeDelta) {
1236
+ const [dx, dy] = nudgeDelta;
1237
+ if (this.actions.nudge(dx, dy, e.shiftKey)) {
1238
+ e.preventDefault();
1239
+ }
985
1240
  }
986
1241
  };
987
1242
  onKeyUp = (e) => {
@@ -1045,6 +1300,7 @@ var InputHandler = class {
1045
1300
  }
1046
1301
  dispatchToolDown(e) {
1047
1302
  if (!this.toolManager || !this.toolContext) return;
1303
+ this.actions.flushPendingNudge();
1048
1304
  this.historyRecorder?.begin();
1049
1305
  this.isToolActive = true;
1050
1306
  this.toolManager.handlePointerDown(this.toPointerState(e), this.toolContext);
@@ -1065,127 +1321,6 @@ var InputHandler = class {
1065
1321
  this.toolManager.handlePointerUp(this.toPointerState(e), this.toolContext);
1066
1322
  this.historyRecorder?.commit();
1067
1323
  }
1068
- deleteSelected() {
1069
- if (!this.toolManager || !this.toolContext) return;
1070
- const tool = this.toolManager.activeTool;
1071
- if (tool?.name !== "select") return;
1072
- const selectTool = tool;
1073
- const ids = selectTool.selectedIds;
1074
- if (ids.length === 0) return;
1075
- this.historyRecorder?.begin();
1076
- for (const id of ids) {
1077
- this.toolContext.store.remove(id);
1078
- }
1079
- this.historyRecorder?.commit();
1080
- this.toolContext.requestRender();
1081
- }
1082
- handleUndo() {
1083
- if (!this.historyStack || !this.toolContext) return;
1084
- this.historyRecorder?.pause();
1085
- this.historyStack.undo(this.toolContext.store);
1086
- this.historyRecorder?.resume();
1087
- this.toolContext.requestRender();
1088
- }
1089
- handleRedo() {
1090
- if (!this.historyStack || !this.toolContext) return;
1091
- this.historyRecorder?.pause();
1092
- this.historyStack.redo(this.toolContext.store);
1093
- this.historyRecorder?.resume();
1094
- this.toolContext.requestRender();
1095
- }
1096
- handleCopy() {
1097
- if (!this.toolManager || !this.toolContext || this.isToolActive) return;
1098
- const tool = this.toolManager.activeTool;
1099
- if (tool?.name !== "select") return;
1100
- const selectTool = tool;
1101
- const ids = selectTool.selectedIds;
1102
- if (ids.length === 0) return;
1103
- this.clipboard = [];
1104
- for (const id of ids) {
1105
- const el = this.toolContext.store.getById(id);
1106
- if (el) this.clipboard.push(structuredClone(el));
1107
- }
1108
- this.pasteCount = 0;
1109
- }
1110
- handlePaste() {
1111
- if (!this.toolManager || !this.toolContext || this.clipboard.length === 0 || this.isToolActive)
1112
- return;
1113
- const tool = this.toolManager.activeTool;
1114
- if (tool?.name !== "select") return;
1115
- const selectTool = tool;
1116
- this.pasteCount++;
1117
- const offset = this.pasteCount * 20;
1118
- const idMap = /* @__PURE__ */ new Map();
1119
- for (const el of this.clipboard) {
1120
- idMap.set(el.id, createId(el.type));
1121
- }
1122
- const newIds = [];
1123
- this.historyRecorder?.begin();
1124
- for (const el of this.clipboard) {
1125
- const clone = structuredClone(el);
1126
- const newId = idMap.get(el.id);
1127
- if (!newId) continue;
1128
- clone.id = newId;
1129
- clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
1130
- if (clone.type === "arrow") {
1131
- const arrow = clone;
1132
- arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
1133
- arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
1134
- delete arrow.cachedControlPoint;
1135
- if (arrow.fromBinding) {
1136
- const newTarget = idMap.get(arrow.fromBinding.elementId);
1137
- if (newTarget) {
1138
- arrow.fromBinding = { elementId: newTarget };
1139
- } else {
1140
- delete arrow.fromBinding;
1141
- }
1142
- }
1143
- if (arrow.toBinding) {
1144
- const newTarget = idMap.get(arrow.toBinding.elementId);
1145
- if (newTarget) {
1146
- arrow.toBinding = { elementId: newTarget };
1147
- } else {
1148
- delete arrow.toBinding;
1149
- }
1150
- }
1151
- }
1152
- if (this.toolContext.activeLayerId) {
1153
- clone.layerId = this.toolContext.activeLayerId;
1154
- }
1155
- this.toolContext.store.add(clone);
1156
- newIds.push(clone.id);
1157
- }
1158
- this.historyRecorder?.commit();
1159
- selectTool.setSelection(newIds);
1160
- this.toolContext.requestRender();
1161
- }
1162
- handleZOrder(operation) {
1163
- if (!this.toolManager || !this.toolContext) return;
1164
- const tool = this.toolManager.activeTool;
1165
- if (tool?.name !== "select") return;
1166
- const selectTool = tool;
1167
- const ids = selectTool.selectedIds;
1168
- if (ids.length === 0) return;
1169
- this.historyRecorder?.begin();
1170
- for (const id of ids) {
1171
- switch (operation) {
1172
- case "forward":
1173
- this.toolContext.store.bringForward(id);
1174
- break;
1175
- case "backward":
1176
- this.toolContext.store.sendBackward(id);
1177
- break;
1178
- case "front":
1179
- this.toolContext.store.bringToFront(id);
1180
- break;
1181
- case "back":
1182
- this.toolContext.store.sendToBack(id);
1183
- break;
1184
- }
1185
- }
1186
- this.historyRecorder?.commit();
1187
- this.toolContext.requestRender();
1188
- }
1189
1324
  cancelToolIfActive(e) {
1190
1325
  if (this.isToolActive) {
1191
1326
  this.dispatchToolUp(e);
@@ -3138,6 +3273,26 @@ var NoteEditor = class {
3138
3273
  }
3139
3274
  };
3140
3275
 
3276
+ // src/elements/bounds.ts
3277
+ function getElementsBoundingBox(elements) {
3278
+ let minX = Infinity;
3279
+ let minY = Infinity;
3280
+ let maxX = -Infinity;
3281
+ let maxY = -Infinity;
3282
+ let found = false;
3283
+ for (const el of elements) {
3284
+ const b = getElementBounds(el);
3285
+ if (!b) continue;
3286
+ found = true;
3287
+ if (b.x < minX) minX = b.x;
3288
+ if (b.y < minY) minY = b.y;
3289
+ if (b.x + b.w > maxX) maxX = b.x + b.w;
3290
+ if (b.y + b.h > maxY) maxY = b.y + b.h;
3291
+ }
3292
+ if (!found) return null;
3293
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
3294
+ }
3295
+
3141
3296
  // src/tools/tool-manager.ts
3142
3297
  var ToolManager = class {
3143
3298
  tools = /* @__PURE__ */ new Map();
@@ -3293,16 +3448,64 @@ var BatchCommand = class {
3293
3448
  }
3294
3449
  };
3295
3450
 
3451
+ // src/history/layer-commands.ts
3452
+ var CreateLayerCommand = class {
3453
+ constructor(manager, layer) {
3454
+ this.manager = manager;
3455
+ this.layer = layer;
3456
+ }
3457
+ execute(_store) {
3458
+ this.manager.addLayerDirect(this.layer);
3459
+ }
3460
+ undo(_store) {
3461
+ this.manager.removeLayerDirect(this.layer.id);
3462
+ }
3463
+ };
3464
+ var RemoveLayerCommand = class {
3465
+ constructor(manager, layer) {
3466
+ this.manager = manager;
3467
+ this.layer = layer;
3468
+ }
3469
+ execute(_store) {
3470
+ this.manager.removeLayerDirect(this.layer.id);
3471
+ }
3472
+ undo(_store) {
3473
+ this.manager.addLayerDirect(this.layer);
3474
+ }
3475
+ };
3476
+ var UpdateLayerCommand = class {
3477
+ constructor(manager, layerId, previous, current) {
3478
+ this.manager = manager;
3479
+ this.layerId = layerId;
3480
+ this.previous = previous;
3481
+ this.current = current;
3482
+ }
3483
+ execute(_store) {
3484
+ this.manager.updateLayerDirect(this.layerId, { ...this.current });
3485
+ }
3486
+ undo(_store) {
3487
+ this.manager.updateLayerDirect(this.layerId, { ...this.previous });
3488
+ }
3489
+ };
3490
+
3296
3491
  // src/history/history-recorder.ts
3297
3492
  var HistoryRecorder = class {
3298
- constructor(store, stack) {
3493
+ constructor(store, stack, layerManager) {
3299
3494
  this.store = store;
3300
3495
  this.stack = stack;
3496
+ this.layerManager = layerManager;
3301
3497
  this.unsubscribers = [
3302
3498
  store.on("add", (el) => this.onAdd(el)),
3303
3499
  store.on("remove", (el) => this.onRemove(el)),
3304
3500
  store.on("update", ({ previous, current }) => this.onUpdate(previous, current))
3305
3501
  ];
3502
+ if (layerManager) {
3503
+ this.unsubscribers.push(
3504
+ layerManager.on("create", (layer) => this.onLayerCreate(layer)),
3505
+ layerManager.on("remove", (layer) => this.onLayerRemove(layer)),
3506
+ layerManager.on("update", ({ previous, current }) => this.onLayerUpdate(previous, current))
3507
+ );
3508
+ }
3306
3509
  }
3307
3510
  recording = true;
3308
3511
  transaction = null;
@@ -3315,6 +3518,9 @@ var HistoryRecorder = class {
3315
3518
  this.recording = true;
3316
3519
  }
3317
3520
  begin() {
3521
+ if (this.transaction !== null) {
3522
+ this.commit();
3523
+ }
3318
3524
  this.transaction = [];
3319
3525
  this.updateSnapshots.clear();
3320
3526
  }
@@ -3363,6 +3569,21 @@ var HistoryRecorder = class {
3363
3569
  this.stack.push(new UpdateElementCommand(current.id, previous, current));
3364
3570
  }
3365
3571
  }
3572
+ onLayerCreate(layer) {
3573
+ if (!this.recording) return;
3574
+ if (!this.layerManager) return;
3575
+ this.record(new CreateLayerCommand(this.layerManager, layer));
3576
+ }
3577
+ onLayerRemove(layer) {
3578
+ if (!this.recording) return;
3579
+ if (!this.layerManager) return;
3580
+ this.record(new RemoveLayerCommand(this.layerManager, layer));
3581
+ }
3582
+ onLayerUpdate(previous, current) {
3583
+ if (!this.recording) return;
3584
+ if (!this.layerManager) return;
3585
+ this.record(new UpdateLayerCommand(this.layerManager, current.id, previous, current));
3586
+ }
3366
3587
  flushUpdateSnapshots() {
3367
3588
  const commands = [];
3368
3589
  for (const [id, previous] of this.updateSnapshots) {
@@ -3795,18 +4016,23 @@ var LayerManager = class {
3795
4016
  addLayerDirect(layer) {
3796
4017
  this.layers.set(layer.id, { ...layer });
3797
4018
  this.syncLayerOrder();
4019
+ this.bus.emit("create", { ...layer });
3798
4020
  this.bus.emit("change", null);
3799
4021
  }
3800
4022
  removeLayerDirect(id) {
4023
+ const layer = this.layers.get(id);
3801
4024
  this.layers.delete(id);
3802
4025
  this.syncLayerOrder();
4026
+ if (layer) this.bus.emit("remove", { ...layer });
3803
4027
  this.bus.emit("change", null);
3804
4028
  }
3805
4029
  updateLayerDirect(id, props) {
3806
4030
  const layer = this.layers.get(id);
3807
4031
  if (!layer) return;
4032
+ const previous = { ...layer };
3808
4033
  Object.assign(layer, props);
3809
4034
  if ("order" in props) this.syncLayerOrder();
4035
+ this.bus.emit("update", { previous, current: { ...layer } });
3810
4036
  this.bus.emit("change", null);
3811
4037
  }
3812
4038
  syncLayerOrder() {
@@ -3899,6 +4125,20 @@ var DomNodeManager = class {
3899
4125
  storeHtmlContent(elementId, dom) {
3900
4126
  this.htmlContent.set(elementId, dom);
3901
4127
  }
4128
+ hasContent(elementId) {
4129
+ return this.htmlContent.has(elementId);
4130
+ }
4131
+ resetHtmlContent(elementId) {
4132
+ this.htmlContent.delete(elementId);
4133
+ this.lastSyncedVersion.delete(elementId);
4134
+ this.lastSyncedZIndex.delete(elementId);
4135
+ const node = this.domNodes.get(elementId);
4136
+ if (!node) return;
4137
+ while (node.firstChild) {
4138
+ node.removeChild(node.firstChild);
4139
+ }
4140
+ delete node.dataset["initialized"];
4141
+ }
3902
4142
  syncDomNode(element, zIndex = 0) {
3903
4143
  let node = this.domNodes.get(element.id);
3904
4144
  if (!node) {
@@ -4440,8 +4680,10 @@ var Viewport = class {
4440
4680
  toolbar: options.toolbar
4441
4681
  });
4442
4682
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
4683
+ this.onHtmlElementMount = options.onHtmlElementMount;
4684
+ this.dropHandler = options.onDrop;
4443
4685
  this.history = new HistoryStack();
4444
- this.historyRecorder = new HistoryRecorder(this.store, this.history);
4686
+ this.historyRecorder = new HistoryRecorder(this.store, this.history, this.layerManager);
4445
4687
  this.wrapper = this.createWrapper();
4446
4688
  this.canvasEl = this.createCanvas();
4447
4689
  this.domLayer = this.createDomLayer();
@@ -4467,7 +4709,8 @@ var Viewport = class {
4467
4709
  toolManager: this.toolManager,
4468
4710
  toolContext: this.toolContext,
4469
4711
  historyRecorder: this.historyRecorder,
4470
- historyStack: this.history
4712
+ historyStack: this.history,
4713
+ fitToContent: () => this.fitToContent()
4471
4714
  });
4472
4715
  this.domNodeManager = new DomNodeManager({
4473
4716
  domLayer: this.domLayer,
@@ -4561,6 +4804,8 @@ var Viewport = class {
4561
4804
  renderLoop;
4562
4805
  domNodeManager;
4563
4806
  interactMode;
4807
+ onHtmlElementMount;
4808
+ dropHandler;
4564
4809
  gridChangeListeners = /* @__PURE__ */ new Set();
4565
4810
  doubleTapDetector = new DoubleTapDetector();
4566
4811
  tapDownX = 0;
@@ -4575,6 +4820,13 @@ var Viewport = class {
4575
4820
  this._snapToGrid = enabled;
4576
4821
  this.toolContext.snapToGrid = enabled;
4577
4822
  }
4823
+ fitToContent(padding = 40) {
4824
+ if (this.wrapper.clientWidth === 0 || this.wrapper.clientHeight === 0) return;
4825
+ const visibleElements = this.store.getAll().filter((el) => this.layerManager.isLayerVisible(el.layerId));
4826
+ const bbox = getElementsBoundingBox(visibleElements);
4827
+ if (!bbox) return;
4828
+ this.camera.fitToContent(bbox, this.wrapper.clientWidth, this.wrapper.clientHeight, padding);
4829
+ }
4578
4830
  requestRender() {
4579
4831
  this.renderLoop.requestRender();
4580
4832
  }
@@ -4593,6 +4845,7 @@ var Viewport = class {
4593
4845
  return exportImage(this.store, options, this.layerManager);
4594
4846
  }
4595
4847
  loadState(state) {
4848
+ this.inputHandler.flushPendingHistory();
4596
4849
  this.historyRecorder.pause();
4597
4850
  this.noteEditor.destroy(this.store);
4598
4851
  this.domNodeManager.clearDomNodes();
@@ -4604,6 +4857,22 @@ var Viewport = class {
4604
4857
  this.layerManager.setActiveLayer(state.activeLayerId);
4605
4858
  }
4606
4859
  this.domNodeManager.reattachHtmlContent(this.store);
4860
+ if (this.onHtmlElementMount) {
4861
+ for (const el of this.store.getElementsByType("html")) {
4862
+ if (!this.domNodeManager.hasContent(el.id)) {
4863
+ this.domNodeManager.syncDomNode(el);
4864
+ const node = this.domNodeManager.getNode(el.id);
4865
+ if (node) {
4866
+ this.onHtmlElementMount(el.id, el.domId, node);
4867
+ node.dataset["initialized"] = "true";
4868
+ Object.assign(node.style, {
4869
+ overflow: "hidden",
4870
+ pointerEvents: el.interactive ? "auto" : "none"
4871
+ });
4872
+ }
4873
+ }
4874
+ }
4875
+ }
4607
4876
  this.history.clear();
4608
4877
  this.historyRecorder.resume();
4609
4878
  this.camera.moveTo(state.camera.position.x, state.camera.position.y);
@@ -4613,6 +4882,7 @@ var Viewport = class {
4613
4882
  this.loadState(parseState(json));
4614
4883
  }
4615
4884
  undo() {
4885
+ this.inputHandler.flushPendingHistory();
4616
4886
  this.historyRecorder.pause();
4617
4887
  const result = this.history.undo(this.store);
4618
4888
  this.historyRecorder.resume();
@@ -4620,6 +4890,7 @@ var Viewport = class {
4620
4890
  return result;
4621
4891
  }
4622
4892
  redo() {
4893
+ this.inputHandler.flushPendingHistory();
4623
4894
  this.historyRecorder.pause();
4624
4895
  const result = this.history.redo(this.store);
4625
4896
  this.historyRecorder.resume();
@@ -4649,6 +4920,19 @@ var Viewport = class {
4649
4920
  this.requestRender();
4650
4921
  return el.id;
4651
4922
  }
4923
+ removeLayer(id) {
4924
+ this.historyRecorder.begin();
4925
+ this.layerManager.removeLayer(id);
4926
+ this.historyRecorder.commit();
4927
+ }
4928
+ updateHtmlElement(id, newContent) {
4929
+ const el = this.store.getById(id);
4930
+ if (!el) throw new Error(`Element not found: ${id}`);
4931
+ if (el.type !== "html") throw new Error(`Element ${id} is not an HTML element`);
4932
+ this.domNodeManager.resetHtmlContent(id);
4933
+ this.domNodeManager.storeHtmlContent(id, newContent);
4934
+ this.requestRender();
4935
+ }
4652
4936
  addGrid(input) {
4653
4937
  const existing = this.store.getElementsByType("grid")[0];
4654
4938
  this.historyRecorder.begin();
@@ -4800,17 +5084,21 @@ var Viewport = class {
4800
5084
  };
4801
5085
  onDrop = (e) => {
4802
5086
  e.preventDefault();
5087
+ const rect = this.wrapper.getBoundingClientRect();
5088
+ const screenPos = { x: e.clientX - rect.left, y: e.clientY - rect.top };
5089
+ const worldPos = this.camera.screenToWorld(screenPos);
5090
+ if (this.dropHandler) {
5091
+ this.dropHandler(e, worldPos);
5092
+ return;
5093
+ }
4803
5094
  const files = e.dataTransfer?.files;
4804
5095
  if (!files) return;
4805
- const rect = this.wrapper.getBoundingClientRect();
4806
5096
  for (const file of files) {
4807
5097
  if (!file.type.startsWith("image/")) continue;
4808
5098
  const reader = new FileReader();
4809
5099
  reader.onload = () => {
4810
5100
  const src = reader.result;
4811
5101
  if (typeof src !== "string") return;
4812
- const screenPos = { x: e.clientX - rect.left, y: e.clientY - rect.top };
4813
- const worldPos = this.camera.screenToWorld(screenPos);
4814
5102
  this.addImage(src, worldPos);
4815
5103
  };
4816
5104
  reader.readAsDataURL(file);
@@ -4922,26 +5210,6 @@ var Viewport = class {
4922
5210
  }
4923
5211
  };
4924
5212
 
4925
- // src/elements/bounds.ts
4926
- function getElementsBoundingBox(elements) {
4927
- let minX = Infinity;
4928
- let minY = Infinity;
4929
- let maxX = -Infinity;
4930
- let maxY = -Infinity;
4931
- let found = false;
4932
- for (const el of elements) {
4933
- const b = getElementBounds(el);
4934
- if (!b) continue;
4935
- found = true;
4936
- if (b.x < minX) minX = b.x;
4937
- if (b.y < minY) minY = b.y;
4938
- if (b.x + b.w > maxX) maxX = b.x + b.w;
4939
- if (b.y + b.h > maxY) maxY = b.y + b.h;
4940
- }
4941
- if (!found) return null;
4942
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
4943
- }
4944
-
4945
5213
  // src/tools/hand-tool.ts
4946
5214
  var HandTool = class {
4947
5215
  name = "hand";
@@ -5433,23 +5701,7 @@ var SelectTool = class {
5433
5701
  });
5434
5702
  }
5435
5703
  }
5436
- const movedNonArrowIds = /* @__PURE__ */ new Set();
5437
- for (const id of this._selectedIds) {
5438
- const el = ctx.store.getById(id);
5439
- if (el && el.type !== "arrow") movedNonArrowIds.add(id);
5440
- }
5441
- if (movedNonArrowIds.size > 0) {
5442
- const updatedArrows = /* @__PURE__ */ new Set();
5443
- for (const id of movedNonArrowIds) {
5444
- const boundArrows = findBoundArrows(id, ctx.store);
5445
- for (const ba of boundArrows) {
5446
- if (updatedArrows.has(ba.id)) continue;
5447
- updatedArrows.add(ba.id);
5448
- const updates = updateBoundArrow(ba, ctx.store);
5449
- if (updates) ctx.store.update(ba.id, updates);
5450
- }
5451
- }
5452
- }
5704
+ this.updateArrowsBoundTo(this._selectedIds, ctx);
5453
5705
  ctx.requestRender();
5454
5706
  return;
5455
5707
  }
@@ -5500,6 +5752,49 @@ var SelectTool = class {
5500
5752
  }
5501
5753
  }
5502
5754
  }
5755
+ updateArrowsBoundTo(ids, ctx) {
5756
+ const movedNonArrowIds = /* @__PURE__ */ new Set();
5757
+ for (const id of ids) {
5758
+ const el = ctx.store.getById(id);
5759
+ if (el && el.type !== "arrow") movedNonArrowIds.add(id);
5760
+ }
5761
+ if (movedNonArrowIds.size === 0) return;
5762
+ const updatedArrows = /* @__PURE__ */ new Set();
5763
+ for (const id of movedNonArrowIds) {
5764
+ const boundArrows = findBoundArrows(id, ctx.store);
5765
+ for (const ba of boundArrows) {
5766
+ if (updatedArrows.has(ba.id)) continue;
5767
+ updatedArrows.add(ba.id);
5768
+ const updates = updateBoundArrow(ba, ctx.store);
5769
+ if (updates) ctx.store.update(ba.id, updates);
5770
+ }
5771
+ }
5772
+ }
5773
+ nudgeSelection(dx, dy, ctx) {
5774
+ let moved = false;
5775
+ for (const id of this._selectedIds) {
5776
+ const el = ctx.store.getById(id);
5777
+ if (!el || el.locked) continue;
5778
+ if (el.type === "arrow") {
5779
+ if (el.fromBinding || el.toBinding) continue;
5780
+ ctx.store.update(id, {
5781
+ position: { x: el.position.x + dx, y: el.position.y + dy },
5782
+ from: { x: el.from.x + dx, y: el.from.y + dy },
5783
+ to: { x: el.to.x + dx, y: el.to.y + dy }
5784
+ });
5785
+ } else {
5786
+ ctx.store.update(id, {
5787
+ position: { x: el.position.x + dx, y: el.position.y + dy }
5788
+ });
5789
+ }
5790
+ moved = true;
5791
+ }
5792
+ if (moved) {
5793
+ this.updateArrowsBoundTo(this._selectedIds, ctx);
5794
+ ctx.requestRender();
5795
+ }
5796
+ return moved;
5797
+ }
5503
5798
  updateHoverCursor(world, ctx) {
5504
5799
  const arrowHit = hitTestArrowHandles(world, this._selectedIds, ctx);
5505
5800
  if (arrowHit) {
@@ -5577,11 +5872,7 @@ var SelectTool = class {
5577
5872
  position: { x, y },
5578
5873
  size: { w, h }
5579
5874
  });
5580
- const boundArrows = findBoundArrows(this.mode.elementId, ctx.store);
5581
- for (const ba of boundArrows) {
5582
- const updates = updateBoundArrow(ba, ctx.store);
5583
- if (updates) ctx.store.update(ba.id, updates);
5584
- }
5875
+ this.updateArrowsBoundTo([this.mode.elementId], ctx);
5585
5876
  ctx.requestRender();
5586
5877
  }
5587
5878
  hitTestResizeHandle(world, ctx) {
@@ -6627,48 +6918,8 @@ var TemplateTool = class {
6627
6918
  }
6628
6919
  };
6629
6920
 
6630
- // src/history/layer-commands.ts
6631
- var CreateLayerCommand = class {
6632
- constructor(manager, layer) {
6633
- this.manager = manager;
6634
- this.layer = layer;
6635
- }
6636
- execute(_store) {
6637
- this.manager.addLayerDirect(this.layer);
6638
- }
6639
- undo(_store) {
6640
- this.manager.removeLayerDirect(this.layer.id);
6641
- }
6642
- };
6643
- var RemoveLayerCommand = class {
6644
- constructor(manager, layer) {
6645
- this.manager = manager;
6646
- this.layer = layer;
6647
- }
6648
- execute(_store) {
6649
- this.manager.removeLayerDirect(this.layer.id);
6650
- }
6651
- undo(_store) {
6652
- this.manager.addLayerDirect(this.layer);
6653
- }
6654
- };
6655
- var UpdateLayerCommand = class {
6656
- constructor(manager, layerId, previous, current) {
6657
- this.manager = manager;
6658
- this.layerId = layerId;
6659
- this.previous = previous;
6660
- this.current = current;
6661
- }
6662
- execute(_store) {
6663
- this.manager.updateLayerDirect(this.layerId, { ...this.current });
6664
- }
6665
- undo(_store) {
6666
- this.manager.updateLayerDirect(this.layerId, { ...this.previous });
6667
- }
6668
- };
6669
-
6670
6921
  // src/index.ts
6671
- var VERSION = "0.15.0";
6922
+ var VERSION = "0.17.0";
6672
6923
  export {
6673
6924
  AddElementCommand,
6674
6925
  ArrowTool,