@fieldnotes/core 0.13.0 → 0.14.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
@@ -285,16 +285,24 @@ function sanitizeAttributes(el, tag) {
285
285
 
286
286
  // src/core/state-serializer.ts
287
287
  var CURRENT_VERSION = 2;
288
- function exportState(elements, camera, layers = []) {
289
- return {
288
+ function exportState(elements, camera, layers = [], activeLayerId) {
289
+ const state = {
290
290
  version: CURRENT_VERSION,
291
291
  camera: {
292
292
  position: { ...camera.position },
293
293
  zoom: camera.zoom
294
294
  },
295
- elements: elements.map((el) => structuredClone(el)),
295
+ elements: elements.map((el) => {
296
+ const clone = structuredClone(el);
297
+ if (clone.type === "arrow") {
298
+ delete clone.cachedControlPoint;
299
+ }
300
+ return clone;
301
+ }),
296
302
  layers: layers.map((l) => ({ ...l }))
297
303
  };
304
+ if (activeLayerId) state.activeLayerId = activeLayerId;
305
+ return state;
298
306
  }
299
307
  function parseState(json) {
300
308
  const data = JSON.parse(json);
@@ -452,12 +460,14 @@ var AutoSave = class {
452
460
  this.key = options.key ?? DEFAULT_KEY;
453
461
  this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
454
462
  this.layerManager = options.layerManager;
463
+ this.onError = options.onError;
455
464
  }
456
465
  key;
457
466
  debounceMs;
458
467
  layerManager;
459
468
  timerId = null;
460
469
  unsubscribers = [];
470
+ onError;
461
471
  start() {
462
472
  const schedule = () => this.scheduleSave();
463
473
  this.unsubscribers = [
@@ -505,8 +515,9 @@ var AutoSave = class {
505
515
  const state = exportState(this.store.snapshot(), this.camera, layers);
506
516
  try {
507
517
  localStorage.setItem(this.key, JSON.stringify(state));
508
- } catch {
518
+ } catch (e) {
509
519
  console.warn("Auto-save failed: storage quota exceeded. State too large for localStorage.");
520
+ this.onError?.(e instanceof Error ? e : new Error(String(e)));
510
521
  }
511
522
  }
512
523
  };
@@ -575,6 +586,15 @@ var Camera = class {
575
586
  h: bottomRight.y - topLeft.y
576
587
  };
577
588
  }
589
+ fitToContent(boundingBox, canvasWidth, canvasHeight, padding = 40) {
590
+ if (boundingBox.w === 0 && boundingBox.h === 0) return;
591
+ const scaleX = canvasWidth / (boundingBox.w + 2 * padding);
592
+ const scaleY = canvasHeight / (boundingBox.h + 2 * padding);
593
+ this.z = Math.min(this.maxZoom, Math.max(this.minZoom, Math.min(scaleX, scaleY)));
594
+ this.x = (canvasWidth - boundingBox.w * this.z) / 2 - boundingBox.x * this.z;
595
+ this.y = (canvasHeight - boundingBox.h * this.z) / 2 - boundingBox.y * this.z;
596
+ this.notifyPanAndZoom();
597
+ }
578
598
  toCSSTransform() {
579
599
  return `translate3d(${this.x}px, ${this.y}px, 0) scale(${this.z})`;
580
600
  }
@@ -730,6 +750,67 @@ var Background = class {
730
750
  }
731
751
  };
732
752
 
753
+ // src/canvas/input-filter.ts
754
+ var InputFilter = class _InputFilter {
755
+ activePenId = null;
756
+ pendingTap = null;
757
+ static MIN_MOVE_DISTANCE = 3;
758
+ filterDown(e) {
759
+ if (e.pointerType === "pen") {
760
+ this.activePenId = e.pointerId;
761
+ return { event: e, action: "dispatch" };
762
+ }
763
+ if (e.pointerType === "touch" && this.activePenId !== null) {
764
+ return { event: e, action: "suppress" };
765
+ }
766
+ if (e.pointerType === "touch") {
767
+ this.pendingTap = { pointerId: e.pointerId, x: e.clientX, y: e.clientY };
768
+ return { event: e, action: "defer" };
769
+ }
770
+ return { event: e, action: "dispatch" };
771
+ }
772
+ filterMove(e) {
773
+ if (e.pointerType === "touch" && this.activePenId !== null) {
774
+ return { event: e, action: "suppress" };
775
+ }
776
+ if (this.pendingTap && e.pointerId === this.pendingTap.pointerId) {
777
+ const dx = e.clientX - this.pendingTap.x;
778
+ const dy = e.clientY - this.pendingTap.y;
779
+ if (dx * dx + dy * dy > _InputFilter.MIN_MOVE_DISTANCE * _InputFilter.MIN_MOVE_DISTANCE) {
780
+ this.pendingTap = null;
781
+ return { event: e, action: "dispatch" };
782
+ }
783
+ return { event: e, action: "suppress" };
784
+ }
785
+ return { event: e, action: "dispatch" };
786
+ }
787
+ filterUp(e) {
788
+ if (e.pointerId === this.activePenId) {
789
+ this.activePenId = null;
790
+ return { event: e, action: "dispatch" };
791
+ }
792
+ if (e.pointerType === "touch" && this.activePenId !== null) {
793
+ return { event: e, action: "suppress" };
794
+ }
795
+ if (this.pendingTap && e.pointerId === this.pendingTap.pointerId) {
796
+ const tap = { x: this.pendingTap.x, y: this.pendingTap.y };
797
+ this.pendingTap = null;
798
+ return { event: e, action: "dispatch", pendingTap: tap };
799
+ }
800
+ return { event: e, action: "dispatch" };
801
+ }
802
+ reset() {
803
+ this.activePenId = null;
804
+ this.pendingTap = null;
805
+ }
806
+ };
807
+
808
+ // src/elements/create-id.ts
809
+ var counter = 0;
810
+ function createId(prefix) {
811
+ return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
812
+ }
813
+
733
814
  // src/canvas/input-handler.ts
734
815
  var ZOOM_SENSITIVITY = 1e-3;
735
816
  var MIDDLE_BUTTON = 1;
@@ -755,13 +836,21 @@ var InputHandler = class {
755
836
  historyRecorder;
756
837
  historyStack;
757
838
  isToolActive = false;
839
+ lastPointerEvent = null;
840
+ inputFilter = new InputFilter();
841
+ deferredDown = null;
758
842
  abortController = new AbortController();
843
+ clipboard = [];
844
+ pasteCount = 0;
759
845
  setToolManager(toolManager, toolContext) {
760
846
  this.toolManager = toolManager;
761
847
  this.toolContext = toolContext;
762
848
  }
763
849
  destroy() {
764
850
  this.abortController.abort();
851
+ this.inputFilter.reset();
852
+ this.deferredDown = null;
853
+ this.lastPointerEvent = null;
765
854
  }
766
855
  bind() {
767
856
  const opts = { signal: this.abortController.signal };
@@ -797,11 +886,18 @@ var InputHandler = class {
797
886
  this.lastPointer = { x: e.clientX, y: e.clientY };
798
887
  return;
799
888
  }
800
- if (this.activePointers.size === 1 && e.button === 0) {
889
+ if (this.activePointers.size === 1 && (e.button === 0 || e.pointerType === "touch" || e.pointerType === "pen")) {
890
+ const result = this.inputFilter.filterDown(e);
891
+ if (result.action === "suppress") return;
892
+ if (result.action === "defer") {
893
+ this.deferredDown = e;
894
+ return;
895
+ }
801
896
  this.dispatchToolDown(e);
802
897
  }
803
898
  };
804
899
  onPointerMove = (e) => {
900
+ this.lastPointerEvent = e;
805
901
  if (this.activePointers.has(e.pointerId)) {
806
902
  this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
807
903
  }
@@ -816,13 +912,26 @@ var InputHandler = class {
816
912
  this.camera.pan(dx, dy);
817
913
  return;
818
914
  }
819
- if (this.isToolActive) {
915
+ if (e.pointerType === "pen" && !this.activePointers.has(e.pointerId)) {
916
+ this.dispatchToolHover(e);
917
+ } else if (this.isToolActive) {
820
918
  this.dispatchToolMove(e);
919
+ } else if (this.deferredDown) {
920
+ const result = this.inputFilter.filterMove(e);
921
+ if (result.action === "dispatch") {
922
+ this.dispatchToolDown(this.deferredDown);
923
+ this.deferredDown = null;
924
+ this.dispatchToolMove(e);
925
+ }
821
926
  } else if (this.activePointers.size === 0) {
822
927
  this.dispatchToolHover(e);
823
928
  }
824
929
  };
825
930
  onPointerUp = (e) => {
931
+ try {
932
+ this.element.releasePointerCapture(e.pointerId);
933
+ } catch {
934
+ }
826
935
  this.activePointers.delete(e.pointerId);
827
936
  if (this.activePointers.size < 2) {
828
937
  this.lastPinchDistance = 0;
@@ -830,9 +939,16 @@ var InputHandler = class {
830
939
  if (this.isPanning && this.activePointers.size === 0) {
831
940
  this.isPanning = false;
832
941
  }
942
+ const upResult = this.inputFilter.filterUp(e);
833
943
  if (this.isToolActive) {
834
944
  this.dispatchToolUp(e);
835
945
  this.isToolActive = false;
946
+ } else if (this.deferredDown && upResult.pendingTap) {
947
+ this.dispatchToolDown(this.deferredDown);
948
+ this.dispatchToolUp(e);
949
+ this.deferredDown = null;
950
+ } else {
951
+ this.deferredDown = null;
836
952
  }
837
953
  };
838
954
  onKeyDown = (e) => {
@@ -851,13 +967,30 @@ var InputHandler = class {
851
967
  e.preventDefault();
852
968
  this.handleRedo();
853
969
  }
970
+ if ((e.ctrlKey || e.metaKey) && e.key === "c") {
971
+ e.preventDefault();
972
+ this.handleCopy();
973
+ }
974
+ if ((e.ctrlKey || e.metaKey) && e.key === "v") {
975
+ e.preventDefault();
976
+ this.handlePaste();
977
+ }
854
978
  };
855
979
  onKeyUp = (e) => {
856
980
  if (e.key === " ") {
857
981
  this.spaceHeld = false;
982
+ if (this.activePointers.size === 0) {
983
+ if (this.lastPointerEvent) {
984
+ this.dispatchToolHover(this.lastPointerEvent);
985
+ } else {
986
+ this.toolContext?.setCursor?.("default");
987
+ }
988
+ }
858
989
  }
859
990
  };
860
991
  startPinch() {
992
+ this.inputFilter.reset();
993
+ this.deferredDown = null;
861
994
  this.isPanning = true;
862
995
  const [a, b] = this.getPinchPoints();
863
996
  this.lastPinchDistance = this.distance(a, b);
@@ -897,7 +1030,9 @@ var InputHandler = class {
897
1030
  return {
898
1031
  x: e.clientX - rect.left,
899
1032
  y: e.clientY - rect.top,
900
- pressure: e.pressure
1033
+ pressure: e.pressure,
1034
+ pointerType: e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse",
1035
+ shiftKey: e.shiftKey
901
1036
  };
902
1037
  }
903
1038
  dispatchToolDown(e) {
@@ -950,11 +1085,120 @@ var InputHandler = class {
950
1085
  this.historyRecorder?.resume();
951
1086
  this.toolContext.requestRender();
952
1087
  }
1088
+ handleCopy() {
1089
+ if (!this.toolManager || !this.toolContext || this.isToolActive) return;
1090
+ const tool = this.toolManager.activeTool;
1091
+ if (tool?.name !== "select") return;
1092
+ const selectTool = tool;
1093
+ const ids = selectTool.selectedIds;
1094
+ if (ids.length === 0) return;
1095
+ this.clipboard = [];
1096
+ for (const id of ids) {
1097
+ const el = this.toolContext.store.getById(id);
1098
+ if (el) this.clipboard.push(structuredClone(el));
1099
+ }
1100
+ this.pasteCount = 0;
1101
+ }
1102
+ handlePaste() {
1103
+ if (!this.toolManager || !this.toolContext || this.clipboard.length === 0 || this.isToolActive)
1104
+ return;
1105
+ const tool = this.toolManager.activeTool;
1106
+ if (tool?.name !== "select") return;
1107
+ const selectTool = tool;
1108
+ this.pasteCount++;
1109
+ const offset = this.pasteCount * 20;
1110
+ const idMap = /* @__PURE__ */ new Map();
1111
+ for (const el of this.clipboard) {
1112
+ idMap.set(el.id, createId(el.type));
1113
+ }
1114
+ const newIds = [];
1115
+ this.historyRecorder?.begin();
1116
+ for (const el of this.clipboard) {
1117
+ const clone = structuredClone(el);
1118
+ const newId = idMap.get(el.id);
1119
+ if (!newId) continue;
1120
+ clone.id = newId;
1121
+ clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
1122
+ if (clone.type === "arrow") {
1123
+ const arrow = clone;
1124
+ arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
1125
+ arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
1126
+ delete arrow.cachedControlPoint;
1127
+ if (arrow.fromBinding) {
1128
+ const newTarget = idMap.get(arrow.fromBinding.elementId);
1129
+ if (newTarget) {
1130
+ arrow.fromBinding = { elementId: newTarget };
1131
+ } else {
1132
+ delete arrow.fromBinding;
1133
+ }
1134
+ }
1135
+ if (arrow.toBinding) {
1136
+ const newTarget = idMap.get(arrow.toBinding.elementId);
1137
+ if (newTarget) {
1138
+ arrow.toBinding = { elementId: newTarget };
1139
+ } else {
1140
+ delete arrow.toBinding;
1141
+ }
1142
+ }
1143
+ }
1144
+ if (this.toolContext.activeLayerId) {
1145
+ clone.layerId = this.toolContext.activeLayerId;
1146
+ }
1147
+ this.toolContext.store.add(clone);
1148
+ newIds.push(clone.id);
1149
+ }
1150
+ this.historyRecorder?.commit();
1151
+ selectTool.setSelection(newIds);
1152
+ this.toolContext.requestRender();
1153
+ }
953
1154
  cancelToolIfActive(e) {
954
1155
  if (this.isToolActive) {
955
1156
  this.dispatchToolUp(e);
956
1157
  this.isToolActive = false;
957
1158
  }
1159
+ this.deferredDown = null;
1160
+ }
1161
+ };
1162
+
1163
+ // src/canvas/double-tap-detector.ts
1164
+ var DEFAULT_TIMEOUT = 300;
1165
+ var DEFAULT_MAX_DISTANCE = 20;
1166
+ var DoubleTapDetector = class {
1167
+ timeout;
1168
+ maxDistance;
1169
+ lastTapTime = 0;
1170
+ lastTapX = 0;
1171
+ lastTapY = 0;
1172
+ hasPendingTap = false;
1173
+ constructor(options) {
1174
+ this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
1175
+ this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
1176
+ }
1177
+ feed(e) {
1178
+ const now = Date.now();
1179
+ const x = e.clientX;
1180
+ const y = e.clientY;
1181
+ if (this.hasPendingTap) {
1182
+ const elapsed = now - this.lastTapTime;
1183
+ const dx = x - this.lastTapX;
1184
+ const dy = y - this.lastTapY;
1185
+ const dist = Math.sqrt(dx * dx + dy * dy);
1186
+ if (elapsed <= this.timeout && dist <= this.maxDistance) {
1187
+ this.reset();
1188
+ return true;
1189
+ }
1190
+ }
1191
+ this.lastTapTime = now;
1192
+ this.lastTapX = x;
1193
+ this.lastTapY = y;
1194
+ this.hasPendingTap = true;
1195
+ return false;
1196
+ }
1197
+ reset() {
1198
+ this.hasPendingTap = false;
1199
+ this.lastTapTime = 0;
1200
+ this.lastTapX = 0;
1201
+ this.lastTapY = 0;
958
1202
  }
959
1203
  };
960
1204
 
@@ -1202,19 +1446,23 @@ var ElementStore = class {
1202
1446
  bus = new EventBus();
1203
1447
  layerOrderMap = /* @__PURE__ */ new Map();
1204
1448
  spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
1449
+ sortedCache = null;
1205
1450
  get count() {
1206
1451
  return this.elements.size;
1207
1452
  }
1208
1453
  setLayerOrder(order) {
1209
1454
  this.layerOrderMap = new Map(order);
1455
+ this.sortedCache = null;
1210
1456
  }
1211
1457
  getAll() {
1212
- return [...this.elements.values()].sort((a, b) => {
1458
+ if (this.sortedCache) return this.sortedCache;
1459
+ this.sortedCache = [...this.elements.values()].sort((a, b) => {
1213
1460
  const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
1214
1461
  const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
1215
1462
  if (layerA !== layerB) return layerA - layerB;
1216
1463
  return a.zIndex - b.zIndex;
1217
1464
  });
1465
+ return this.sortedCache;
1218
1466
  }
1219
1467
  getById(id) {
1220
1468
  return this.elements.get(id);
@@ -1225,6 +1473,7 @@ var ElementStore = class {
1225
1473
  );
1226
1474
  }
1227
1475
  add(element) {
1476
+ this.sortedCache = null;
1228
1477
  this.elements.set(element.id, element);
1229
1478
  const bounds = getElementBounds(element);
1230
1479
  if (bounds) this.spatialIndex.insert(element.id, bounds);
@@ -1233,11 +1482,15 @@ var ElementStore = class {
1233
1482
  update(id, partial) {
1234
1483
  const existing = this.elements.get(id);
1235
1484
  if (!existing) return;
1485
+ this.sortedCache = null;
1236
1486
  const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
1237
1487
  if (updated.type === "arrow") {
1238
1488
  const arrow = updated;
1239
1489
  arrow.cachedControlPoint = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1240
1490
  }
1491
+ if (updated.type === "note" && "text" in partial) {
1492
+ updated.text = sanitizeNoteHtml(updated.text);
1493
+ }
1241
1494
  this.elements.set(id, updated);
1242
1495
  const newBounds = getElementBounds(updated);
1243
1496
  if (newBounds) {
@@ -1248,11 +1501,13 @@ var ElementStore = class {
1248
1501
  remove(id) {
1249
1502
  const element = this.elements.get(id);
1250
1503
  if (!element) return;
1504
+ this.sortedCache = null;
1251
1505
  this.elements.delete(id);
1252
1506
  this.spatialIndex.remove(id);
1253
1507
  this.bus.emit("remove", element);
1254
1508
  }
1255
1509
  clear() {
1510
+ this.sortedCache = null;
1256
1511
  this.elements.clear();
1257
1512
  this.spatialIndex.clear();
1258
1513
  this.bus.emit("clear", null);
@@ -1261,6 +1516,7 @@ var ElementStore = class {
1261
1516
  return this.getAll().map((el) => ({ ...el }));
1262
1517
  }
1263
1518
  loadSnapshot(elements) {
1519
+ this.sortedCache = null;
1264
1520
  this.elements.clear();
1265
1521
  this.spatialIndex.clear();
1266
1522
  for (const el of elements) {
@@ -1268,6 +1524,10 @@ var ElementStore = class {
1268
1524
  const bounds = getElementBounds(el);
1269
1525
  if (bounds) this.spatialIndex.insert(el.id, bounds);
1270
1526
  }
1527
+ this.bus.emit("clear", null);
1528
+ for (const el of elements) {
1529
+ this.bus.emit("add", el);
1530
+ }
1271
1531
  }
1272
1532
  queryRect(rect) {
1273
1533
  const ids = this.spatialIndex.query(rect);
@@ -2317,12 +2577,6 @@ var ElementRenderer = class {
2317
2577
  }
2318
2578
  };
2319
2579
 
2320
- // src/elements/create-id.ts
2321
- var counter = 0;
2322
- function createId(prefix) {
2323
- return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
2324
- }
2325
-
2326
2580
  // src/elements/element-factory.ts
2327
2581
  var DEFAULT_NOTE_FONT_SIZE = 18;
2328
2582
  function createStroke(input) {
@@ -2348,7 +2602,7 @@ function createNote(input) {
2348
2602
  locked: input.locked ?? false,
2349
2603
  layerId: input.layerId ?? "",
2350
2604
  size: input.size ?? { w: 200, h: 100 },
2351
- text: input.text ?? "",
2605
+ text: sanitizeNoteHtml(input.text ?? ""),
2352
2606
  backgroundColor: input.backgroundColor ?? "#ffeb3b",
2353
2607
  textColor: input.textColor ?? "#000000",
2354
2608
  fontSize: input.fontSize ?? DEFAULT_NOTE_FONT_SIZE
@@ -2397,6 +2651,7 @@ function createHtmlElement(input) {
2397
2651
  size: input.size
2398
2652
  };
2399
2653
  if (input.domId) el.domId = input.domId;
2654
+ if (input.interactive) el.interactive = input.interactive;
2400
2655
  return el;
2401
2656
  }
2402
2657
  function createShape(input) {
@@ -2509,7 +2764,7 @@ function getActiveFormats() {
2509
2764
  }
2510
2765
 
2511
2766
  // src/elements/note-toolbar.ts
2512
- var TOOLBAR_HEIGHT = 32;
2767
+ var TOOLBAR_HEIGHT = 52;
2513
2768
  var TOOLBAR_GAP = 4;
2514
2769
  var FORMAT_BUTTONS = [
2515
2770
  { label: "B", format: "bold", command: "bold" },
@@ -2596,9 +2851,9 @@ var NoteToolbar = class {
2596
2851
  fontWeight: config.format === "bold" ? "bold" : "normal",
2597
2852
  fontStyle: config.format === "italic" ? "italic" : "normal",
2598
2853
  textDecoration: config.format === "underline" ? "underline" : config.format === "strikethrough" ? "line-through" : "none",
2599
- minWidth: "24px",
2600
- height: "24px",
2601
- lineHeight: "24px"
2854
+ minWidth: "44px",
2855
+ height: "44px",
2856
+ lineHeight: "44px"
2602
2857
  });
2603
2858
  btn.addEventListener("pointerdown", (e) => {
2604
2859
  e.preventDefault();
@@ -2616,7 +2871,7 @@ var NoteToolbar = class {
2616
2871
  cursor: "pointer",
2617
2872
  padding: "2px",
2618
2873
  fontSize: "12px",
2619
- height: "24px",
2874
+ height: "44px",
2620
2875
  marginLeft: "4px"
2621
2876
  });
2622
2877
  for (const preset of this.fontSizePresets) {
@@ -3614,10 +3869,13 @@ var DomNodeManager = class {
3614
3869
  wordWrap: "break-word"
3615
3870
  });
3616
3871
  node.innerHTML = element.text || "";
3617
- node.addEventListener("dblclick", (e) => {
3618
- e.stopPropagation();
3619
- const id = node.dataset["elementId"];
3620
- if (id) this.onEditRequest(id);
3872
+ const detector = new DoubleTapDetector();
3873
+ node.addEventListener("pointerup", (e) => {
3874
+ if (detector.feed(e)) {
3875
+ e.stopPropagation();
3876
+ const id = node.dataset["elementId"];
3877
+ if (id) this.onEditRequest(id);
3878
+ }
3621
3879
  });
3622
3880
  }
3623
3881
  if (!this.isEditingElement(element.id)) {
@@ -3630,15 +3888,19 @@ var DomNodeManager = class {
3630
3888
  node.style.fontSize = `${element.fontSize ?? DEFAULT_NOTE_FONT_SIZE}px`;
3631
3889
  }
3632
3890
  }
3633
- if (element.type === "html" && !node.dataset["initialized"]) {
3634
- const content = this.htmlContent.get(element.id);
3635
- if (content) {
3636
- node.dataset["initialized"] = "true";
3637
- Object.assign(node.style, {
3638
- overflow: "hidden",
3639
- pointerEvents: "none"
3640
- });
3641
- node.appendChild(content);
3891
+ if (element.type === "html") {
3892
+ if (!node.dataset["initialized"]) {
3893
+ const content = this.htmlContent.get(element.id);
3894
+ if (content) {
3895
+ node.dataset["initialized"] = "true";
3896
+ Object.assign(node.style, {
3897
+ overflow: "hidden",
3898
+ pointerEvents: element.interactive ? "auto" : "none"
3899
+ });
3900
+ node.appendChild(content);
3901
+ }
3902
+ } else {
3903
+ node.style.pointerEvents = element.interactive ? "auto" : "none";
3642
3904
  }
3643
3905
  }
3644
3906
  if (element.type === "text") {
@@ -3660,10 +3922,13 @@ var DomNodeManager = class {
3660
3922
  lineHeight: "1.4"
3661
3923
  });
3662
3924
  node.textContent = element.text || "";
3663
- node.addEventListener("dblclick", (e) => {
3664
- e.stopPropagation();
3665
- const id = node.dataset["elementId"];
3666
- if (id) this.onEditRequest(id);
3925
+ const detector = new DoubleTapDetector();
3926
+ node.addEventListener("pointerup", (e) => {
3927
+ if (detector.feed(e)) {
3928
+ e.stopPropagation();
3929
+ const id = node.dataset["elementId"];
3930
+ if (id) this.onEditRequest(id);
3931
+ }
3667
3932
  });
3668
3933
  }
3669
3934
  if (!this.isEditingElement(element.id)) {
@@ -4152,7 +4417,8 @@ var Viewport = class {
4152
4417
  this.toolContext.activeLayerId = this.layerManager.activeLayerId;
4153
4418
  this.requestRender();
4154
4419
  });
4155
- this.wrapper.addEventListener("dblclick", this.onDblClick);
4420
+ this.wrapper.addEventListener("pointerdown", this.onTapDown);
4421
+ this.wrapper.addEventListener("pointerup", this.onDoubleTap);
4156
4422
  this.wrapper.addEventListener("dragover", this.onDragOver);
4157
4423
  this.wrapper.addEventListener("drop", this.onDrop);
4158
4424
  this.observeResize();
@@ -4183,6 +4449,9 @@ var Viewport = class {
4183
4449
  domNodeManager;
4184
4450
  interactMode;
4185
4451
  gridChangeListeners = /* @__PURE__ */ new Set();
4452
+ doubleTapDetector = new DoubleTapDetector();
4453
+ tapDownX = 0;
4454
+ tapDownY = 0;
4186
4455
  get ctx() {
4187
4456
  return this.canvasEl.getContext("2d");
4188
4457
  }
@@ -4197,7 +4466,12 @@ var Viewport = class {
4197
4466
  this.renderLoop.requestRender();
4198
4467
  }
4199
4468
  exportState() {
4200
- return exportState(this.store.snapshot(), this.camera, this.layerManager.snapshot());
4469
+ return exportState(
4470
+ this.store.snapshot(),
4471
+ this.camera,
4472
+ this.layerManager.snapshot(),
4473
+ this.layerManager.activeLayerId
4474
+ );
4201
4475
  }
4202
4476
  exportJSON() {
4203
4477
  return JSON.stringify(this.exportState());
@@ -4213,6 +4487,9 @@ var Viewport = class {
4213
4487
  if (state.layers && state.layers.length > 0) {
4214
4488
  this.layerManager.loadSnapshot(state.layers);
4215
4489
  }
4490
+ if (state.activeLayerId) {
4491
+ this.layerManager.setActiveLayer(state.activeLayerId);
4492
+ }
4216
4493
  this.domNodeManager.reattachHtmlContent(this.store);
4217
4494
  this.history.clear();
4218
4495
  this.historyRecorder.resume();
@@ -4320,7 +4597,8 @@ var Viewport = class {
4320
4597
  this.interactMode.destroy();
4321
4598
  this.noteEditor.destroy(this.store);
4322
4599
  this.historyRecorder.destroy();
4323
- this.wrapper.removeEventListener("dblclick", this.onDblClick);
4600
+ this.wrapper.removeEventListener("pointerdown", this.onTapDown);
4601
+ this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
4324
4602
  this.wrapper.removeEventListener("dragover", this.onDragOver);
4325
4603
  this.wrapper.removeEventListener("drop", this.onDrop);
4326
4604
  this.inputHandler.destroy();
@@ -4358,7 +4636,17 @@ var Viewport = class {
4358
4636
  }
4359
4637
  }
4360
4638
  }
4361
- onDblClick = (e) => {
4639
+ onTapDown = (e) => {
4640
+ this.tapDownX = e.clientX;
4641
+ this.tapDownY = e.clientY;
4642
+ };
4643
+ onDoubleTap = (e) => {
4644
+ const dx = e.clientX - this.tapDownX;
4645
+ const dy = e.clientY - this.tapDownY;
4646
+ const moved = Math.sqrt(dx * dx + dy * dy);
4647
+ if (moved > 10) return;
4648
+ if (!this.doubleTapDetector.feed(e)) return;
4649
+ if (typeof document.elementFromPoint !== "function") return;
4362
4650
  const el = document.elementFromPoint(e.clientX, e.clientY);
4363
4651
  const nodeEl = el?.closest("[data-element-id]");
4364
4652
  if (nodeEl) {
@@ -4455,7 +4743,10 @@ var Viewport = class {
4455
4743
  position: "relative",
4456
4744
  width: "100%",
4457
4745
  height: "100%",
4458
- overflow: "hidden"
4746
+ overflow: "hidden",
4747
+ overscrollBehavior: "none",
4748
+ userSelect: "none",
4749
+ webkitUserSelect: "none"
4459
4750
  });
4460
4751
  return el;
4461
4752
  }
@@ -4518,6 +4809,26 @@ var Viewport = class {
4518
4809
  }
4519
4810
  };
4520
4811
 
4812
+ // src/elements/bounds.ts
4813
+ function getElementsBoundingBox(elements) {
4814
+ let minX = Infinity;
4815
+ let minY = Infinity;
4816
+ let maxX = -Infinity;
4817
+ let maxY = -Infinity;
4818
+ let found = false;
4819
+ for (const el of elements) {
4820
+ const b = getElementBounds(el);
4821
+ if (!b) continue;
4822
+ found = true;
4823
+ if (b.x < minX) minX = b.x;
4824
+ if (b.y < minY) minY = b.y;
4825
+ if (b.x + b.w > maxX) maxX = b.x + b.w;
4826
+ if (b.y + b.h > maxY) maxY = b.y + b.h;
4827
+ }
4828
+ if (!found) return null;
4829
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
4830
+ }
4831
+
4521
4832
  // src/tools/hand-tool.ts
4522
4833
  var HandTool = class {
4523
4834
  name = "hand";
@@ -4870,9 +5181,15 @@ var SelectTool = class {
4870
5181
  lastWorld = { x: 0, y: 0 };
4871
5182
  currentWorld = { x: 0, y: 0 };
4872
5183
  ctx = null;
5184
+ pendingSingleSelectId = null;
5185
+ hasDragged = false;
4873
5186
  get selectedIds() {
4874
5187
  return [...this._selectedIds];
4875
5188
  }
5189
+ setSelection(ids) {
5190
+ this._selectedIds = ids;
5191
+ this.ctx?.requestRender();
5192
+ }
4876
5193
  get isMarqueeActive() {
4877
5194
  return this.mode.type === "marquee";
4878
5195
  }
@@ -4921,13 +5238,27 @@ var SelectTool = class {
4921
5238
  return;
4922
5239
  }
4923
5240
  }
5241
+ this.pendingSingleSelectId = null;
5242
+ this.hasDragged = false;
4924
5243
  const hit = this.hitTest(world, ctx);
4925
5244
  if (hit) {
4926
5245
  const alreadySelected = this._selectedIds.includes(hit.id);
4927
- if (!alreadySelected) {
4928
- this._selectedIds = [hit.id];
5246
+ if (state.shiftKey) {
5247
+ if (alreadySelected) {
5248
+ this._selectedIds = this._selectedIds.filter((id) => id !== hit.id);
5249
+ this.mode = { type: "idle" };
5250
+ } else {
5251
+ this._selectedIds = [...this._selectedIds, hit.id];
5252
+ this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
5253
+ }
5254
+ } else {
5255
+ if (!alreadySelected) {
5256
+ this._selectedIds = [hit.id];
5257
+ } else if (this._selectedIds.length > 1) {
5258
+ this.pendingSingleSelectId = hit.id;
5259
+ }
5260
+ this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
4929
5261
  }
4930
- this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
4931
5262
  } else {
4932
5263
  this._selectedIds = [];
4933
5264
  this.mode = { type: "marquee", start: world };
@@ -4953,6 +5284,7 @@ var SelectTool = class {
4953
5284
  return;
4954
5285
  }
4955
5286
  if (this.mode.type === "dragging" && this._selectedIds.length > 0) {
5287
+ this.hasDragged = true;
4956
5288
  ctx.setCursor?.("move");
4957
5289
  const snapped = this.snap(world, ctx);
4958
5290
  const dx = snapped.x - this.lastWorld.x;
@@ -5021,6 +5353,11 @@ var SelectTool = class {
5021
5353
  }
5022
5354
  ctx.requestRender();
5023
5355
  }
5356
+ if (!this.hasDragged && this.pendingSingleSelectId !== null) {
5357
+ this._selectedIds = [this.pendingSingleSelectId];
5358
+ }
5359
+ this.pendingSingleSelectId = null;
5360
+ this.hasDragged = false;
5024
5361
  this.mode = { type: "idle" };
5025
5362
  ctx.setCursor?.("default");
5026
5363
  }
@@ -6201,7 +6538,7 @@ var UpdateLayerCommand = class {
6201
6538
  };
6202
6539
 
6203
6540
  // src/index.ts
6204
- var VERSION = "0.11.0";
6541
+ var VERSION = "0.14.0";
6205
6542
  export {
6206
6543
  AddElementCommand,
6207
6544
  ArrowTool,
@@ -6212,6 +6549,7 @@ export {
6212
6549
  CreateLayerCommand,
6213
6550
  DEFAULT_FONT_SIZE_PRESETS,
6214
6551
  DEFAULT_NOTE_FONT_SIZE,
6552
+ DoubleTapDetector,
6215
6553
  ElementRenderer,
6216
6554
  ElementStore,
6217
6555
  EraserTool,
@@ -6220,6 +6558,7 @@ export {
6220
6558
  HistoryRecorder,
6221
6559
  HistoryStack,
6222
6560
  ImageTool,
6561
+ InputFilter,
6223
6562
  InputHandler,
6224
6563
  LayerManager,
6225
6564
  MeasureTool,
@@ -6265,6 +6604,7 @@ export {
6265
6604
  getEdgeIntersection,
6266
6605
  getElementBounds,
6267
6606
  getElementCenter,
6607
+ getElementsBoundingBox,
6268
6608
  getHexCellsInCone,
6269
6609
  getHexCellsInLine,
6270
6610
  getHexCellsInRadius,