@fieldnotes/core 0.59.0 → 0.60.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
@@ -410,6 +410,7 @@ declare class ToolManager {
410
410
  private tools;
411
411
  private current;
412
412
  private changeListeners;
413
+ private registerListeners;
413
414
  get activeTool(): Tool | null;
414
415
  get toolNames(): string[];
415
416
  register(tool: Tool): void;
@@ -419,6 +420,7 @@ declare class ToolManager {
419
420
  handlePointerMove(state: PointerState, ctx: ToolContext): void;
420
421
  handlePointerUp(state: PointerState, ctx: ToolContext): void;
421
422
  onChange(listener: (name: string) => void): () => void;
423
+ onRegister(listener: (tool: Tool) => void): () => void;
422
424
  }
423
425
 
424
426
  interface Command {
@@ -558,6 +560,11 @@ declare function getElementStyle(element: CanvasElement): ElementStyle;
558
560
 
559
561
  type RotateDirection = 'cw' | 'ccw';
560
562
 
563
+ interface SelectionStyleDetails {
564
+ common: ElementStyle;
565
+ applicable: readonly (keyof ElementStyle)[];
566
+ mixed: readonly (keyof ElementStyle)[];
567
+ }
561
568
  type AlignEdge = 'left' | 'center-x' | 'right' | 'top' | 'middle' | 'bottom';
562
569
  type DistributeAxis = 'horizontal' | 'vertical';
563
570
 
@@ -637,6 +644,11 @@ declare class Viewport {
637
644
  private minimap;
638
645
  private readonly htmlRenderers;
639
646
  private readonly resizeListeners;
647
+ private readonly selectionListeners;
648
+ private detachSelectionSource;
649
+ private unsubToolRegister;
650
+ private pendingSelectionPrune;
651
+ private unsubRecorderEnd;
640
652
  constructor(container: HTMLElement, options?: ViewportOptions);
641
653
  get ctx(): CanvasRenderingContext2D | null;
642
654
  get snapToGrid(): boolean;
@@ -730,12 +742,34 @@ declare class Viewport {
730
742
  getGridInfo(): GridInfo | null;
731
743
  onGridChange(listener: (info: GridInfo | null) => void): () => void;
732
744
  private getSelectTool;
745
+ private pruneSelection;
746
+ private handleRemovedElement;
747
+ private static isSelectionSource;
748
+ private emitSelectionChange;
749
+ private attachSelectionSource;
750
+ /**
751
+ * getSelectedIds() and the onSelectionChange emitter never surface stale ids:
752
+ * once the enclosing history transaction completes, both reflect
753
+ * the current selection.
754
+ */
733
755
  getSelectedIds(): string[];
734
756
  runAction(action: string): void;
735
757
  canPaste(): boolean;
736
758
  openContextMenu(screenPos: Point): void;
759
+ /**
760
+ * Persistent, viewport-owned selection-change emitter. Subscribing works
761
+ * regardless of whether a select tool is registered yet; it forwards
762
+ * events from whichever select tool is currently attached via
763
+ * `toolManager.onRegister`. Never delivers stale ids once the enclosing
764
+ * history transaction completes.
765
+ */
737
766
  onSelectionChange(listener: () => void): () => void;
738
767
  getSelectionStyle(): ElementStyle | null;
768
+ /**
769
+ * Unlike `getSelectionStyle()` — which returns `{}` for a style-less
770
+ * selection — this returns `null` when no style field is applicable.
771
+ */
772
+ getSelectionStyleDetails(): SelectionStyleDetails | null;
739
773
  applyStyleToSelection(style: ElementStyle): void;
740
774
  groupSelection(): void;
741
775
  ungroupSelection(): void;
@@ -1755,6 +1789,6 @@ declare class TemplateTool implements Tool {
1755
1789
  private notifyOptionsChange;
1756
1790
  }
1757
1791
 
1758
- declare const VERSION = "0.59.0";
1792
+ declare const VERSION = "0.60.0";
1759
1793
 
1760
- 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 ExportAssetError, type ExportAssetErrorReason, type ExportImageOptions, type ExportResourceOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type HtmlExportError, type HtmlExportErrorReason, type HtmlExportOptions, type HtmlExportRenderer, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LASER_TRAIL_PRESENCE_KIND, LaserTool, type LaserToolOptions, type LaserTrailEmission, type LaserTrailPresence, type Layer, LayerManager, LocalStorageAdapter, MEASURE_PRESENCE_KIND, type MeasureEmission, type MeasurePresence, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, MinimapController, type MinimapControllerOptions, type NoteElement, NoteTool, type NoteToolOptions, type OverlayRenderer, PING_PRESENCE_KIND, PencilTool, type PencilToolOptions, type PingEmission, PingInput, type PingInputHost, type PingInputOptions, type PingPresence, PingTool, type PingToolOptions, type Point, type PointerState, RemoteLaserOverlay, type RemoteLaserOverlayHost, type RemoteLaserOverlayOptions, RemoteMeasureOverlay, type RemoteMeasureOverlayHost, type RemoteMeasureOverlayOptions, RemotePingOverlay, type RemotePingOverlayHost, type RemotePingOverlayOptions, type RenderStatsSnapshot, type RotateDirection, 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, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isLaserTrailPresence, isMeasurePresence, isNearBezier, isPingPresence, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toLaserTrailPresence, toMeasurePresence, toPingPresence, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
1794
+ 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 ExportAssetError, type ExportAssetErrorReason, type ExportImageOptions, type ExportResourceOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type HtmlExportError, type HtmlExportErrorReason, type HtmlExportOptions, type HtmlExportRenderer, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LASER_TRAIL_PRESENCE_KIND, LaserTool, type LaserToolOptions, type LaserTrailEmission, type LaserTrailPresence, type Layer, LayerManager, LocalStorageAdapter, MEASURE_PRESENCE_KIND, type MeasureEmission, type MeasurePresence, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, MinimapController, type MinimapControllerOptions, type NoteElement, NoteTool, type NoteToolOptions, type OverlayRenderer, PING_PRESENCE_KIND, PencilTool, type PencilToolOptions, type PingEmission, PingInput, type PingInputHost, type PingInputOptions, type PingPresence, PingTool, type PingToolOptions, type Point, type PointerState, RemoteLaserOverlay, type RemoteLaserOverlayHost, type RemoteLaserOverlayOptions, RemoteMeasureOverlay, type RemoteMeasureOverlayHost, type RemoteMeasureOverlayOptions, RemotePingOverlay, type RemotePingOverlayHost, type RemotePingOverlayOptions, type RenderStatsSnapshot, type RotateDirection, SelectTool, type SelectionStyleDetails, 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, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isLaserTrailPresence, isMeasurePresence, isNearBezier, isPingPresence, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toLaserTrailPresence, toMeasurePresence, toPingPresence, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
package/dist/index.d.ts CHANGED
@@ -410,6 +410,7 @@ declare class ToolManager {
410
410
  private tools;
411
411
  private current;
412
412
  private changeListeners;
413
+ private registerListeners;
413
414
  get activeTool(): Tool | null;
414
415
  get toolNames(): string[];
415
416
  register(tool: Tool): void;
@@ -419,6 +420,7 @@ declare class ToolManager {
419
420
  handlePointerMove(state: PointerState, ctx: ToolContext): void;
420
421
  handlePointerUp(state: PointerState, ctx: ToolContext): void;
421
422
  onChange(listener: (name: string) => void): () => void;
423
+ onRegister(listener: (tool: Tool) => void): () => void;
422
424
  }
423
425
 
424
426
  interface Command {
@@ -558,6 +560,11 @@ declare function getElementStyle(element: CanvasElement): ElementStyle;
558
560
 
559
561
  type RotateDirection = 'cw' | 'ccw';
560
562
 
563
+ interface SelectionStyleDetails {
564
+ common: ElementStyle;
565
+ applicable: readonly (keyof ElementStyle)[];
566
+ mixed: readonly (keyof ElementStyle)[];
567
+ }
561
568
  type AlignEdge = 'left' | 'center-x' | 'right' | 'top' | 'middle' | 'bottom';
562
569
  type DistributeAxis = 'horizontal' | 'vertical';
563
570
 
@@ -637,6 +644,11 @@ declare class Viewport {
637
644
  private minimap;
638
645
  private readonly htmlRenderers;
639
646
  private readonly resizeListeners;
647
+ private readonly selectionListeners;
648
+ private detachSelectionSource;
649
+ private unsubToolRegister;
650
+ private pendingSelectionPrune;
651
+ private unsubRecorderEnd;
640
652
  constructor(container: HTMLElement, options?: ViewportOptions);
641
653
  get ctx(): CanvasRenderingContext2D | null;
642
654
  get snapToGrid(): boolean;
@@ -730,12 +742,34 @@ declare class Viewport {
730
742
  getGridInfo(): GridInfo | null;
731
743
  onGridChange(listener: (info: GridInfo | null) => void): () => void;
732
744
  private getSelectTool;
745
+ private pruneSelection;
746
+ private handleRemovedElement;
747
+ private static isSelectionSource;
748
+ private emitSelectionChange;
749
+ private attachSelectionSource;
750
+ /**
751
+ * getSelectedIds() and the onSelectionChange emitter never surface stale ids:
752
+ * once the enclosing history transaction completes, both reflect
753
+ * the current selection.
754
+ */
733
755
  getSelectedIds(): string[];
734
756
  runAction(action: string): void;
735
757
  canPaste(): boolean;
736
758
  openContextMenu(screenPos: Point): void;
759
+ /**
760
+ * Persistent, viewport-owned selection-change emitter. Subscribing works
761
+ * regardless of whether a select tool is registered yet; it forwards
762
+ * events from whichever select tool is currently attached via
763
+ * `toolManager.onRegister`. Never delivers stale ids once the enclosing
764
+ * history transaction completes.
765
+ */
737
766
  onSelectionChange(listener: () => void): () => void;
738
767
  getSelectionStyle(): ElementStyle | null;
768
+ /**
769
+ * Unlike `getSelectionStyle()` — which returns `{}` for a style-less
770
+ * selection — this returns `null` when no style field is applicable.
771
+ */
772
+ getSelectionStyleDetails(): SelectionStyleDetails | null;
739
773
  applyStyleToSelection(style: ElementStyle): void;
740
774
  groupSelection(): void;
741
775
  ungroupSelection(): void;
@@ -1755,6 +1789,6 @@ declare class TemplateTool implements Tool {
1755
1789
  private notifyOptionsChange;
1756
1790
  }
1757
1791
 
1758
- declare const VERSION = "0.59.0";
1792
+ declare const VERSION = "0.60.0";
1759
1793
 
1760
- 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 ExportAssetError, type ExportAssetErrorReason, type ExportImageOptions, type ExportResourceOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type HtmlExportError, type HtmlExportErrorReason, type HtmlExportOptions, type HtmlExportRenderer, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LASER_TRAIL_PRESENCE_KIND, LaserTool, type LaserToolOptions, type LaserTrailEmission, type LaserTrailPresence, type Layer, LayerManager, LocalStorageAdapter, MEASURE_PRESENCE_KIND, type MeasureEmission, type MeasurePresence, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, MinimapController, type MinimapControllerOptions, type NoteElement, NoteTool, type NoteToolOptions, type OverlayRenderer, PING_PRESENCE_KIND, PencilTool, type PencilToolOptions, type PingEmission, PingInput, type PingInputHost, type PingInputOptions, type PingPresence, PingTool, type PingToolOptions, type Point, type PointerState, RemoteLaserOverlay, type RemoteLaserOverlayHost, type RemoteLaserOverlayOptions, RemoteMeasureOverlay, type RemoteMeasureOverlayHost, type RemoteMeasureOverlayOptions, RemotePingOverlay, type RemotePingOverlayHost, type RemotePingOverlayOptions, type RenderStatsSnapshot, type RotateDirection, 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, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isLaserTrailPresence, isMeasurePresence, isNearBezier, isPingPresence, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toLaserTrailPresence, toMeasurePresence, toPingPresence, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
1794
+ 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 ExportAssetError, type ExportAssetErrorReason, type ExportImageOptions, type ExportResourceOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type HtmlExportError, type HtmlExportErrorReason, type HtmlExportOptions, type HtmlExportRenderer, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LASER_TRAIL_PRESENCE_KIND, LaserTool, type LaserToolOptions, type LaserTrailEmission, type LaserTrailPresence, type Layer, LayerManager, LocalStorageAdapter, MEASURE_PRESENCE_KIND, type MeasureEmission, type MeasurePresence, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, MinimapController, type MinimapControllerOptions, type NoteElement, NoteTool, type NoteToolOptions, type OverlayRenderer, PING_PRESENCE_KIND, PencilTool, type PencilToolOptions, type PingEmission, PingInput, type PingInputHost, type PingInputOptions, type PingPresence, PingTool, type PingToolOptions, type Point, type PointerState, RemoteLaserOverlay, type RemoteLaserOverlayHost, type RemoteLaserOverlayOptions, RemoteMeasureOverlay, type RemoteMeasureOverlayHost, type RemoteMeasureOverlayOptions, RemotePingOverlay, type RemotePingOverlayHost, type RemotePingOverlayOptions, type RenderStatsSnapshot, type RotateDirection, SelectTool, type SelectionStyleDetails, 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, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isLaserTrailPresence, isMeasurePresence, isNearBezier, isPingPresence, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toLaserTrailPresence, toMeasurePresence, toPingPresence, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
package/dist/index.js CHANGED
@@ -5170,6 +5170,7 @@ var ToolManager = class {
5170
5170
  tools = /* @__PURE__ */ new Map();
5171
5171
  current = null;
5172
5172
  changeListeners = /* @__PURE__ */ new Set();
5173
+ registerListeners = /* @__PURE__ */ new Set();
5173
5174
  get activeTool() {
5174
5175
  return this.current;
5175
5176
  }
@@ -5178,6 +5179,7 @@ var ToolManager = class {
5178
5179
  }
5179
5180
  register(tool) {
5180
5181
  this.tools.set(tool.name, tool);
5182
+ this.registerListeners.forEach((fn) => fn(tool));
5181
5183
  }
5182
5184
  getTool(name) {
5183
5185
  return this.tools.get(name);
@@ -5203,6 +5205,10 @@ var ToolManager = class {
5203
5205
  this.changeListeners.add(listener);
5204
5206
  return () => this.changeListeners.delete(listener);
5205
5207
  }
5208
+ onRegister(listener) {
5209
+ this.registerListeners.add(listener);
5210
+ return () => this.registerListeners.delete(listener);
5211
+ }
5206
5212
  };
5207
5213
 
5208
5214
  // src/history/history-stack.ts
@@ -5394,6 +5400,7 @@ var HistoryRecorder = class {
5394
5400
  generation = 0;
5395
5401
  updateSnapshots = /* @__PURE__ */ new Map();
5396
5402
  unsubscribers;
5403
+ transactionEndListeners = /* @__PURE__ */ new Set();
5397
5404
  pause() {
5398
5405
  this.recording = false;
5399
5406
  }
@@ -5411,19 +5418,35 @@ var HistoryRecorder = class {
5411
5418
  get currentTransactionId() {
5412
5419
  return this.transaction !== null ? this.generation : null;
5413
5420
  }
5421
+ onTransactionEnd(listener) {
5422
+ this.transactionEndListeners.add(listener);
5423
+ return () => this.transactionEndListeners.delete(listener);
5424
+ }
5425
+ notifyTransactionEnd() {
5426
+ for (const listener of this.transactionEndListeners) {
5427
+ try {
5428
+ listener();
5429
+ } catch {
5430
+ }
5431
+ }
5432
+ }
5414
5433
  commit() {
5415
5434
  if (!this.transaction) return;
5416
5435
  const finalCommands = this.flushUpdateSnapshots();
5417
5436
  const all = [...this.transaction, ...finalCommands];
5418
5437
  this.transaction = null;
5419
5438
  this.updateSnapshots.clear();
5420
- if (all.length === 0) return;
5421
- const first = all[0];
5422
- this.stack.push(all.length === 1 && first ? first : new BatchCommand(all));
5439
+ if (all.length > 0) {
5440
+ const first = all[0];
5441
+ this.stack.push(all.length === 1 && first ? first : new BatchCommand(all));
5442
+ }
5443
+ this.notifyTransactionEnd();
5423
5444
  }
5424
5445
  rollback() {
5446
+ const wasOpen = this.transaction !== null;
5425
5447
  this.transaction = null;
5426
5448
  this.updateSnapshots.clear();
5449
+ if (wasOpen) this.notifyTransactionEnd();
5427
5450
  }
5428
5451
  destroy() {
5429
5452
  this.unsubscribers.forEach((fn) => fn());
@@ -7715,6 +7738,14 @@ function rotateElementPatch(el, bounds, pivot, delta) {
7715
7738
  }
7716
7739
 
7717
7740
  // src/canvas/selection-ops.ts
7741
+ var STYLE_FIELDS = [
7742
+ "color",
7743
+ "fillColor",
7744
+ "strokeWidth",
7745
+ "opacity",
7746
+ "fontSize",
7747
+ "strokeStyle"
7748
+ ];
7718
7749
  function sharedValue(values) {
7719
7750
  const present = values.filter((v) => v !== void 0);
7720
7751
  if (present.length === 0) return void 0;
@@ -7749,6 +7780,42 @@ var SelectionOps = class {
7749
7780
  if (strokeStyle !== void 0) result.strokeStyle = strokeStyle;
7750
7781
  return result;
7751
7782
  }
7783
+ /**
7784
+ * Unlike `getStyle()` — which returns `{}` for a selection whose elements
7785
+ * have no applicable style fields (e.g. images) — this returns `null` when
7786
+ * no field is applicable, so callers can distinguish "nothing to show" from
7787
+ * "everything shared but empty".
7788
+ */
7789
+ getStyleDetails() {
7790
+ const ids = this.deps.getSelectedIds();
7791
+ if (ids.length === 0) return null;
7792
+ const styles = [];
7793
+ for (const id of ids) {
7794
+ const el = this.deps.store.getById(id);
7795
+ if (el) styles.push(getElementStyle(el));
7796
+ }
7797
+ if (styles.length === 0) return null;
7798
+ const common = {};
7799
+ const applicable = [];
7800
+ const mixed = [];
7801
+ for (const field of STYLE_FIELDS) {
7802
+ const allValues = styles.map((s) => s[field]);
7803
+ const values = [];
7804
+ for (const v of allValues) {
7805
+ if (v !== void 0) values.push(v);
7806
+ }
7807
+ if (values.length === 0) continue;
7808
+ applicable.push(field);
7809
+ const distinct = new Set(values);
7810
+ if (distinct.size > 1) {
7811
+ mixed.push(field);
7812
+ } else {
7813
+ common[field] = values[0];
7814
+ }
7815
+ }
7816
+ if (applicable.length === 0) return null;
7817
+ return { common, applicable, mixed };
7818
+ }
7752
7819
  applyStyle(style) {
7753
7820
  const ids = this.deps.getSelectedIds();
7754
7821
  if (ids.length === 0) return;
@@ -8123,7 +8190,7 @@ var ViewportInteractions = class {
8123
8190
  var EMPTY_IDS = [];
8124
8191
  function noop() {
8125
8192
  }
8126
- var Viewport = class {
8193
+ var Viewport = class _Viewport {
8127
8194
  constructor(container, options = {}) {
8128
8195
  this.container = container;
8129
8196
  this.camera = new Camera(options.camera);
@@ -8132,6 +8199,13 @@ var Viewport = class {
8132
8199
  this.store = new ElementStore();
8133
8200
  this.layerManager = new LayerManager(this.store);
8134
8201
  this.toolManager = new ToolManager();
8202
+ this.unsubToolRegister = this.toolManager.onRegister((tool) => {
8203
+ if (_Viewport.isSelectionSource(tool)) this.attachSelectionSource(tool);
8204
+ });
8205
+ const existingSelect = this.getSelectTool();
8206
+ if (existingSelect && _Viewport.isSelectionSource(existingSelect)) {
8207
+ this.attachSelectionSource(existingSelect);
8208
+ }
8135
8209
  this.renderer = new ElementRenderer();
8136
8210
  this.renderer.setStore(this.store);
8137
8211
  this.renderer.setCamera(this.camera);
@@ -8166,6 +8240,11 @@ var Viewport = class {
8166
8240
  this.dropHandler = options.onDrop;
8167
8241
  this.history = new HistoryStack();
8168
8242
  this.historyRecorder = new HistoryRecorder(this.store, this.history, this.layerManager);
8243
+ this.unsubRecorderEnd = this.historyRecorder.onTransactionEnd(() => {
8244
+ if (!this.pendingSelectionPrune) return;
8245
+ this.pendingSelectionPrune = false;
8246
+ this.pruneSelection();
8247
+ });
8169
8248
  this.selectionOps = new SelectionOps({
8170
8249
  store: this.store,
8171
8250
  recorder: this.historyRecorder,
@@ -8285,6 +8364,7 @@ var Viewport = class {
8285
8364
  this.domNodeManager.removeDomNode(el.id);
8286
8365
  this.renderLoop.markLayerDirty(el.layerId);
8287
8366
  this.requestRender();
8367
+ this.handleRemovedElement(el.id);
8288
8368
  }),
8289
8369
  this.store.on("update", ({ previous, current }) => {
8290
8370
  if (current.type === "grid") this.gridController.syncContext();
@@ -8299,6 +8379,7 @@ var Viewport = class {
8299
8379
  this.renderLoop.markAllLayersDirty();
8300
8380
  this.gridController.syncContext();
8301
8381
  this.requestRender();
8382
+ this.pruneSelection();
8302
8383
  })
8303
8384
  ];
8304
8385
  this.layerManager.on("change", () => {
@@ -8368,6 +8449,13 @@ var Viewport = class {
8368
8449
  minimap = null;
8369
8450
  htmlRenderers = /* @__PURE__ */ new Map();
8370
8451
  resizeListeners = /* @__PURE__ */ new Set();
8452
+ selectionListeners = /* @__PURE__ */ new Set();
8453
+ detachSelectionSource = null;
8454
+ unsubToolRegister = () => {
8455
+ };
8456
+ pendingSelectionPrune = false;
8457
+ unsubRecorderEnd = () => {
8458
+ };
8371
8459
  get ctx() {
8372
8460
  return this.canvasEl.getContext("2d");
8373
8461
  }
@@ -8626,6 +8714,42 @@ var Viewport = class {
8626
8714
  getSelectTool() {
8627
8715
  return this.toolManager.getTool("select");
8628
8716
  }
8717
+ pruneSelection() {
8718
+ const tool = this.getSelectTool();
8719
+ if (!tool) return;
8720
+ const ids = tool.selectedIds;
8721
+ const filtered = ids.filter((id) => this.store.getById(id) !== void 0);
8722
+ if (filtered.length !== ids.length) tool.setSelection(filtered);
8723
+ }
8724
+ handleRemovedElement(id) {
8725
+ if (!this.getSelectedIds().includes(id)) return;
8726
+ if (this.historyRecorder.currentTransactionId !== null) {
8727
+ this.pendingSelectionPrune = true;
8728
+ return;
8729
+ }
8730
+ this.pruneSelection();
8731
+ }
8732
+ static isSelectionSource(tool) {
8733
+ const candidate = tool;
8734
+ return tool.name === "select" && typeof candidate.onSelectionChange === "function" && typeof candidate.setSelection === "function";
8735
+ }
8736
+ emitSelectionChange() {
8737
+ for (const listener of this.selectionListeners) {
8738
+ try {
8739
+ listener();
8740
+ } catch {
8741
+ }
8742
+ }
8743
+ }
8744
+ attachSelectionSource(tool) {
8745
+ this.detachSelectionSource?.();
8746
+ this.detachSelectionSource = tool.onSelectionChange(() => this.emitSelectionChange());
8747
+ }
8748
+ /**
8749
+ * getSelectedIds() and the onSelectionChange emitter never surface stale ids:
8750
+ * once the enclosing history transaction completes, both reflect
8751
+ * the current selection.
8752
+ */
8629
8753
  getSelectedIds() {
8630
8754
  return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
8631
8755
  }
@@ -8659,13 +8783,29 @@ var Viewport = class {
8659
8783
  if (items.length === 0) return;
8660
8784
  this.contextMenu.open(items, screenPos);
8661
8785
  }
8786
+ /**
8787
+ * Persistent, viewport-owned selection-change emitter. Subscribing works
8788
+ * regardless of whether a select tool is registered yet; it forwards
8789
+ * events from whichever select tool is currently attached via
8790
+ * `toolManager.onRegister`. Never delivers stale ids once the enclosing
8791
+ * history transaction completes.
8792
+ */
8662
8793
  onSelectionChange(listener) {
8663
- const tool = this.getSelectTool();
8664
- return tool ? tool.onSelectionChange(listener) : noop;
8794
+ this.selectionListeners.add(listener);
8795
+ return () => {
8796
+ this.selectionListeners.delete(listener);
8797
+ };
8665
8798
  }
8666
8799
  getSelectionStyle() {
8667
8800
  return this.selectionOps.getStyle();
8668
8801
  }
8802
+ /**
8803
+ * Unlike `getSelectionStyle()` — which returns `{}` for a style-less
8804
+ * selection — this returns `null` when no style field is applicable.
8805
+ */
8806
+ getSelectionStyleDetails() {
8807
+ return this.selectionOps.getStyleDetails();
8808
+ }
8669
8809
  applyStyleToSelection(style) {
8670
8810
  this.selectionOps.applyStyle(style);
8671
8811
  }
@@ -8714,6 +8854,11 @@ var Viewport = class {
8714
8854
  this.inputHandler.destroy();
8715
8855
  this.unsubCamera();
8716
8856
  this.unsubToolChange();
8857
+ this.unsubToolRegister();
8858
+ this.unsubRecorderEnd();
8859
+ this.detachSelectionSource?.();
8860
+ this.detachSelectionSource = null;
8861
+ this.selectionListeners.clear();
8717
8862
  this.unsubStore.forEach((fn) => fn());
8718
8863
  this.resizeObserver?.disconnect();
8719
8864
  this.resizeObserver = null;
@@ -12315,7 +12460,7 @@ var PingTool = class {
12315
12460
  };
12316
12461
 
12317
12462
  // src/index.ts
12318
- var VERSION = "0.59.0";
12463
+ var VERSION = "0.60.0";
12319
12464
  export {
12320
12465
  ArrowTool,
12321
12466
  AutoSave,