@fieldnotes/core 0.44.0 → 0.46.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.d.cts CHANGED
@@ -162,6 +162,14 @@ interface ElementUpdateEvent {
162
162
  previous: CanvasElement;
163
163
  current: CanvasElement;
164
164
  }
165
+ interface ElementChangeMeta {
166
+ /**
167
+ * Identifies what caused the change. `undefined` or `'local'` = a local change (recorded to undo
168
+ * history and observed as local). Any other value (e.g. `'remote'`) marks an externally-applied
169
+ * change: it is NOT recorded to undo history, and is tagged so observers can avoid re-broadcasting it.
170
+ */
171
+ origin?: string;
172
+ }
165
173
  interface ElementStoreEvents {
166
174
  add: CanvasElement;
167
175
  remove: CanvasElement;
@@ -184,19 +192,19 @@ declare class ElementStore {
184
192
  type: T;
185
193
  }>[];
186
194
  private indexBounds;
187
- add(element: CanvasElement): void;
188
- update(id: string, partial: Partial<CanvasElement>): void;
189
- remove(id: string): void;
190
- clear(): void;
195
+ add(element: CanvasElement, meta?: ElementChangeMeta): void;
196
+ update(id: string, partial: Partial<CanvasElement>, meta?: ElementChangeMeta): void;
197
+ remove(id: string, meta?: ElementChangeMeta): void;
198
+ clear(meta?: ElementChangeMeta): void;
191
199
  snapshot(): CanvasElement[];
192
- loadSnapshot(elements: CanvasElement[]): void;
200
+ loadSnapshot(elements: CanvasElement[], meta?: ElementChangeMeta): void;
193
201
  bringToFront(id: string): void;
194
202
  sendToBack(id: string): void;
195
203
  bringForward(id: string): void;
196
204
  sendBackward(id: string): void;
197
205
  queryRect(rect: Bounds): CanvasElement[];
198
206
  queryPoint(point: Point): CanvasElement[];
199
- on<K extends keyof ElementStoreEvents>(event: K, listener: (data: ElementStoreEvents[K]) => void): () => void;
207
+ on<K extends keyof ElementStoreEvents>(event: K, listener: (data: ElementStoreEvents[K], meta: ElementChangeMeta) => void): () => void;
200
208
  onChange(listener: () => void): () => void;
201
209
  }
202
210
 
@@ -304,10 +312,17 @@ declare class LayerManager {
304
312
  private findFallbackLayer;
305
313
  }
306
314
 
315
+ interface StorageAdapter {
316
+ load(key: string): Promise<string | null>;
317
+ save(key: string, value: string): Promise<void>;
318
+ clear(key: string): Promise<void>;
319
+ }
320
+
307
321
  interface AutoSaveOptions {
308
322
  key?: string;
309
323
  debounceMs?: number;
310
324
  layerManager?: LayerManager;
325
+ adapter?: StorageAdapter;
311
326
  onError?: (error: Error) => void;
312
327
  }
313
328
  declare class AutoSave {
@@ -316,19 +331,52 @@ declare class AutoSave {
316
331
  private readonly key;
317
332
  private readonly debounceMs;
318
333
  private readonly layerManager?;
334
+ private readonly adapter;
319
335
  private timerId;
320
336
  private unsubscribers;
321
337
  private readonly onError?;
338
+ private saving;
339
+ private pendingSave;
322
340
  constructor(store: ElementStore, camera: Camera, options?: AutoSaveOptions);
323
341
  start(): void;
324
342
  stop(): void;
325
- load(): CanvasState | null;
326
- clear(): void;
343
+ load(): Promise<CanvasState | null>;
344
+ clear(): Promise<void>;
327
345
  private scheduleSave;
328
346
  private cancelPending;
329
347
  private save;
330
348
  }
331
349
 
350
+ declare class MemoryAdapter implements StorageAdapter {
351
+ private store;
352
+ load(key: string): Promise<string | null>;
353
+ save(key: string, value: string): Promise<void>;
354
+ clear(key: string): Promise<void>;
355
+ }
356
+
357
+ declare class LocalStorageAdapter implements StorageAdapter {
358
+ load(key: string): Promise<string | null>;
359
+ save(key: string, value: string): Promise<void>;
360
+ clear(key: string): Promise<void>;
361
+ }
362
+
363
+ interface IndexedDBAdapterOptions {
364
+ dbName?: string;
365
+ storeName?: string;
366
+ indexedDB?: IDBFactory;
367
+ }
368
+ declare class IndexedDBAdapter implements StorageAdapter {
369
+ private readonly dbName;
370
+ private readonly storeName;
371
+ private readonly idb;
372
+ private dbPromise;
373
+ constructor(options?: IndexedDBAdapterOptions);
374
+ private open;
375
+ load(key: string): Promise<string | null>;
376
+ save(key: string, value: string): Promise<void>;
377
+ clear(key: string): Promise<void>;
378
+ }
379
+
332
380
  type BackgroundPattern = 'dots' | 'grid' | 'none';
333
381
  interface BackgroundOptions {
334
382
  pattern?: BackgroundPattern;
@@ -1065,6 +1113,6 @@ declare class LaserTool implements Tool {
1065
1113
  private notifyOptionsChange;
1066
1114
  }
1067
1115
 
1068
- declare const VERSION = "0.44.0";
1116
+ declare const VERSION = "0.46.0";
1069
1117
 
1070
- export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
1118
+ export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, LocalStorageAdapter, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
package/dist/index.d.ts CHANGED
@@ -162,6 +162,14 @@ interface ElementUpdateEvent {
162
162
  previous: CanvasElement;
163
163
  current: CanvasElement;
164
164
  }
165
+ interface ElementChangeMeta {
166
+ /**
167
+ * Identifies what caused the change. `undefined` or `'local'` = a local change (recorded to undo
168
+ * history and observed as local). Any other value (e.g. `'remote'`) marks an externally-applied
169
+ * change: it is NOT recorded to undo history, and is tagged so observers can avoid re-broadcasting it.
170
+ */
171
+ origin?: string;
172
+ }
165
173
  interface ElementStoreEvents {
166
174
  add: CanvasElement;
167
175
  remove: CanvasElement;
@@ -184,19 +192,19 @@ declare class ElementStore {
184
192
  type: T;
185
193
  }>[];
186
194
  private indexBounds;
187
- add(element: CanvasElement): void;
188
- update(id: string, partial: Partial<CanvasElement>): void;
189
- remove(id: string): void;
190
- clear(): void;
195
+ add(element: CanvasElement, meta?: ElementChangeMeta): void;
196
+ update(id: string, partial: Partial<CanvasElement>, meta?: ElementChangeMeta): void;
197
+ remove(id: string, meta?: ElementChangeMeta): void;
198
+ clear(meta?: ElementChangeMeta): void;
191
199
  snapshot(): CanvasElement[];
192
- loadSnapshot(elements: CanvasElement[]): void;
200
+ loadSnapshot(elements: CanvasElement[], meta?: ElementChangeMeta): void;
193
201
  bringToFront(id: string): void;
194
202
  sendToBack(id: string): void;
195
203
  bringForward(id: string): void;
196
204
  sendBackward(id: string): void;
197
205
  queryRect(rect: Bounds): CanvasElement[];
198
206
  queryPoint(point: Point): CanvasElement[];
199
- on<K extends keyof ElementStoreEvents>(event: K, listener: (data: ElementStoreEvents[K]) => void): () => void;
207
+ on<K extends keyof ElementStoreEvents>(event: K, listener: (data: ElementStoreEvents[K], meta: ElementChangeMeta) => void): () => void;
200
208
  onChange(listener: () => void): () => void;
201
209
  }
202
210
 
@@ -304,10 +312,17 @@ declare class LayerManager {
304
312
  private findFallbackLayer;
305
313
  }
306
314
 
315
+ interface StorageAdapter {
316
+ load(key: string): Promise<string | null>;
317
+ save(key: string, value: string): Promise<void>;
318
+ clear(key: string): Promise<void>;
319
+ }
320
+
307
321
  interface AutoSaveOptions {
308
322
  key?: string;
309
323
  debounceMs?: number;
310
324
  layerManager?: LayerManager;
325
+ adapter?: StorageAdapter;
311
326
  onError?: (error: Error) => void;
312
327
  }
313
328
  declare class AutoSave {
@@ -316,19 +331,52 @@ declare class AutoSave {
316
331
  private readonly key;
317
332
  private readonly debounceMs;
318
333
  private readonly layerManager?;
334
+ private readonly adapter;
319
335
  private timerId;
320
336
  private unsubscribers;
321
337
  private readonly onError?;
338
+ private saving;
339
+ private pendingSave;
322
340
  constructor(store: ElementStore, camera: Camera, options?: AutoSaveOptions);
323
341
  start(): void;
324
342
  stop(): void;
325
- load(): CanvasState | null;
326
- clear(): void;
343
+ load(): Promise<CanvasState | null>;
344
+ clear(): Promise<void>;
327
345
  private scheduleSave;
328
346
  private cancelPending;
329
347
  private save;
330
348
  }
331
349
 
350
+ declare class MemoryAdapter implements StorageAdapter {
351
+ private store;
352
+ load(key: string): Promise<string | null>;
353
+ save(key: string, value: string): Promise<void>;
354
+ clear(key: string): Promise<void>;
355
+ }
356
+
357
+ declare class LocalStorageAdapter implements StorageAdapter {
358
+ load(key: string): Promise<string | null>;
359
+ save(key: string, value: string): Promise<void>;
360
+ clear(key: string): Promise<void>;
361
+ }
362
+
363
+ interface IndexedDBAdapterOptions {
364
+ dbName?: string;
365
+ storeName?: string;
366
+ indexedDB?: IDBFactory;
367
+ }
368
+ declare class IndexedDBAdapter implements StorageAdapter {
369
+ private readonly dbName;
370
+ private readonly storeName;
371
+ private readonly idb;
372
+ private dbPromise;
373
+ constructor(options?: IndexedDBAdapterOptions);
374
+ private open;
375
+ load(key: string): Promise<string | null>;
376
+ save(key: string, value: string): Promise<void>;
377
+ clear(key: string): Promise<void>;
378
+ }
379
+
332
380
  type BackgroundPattern = 'dots' | 'grid' | 'none';
333
381
  interface BackgroundOptions {
334
382
  pattern?: BackgroundPattern;
@@ -1065,6 +1113,6 @@ declare class LaserTool implements Tool {
1065
1113
  private notifyOptionsChange;
1066
1114
  }
1067
1115
 
1068
- declare const VERSION = "0.44.0";
1116
+ declare const VERSION = "0.46.0";
1069
1117
 
1070
- export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
1118
+ export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, LocalStorageAdapter, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
package/dist/index.js CHANGED
@@ -285,6 +285,22 @@ function migrateElement(obj) {
285
285
  }
286
286
  }
287
287
 
288
+ // src/core/storage/local-storage-adapter.ts
289
+ var LocalStorageAdapter = class {
290
+ async load(key) {
291
+ if (typeof localStorage === "undefined") return null;
292
+ return localStorage.getItem(key);
293
+ }
294
+ async save(key, value) {
295
+ if (typeof localStorage === "undefined") return;
296
+ localStorage.setItem(key, value);
297
+ }
298
+ async clear(key) {
299
+ if (typeof localStorage === "undefined") return;
300
+ localStorage.removeItem(key);
301
+ }
302
+ };
303
+
288
304
  // src/core/auto-save.ts
289
305
  var DEFAULT_KEY = "fieldnotes-autosave";
290
306
  var DEFAULT_DEBOUNCE_MS = 1e3;
@@ -295,14 +311,18 @@ var AutoSave = class {
295
311
  this.key = options.key ?? DEFAULT_KEY;
296
312
  this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
297
313
  this.layerManager = options.layerManager;
314
+ this.adapter = options.adapter ?? new LocalStorageAdapter();
298
315
  this.onError = options.onError;
299
316
  }
300
317
  key;
301
318
  debounceMs;
302
319
  layerManager;
320
+ adapter;
303
321
  timerId = null;
304
322
  unsubscribers = [];
305
323
  onError;
324
+ saving = false;
325
+ pendingSave = false;
306
326
  start() {
307
327
  const schedule = () => this.scheduleSave();
308
328
  this.unsubscribers = [
@@ -320,9 +340,8 @@ var AutoSave = class {
320
340
  this.unsubscribers.forEach((fn) => fn());
321
341
  this.unsubscribers = [];
322
342
  }
323
- load() {
324
- if (typeof localStorage === "undefined") return null;
325
- const json = localStorage.getItem(this.key);
343
+ async load() {
344
+ const json = await this.adapter.load(this.key);
326
345
  if (!json) return null;
327
346
  try {
328
347
  return parseState(json);
@@ -330,13 +349,12 @@ var AutoSave = class {
330
349
  return null;
331
350
  }
332
351
  }
333
- clear() {
334
- if (typeof localStorage === "undefined") return;
335
- localStorage.removeItem(this.key);
352
+ async clear() {
353
+ await this.adapter.clear(this.key);
336
354
  }
337
355
  scheduleSave() {
338
356
  this.cancelPending();
339
- this.timerId = setTimeout(() => this.save(), this.debounceMs);
357
+ this.timerId = setTimeout(() => void this.save(), this.debounceMs);
340
358
  }
341
359
  cancelPending() {
342
360
  if (this.timerId !== null) {
@@ -344,19 +362,108 @@ var AutoSave = class {
344
362
  this.timerId = null;
345
363
  }
346
364
  }
347
- save() {
348
- if (typeof localStorage === "undefined") return;
349
- const layers = this.layerManager?.snapshot() ?? [];
350
- const state = exportState(this.store.snapshot(), this.camera, layers);
365
+ async save() {
366
+ if (this.saving) {
367
+ this.pendingSave = true;
368
+ return;
369
+ }
370
+ this.saving = true;
351
371
  try {
352
- localStorage.setItem(this.key, JSON.stringify(state));
372
+ const layers = this.layerManager?.snapshot() ?? [];
373
+ const state = exportState(this.store.snapshot(), this.camera, layers);
374
+ await this.adapter.save(this.key, JSON.stringify(state));
353
375
  } catch (e) {
354
- console.warn("Auto-save failed: storage quota exceeded. State too large for localStorage.");
355
376
  this.onError?.(e instanceof Error ? e : new Error(String(e)));
377
+ } finally {
378
+ this.saving = false;
379
+ if (this.pendingSave) {
380
+ this.pendingSave = false;
381
+ void this.save();
382
+ }
356
383
  }
357
384
  }
358
385
  };
359
386
 
387
+ // src/core/storage/memory-adapter.ts
388
+ var MemoryAdapter = class {
389
+ store = /* @__PURE__ */ new Map();
390
+ async load(key) {
391
+ return this.store.get(key) ?? null;
392
+ }
393
+ async save(key, value) {
394
+ this.store.set(key, value);
395
+ }
396
+ async clear(key) {
397
+ this.store.delete(key);
398
+ }
399
+ };
400
+
401
+ // src/core/storage/indexeddb-adapter.ts
402
+ var DEFAULT_DB = "fieldnotes";
403
+ var DEFAULT_STORE = "state";
404
+ var IndexedDBAdapter = class {
405
+ dbName;
406
+ storeName;
407
+ idb;
408
+ dbPromise = null;
409
+ constructor(options = {}) {
410
+ this.dbName = options.dbName ?? DEFAULT_DB;
411
+ this.storeName = options.storeName ?? DEFAULT_STORE;
412
+ this.idb = options.indexedDB ?? (typeof indexedDB !== "undefined" ? indexedDB : null);
413
+ }
414
+ open() {
415
+ const idb = this.idb;
416
+ if (!idb) return Promise.reject(new Error("IndexedDB unavailable"));
417
+ if (!this.dbPromise) {
418
+ const storeName = this.storeName;
419
+ this.dbPromise = new Promise((resolve, reject) => {
420
+ const req = idb.open(this.dbName, 1);
421
+ req.onupgradeneeded = () => {
422
+ const db = req.result;
423
+ if (!db.objectStoreNames.contains(storeName)) db.createObjectStore(storeName);
424
+ };
425
+ req.onsuccess = () => resolve(req.result);
426
+ req.onerror = () => reject(req.error ?? new Error("IndexedDB open failed"));
427
+ });
428
+ }
429
+ return this.dbPromise;
430
+ }
431
+ async load(key) {
432
+ if (!this.idb) return null;
433
+ const db = await this.open();
434
+ return new Promise((resolve, reject) => {
435
+ const req = db.transaction(this.storeName, "readonly").objectStore(this.storeName).get(key);
436
+ req.onsuccess = () => {
437
+ const v = req.result;
438
+ resolve(typeof v === "string" ? v : null);
439
+ };
440
+ req.onerror = () => reject(req.error ?? new Error("IndexedDB read failed"));
441
+ });
442
+ }
443
+ async save(key, value) {
444
+ if (!this.idb) return;
445
+ const db = await this.open();
446
+ return new Promise((resolve, reject) => {
447
+ const tx = db.transaction(this.storeName, "readwrite");
448
+ tx.objectStore(this.storeName).put(value, key);
449
+ tx.oncomplete = () => resolve();
450
+ tx.onerror = () => reject(tx.error ?? new Error("IndexedDB write failed"));
451
+ tx.onabort = () => reject(tx.error ?? new Error("IndexedDB write aborted"));
452
+ });
453
+ }
454
+ async clear(key) {
455
+ if (!this.idb) return;
456
+ const db = await this.open();
457
+ return new Promise((resolve, reject) => {
458
+ const tx = db.transaction(this.storeName, "readwrite");
459
+ tx.objectStore(this.storeName).delete(key);
460
+ tx.oncomplete = () => resolve();
461
+ tx.onerror = () => reject(tx.error ?? new Error("IndexedDB delete failed"));
462
+ tx.onabort = () => reject(tx.error ?? new Error("IndexedDB delete aborted"));
463
+ });
464
+ }
465
+ };
466
+
360
467
  // src/canvas/camera.ts
361
468
  var DEFAULT_MIN_ZOOM = 0.1;
362
469
  var DEFAULT_MAX_ZOOM = 10;
@@ -2039,6 +2146,7 @@ var Background = class {
2039
2146
  };
2040
2147
 
2041
2148
  // src/core/event-bus.ts
2149
+ var EMPTY_META = Object.freeze({});
2042
2150
  var EventBus = class {
2043
2151
  listeners = /* @__PURE__ */ new Map();
2044
2152
  on(event, listener) {
@@ -2054,10 +2162,10 @@ var EventBus = class {
2054
2162
  off(event, listener) {
2055
2163
  this.listeners.get(event)?.delete(listener);
2056
2164
  }
2057
- emit(event, data) {
2165
+ emit(event, data, meta = EMPTY_META) {
2058
2166
  this.listeners.get(event)?.forEach((listener) => {
2059
2167
  try {
2060
- listener(data);
2168
+ listener(data, meta);
2061
2169
  } catch (err) {
2062
2170
  console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
2063
2171
  }
@@ -2377,15 +2485,15 @@ var ElementStore = class {
2377
2485
  const angle = element.rotation ?? 0;
2378
2486
  return angle === 0 ? bounds : rotatedAABB(bounds, angle);
2379
2487
  }
2380
- add(element) {
2488
+ add(element, meta) {
2381
2489
  this.sortedCache = null;
2382
2490
  this._versions.set(element.id, 0);
2383
2491
  this.elements.set(element.id, element);
2384
2492
  const bounds = this.indexBounds(element);
2385
2493
  if (bounds) this.spatialIndex.insert(element.id, bounds);
2386
- this.bus.emit("add", element);
2494
+ this.bus.emit("add", element, meta);
2387
2495
  }
2388
- update(id, partial) {
2496
+ update(id, partial, meta) {
2389
2497
  const existing = this.elements.get(id);
2390
2498
  if (!existing) return;
2391
2499
  this.sortedCache = null;
@@ -2412,28 +2520,28 @@ var ElementStore = class {
2412
2520
  if (newBounds) {
2413
2521
  this.spatialIndex.update(id, newBounds);
2414
2522
  }
2415
- this.bus.emit("update", { previous: existing, current: updated });
2523
+ this.bus.emit("update", { previous: existing, current: updated }, meta);
2416
2524
  }
2417
- remove(id) {
2525
+ remove(id, meta) {
2418
2526
  const element = this.elements.get(id);
2419
2527
  if (!element) return;
2420
2528
  this.sortedCache = null;
2421
2529
  this._versions.delete(id);
2422
2530
  this.elements.delete(id);
2423
2531
  this.spatialIndex.remove(id);
2424
- this.bus.emit("remove", element);
2532
+ this.bus.emit("remove", element, meta);
2425
2533
  }
2426
- clear() {
2534
+ clear(meta) {
2427
2535
  this.sortedCache = null;
2428
2536
  this._versions.clear();
2429
2537
  this.elements.clear();
2430
2538
  this.spatialIndex.clear();
2431
- this.bus.emit("clear", null);
2539
+ this.bus.emit("clear", null, meta);
2432
2540
  }
2433
2541
  snapshot() {
2434
2542
  return this.getAll().map((el) => ({ ...el }));
2435
2543
  }
2436
- loadSnapshot(elements) {
2544
+ loadSnapshot(elements, meta) {
2437
2545
  this.sortedCache = null;
2438
2546
  this._versions.clear();
2439
2547
  this.elements.clear();
@@ -2450,9 +2558,9 @@ var ElementStore = class {
2450
2558
  el.cachedControlPoint = getArrowControlPoint(el.from, el.to, el.bend);
2451
2559
  }
2452
2560
  }
2453
- this.bus.emit("clear", null);
2561
+ this.bus.emit("clear", null, meta);
2454
2562
  for (const el of elements) {
2455
- this.bus.emit("add", el);
2563
+ this.bus.emit("add", el, meta);
2456
2564
  }
2457
2565
  }
2458
2566
  bringToFront(id) {
@@ -4727,15 +4835,18 @@ var UpdateLayerCommand = class {
4727
4835
  };
4728
4836
 
4729
4837
  // src/history/history-recorder.ts
4838
+ function isExternalChange(meta) {
4839
+ return meta.origin !== void 0 && meta.origin !== "local";
4840
+ }
4730
4841
  var HistoryRecorder = class {
4731
4842
  constructor(store, stack, layerManager) {
4732
4843
  this.store = store;
4733
4844
  this.stack = stack;
4734
4845
  this.layerManager = layerManager;
4735
4846
  this.unsubscribers = [
4736
- store.on("add", (el) => this.onAdd(el)),
4737
- store.on("remove", (el) => this.onRemove(el)),
4738
- store.on("update", ({ previous, current }) => this.onUpdate(previous, current))
4847
+ store.on("add", (el, meta) => this.onAdd(el, meta)),
4848
+ store.on("remove", (el, meta) => this.onRemove(el, meta)),
4849
+ store.on("update", ({ previous, current }, meta) => this.onUpdate(previous, current, meta))
4739
4850
  ];
4740
4851
  if (layerManager) {
4741
4852
  this.unsubscribers.push(
@@ -4791,18 +4902,21 @@ var HistoryRecorder = class {
4791
4902
  this.stack.push(command);
4792
4903
  }
4793
4904
  }
4794
- onAdd(element) {
4905
+ onAdd(element, meta) {
4906
+ if (isExternalChange(meta)) return;
4795
4907
  if (!this.recording) return;
4796
4908
  this.record(new AddElementCommand(element));
4797
4909
  }
4798
- onRemove(element) {
4910
+ onRemove(element, meta) {
4911
+ if (isExternalChange(meta)) return;
4799
4912
  if (!this.recording) return;
4800
4913
  if (this.transaction && this.updateSnapshots.has(element.id)) {
4801
4914
  this.updateSnapshots.delete(element.id);
4802
4915
  }
4803
4916
  this.record(new RemoveElementCommand(element));
4804
4917
  }
4805
- onUpdate(previous, current) {
4918
+ onUpdate(previous, current, meta) {
4919
+ if (isExternalChange(meta)) return;
4806
4920
  if (!this.recording) return;
4807
4921
  if (this.transaction) {
4808
4922
  if (!this.updateSnapshots.has(current.id)) {
@@ -9974,7 +10088,7 @@ var LaserTool = class {
9974
10088
  };
9975
10089
 
9976
10090
  // src/index.ts
9977
- var VERSION = "0.44.0";
10091
+ var VERSION = "0.46.0";
9978
10092
  export {
9979
10093
  ArrowTool,
9980
10094
  AutoSave,
@@ -9985,9 +10099,12 @@ export {
9985
10099
  HandTool,
9986
10100
  HistoryStack,
9987
10101
  ImageTool,
10102
+ IndexedDBAdapter,
9988
10103
  LaserTool,
9989
10104
  LayerManager,
10105
+ LocalStorageAdapter,
9990
10106
  MeasureTool,
10107
+ MemoryAdapter,
9991
10108
  NoteTool,
9992
10109
  PencilTool,
9993
10110
  SelectTool,