@fieldnotes/core 0.19.0 → 0.21.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
@@ -588,12 +588,17 @@ interface ViewportOptions {
588
588
  background?: BackgroundOptions;
589
589
  fontSizePresets?: FontSizePreset[];
590
590
  toolbar?: boolean;
591
+ placeholder?: string;
591
592
  shortcuts?: ShortcutOptions;
592
593
  onHtmlElementMount?: (elementId: string, domId: string | undefined, container: HTMLDivElement) => void;
593
594
  onDrop?: (event: DragEvent, worldPosition: {
594
595
  x: number;
595
596
  y: number;
596
597
  }) => void;
598
+ onImageError?: (info: {
599
+ src: string;
600
+ elementIds: string[];
601
+ }) => void;
597
602
  }
598
603
  declare class Viewport {
599
604
  private readonly container;
@@ -694,12 +699,14 @@ declare class ElementRenderer {
694
699
  private store;
695
700
  private imageCache;
696
701
  private onImageLoad;
702
+ private onImageError;
697
703
  private camera;
698
704
  private canvasSize;
699
705
  private hexTileCache;
700
706
  private hexTileCacheKey;
701
707
  setStore(store: ElementStore): void;
702
708
  setOnImageLoad(callback: () => void): void;
709
+ setOnImageError(callback: (src: string) => void): void;
703
710
  setCamera(camera: Camera): void;
704
711
  setCanvasSize(w: number, h: number): void;
705
712
  isDomElement(element: CanvasElement): boolean;
@@ -717,6 +724,7 @@ declare class ElementRenderer {
717
724
  private renderHexTemplate;
718
725
  private renderRadiusMarker;
719
726
  private renderImage;
727
+ private renderImagePlaceholder;
720
728
  private getHexTile;
721
729
  private getImage;
722
730
  }
@@ -724,6 +732,7 @@ declare class ElementRenderer {
724
732
  interface NoteEditorOptions {
725
733
  fontSizePresets?: FontSizePreset[];
726
734
  toolbar?: boolean;
735
+ placeholder?: string;
727
736
  }
728
737
  declare class NoteEditor {
729
738
  private editingId;
@@ -731,9 +740,11 @@ declare class NoteEditor {
731
740
  private blurHandler;
732
741
  private keyHandler;
733
742
  private pointerHandler;
743
+ private inputHandler;
734
744
  private pendingEditId;
735
745
  private onStopCallback;
736
746
  private toolbar;
747
+ private readonly placeholder;
737
748
  constructor(options?: NoteEditorOptions);
738
749
  get isEditing(): boolean;
739
750
  get editingElementId(): string | null;
@@ -756,6 +767,7 @@ interface StyledRun extends RunStyle {
756
767
  text: string;
757
768
  }
758
769
  declare function sanitizeNoteHtml(html: string): string;
770
+ declare function isNoteContentEmpty(html: string): boolean;
759
771
 
760
772
  interface ActiveFormats {
761
773
  bold: boolean;
@@ -985,6 +997,7 @@ declare class SelectTool implements Tool {
985
997
  private pendingSingleSelectId;
986
998
  private hasDragged;
987
999
  private resizeAspectRatio;
1000
+ private hoveredId;
988
1001
  get selectedIds(): string[];
989
1002
  setSelection(ids: string[]): void;
990
1003
  get isMarqueeActive(): boolean;
@@ -999,6 +1012,7 @@ declare class SelectTool implements Tool {
999
1012
  private updateArrowsBoundTo;
1000
1013
  nudgeSelection(dx: number, dy: number, ctx: ToolContext): boolean;
1001
1014
  private updateHoverCursor;
1015
+ private setHovered;
1002
1016
  private handleResize;
1003
1017
  private hitTestResizeHandle;
1004
1018
  private getHandlePositions;
@@ -1233,6 +1247,6 @@ declare class UpdateLayerCommand implements Command {
1233
1247
  undo(_store: ElementStore): void;
1234
1248
  }
1235
1249
 
1236
- declare const VERSION = "0.19.0";
1250
+ declare const VERSION = "0.21.0";
1237
1251
 
1238
- export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type InputHandlerOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
1252
+ export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type InputHandlerOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, isNoteContentEmpty, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
package/dist/index.d.ts CHANGED
@@ -588,12 +588,17 @@ interface ViewportOptions {
588
588
  background?: BackgroundOptions;
589
589
  fontSizePresets?: FontSizePreset[];
590
590
  toolbar?: boolean;
591
+ placeholder?: string;
591
592
  shortcuts?: ShortcutOptions;
592
593
  onHtmlElementMount?: (elementId: string, domId: string | undefined, container: HTMLDivElement) => void;
593
594
  onDrop?: (event: DragEvent, worldPosition: {
594
595
  x: number;
595
596
  y: number;
596
597
  }) => void;
598
+ onImageError?: (info: {
599
+ src: string;
600
+ elementIds: string[];
601
+ }) => void;
597
602
  }
598
603
  declare class Viewport {
599
604
  private readonly container;
@@ -694,12 +699,14 @@ declare class ElementRenderer {
694
699
  private store;
695
700
  private imageCache;
696
701
  private onImageLoad;
702
+ private onImageError;
697
703
  private camera;
698
704
  private canvasSize;
699
705
  private hexTileCache;
700
706
  private hexTileCacheKey;
701
707
  setStore(store: ElementStore): void;
702
708
  setOnImageLoad(callback: () => void): void;
709
+ setOnImageError(callback: (src: string) => void): void;
703
710
  setCamera(camera: Camera): void;
704
711
  setCanvasSize(w: number, h: number): void;
705
712
  isDomElement(element: CanvasElement): boolean;
@@ -717,6 +724,7 @@ declare class ElementRenderer {
717
724
  private renderHexTemplate;
718
725
  private renderRadiusMarker;
719
726
  private renderImage;
727
+ private renderImagePlaceholder;
720
728
  private getHexTile;
721
729
  private getImage;
722
730
  }
@@ -724,6 +732,7 @@ declare class ElementRenderer {
724
732
  interface NoteEditorOptions {
725
733
  fontSizePresets?: FontSizePreset[];
726
734
  toolbar?: boolean;
735
+ placeholder?: string;
727
736
  }
728
737
  declare class NoteEditor {
729
738
  private editingId;
@@ -731,9 +740,11 @@ declare class NoteEditor {
731
740
  private blurHandler;
732
741
  private keyHandler;
733
742
  private pointerHandler;
743
+ private inputHandler;
734
744
  private pendingEditId;
735
745
  private onStopCallback;
736
746
  private toolbar;
747
+ private readonly placeholder;
737
748
  constructor(options?: NoteEditorOptions);
738
749
  get isEditing(): boolean;
739
750
  get editingElementId(): string | null;
@@ -756,6 +767,7 @@ interface StyledRun extends RunStyle {
756
767
  text: string;
757
768
  }
758
769
  declare function sanitizeNoteHtml(html: string): string;
770
+ declare function isNoteContentEmpty(html: string): boolean;
759
771
 
760
772
  interface ActiveFormats {
761
773
  bold: boolean;
@@ -985,6 +997,7 @@ declare class SelectTool implements Tool {
985
997
  private pendingSingleSelectId;
986
998
  private hasDragged;
987
999
  private resizeAspectRatio;
1000
+ private hoveredId;
988
1001
  get selectedIds(): string[];
989
1002
  setSelection(ids: string[]): void;
990
1003
  get isMarqueeActive(): boolean;
@@ -999,6 +1012,7 @@ declare class SelectTool implements Tool {
999
1012
  private updateArrowsBoundTo;
1000
1013
  nudgeSelection(dx: number, dy: number, ctx: ToolContext): boolean;
1001
1014
  private updateHoverCursor;
1015
+ private setHovered;
1002
1016
  private handleResize;
1003
1017
  private hitTestResizeHandle;
1004
1018
  private getHandlePositions;
@@ -1233,6 +1247,6 @@ declare class UpdateLayerCommand implements Command {
1233
1247
  undo(_store: ElementStore): void;
1234
1248
  }
1235
1249
 
1236
- declare const VERSION = "0.19.0";
1250
+ declare const VERSION = "0.21.0";
1237
1251
 
1238
- export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type InputHandlerOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
1252
+ export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type InputHandlerOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, isNoteContentEmpty, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
package/dist/index.js CHANGED
@@ -273,6 +273,12 @@ function sanitizeNode(node) {
273
273
  sanitizeNode(el);
274
274
  }
275
275
  }
276
+ function isNoteContentEmpty(html) {
277
+ if (!html) return true;
278
+ const doc = new DOMParser().parseFromString(html, "text/html");
279
+ const text = doc.body.textContent ?? "";
280
+ return text.replace(/\u00a0/g, " ").trim().length === 0;
281
+ }
276
282
  function sanitizeAttributes(el, tag) {
277
283
  const attrs = Array.from(el.attributes);
278
284
  for (const attr of attrs) {
@@ -2603,6 +2609,7 @@ var ElementRenderer = class {
2603
2609
  store = null;
2604
2610
  imageCache = /* @__PURE__ */ new Map();
2605
2611
  onImageLoad = null;
2612
+ onImageError = null;
2606
2613
  camera = null;
2607
2614
  canvasSize = null;
2608
2615
  hexTileCache = null;
@@ -2613,6 +2620,9 @@ var ElementRenderer = class {
2613
2620
  setOnImageLoad(callback) {
2614
2621
  this.onImageLoad = callback;
2615
2622
  }
2623
+ setOnImageError(callback) {
2624
+ this.onImageError = callback;
2625
+ }
2616
2626
  setCamera(camera) {
2617
2627
  this.camera = camera;
2618
2628
  }
@@ -2971,6 +2981,10 @@ var ElementRenderer = class {
2971
2981
  ctx.restore();
2972
2982
  }
2973
2983
  renderImage(ctx, image) {
2984
+ if (this.imageCache.get(image.src) === "failed") {
2985
+ this.renderImagePlaceholder(ctx, image);
2986
+ return;
2987
+ }
2974
2988
  const img = this.getImage(image.src);
2975
2989
  if (!img) return;
2976
2990
  ctx.drawImage(
@@ -2981,6 +2995,27 @@ var ElementRenderer = class {
2981
2995
  image.size.h
2982
2996
  );
2983
2997
  }
2998
+ renderImagePlaceholder(ctx, image) {
2999
+ const { x, y } = image.position;
3000
+ const { w, h } = image.size;
3001
+ ctx.save();
3002
+ ctx.fillStyle = "#eeeeee";
3003
+ ctx.fillRect(x, y, w, h);
3004
+ ctx.strokeStyle = "#bdbdbd";
3005
+ ctx.lineWidth = 1;
3006
+ ctx.strokeRect(x, y, w, h);
3007
+ const glyph = Math.min(24, w / 2, h / 2);
3008
+ const cx = x + w / 2;
3009
+ const cy = y + h / 2;
3010
+ ctx.strokeStyle = "#9e9e9e";
3011
+ ctx.lineWidth = 2;
3012
+ ctx.beginPath();
3013
+ ctx.arc(cx, cy, glyph / 2, 0, Math.PI * 2);
3014
+ ctx.moveTo(cx - glyph / 2, cy + glyph / 2);
3015
+ ctx.lineTo(cx + glyph / 2, cy - glyph / 2);
3016
+ ctx.stroke();
3017
+ ctx.restore();
3018
+ }
2984
3019
  getHexTile(cellSize, orientation, strokeColor, strokeWidth, opacity, scale) {
2985
3020
  const key = `${cellSize}:${orientation}:${strokeColor}:${strokeWidth}:${opacity}:${scale}`;
2986
3021
  if (this.hexTileCacheKey === key && this.hexTileCache) {
@@ -2996,6 +3031,7 @@ var ElementRenderer = class {
2996
3031
  getImage(src) {
2997
3032
  const cached = this.imageCache.get(src);
2998
3033
  if (cached) {
3034
+ if (cached === "failed") return null;
2999
3035
  if (cached instanceof HTMLImageElement) return cached.complete ? cached : null;
3000
3036
  return cached;
3001
3037
  }
@@ -3012,6 +3048,11 @@ var ElementRenderer = class {
3012
3048
  });
3013
3049
  }
3014
3050
  };
3051
+ img.onerror = () => {
3052
+ this.imageCache.set(src, "failed");
3053
+ this.onImageError?.(src);
3054
+ this.onImageLoad?.();
3055
+ };
3015
3056
  return null;
3016
3057
  }
3017
3058
  };
@@ -3364,17 +3405,36 @@ var FORMAT_SHORTCUTS = {
3364
3405
  i: toggleItalic,
3365
3406
  u: toggleUnderline
3366
3407
  };
3408
+ function ensureEditorStyles() {
3409
+ if (document.querySelector("style[data-fieldnotes-editor]")) return;
3410
+ const style = document.createElement("style");
3411
+ style.setAttribute("data-fieldnotes-editor", "");
3412
+ style.textContent = `[data-fn-placeholder][data-fn-empty='true']::before {
3413
+ content: attr(data-fn-placeholder);
3414
+ color: #9e9e9e;
3415
+ position: absolute;
3416
+ pointer-events: none;
3417
+ }`;
3418
+ document.head.appendChild(style);
3419
+ }
3420
+ function isNodeEmpty(node) {
3421
+ const text = node.textContent ?? "";
3422
+ return text.replace(/\u00a0/g, " ").trim().length === 0;
3423
+ }
3367
3424
  var NoteEditor = class {
3368
3425
  editingId = null;
3369
3426
  editingNode = null;
3370
3427
  blurHandler = null;
3371
3428
  keyHandler = null;
3372
3429
  pointerHandler = null;
3430
+ inputHandler = null;
3373
3431
  pendingEditId = null;
3374
3432
  onStopCallback = null;
3375
3433
  toolbar;
3434
+ placeholder;
3376
3435
  constructor(options) {
3377
3436
  this.toolbar = options?.toolbar === false ? null : new NoteToolbar(options?.fontSizePresets);
3437
+ this.placeholder = options?.placeholder ?? "Type\u2026";
3378
3438
  }
3379
3439
  get isEditing() {
3380
3440
  return this.editingId !== null;
@@ -3409,6 +3469,11 @@ var NoteEditor = class {
3409
3469
  if (this.pointerHandler) {
3410
3470
  this.editingNode.removeEventListener("pointerdown", this.pointerHandler);
3411
3471
  }
3472
+ if (this.inputHandler) {
3473
+ this.editingNode.removeEventListener("input", this.inputHandler);
3474
+ }
3475
+ this.editingNode.removeAttribute("data-fn-placeholder");
3476
+ this.editingNode.removeAttribute("data-fn-empty");
3412
3477
  const text = sanitizeNoteHtml(this.editingNode.innerHTML);
3413
3478
  store.update(this.editingId, { text });
3414
3479
  this.editingNode.contentEditable = "false";
@@ -3425,6 +3490,7 @@ var NoteEditor = class {
3425
3490
  this.blurHandler = null;
3426
3491
  this.keyHandler = null;
3427
3492
  this.pointerHandler = null;
3493
+ this.inputHandler = null;
3428
3494
  }
3429
3495
  destroy(store) {
3430
3496
  this.pendingEditId = null;
@@ -3455,6 +3521,13 @@ var NoteEditor = class {
3455
3521
  selection.removeAllRanges();
3456
3522
  selection.addRange(range);
3457
3523
  }
3524
+ ensureEditorStyles();
3525
+ node.setAttribute("data-fn-placeholder", this.placeholder);
3526
+ node.setAttribute("data-fn-empty", String(isNodeEmpty(node)));
3527
+ this.inputHandler = () => {
3528
+ node.setAttribute("data-fn-empty", String(isNodeEmpty(node)));
3529
+ };
3530
+ node.addEventListener("input", this.inputHandler);
3458
3531
  this.toolbar?.show(node);
3459
3532
  this.blurHandler = (e) => {
3460
3533
  const related = e.relatedTarget;
@@ -4891,9 +4964,21 @@ var Viewport = class {
4891
4964
  this.renderLoop.markAllLayersDirty();
4892
4965
  this.requestRender();
4893
4966
  });
4967
+ this.renderer.setOnImageError((src) => {
4968
+ const elementIds = [];
4969
+ for (const el of this.store.getAll()) {
4970
+ if (el.type === "image" && el.src === src) elementIds.push(el.id);
4971
+ }
4972
+ if (options.onImageError) {
4973
+ options.onImageError({ src, elementIds });
4974
+ } else {
4975
+ console.warn(`[fieldnotes] image failed to load: ${src}`);
4976
+ }
4977
+ });
4894
4978
  this.noteEditor = new NoteEditor({
4895
4979
  fontSizePresets: options.fontSizePresets,
4896
- toolbar: options.toolbar
4980
+ toolbar: options.toolbar,
4981
+ placeholder: options.placeholder
4897
4982
  });
4898
4983
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
4899
4984
  this.onHtmlElementMount = options.onHtmlElementMount;
@@ -5239,7 +5324,16 @@ var Viewport = class {
5239
5324
  }
5240
5325
  onTextEditStop(elementId) {
5241
5326
  const element = this.store.getById(elementId);
5242
- if (!element || element.type !== "text") return;
5327
+ if (!element) return;
5328
+ if (element.type === "note") {
5329
+ if (isNoteContentEmpty(element.text)) {
5330
+ this.historyRecorder.begin();
5331
+ this.store.remove(elementId);
5332
+ this.historyRecorder.commit();
5333
+ }
5334
+ return;
5335
+ }
5336
+ if (element.type !== "text") return;
5243
5337
  if (!element.text || element.text.trim() === "") {
5244
5338
  this.historyRecorder.begin();
5245
5339
  this.store.remove(elementId);
@@ -5585,6 +5679,43 @@ var PencilTool = class {
5585
5679
  }
5586
5680
  };
5587
5681
 
5682
+ // src/elements/stroke-hit.ts
5683
+ function distSqToSegment(p, a, b) {
5684
+ const abx = b.x - a.x;
5685
+ const aby = b.y - a.y;
5686
+ const apx = p.x - a.x;
5687
+ const apy = p.y - a.y;
5688
+ const lenSq = abx * abx + aby * aby;
5689
+ if (lenSq === 0) {
5690
+ return apx * apx + apy * apy;
5691
+ }
5692
+ const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
5693
+ const dx = p.x - (a.x + t * abx);
5694
+ const dy = p.y - (a.y + t * aby);
5695
+ return dx * dx + dy * dy;
5696
+ }
5697
+ function hitTestStroke(stroke, point, radius) {
5698
+ const bounds = getElementBounds(stroke);
5699
+ if (!bounds) return false;
5700
+ if (point.x < bounds.x - radius || point.x > bounds.x + bounds.w + radius || point.y < bounds.y - radius || point.y > bounds.y + bounds.h + radius) {
5701
+ return false;
5702
+ }
5703
+ const radiusSq = radius * radius;
5704
+ const local = { x: point.x - stroke.position.x, y: point.y - stroke.position.y };
5705
+ const { segments } = getStrokeRenderData(stroke);
5706
+ if (segments.length === 0) {
5707
+ const p = stroke.points[0];
5708
+ if (!p) return false;
5709
+ const dx = p.x - local.x;
5710
+ const dy = p.y - local.y;
5711
+ return dx * dx + dy * dy <= radiusSq;
5712
+ }
5713
+ for (const seg of segments) {
5714
+ if (distSqToSegment(local, seg.start, seg.end) <= radiusSq) return true;
5715
+ }
5716
+ return false;
5717
+ }
5718
+
5588
5719
  // src/tools/eraser-tool.ts
5589
5720
  var DEFAULT_RADIUS = 20;
5590
5721
  function makeEraserCursor(radius) {
@@ -5643,12 +5774,7 @@ var EraserTool = class {
5643
5774
  if (erased) ctx.requestRender();
5644
5775
  }
5645
5776
  strokeIntersects(stroke, point) {
5646
- const radiusSq = this.radius * this.radius;
5647
- return stroke.points.some((p) => {
5648
- const dx = p.x + stroke.position.x - point.x;
5649
- const dy = p.y + stroke.position.y - point.y;
5650
- return dx * dx + dy * dy <= radiusSq;
5651
- });
5777
+ return hitTestStroke(stroke, point, this.radius);
5652
5778
  }
5653
5779
  };
5654
5780
 
@@ -5788,6 +5914,7 @@ var SelectTool = class {
5788
5914
  pendingSingleSelectId = null;
5789
5915
  hasDragged = false;
5790
5916
  resizeAspectRatio = 0;
5917
+ hoveredId = null;
5791
5918
  get selectedIds() {
5792
5919
  return [...this._selectedIds];
5793
5920
  }
@@ -5804,6 +5931,7 @@ var SelectTool = class {
5804
5931
  onDeactivate(ctx) {
5805
5932
  this._selectedIds = [];
5806
5933
  this.mode = { type: "idle" };
5934
+ this.hoveredId = null;
5807
5935
  ctx.setCursor?.("default");
5808
5936
  }
5809
5937
  snap(point, ctx) {
@@ -5811,6 +5939,7 @@ var SelectTool = class {
5811
5939
  }
5812
5940
  onPointerDown(state, ctx) {
5813
5941
  this.ctx = ctx;
5942
+ this.setHovered(null, ctx);
5814
5943
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
5815
5944
  this.lastWorld = this.snap(world, ctx);
5816
5945
  this.currentWorld = world;
@@ -5953,7 +6082,8 @@ var SelectTool = class {
5953
6082
  }
5954
6083
  onHover(state, ctx) {
5955
6084
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
5956
- this.updateHoverCursor(world, ctx);
6085
+ const hoverId = this.updateHoverCursor(world, ctx);
6086
+ this.setHovered(hoverId, ctx);
5957
6087
  }
5958
6088
  renderOverlay(canvasCtx) {
5959
6089
  this.renderMarquee(canvasCtx);
@@ -5974,6 +6104,23 @@ var SelectTool = class {
5974
6104
  canvasCtx.restore();
5975
6105
  }
5976
6106
  }
6107
+ if (this.hoveredId && this.ctx && this.mode.type === "idle") {
6108
+ if (!this._selectedIds.includes(this.hoveredId)) {
6109
+ const el = this.ctx.store.getById(this.hoveredId);
6110
+ if (el) {
6111
+ const b = getElementBounds(el);
6112
+ if (b) {
6113
+ canvasCtx.save();
6114
+ canvasCtx.strokeStyle = "#2196F3";
6115
+ canvasCtx.globalAlpha = 0.35;
6116
+ canvasCtx.lineWidth = 1.5 / this.ctx.camera.zoom;
6117
+ canvasCtx.setLineDash([]);
6118
+ canvasCtx.strokeRect(b.x, b.y, b.w, b.h);
6119
+ canvasCtx.restore();
6120
+ }
6121
+ }
6122
+ }
6123
+ }
5977
6124
  }
5978
6125
  updateArrowsBoundTo(ids, ctx) {
5979
6126
  const movedNonArrowIds = /* @__PURE__ */ new Set();
@@ -6022,20 +6169,26 @@ var SelectTool = class {
6022
6169
  const arrowHit = hitTestArrowHandles(world, this._selectedIds, ctx);
6023
6170
  if (arrowHit) {
6024
6171
  ctx.setCursor?.(getArrowHandleCursor(arrowHit.handle, false));
6025
- return;
6172
+ return null;
6026
6173
  }
6027
6174
  const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
6028
6175
  if (templateResizeHit) {
6029
6176
  ctx.setCursor?.("nwse-resize");
6030
- return;
6177
+ return null;
6031
6178
  }
6032
6179
  const resizeHit = this.hitTestResizeHandle(world, ctx);
6033
6180
  if (resizeHit) {
6034
6181
  ctx.setCursor?.(HANDLE_CURSORS[resizeHit.handle]);
6035
- return;
6182
+ return null;
6036
6183
  }
6037
6184
  const hit = this.hitTest(world, ctx);
6038
6185
  ctx.setCursor?.(hit ? "move" : "default");
6186
+ return hit ? hit.id : null;
6187
+ }
6188
+ setHovered(id, ctx) {
6189
+ if (this.hoveredId === id) return;
6190
+ this.hoveredId = id;
6191
+ ctx.requestRender();
6039
6192
  }
6040
6193
  handleResize(world, ctx, shiftKey = false) {
6041
6194
  if (this.mode.type !== "resizing") return;
@@ -6301,12 +6454,7 @@ var SelectTool = class {
6301
6454
  return point.x >= el.position.x && point.x <= el.position.x + s.w && point.y >= el.position.y && point.y <= el.position.y + s.h;
6302
6455
  }
6303
6456
  if (el.type === "stroke") {
6304
- const HIT_RADIUS = 10;
6305
- return el.points.some((p) => {
6306
- const dx = p.x + el.position.x - point.x;
6307
- const dy = p.y + el.position.y - point.y;
6308
- return dx * dx + dy * dy <= HIT_RADIUS * HIT_RADIUS;
6309
- });
6457
+ return hitTestStroke(el, point, 10);
6310
6458
  }
6311
6459
  if (el.type === "arrow") {
6312
6460
  return isNearBezier(point, el.from, el.to, el.bend, 10);
@@ -7142,7 +7290,7 @@ var TemplateTool = class {
7142
7290
  };
7143
7291
 
7144
7292
  // src/index.ts
7145
- var VERSION = "0.19.0";
7293
+ var VERSION = "0.21.0";
7146
7294
  export {
7147
7295
  AddElementCommand,
7148
7296
  ArrowTool,
@@ -7216,6 +7364,7 @@ export {
7216
7364
  getHexDistance,
7217
7365
  isBindable,
7218
7366
  isNearBezier,
7367
+ isNoteContentEmpty,
7219
7368
  parseState,
7220
7369
  sanitizeNoteHtml,
7221
7370
  setFontSize,