@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.cjs CHANGED
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  CreateLayerCommand: () => CreateLayerCommand,
30
30
  DEFAULT_FONT_SIZE_PRESETS: () => DEFAULT_FONT_SIZE_PRESETS,
31
31
  DEFAULT_NOTE_FONT_SIZE: () => DEFAULT_NOTE_FONT_SIZE,
32
+ DoubleTapDetector: () => DoubleTapDetector,
32
33
  ElementRenderer: () => ElementRenderer,
33
34
  ElementStore: () => ElementStore,
34
35
  EraserTool: () => EraserTool,
@@ -37,6 +38,7 @@ __export(index_exports, {
37
38
  HistoryRecorder: () => HistoryRecorder,
38
39
  HistoryStack: () => HistoryStack,
39
40
  ImageTool: () => ImageTool,
41
+ InputFilter: () => InputFilter,
40
42
  InputHandler: () => InputHandler,
41
43
  LayerManager: () => LayerManager,
42
44
  MeasureTool: () => MeasureTool,
@@ -82,6 +84,7 @@ __export(index_exports, {
82
84
  getEdgeIntersection: () => getEdgeIntersection,
83
85
  getElementBounds: () => getElementBounds,
84
86
  getElementCenter: () => getElementCenter,
87
+ getElementsBoundingBox: () => getElementsBoundingBox,
85
88
  getHexCellsInCone: () => getHexCellsInCone,
86
89
  getHexCellsInLine: () => getHexCellsInLine,
87
90
  getHexCellsInRadius: () => getHexCellsInRadius,
@@ -391,16 +394,24 @@ function sanitizeAttributes(el, tag) {
391
394
 
392
395
  // src/core/state-serializer.ts
393
396
  var CURRENT_VERSION = 2;
394
- function exportState(elements, camera, layers = []) {
395
- return {
397
+ function exportState(elements, camera, layers = [], activeLayerId) {
398
+ const state = {
396
399
  version: CURRENT_VERSION,
397
400
  camera: {
398
401
  position: { ...camera.position },
399
402
  zoom: camera.zoom
400
403
  },
401
- elements: elements.map((el) => structuredClone(el)),
404
+ elements: elements.map((el) => {
405
+ const clone = structuredClone(el);
406
+ if (clone.type === "arrow") {
407
+ delete clone.cachedControlPoint;
408
+ }
409
+ return clone;
410
+ }),
402
411
  layers: layers.map((l) => ({ ...l }))
403
412
  };
413
+ if (activeLayerId) state.activeLayerId = activeLayerId;
414
+ return state;
404
415
  }
405
416
  function parseState(json) {
406
417
  const data = JSON.parse(json);
@@ -558,12 +569,14 @@ var AutoSave = class {
558
569
  this.key = options.key ?? DEFAULT_KEY;
559
570
  this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
560
571
  this.layerManager = options.layerManager;
572
+ this.onError = options.onError;
561
573
  }
562
574
  key;
563
575
  debounceMs;
564
576
  layerManager;
565
577
  timerId = null;
566
578
  unsubscribers = [];
579
+ onError;
567
580
  start() {
568
581
  const schedule = () => this.scheduleSave();
569
582
  this.unsubscribers = [
@@ -611,8 +624,9 @@ var AutoSave = class {
611
624
  const state = exportState(this.store.snapshot(), this.camera, layers);
612
625
  try {
613
626
  localStorage.setItem(this.key, JSON.stringify(state));
614
- } catch {
627
+ } catch (e) {
615
628
  console.warn("Auto-save failed: storage quota exceeded. State too large for localStorage.");
629
+ this.onError?.(e instanceof Error ? e : new Error(String(e)));
616
630
  }
617
631
  }
618
632
  };
@@ -681,6 +695,15 @@ var Camera = class {
681
695
  h: bottomRight.y - topLeft.y
682
696
  };
683
697
  }
698
+ fitToContent(boundingBox, canvasWidth, canvasHeight, padding = 40) {
699
+ if (boundingBox.w === 0 && boundingBox.h === 0) return;
700
+ const scaleX = canvasWidth / (boundingBox.w + 2 * padding);
701
+ const scaleY = canvasHeight / (boundingBox.h + 2 * padding);
702
+ this.z = Math.min(this.maxZoom, Math.max(this.minZoom, Math.min(scaleX, scaleY)));
703
+ this.x = (canvasWidth - boundingBox.w * this.z) / 2 - boundingBox.x * this.z;
704
+ this.y = (canvasHeight - boundingBox.h * this.z) / 2 - boundingBox.y * this.z;
705
+ this.notifyPanAndZoom();
706
+ }
684
707
  toCSSTransform() {
685
708
  return `translate3d(${this.x}px, ${this.y}px, 0) scale(${this.z})`;
686
709
  }
@@ -836,6 +859,67 @@ var Background = class {
836
859
  }
837
860
  };
838
861
 
862
+ // src/canvas/input-filter.ts
863
+ var InputFilter = class _InputFilter {
864
+ activePenId = null;
865
+ pendingTap = null;
866
+ static MIN_MOVE_DISTANCE = 3;
867
+ filterDown(e) {
868
+ if (e.pointerType === "pen") {
869
+ this.activePenId = e.pointerId;
870
+ return { event: e, action: "dispatch" };
871
+ }
872
+ if (e.pointerType === "touch" && this.activePenId !== null) {
873
+ return { event: e, action: "suppress" };
874
+ }
875
+ if (e.pointerType === "touch") {
876
+ this.pendingTap = { pointerId: e.pointerId, x: e.clientX, y: e.clientY };
877
+ return { event: e, action: "defer" };
878
+ }
879
+ return { event: e, action: "dispatch" };
880
+ }
881
+ filterMove(e) {
882
+ if (e.pointerType === "touch" && this.activePenId !== null) {
883
+ return { event: e, action: "suppress" };
884
+ }
885
+ if (this.pendingTap && e.pointerId === this.pendingTap.pointerId) {
886
+ const dx = e.clientX - this.pendingTap.x;
887
+ const dy = e.clientY - this.pendingTap.y;
888
+ if (dx * dx + dy * dy > _InputFilter.MIN_MOVE_DISTANCE * _InputFilter.MIN_MOVE_DISTANCE) {
889
+ this.pendingTap = null;
890
+ return { event: e, action: "dispatch" };
891
+ }
892
+ return { event: e, action: "suppress" };
893
+ }
894
+ return { event: e, action: "dispatch" };
895
+ }
896
+ filterUp(e) {
897
+ if (e.pointerId === this.activePenId) {
898
+ this.activePenId = null;
899
+ return { event: e, action: "dispatch" };
900
+ }
901
+ if (e.pointerType === "touch" && this.activePenId !== null) {
902
+ return { event: e, action: "suppress" };
903
+ }
904
+ if (this.pendingTap && e.pointerId === this.pendingTap.pointerId) {
905
+ const tap = { x: this.pendingTap.x, y: this.pendingTap.y };
906
+ this.pendingTap = null;
907
+ return { event: e, action: "dispatch", pendingTap: tap };
908
+ }
909
+ return { event: e, action: "dispatch" };
910
+ }
911
+ reset() {
912
+ this.activePenId = null;
913
+ this.pendingTap = null;
914
+ }
915
+ };
916
+
917
+ // src/elements/create-id.ts
918
+ var counter = 0;
919
+ function createId(prefix) {
920
+ return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
921
+ }
922
+
839
923
  // src/canvas/input-handler.ts
840
924
  var ZOOM_SENSITIVITY = 1e-3;
841
925
  var MIDDLE_BUTTON = 1;
@@ -861,13 +945,21 @@ var InputHandler = class {
861
945
  historyRecorder;
862
946
  historyStack;
863
947
  isToolActive = false;
948
+ lastPointerEvent = null;
949
+ inputFilter = new InputFilter();
950
+ deferredDown = null;
864
951
  abortController = new AbortController();
952
+ clipboard = [];
953
+ pasteCount = 0;
865
954
  setToolManager(toolManager, toolContext) {
866
955
  this.toolManager = toolManager;
867
956
  this.toolContext = toolContext;
868
957
  }
869
958
  destroy() {
870
959
  this.abortController.abort();
960
+ this.inputFilter.reset();
961
+ this.deferredDown = null;
962
+ this.lastPointerEvent = null;
871
963
  }
872
964
  bind() {
873
965
  const opts = { signal: this.abortController.signal };
@@ -903,11 +995,18 @@ var InputHandler = class {
903
995
  this.lastPointer = { x: e.clientX, y: e.clientY };
904
996
  return;
905
997
  }
906
- if (this.activePointers.size === 1 && e.button === 0) {
998
+ if (this.activePointers.size === 1 && (e.button === 0 || e.pointerType === "touch" || e.pointerType === "pen")) {
999
+ const result = this.inputFilter.filterDown(e);
1000
+ if (result.action === "suppress") return;
1001
+ if (result.action === "defer") {
1002
+ this.deferredDown = e;
1003
+ return;
1004
+ }
907
1005
  this.dispatchToolDown(e);
908
1006
  }
909
1007
  };
910
1008
  onPointerMove = (e) => {
1009
+ this.lastPointerEvent = e;
911
1010
  if (this.activePointers.has(e.pointerId)) {
912
1011
  this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
913
1012
  }
@@ -922,13 +1021,26 @@ var InputHandler = class {
922
1021
  this.camera.pan(dx, dy);
923
1022
  return;
924
1023
  }
925
- if (this.isToolActive) {
1024
+ if (e.pointerType === "pen" && !this.activePointers.has(e.pointerId)) {
1025
+ this.dispatchToolHover(e);
1026
+ } else if (this.isToolActive) {
926
1027
  this.dispatchToolMove(e);
1028
+ } else if (this.deferredDown) {
1029
+ const result = this.inputFilter.filterMove(e);
1030
+ if (result.action === "dispatch") {
1031
+ this.dispatchToolDown(this.deferredDown);
1032
+ this.deferredDown = null;
1033
+ this.dispatchToolMove(e);
1034
+ }
927
1035
  } else if (this.activePointers.size === 0) {
928
1036
  this.dispatchToolHover(e);
929
1037
  }
930
1038
  };
931
1039
  onPointerUp = (e) => {
1040
+ try {
1041
+ this.element.releasePointerCapture(e.pointerId);
1042
+ } catch {
1043
+ }
932
1044
  this.activePointers.delete(e.pointerId);
933
1045
  if (this.activePointers.size < 2) {
934
1046
  this.lastPinchDistance = 0;
@@ -936,9 +1048,16 @@ var InputHandler = class {
936
1048
  if (this.isPanning && this.activePointers.size === 0) {
937
1049
  this.isPanning = false;
938
1050
  }
1051
+ const upResult = this.inputFilter.filterUp(e);
939
1052
  if (this.isToolActive) {
940
1053
  this.dispatchToolUp(e);
941
1054
  this.isToolActive = false;
1055
+ } else if (this.deferredDown && upResult.pendingTap) {
1056
+ this.dispatchToolDown(this.deferredDown);
1057
+ this.dispatchToolUp(e);
1058
+ this.deferredDown = null;
1059
+ } else {
1060
+ this.deferredDown = null;
942
1061
  }
943
1062
  };
944
1063
  onKeyDown = (e) => {
@@ -957,13 +1076,30 @@ var InputHandler = class {
957
1076
  e.preventDefault();
958
1077
  this.handleRedo();
959
1078
  }
1079
+ if ((e.ctrlKey || e.metaKey) && e.key === "c") {
1080
+ e.preventDefault();
1081
+ this.handleCopy();
1082
+ }
1083
+ if ((e.ctrlKey || e.metaKey) && e.key === "v") {
1084
+ e.preventDefault();
1085
+ this.handlePaste();
1086
+ }
960
1087
  };
961
1088
  onKeyUp = (e) => {
962
1089
  if (e.key === " ") {
963
1090
  this.spaceHeld = false;
1091
+ if (this.activePointers.size === 0) {
1092
+ if (this.lastPointerEvent) {
1093
+ this.dispatchToolHover(this.lastPointerEvent);
1094
+ } else {
1095
+ this.toolContext?.setCursor?.("default");
1096
+ }
1097
+ }
964
1098
  }
965
1099
  };
966
1100
  startPinch() {
1101
+ this.inputFilter.reset();
1102
+ this.deferredDown = null;
967
1103
  this.isPanning = true;
968
1104
  const [a, b] = this.getPinchPoints();
969
1105
  this.lastPinchDistance = this.distance(a, b);
@@ -1003,7 +1139,9 @@ var InputHandler = class {
1003
1139
  return {
1004
1140
  x: e.clientX - rect.left,
1005
1141
  y: e.clientY - rect.top,
1006
- pressure: e.pressure
1142
+ pressure: e.pressure,
1143
+ pointerType: e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse",
1144
+ shiftKey: e.shiftKey
1007
1145
  };
1008
1146
  }
1009
1147
  dispatchToolDown(e) {
@@ -1056,11 +1194,120 @@ var InputHandler = class {
1056
1194
  this.historyRecorder?.resume();
1057
1195
  this.toolContext.requestRender();
1058
1196
  }
1197
+ handleCopy() {
1198
+ if (!this.toolManager || !this.toolContext || this.isToolActive) return;
1199
+ const tool = this.toolManager.activeTool;
1200
+ if (tool?.name !== "select") return;
1201
+ const selectTool = tool;
1202
+ const ids = selectTool.selectedIds;
1203
+ if (ids.length === 0) return;
1204
+ this.clipboard = [];
1205
+ for (const id of ids) {
1206
+ const el = this.toolContext.store.getById(id);
1207
+ if (el) this.clipboard.push(structuredClone(el));
1208
+ }
1209
+ this.pasteCount = 0;
1210
+ }
1211
+ handlePaste() {
1212
+ if (!this.toolManager || !this.toolContext || this.clipboard.length === 0 || this.isToolActive)
1213
+ return;
1214
+ const tool = this.toolManager.activeTool;
1215
+ if (tool?.name !== "select") return;
1216
+ const selectTool = tool;
1217
+ this.pasteCount++;
1218
+ const offset = this.pasteCount * 20;
1219
+ const idMap = /* @__PURE__ */ new Map();
1220
+ for (const el of this.clipboard) {
1221
+ idMap.set(el.id, createId(el.type));
1222
+ }
1223
+ const newIds = [];
1224
+ this.historyRecorder?.begin();
1225
+ for (const el of this.clipboard) {
1226
+ const clone = structuredClone(el);
1227
+ const newId = idMap.get(el.id);
1228
+ if (!newId) continue;
1229
+ clone.id = newId;
1230
+ clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
1231
+ if (clone.type === "arrow") {
1232
+ const arrow = clone;
1233
+ arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
1234
+ arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
1235
+ delete arrow.cachedControlPoint;
1236
+ if (arrow.fromBinding) {
1237
+ const newTarget = idMap.get(arrow.fromBinding.elementId);
1238
+ if (newTarget) {
1239
+ arrow.fromBinding = { elementId: newTarget };
1240
+ } else {
1241
+ delete arrow.fromBinding;
1242
+ }
1243
+ }
1244
+ if (arrow.toBinding) {
1245
+ const newTarget = idMap.get(arrow.toBinding.elementId);
1246
+ if (newTarget) {
1247
+ arrow.toBinding = { elementId: newTarget };
1248
+ } else {
1249
+ delete arrow.toBinding;
1250
+ }
1251
+ }
1252
+ }
1253
+ if (this.toolContext.activeLayerId) {
1254
+ clone.layerId = this.toolContext.activeLayerId;
1255
+ }
1256
+ this.toolContext.store.add(clone);
1257
+ newIds.push(clone.id);
1258
+ }
1259
+ this.historyRecorder?.commit();
1260
+ selectTool.setSelection(newIds);
1261
+ this.toolContext.requestRender();
1262
+ }
1059
1263
  cancelToolIfActive(e) {
1060
1264
  if (this.isToolActive) {
1061
1265
  this.dispatchToolUp(e);
1062
1266
  this.isToolActive = false;
1063
1267
  }
1268
+ this.deferredDown = null;
1269
+ }
1270
+ };
1271
+
1272
+ // src/canvas/double-tap-detector.ts
1273
+ var DEFAULT_TIMEOUT = 300;
1274
+ var DEFAULT_MAX_DISTANCE = 20;
1275
+ var DoubleTapDetector = class {
1276
+ timeout;
1277
+ maxDistance;
1278
+ lastTapTime = 0;
1279
+ lastTapX = 0;
1280
+ lastTapY = 0;
1281
+ hasPendingTap = false;
1282
+ constructor(options) {
1283
+ this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
1284
+ this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
1285
+ }
1286
+ feed(e) {
1287
+ const now = Date.now();
1288
+ const x = e.clientX;
1289
+ const y = e.clientY;
1290
+ if (this.hasPendingTap) {
1291
+ const elapsed = now - this.lastTapTime;
1292
+ const dx = x - this.lastTapX;
1293
+ const dy = y - this.lastTapY;
1294
+ const dist = Math.sqrt(dx * dx + dy * dy);
1295
+ if (elapsed <= this.timeout && dist <= this.maxDistance) {
1296
+ this.reset();
1297
+ return true;
1298
+ }
1299
+ }
1300
+ this.lastTapTime = now;
1301
+ this.lastTapX = x;
1302
+ this.lastTapY = y;
1303
+ this.hasPendingTap = true;
1304
+ return false;
1305
+ }
1306
+ reset() {
1307
+ this.hasPendingTap = false;
1308
+ this.lastTapTime = 0;
1309
+ this.lastTapX = 0;
1310
+ this.lastTapY = 0;
1064
1311
  }
1065
1312
  };
1066
1313
 
@@ -1308,19 +1555,23 @@ var ElementStore = class {
1308
1555
  bus = new EventBus();
1309
1556
  layerOrderMap = /* @__PURE__ */ new Map();
1310
1557
  spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
1558
+ sortedCache = null;
1311
1559
  get count() {
1312
1560
  return this.elements.size;
1313
1561
  }
1314
1562
  setLayerOrder(order) {
1315
1563
  this.layerOrderMap = new Map(order);
1564
+ this.sortedCache = null;
1316
1565
  }
1317
1566
  getAll() {
1318
- return [...this.elements.values()].sort((a, b) => {
1567
+ if (this.sortedCache) return this.sortedCache;
1568
+ this.sortedCache = [...this.elements.values()].sort((a, b) => {
1319
1569
  const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
1320
1570
  const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
1321
1571
  if (layerA !== layerB) return layerA - layerB;
1322
1572
  return a.zIndex - b.zIndex;
1323
1573
  });
1574
+ return this.sortedCache;
1324
1575
  }
1325
1576
  getById(id) {
1326
1577
  return this.elements.get(id);
@@ -1331,6 +1582,7 @@ var ElementStore = class {
1331
1582
  );
1332
1583
  }
1333
1584
  add(element) {
1585
+ this.sortedCache = null;
1334
1586
  this.elements.set(element.id, element);
1335
1587
  const bounds = getElementBounds(element);
1336
1588
  if (bounds) this.spatialIndex.insert(element.id, bounds);
@@ -1339,11 +1591,15 @@ var ElementStore = class {
1339
1591
  update(id, partial) {
1340
1592
  const existing = this.elements.get(id);
1341
1593
  if (!existing) return;
1594
+ this.sortedCache = null;
1342
1595
  const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
1343
1596
  if (updated.type === "arrow") {
1344
1597
  const arrow = updated;
1345
1598
  arrow.cachedControlPoint = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1346
1599
  }
1600
+ if (updated.type === "note" && "text" in partial) {
1601
+ updated.text = sanitizeNoteHtml(updated.text);
1602
+ }
1347
1603
  this.elements.set(id, updated);
1348
1604
  const newBounds = getElementBounds(updated);
1349
1605
  if (newBounds) {
@@ -1354,11 +1610,13 @@ var ElementStore = class {
1354
1610
  remove(id) {
1355
1611
  const element = this.elements.get(id);
1356
1612
  if (!element) return;
1613
+ this.sortedCache = null;
1357
1614
  this.elements.delete(id);
1358
1615
  this.spatialIndex.remove(id);
1359
1616
  this.bus.emit("remove", element);
1360
1617
  }
1361
1618
  clear() {
1619
+ this.sortedCache = null;
1362
1620
  this.elements.clear();
1363
1621
  this.spatialIndex.clear();
1364
1622
  this.bus.emit("clear", null);
@@ -1367,6 +1625,7 @@ var ElementStore = class {
1367
1625
  return this.getAll().map((el) => ({ ...el }));
1368
1626
  }
1369
1627
  loadSnapshot(elements) {
1628
+ this.sortedCache = null;
1370
1629
  this.elements.clear();
1371
1630
  this.spatialIndex.clear();
1372
1631
  for (const el of elements) {
@@ -1374,6 +1633,10 @@ var ElementStore = class {
1374
1633
  const bounds = getElementBounds(el);
1375
1634
  if (bounds) this.spatialIndex.insert(el.id, bounds);
1376
1635
  }
1636
+ this.bus.emit("clear", null);
1637
+ for (const el of elements) {
1638
+ this.bus.emit("add", el);
1639
+ }
1377
1640
  }
1378
1641
  queryRect(rect) {
1379
1642
  const ids = this.spatialIndex.query(rect);
@@ -2423,12 +2686,6 @@ var ElementRenderer = class {
2423
2686
  }
2424
2687
  };
2425
2688
 
2426
- // src/elements/create-id.ts
2427
- var counter = 0;
2428
- function createId(prefix) {
2429
- return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
2430
- }
2431
-
2432
2689
  // src/elements/element-factory.ts
2433
2690
  var DEFAULT_NOTE_FONT_SIZE = 18;
2434
2691
  function createStroke(input) {
@@ -2454,7 +2711,7 @@ function createNote(input) {
2454
2711
  locked: input.locked ?? false,
2455
2712
  layerId: input.layerId ?? "",
2456
2713
  size: input.size ?? { w: 200, h: 100 },
2457
- text: input.text ?? "",
2714
+ text: sanitizeNoteHtml(input.text ?? ""),
2458
2715
  backgroundColor: input.backgroundColor ?? "#ffeb3b",
2459
2716
  textColor: input.textColor ?? "#000000",
2460
2717
  fontSize: input.fontSize ?? DEFAULT_NOTE_FONT_SIZE
@@ -2503,6 +2760,7 @@ function createHtmlElement(input) {
2503
2760
  size: input.size
2504
2761
  };
2505
2762
  if (input.domId) el.domId = input.domId;
2763
+ if (input.interactive) el.interactive = input.interactive;
2506
2764
  return el;
2507
2765
  }
2508
2766
  function createShape(input) {
@@ -2615,7 +2873,7 @@ function getActiveFormats() {
2615
2873
  }
2616
2874
 
2617
2875
  // src/elements/note-toolbar.ts
2618
- var TOOLBAR_HEIGHT = 32;
2876
+ var TOOLBAR_HEIGHT = 52;
2619
2877
  var TOOLBAR_GAP = 4;
2620
2878
  var FORMAT_BUTTONS = [
2621
2879
  { label: "B", format: "bold", command: "bold" },
@@ -2702,9 +2960,9 @@ var NoteToolbar = class {
2702
2960
  fontWeight: config.format === "bold" ? "bold" : "normal",
2703
2961
  fontStyle: config.format === "italic" ? "italic" : "normal",
2704
2962
  textDecoration: config.format === "underline" ? "underline" : config.format === "strikethrough" ? "line-through" : "none",
2705
- minWidth: "24px",
2706
- height: "24px",
2707
- lineHeight: "24px"
2963
+ minWidth: "44px",
2964
+ height: "44px",
2965
+ lineHeight: "44px"
2708
2966
  });
2709
2967
  btn.addEventListener("pointerdown", (e) => {
2710
2968
  e.preventDefault();
@@ -2722,7 +2980,7 @@ var NoteToolbar = class {
2722
2980
  cursor: "pointer",
2723
2981
  padding: "2px",
2724
2982
  fontSize: "12px",
2725
- height: "24px",
2983
+ height: "44px",
2726
2984
  marginLeft: "4px"
2727
2985
  });
2728
2986
  for (const preset of this.fontSizePresets) {
@@ -3720,10 +3978,13 @@ var DomNodeManager = class {
3720
3978
  wordWrap: "break-word"
3721
3979
  });
3722
3980
  node.innerHTML = element.text || "";
3723
- node.addEventListener("dblclick", (e) => {
3724
- e.stopPropagation();
3725
- const id = node.dataset["elementId"];
3726
- if (id) this.onEditRequest(id);
3981
+ const detector = new DoubleTapDetector();
3982
+ node.addEventListener("pointerup", (e) => {
3983
+ if (detector.feed(e)) {
3984
+ e.stopPropagation();
3985
+ const id = node.dataset["elementId"];
3986
+ if (id) this.onEditRequest(id);
3987
+ }
3727
3988
  });
3728
3989
  }
3729
3990
  if (!this.isEditingElement(element.id)) {
@@ -3736,15 +3997,19 @@ var DomNodeManager = class {
3736
3997
  node.style.fontSize = `${element.fontSize ?? DEFAULT_NOTE_FONT_SIZE}px`;
3737
3998
  }
3738
3999
  }
3739
- if (element.type === "html" && !node.dataset["initialized"]) {
3740
- const content = this.htmlContent.get(element.id);
3741
- if (content) {
3742
- node.dataset["initialized"] = "true";
3743
- Object.assign(node.style, {
3744
- overflow: "hidden",
3745
- pointerEvents: "none"
3746
- });
3747
- node.appendChild(content);
4000
+ if (element.type === "html") {
4001
+ if (!node.dataset["initialized"]) {
4002
+ const content = this.htmlContent.get(element.id);
4003
+ if (content) {
4004
+ node.dataset["initialized"] = "true";
4005
+ Object.assign(node.style, {
4006
+ overflow: "hidden",
4007
+ pointerEvents: element.interactive ? "auto" : "none"
4008
+ });
4009
+ node.appendChild(content);
4010
+ }
4011
+ } else {
4012
+ node.style.pointerEvents = element.interactive ? "auto" : "none";
3748
4013
  }
3749
4014
  }
3750
4015
  if (element.type === "text") {
@@ -3766,10 +4031,13 @@ var DomNodeManager = class {
3766
4031
  lineHeight: "1.4"
3767
4032
  });
3768
4033
  node.textContent = element.text || "";
3769
- node.addEventListener("dblclick", (e) => {
3770
- e.stopPropagation();
3771
- const id = node.dataset["elementId"];
3772
- if (id) this.onEditRequest(id);
4034
+ const detector = new DoubleTapDetector();
4035
+ node.addEventListener("pointerup", (e) => {
4036
+ if (detector.feed(e)) {
4037
+ e.stopPropagation();
4038
+ const id = node.dataset["elementId"];
4039
+ if (id) this.onEditRequest(id);
4040
+ }
3773
4041
  });
3774
4042
  }
3775
4043
  if (!this.isEditingElement(element.id)) {
@@ -4258,7 +4526,8 @@ var Viewport = class {
4258
4526
  this.toolContext.activeLayerId = this.layerManager.activeLayerId;
4259
4527
  this.requestRender();
4260
4528
  });
4261
- this.wrapper.addEventListener("dblclick", this.onDblClick);
4529
+ this.wrapper.addEventListener("pointerdown", this.onTapDown);
4530
+ this.wrapper.addEventListener("pointerup", this.onDoubleTap);
4262
4531
  this.wrapper.addEventListener("dragover", this.onDragOver);
4263
4532
  this.wrapper.addEventListener("drop", this.onDrop);
4264
4533
  this.observeResize();
@@ -4289,6 +4558,9 @@ var Viewport = class {
4289
4558
  domNodeManager;
4290
4559
  interactMode;
4291
4560
  gridChangeListeners = /* @__PURE__ */ new Set();
4561
+ doubleTapDetector = new DoubleTapDetector();
4562
+ tapDownX = 0;
4563
+ tapDownY = 0;
4292
4564
  get ctx() {
4293
4565
  return this.canvasEl.getContext("2d");
4294
4566
  }
@@ -4303,7 +4575,12 @@ var Viewport = class {
4303
4575
  this.renderLoop.requestRender();
4304
4576
  }
4305
4577
  exportState() {
4306
- return exportState(this.store.snapshot(), this.camera, this.layerManager.snapshot());
4578
+ return exportState(
4579
+ this.store.snapshot(),
4580
+ this.camera,
4581
+ this.layerManager.snapshot(),
4582
+ this.layerManager.activeLayerId
4583
+ );
4307
4584
  }
4308
4585
  exportJSON() {
4309
4586
  return JSON.stringify(this.exportState());
@@ -4319,6 +4596,9 @@ var Viewport = class {
4319
4596
  if (state.layers && state.layers.length > 0) {
4320
4597
  this.layerManager.loadSnapshot(state.layers);
4321
4598
  }
4599
+ if (state.activeLayerId) {
4600
+ this.layerManager.setActiveLayer(state.activeLayerId);
4601
+ }
4322
4602
  this.domNodeManager.reattachHtmlContent(this.store);
4323
4603
  this.history.clear();
4324
4604
  this.historyRecorder.resume();
@@ -4426,7 +4706,8 @@ var Viewport = class {
4426
4706
  this.interactMode.destroy();
4427
4707
  this.noteEditor.destroy(this.store);
4428
4708
  this.historyRecorder.destroy();
4429
- this.wrapper.removeEventListener("dblclick", this.onDblClick);
4709
+ this.wrapper.removeEventListener("pointerdown", this.onTapDown);
4710
+ this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
4430
4711
  this.wrapper.removeEventListener("dragover", this.onDragOver);
4431
4712
  this.wrapper.removeEventListener("drop", this.onDrop);
4432
4713
  this.inputHandler.destroy();
@@ -4464,7 +4745,17 @@ var Viewport = class {
4464
4745
  }
4465
4746
  }
4466
4747
  }
4467
- onDblClick = (e) => {
4748
+ onTapDown = (e) => {
4749
+ this.tapDownX = e.clientX;
4750
+ this.tapDownY = e.clientY;
4751
+ };
4752
+ onDoubleTap = (e) => {
4753
+ const dx = e.clientX - this.tapDownX;
4754
+ const dy = e.clientY - this.tapDownY;
4755
+ const moved = Math.sqrt(dx * dx + dy * dy);
4756
+ if (moved > 10) return;
4757
+ if (!this.doubleTapDetector.feed(e)) return;
4758
+ if (typeof document.elementFromPoint !== "function") return;
4468
4759
  const el = document.elementFromPoint(e.clientX, e.clientY);
4469
4760
  const nodeEl = el?.closest("[data-element-id]");
4470
4761
  if (nodeEl) {
@@ -4561,7 +4852,10 @@ var Viewport = class {
4561
4852
  position: "relative",
4562
4853
  width: "100%",
4563
4854
  height: "100%",
4564
- overflow: "hidden"
4855
+ overflow: "hidden",
4856
+ overscrollBehavior: "none",
4857
+ userSelect: "none",
4858
+ webkitUserSelect: "none"
4565
4859
  });
4566
4860
  return el;
4567
4861
  }
@@ -4624,6 +4918,26 @@ var Viewport = class {
4624
4918
  }
4625
4919
  };
4626
4920
 
4921
+ // src/elements/bounds.ts
4922
+ function getElementsBoundingBox(elements) {
4923
+ let minX = Infinity;
4924
+ let minY = Infinity;
4925
+ let maxX = -Infinity;
4926
+ let maxY = -Infinity;
4927
+ let found = false;
4928
+ for (const el of elements) {
4929
+ const b = getElementBounds(el);
4930
+ if (!b) continue;
4931
+ found = true;
4932
+ if (b.x < minX) minX = b.x;
4933
+ if (b.y < minY) minY = b.y;
4934
+ if (b.x + b.w > maxX) maxX = b.x + b.w;
4935
+ if (b.y + b.h > maxY) maxY = b.y + b.h;
4936
+ }
4937
+ if (!found) return null;
4938
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
4939
+ }
4940
+
4627
4941
  // src/tools/hand-tool.ts
4628
4942
  var HandTool = class {
4629
4943
  name = "hand";
@@ -4976,9 +5290,15 @@ var SelectTool = class {
4976
5290
  lastWorld = { x: 0, y: 0 };
4977
5291
  currentWorld = { x: 0, y: 0 };
4978
5292
  ctx = null;
5293
+ pendingSingleSelectId = null;
5294
+ hasDragged = false;
4979
5295
  get selectedIds() {
4980
5296
  return [...this._selectedIds];
4981
5297
  }
5298
+ setSelection(ids) {
5299
+ this._selectedIds = ids;
5300
+ this.ctx?.requestRender();
5301
+ }
4982
5302
  get isMarqueeActive() {
4983
5303
  return this.mode.type === "marquee";
4984
5304
  }
@@ -5027,13 +5347,27 @@ var SelectTool = class {
5027
5347
  return;
5028
5348
  }
5029
5349
  }
5350
+ this.pendingSingleSelectId = null;
5351
+ this.hasDragged = false;
5030
5352
  const hit = this.hitTest(world, ctx);
5031
5353
  if (hit) {
5032
5354
  const alreadySelected = this._selectedIds.includes(hit.id);
5033
- if (!alreadySelected) {
5034
- this._selectedIds = [hit.id];
5355
+ if (state.shiftKey) {
5356
+ if (alreadySelected) {
5357
+ this._selectedIds = this._selectedIds.filter((id) => id !== hit.id);
5358
+ this.mode = { type: "idle" };
5359
+ } else {
5360
+ this._selectedIds = [...this._selectedIds, hit.id];
5361
+ this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
5362
+ }
5363
+ } else {
5364
+ if (!alreadySelected) {
5365
+ this._selectedIds = [hit.id];
5366
+ } else if (this._selectedIds.length > 1) {
5367
+ this.pendingSingleSelectId = hit.id;
5368
+ }
5369
+ this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
5035
5370
  }
5036
- this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
5037
5371
  } else {
5038
5372
  this._selectedIds = [];
5039
5373
  this.mode = { type: "marquee", start: world };
@@ -5059,6 +5393,7 @@ var SelectTool = class {
5059
5393
  return;
5060
5394
  }
5061
5395
  if (this.mode.type === "dragging" && this._selectedIds.length > 0) {
5396
+ this.hasDragged = true;
5062
5397
  ctx.setCursor?.("move");
5063
5398
  const snapped = this.snap(world, ctx);
5064
5399
  const dx = snapped.x - this.lastWorld.x;
@@ -5127,6 +5462,11 @@ var SelectTool = class {
5127
5462
  }
5128
5463
  ctx.requestRender();
5129
5464
  }
5465
+ if (!this.hasDragged && this.pendingSingleSelectId !== null) {
5466
+ this._selectedIds = [this.pendingSingleSelectId];
5467
+ }
5468
+ this.pendingSingleSelectId = null;
5469
+ this.hasDragged = false;
5130
5470
  this.mode = { type: "idle" };
5131
5471
  ctx.setCursor?.("default");
5132
5472
  }
@@ -6307,7 +6647,7 @@ var UpdateLayerCommand = class {
6307
6647
  };
6308
6648
 
6309
6649
  // src/index.ts
6310
- var VERSION = "0.11.0";
6650
+ var VERSION = "0.14.0";
6311
6651
  // Annotate the CommonJS export names for ESM import in node:
6312
6652
  0 && (module.exports = {
6313
6653
  AddElementCommand,
@@ -6319,6 +6659,7 @@ var VERSION = "0.11.0";
6319
6659
  CreateLayerCommand,
6320
6660
  DEFAULT_FONT_SIZE_PRESETS,
6321
6661
  DEFAULT_NOTE_FONT_SIZE,
6662
+ DoubleTapDetector,
6322
6663
  ElementRenderer,
6323
6664
  ElementStore,
6324
6665
  EraserTool,
@@ -6327,6 +6668,7 @@ var VERSION = "0.11.0";
6327
6668
  HistoryRecorder,
6328
6669
  HistoryStack,
6329
6670
  ImageTool,
6671
+ InputFilter,
6330
6672
  InputHandler,
6331
6673
  LayerManager,
6332
6674
  MeasureTool,
@@ -6372,6 +6714,7 @@ var VERSION = "0.11.0";
6372
6714
  getEdgeIntersection,
6373
6715
  getElementBounds,
6374
6716
  getElementCenter,
6717
+ getElementsBoundingBox,
6375
6718
  getHexCellsInCone,
6376
6719
  getHexCellsInLine,
6377
6720
  getHexCellsInRadius,