@fieldnotes/core 0.26.0 → 0.28.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
@@ -50,6 +50,8 @@ interface ArrowElement extends BaseElement {
50
50
  toBinding?: Binding;
51
51
  /** Derived from from/to/bend. Redundant in serialized state — safe to omit. */
52
52
  cachedControlPoint?: Point;
53
+ /** Optional text rendered at the curve midpoint. */
54
+ label?: string;
53
55
  }
54
56
  interface ImageElement extends BaseElement {
55
57
  type: 'image';
@@ -394,6 +396,16 @@ interface RenderStatsSnapshot {
394
396
  frameCount: number;
395
397
  }
396
398
 
399
+ interface ElementStyle {
400
+ color?: string;
401
+ fillColor?: string;
402
+ strokeWidth?: number;
403
+ opacity?: number;
404
+ fontSize?: number;
405
+ }
406
+ declare function styleToPatch(element: CanvasElement, style: ElementStyle): Partial<CanvasElement>;
407
+ declare function getElementStyle(element: CanvasElement): ElementStyle;
408
+
397
409
  interface GridInfo {
398
410
  gridType: 'square' | 'hex';
399
411
  hexOrientation: 'pointy' | 'flat';
@@ -436,6 +448,7 @@ declare class Viewport {
436
448
  private readonly background;
437
449
  private readonly renderer;
438
450
  private readonly noteEditor;
451
+ private readonly arrowLabelEditor;
439
452
  private readonly historyRecorder;
440
453
  readonly toolContext: ToolContext;
441
454
  private readonly marginViewport;
@@ -494,6 +507,11 @@ declare class Viewport {
494
507
  removeGrid(): void;
495
508
  getGridInfo(): GridInfo | null;
496
509
  onGridChange(listener: (info: GridInfo | null) => void): () => void;
510
+ private getSelectTool;
511
+ getSelectedIds(): string[];
512
+ onSelectionChange(listener: () => void): () => void;
513
+ getSelectionStyle(): ElementStyle | null;
514
+ applyStyleToSelection(style: ElementStyle): void;
497
515
  getRenderStats(): RenderStatsSnapshot;
498
516
  logPerformance(intervalMs?: number): () => void;
499
517
  destroy(): void;
@@ -502,6 +520,8 @@ declare class Viewport {
502
520
  private onTextEditStop;
503
521
  private onTapDown;
504
522
  private onDoubleTap;
523
+ private findArrowAt;
524
+ private startArrowLabelEdit;
505
525
  private hitTestWorld;
506
526
  stopInteracting(): void;
507
527
  private onDragOver;
@@ -559,6 +579,7 @@ interface ArrowInput extends BaseDefaults {
559
579
  width?: number;
560
580
  fromBinding?: Binding;
561
581
  toBinding?: Binding;
582
+ label?: string;
562
583
  }
563
584
  interface ImageInput extends BaseDefaults {
564
585
  position: Point;
@@ -700,6 +721,7 @@ declare class EraserTool implements Tool {
700
721
  declare class SelectTool implements Tool {
701
722
  readonly name = "select";
702
723
  private _selectedIds;
724
+ private selectionListeners;
703
725
  private mode;
704
726
  private lastWorld;
705
727
  private currentWorld;
@@ -709,6 +731,8 @@ declare class SelectTool implements Tool {
709
731
  private resizeAspectRatio;
710
732
  private hoveredId;
711
733
  get selectedIds(): string[];
734
+ onSelectionChange(listener: () => void): () => void;
735
+ private setSelectedIds;
712
736
  setSelection(ids: string[]): void;
713
737
  get isMarqueeActive(): boolean;
714
738
  onActivate(ctx: ToolContext): void;
@@ -933,6 +957,6 @@ declare class TemplateTool implements Tool {
933
957
  private notifyOptionsChange;
934
958
  }
935
959
 
936
- declare const VERSION = "0.26.0";
960
+ declare const VERSION = "0.28.0";
937
961
 
938
- export { type ActiveFormats, type ArrowElement, 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, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, 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 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, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
962
+ export { type ActiveFormats, type ArrowElement, 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, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, 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 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, 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
@@ -50,6 +50,8 @@ interface ArrowElement extends BaseElement {
50
50
  toBinding?: Binding;
51
51
  /** Derived from from/to/bend. Redundant in serialized state — safe to omit. */
52
52
  cachedControlPoint?: Point;
53
+ /** Optional text rendered at the curve midpoint. */
54
+ label?: string;
53
55
  }
54
56
  interface ImageElement extends BaseElement {
55
57
  type: 'image';
@@ -394,6 +396,16 @@ interface RenderStatsSnapshot {
394
396
  frameCount: number;
395
397
  }
396
398
 
399
+ interface ElementStyle {
400
+ color?: string;
401
+ fillColor?: string;
402
+ strokeWidth?: number;
403
+ opacity?: number;
404
+ fontSize?: number;
405
+ }
406
+ declare function styleToPatch(element: CanvasElement, style: ElementStyle): Partial<CanvasElement>;
407
+ declare function getElementStyle(element: CanvasElement): ElementStyle;
408
+
397
409
  interface GridInfo {
398
410
  gridType: 'square' | 'hex';
399
411
  hexOrientation: 'pointy' | 'flat';
@@ -436,6 +448,7 @@ declare class Viewport {
436
448
  private readonly background;
437
449
  private readonly renderer;
438
450
  private readonly noteEditor;
451
+ private readonly arrowLabelEditor;
439
452
  private readonly historyRecorder;
440
453
  readonly toolContext: ToolContext;
441
454
  private readonly marginViewport;
@@ -494,6 +507,11 @@ declare class Viewport {
494
507
  removeGrid(): void;
495
508
  getGridInfo(): GridInfo | null;
496
509
  onGridChange(listener: (info: GridInfo | null) => void): () => void;
510
+ private getSelectTool;
511
+ getSelectedIds(): string[];
512
+ onSelectionChange(listener: () => void): () => void;
513
+ getSelectionStyle(): ElementStyle | null;
514
+ applyStyleToSelection(style: ElementStyle): void;
497
515
  getRenderStats(): RenderStatsSnapshot;
498
516
  logPerformance(intervalMs?: number): () => void;
499
517
  destroy(): void;
@@ -502,6 +520,8 @@ declare class Viewport {
502
520
  private onTextEditStop;
503
521
  private onTapDown;
504
522
  private onDoubleTap;
523
+ private findArrowAt;
524
+ private startArrowLabelEdit;
505
525
  private hitTestWorld;
506
526
  stopInteracting(): void;
507
527
  private onDragOver;
@@ -559,6 +579,7 @@ interface ArrowInput extends BaseDefaults {
559
579
  width?: number;
560
580
  fromBinding?: Binding;
561
581
  toBinding?: Binding;
582
+ label?: string;
562
583
  }
563
584
  interface ImageInput extends BaseDefaults {
564
585
  position: Point;
@@ -700,6 +721,7 @@ declare class EraserTool implements Tool {
700
721
  declare class SelectTool implements Tool {
701
722
  readonly name = "select";
702
723
  private _selectedIds;
724
+ private selectionListeners;
703
725
  private mode;
704
726
  private lastWorld;
705
727
  private currentWorld;
@@ -709,6 +731,8 @@ declare class SelectTool implements Tool {
709
731
  private resizeAspectRatio;
710
732
  private hoveredId;
711
733
  get selectedIds(): string[];
734
+ onSelectionChange(listener: () => void): () => void;
735
+ private setSelectedIds;
712
736
  setSelection(ids: string[]): void;
713
737
  get isMarqueeActive(): boolean;
714
738
  onActivate(ctx: ToolContext): void;
@@ -933,6 +957,6 @@ declare class TemplateTool implements Tool {
933
957
  private notifyOptionsChange;
934
958
  }
935
959
 
936
- declare const VERSION = "0.26.0";
960
+ declare const VERSION = "0.28.0";
937
961
 
938
- export { type ActiveFormats, type ArrowElement, 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, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, 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 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, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
962
+ export { type ActiveFormats, type ArrowElement, 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, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, 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 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, 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
@@ -2658,6 +2658,7 @@ function drawHexPath(ctx, cx, cy, cellSize, orientation) {
2658
2658
  var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
2659
2659
  var ARROWHEAD_LENGTH = 12;
2660
2660
  var ARROWHEAD_ANGLE = Math.PI / 6;
2661
+ var ARROW_LABEL_FONT_SIZE = 14;
2661
2662
  var ElementRenderer = class {
2662
2663
  store = null;
2663
2664
  imageCache = /* @__PURE__ */ new Map();
@@ -2668,6 +2669,7 @@ var ElementRenderer = class {
2668
2669
  hexTileCache = null;
2669
2670
  hexTileCacheKey = "";
2670
2671
  gridBoundsOverride = null;
2672
+ labelEditingId = null;
2671
2673
  setStore(store) {
2672
2674
  this.store = store;
2673
2675
  }
@@ -2686,6 +2688,9 @@ var ElementRenderer = class {
2686
2688
  setGridBoundsOverride(bounds) {
2687
2689
  this.gridBoundsOverride = bounds;
2688
2690
  }
2691
+ setLabelEditingId(id) {
2692
+ this.labelEditingId = id;
2693
+ }
2689
2694
  isDomElement(element) {
2690
2695
  return DOM_ELEMENT_TYPES.has(element.type);
2691
2696
  }
@@ -2762,6 +2767,28 @@ var ElementRenderer = class {
2762
2767
  ctx.stroke();
2763
2768
  this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
2764
2769
  ctx.restore();
2770
+ this.renderArrowLabel(ctx, arrow);
2771
+ }
2772
+ renderArrowLabel(ctx, arrow) {
2773
+ if (!arrow.label || arrow.label.length === 0) return;
2774
+ if (arrow.id === this.labelEditingId) return;
2775
+ const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
2776
+ ctx.save();
2777
+ ctx.font = `${ARROW_LABEL_FONT_SIZE}px system-ui, sans-serif`;
2778
+ const metrics = ctx.measureText(arrow.label);
2779
+ const padX = 6;
2780
+ const padY = 4;
2781
+ const w = metrics.width + padX * 2;
2782
+ const h = ARROW_LABEL_FONT_SIZE + padY * 2;
2783
+ ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
2784
+ ctx.beginPath();
2785
+ ctx.roundRect(mid.x - w / 2, mid.y - h / 2, w, h, 4);
2786
+ ctx.fill();
2787
+ ctx.fillStyle = "#1a1a1a";
2788
+ ctx.textAlign = "center";
2789
+ ctx.textBaseline = "middle";
2790
+ ctx.fillText(arrow.label, mid.x, mid.y);
2791
+ ctx.restore();
2765
2792
  }
2766
2793
  renderArrowhead(ctx, arrow, tip, angle) {
2767
2794
  ctx.beginPath();
@@ -3172,6 +3199,7 @@ function createArrow(input) {
3172
3199
  };
3173
3200
  if (input.fromBinding) result.fromBinding = input.fromBinding;
3174
3201
  if (input.toBinding) result.toBinding = input.toBinding;
3202
+ if (input.label !== void 0) result.label = input.label;
3175
3203
  return result;
3176
3204
  }
3177
3205
  function createImage(input) {
@@ -3635,6 +3663,84 @@ var NoteEditor = class {
3635
3663
  }
3636
3664
  };
3637
3665
 
3666
+ // src/elements/arrow-label-editor.ts
3667
+ var ArrowLabelEditor = class {
3668
+ input = null;
3669
+ done = false;
3670
+ get isEditing() {
3671
+ return this.input !== null;
3672
+ }
3673
+ startEditing(start) {
3674
+ if (this.input) this.cleanup();
3675
+ const { arrow, layer, store, recorder, onDone } = start;
3676
+ const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
3677
+ const input = document.createElement("input");
3678
+ input.type = "text";
3679
+ input.value = arrow.label ?? "";
3680
+ Object.assign(input.style, {
3681
+ position: "absolute",
3682
+ left: `${mid.x}px`,
3683
+ top: `${mid.y}px`,
3684
+ transform: "translate(-50%, -50%)",
3685
+ // domLayer is pointer-events:none; the input must opt back in to receive taps/clicks.
3686
+ pointerEvents: "auto",
3687
+ font: "14px system-ui, sans-serif",
3688
+ padding: "2px 6px",
3689
+ border: "1px solid #2196F3",
3690
+ borderRadius: "4px",
3691
+ background: "#ffffff",
3692
+ color: "#1a1a1a",
3693
+ outline: "none",
3694
+ minWidth: "40px"
3695
+ });
3696
+ this.done = false;
3697
+ const commit = () => {
3698
+ if (this.done) return;
3699
+ this.done = true;
3700
+ const next = input.value.trim() || void 0;
3701
+ if (next !== arrow.label) {
3702
+ recorder.begin();
3703
+ store.update(arrow.id, { label: next });
3704
+ recorder.commit();
3705
+ }
3706
+ this.cleanup();
3707
+ onDone();
3708
+ };
3709
+ const cancel = () => {
3710
+ if (this.done) return;
3711
+ this.done = true;
3712
+ this.cleanup();
3713
+ onDone();
3714
+ };
3715
+ input.addEventListener("keydown", (e) => {
3716
+ if (e.key === "Enter") {
3717
+ e.preventDefault();
3718
+ commit();
3719
+ } else if (e.key === "Escape") {
3720
+ e.preventDefault();
3721
+ cancel();
3722
+ }
3723
+ e.stopPropagation();
3724
+ });
3725
+ input.addEventListener("blur", commit);
3726
+ layer.appendChild(input);
3727
+ this.input = input;
3728
+ input.focus();
3729
+ input.select();
3730
+ }
3731
+ /** Abort any in-progress edit without committing (e.g. on viewport teardown). */
3732
+ cancel() {
3733
+ this.done = true;
3734
+ this.cleanup();
3735
+ }
3736
+ cleanup() {
3737
+ if (this.input) {
3738
+ this.input.remove();
3739
+ this.input = null;
3740
+ }
3741
+ }
3742
+ };
3743
+
3638
3744
  // src/tools/tool-manager.ts
3639
3745
  var ToolManager = class {
3640
3746
  tools = /* @__PURE__ */ new Map();
@@ -5155,7 +5261,104 @@ var MarginViewport = class {
5155
5261
  }
5156
5262
  };
5157
5263
 
5264
+ // src/elements/element-style.ts
5265
+ function styleToPatch(element, style) {
5266
+ const { color, fillColor, strokeWidth, opacity, fontSize } = style;
5267
+ switch (element.type) {
5268
+ case "stroke":
5269
+ return {
5270
+ ...color !== void 0 ? { color } : {},
5271
+ ...strokeWidth !== void 0 ? { width: strokeWidth } : {},
5272
+ ...opacity !== void 0 ? { opacity } : {}
5273
+ };
5274
+ case "arrow":
5275
+ return {
5276
+ ...color !== void 0 ? { color } : {},
5277
+ ...strokeWidth !== void 0 ? { width: strokeWidth } : {}
5278
+ };
5279
+ case "shape":
5280
+ return {
5281
+ ...color !== void 0 ? { strokeColor: color } : {},
5282
+ ...fillColor !== void 0 ? { fillColor } : {},
5283
+ ...strokeWidth !== void 0 ? { strokeWidth } : {}
5284
+ };
5285
+ case "text":
5286
+ return {
5287
+ ...color !== void 0 ? { color } : {},
5288
+ ...fontSize !== void 0 ? { fontSize } : {}
5289
+ };
5290
+ case "note":
5291
+ return {
5292
+ ...color !== void 0 ? { textColor: color } : {},
5293
+ ...fillColor !== void 0 ? { backgroundColor: fillColor } : {},
5294
+ ...fontSize !== void 0 ? { fontSize } : {}
5295
+ };
5296
+ case "grid":
5297
+ return {
5298
+ ...color !== void 0 ? { strokeColor: color } : {},
5299
+ ...strokeWidth !== void 0 ? { strokeWidth } : {},
5300
+ ...opacity !== void 0 ? { opacity } : {}
5301
+ };
5302
+ case "template":
5303
+ return {
5304
+ ...color !== void 0 ? { strokeColor: color } : {},
5305
+ ...fillColor !== void 0 ? { fillColor } : {},
5306
+ ...strokeWidth !== void 0 ? { strokeWidth } : {},
5307
+ ...opacity !== void 0 ? { opacity } : {}
5308
+ };
5309
+ default:
5310
+ return {};
5311
+ }
5312
+ }
5313
+ function getElementStyle(element) {
5314
+ switch (element.type) {
5315
+ case "stroke":
5316
+ return { color: element.color, strokeWidth: element.width, opacity: element.opacity };
5317
+ case "arrow":
5318
+ return { color: element.color, strokeWidth: element.width };
5319
+ case "shape":
5320
+ return {
5321
+ color: element.strokeColor,
5322
+ fillColor: element.fillColor,
5323
+ strokeWidth: element.strokeWidth
5324
+ };
5325
+ case "text":
5326
+ return { color: element.color, fontSize: element.fontSize };
5327
+ case "note":
5328
+ return {
5329
+ color: element.textColor,
5330
+ fillColor: element.backgroundColor,
5331
+ ...element.fontSize !== void 0 ? { fontSize: element.fontSize } : {}
5332
+ };
5333
+ case "grid":
5334
+ return {
5335
+ color: element.strokeColor,
5336
+ strokeWidth: element.strokeWidth,
5337
+ opacity: element.opacity
5338
+ };
5339
+ case "template":
5340
+ return {
5341
+ color: element.strokeColor,
5342
+ fillColor: element.fillColor,
5343
+ strokeWidth: element.strokeWidth,
5344
+ opacity: element.opacity
5345
+ };
5346
+ default:
5347
+ return {};
5348
+ }
5349
+ }
5350
+
5158
5351
  // src/canvas/viewport.ts
5352
+ var EMPTY_IDS = [];
5353
+ var ARROW_HIT_THRESHOLD = 10;
5354
+ function noop() {
5355
+ }
5356
+ function sharedValue(values) {
5357
+ const present = values.filter((v) => v !== void 0);
5358
+ if (present.length === 0) return void 0;
5359
+ const first = present[0];
5360
+ return present.every((v) => v === first) ? first : void 0;
5361
+ }
5159
5362
  var Viewport = class {
5160
5363
  constructor(container, options = {}) {
5161
5364
  this.container = container;
@@ -5189,6 +5392,7 @@ var Viewport = class {
5189
5392
  placeholder: options.placeholder
5190
5393
  });
5191
5394
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
5395
+ this.arrowLabelEditor = new ArrowLabelEditor();
5192
5396
  this.noteEditor.setHistoryHooks(
5193
5397
  () => this.historyRecorder.begin(),
5194
5398
  () => this.historyRecorder.commit()
@@ -5315,6 +5519,7 @@ var Viewport = class {
5315
5519
  background;
5316
5520
  renderer;
5317
5521
  noteEditor;
5522
+ arrowLabelEditor;
5318
5523
  historyRecorder;
5319
5524
  toolContext;
5320
5525
  marginViewport;
@@ -5507,6 +5712,52 @@ var Viewport = class {
5507
5712
  this.gridChangeListeners.delete(listener);
5508
5713
  };
5509
5714
  }
5715
+ getSelectTool() {
5716
+ return this.toolManager.getTool("select");
5717
+ }
5718
+ getSelectedIds() {
5719
+ return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
5720
+ }
5721
+ onSelectionChange(listener) {
5722
+ const tool = this.getSelectTool();
5723
+ return tool ? tool.onSelectionChange(listener) : noop;
5724
+ }
5725
+ getSelectionStyle() {
5726
+ const ids = this.getSelectedIds();
5727
+ if (ids.length === 0) return null;
5728
+ const styles = [];
5729
+ for (const id of ids) {
5730
+ const el = this.store.getById(id);
5731
+ if (el) styles.push(getElementStyle(el));
5732
+ }
5733
+ if (styles.length === 0) return null;
5734
+ const result = {};
5735
+ const color = sharedValue(styles.map((s) => s.color));
5736
+ if (color !== void 0) result.color = color;
5737
+ const fillColor = sharedValue(styles.map((s) => s.fillColor));
5738
+ if (fillColor !== void 0) result.fillColor = fillColor;
5739
+ const strokeWidth = sharedValue(styles.map((s) => s.strokeWidth));
5740
+ if (strokeWidth !== void 0) result.strokeWidth = strokeWidth;
5741
+ const opacity = sharedValue(styles.map((s) => s.opacity));
5742
+ if (opacity !== void 0) result.opacity = opacity;
5743
+ const fontSize = sharedValue(styles.map((s) => s.fontSize));
5744
+ if (fontSize !== void 0) result.fontSize = fontSize;
5745
+ return result;
5746
+ }
5747
+ applyStyleToSelection(style) {
5748
+ const ids = this.getSelectedIds();
5749
+ if (ids.length === 0) return;
5750
+ this.historyRecorder.begin();
5751
+ for (const id of ids) {
5752
+ const el = this.store.getById(id);
5753
+ if (!el) continue;
5754
+ const patch = styleToPatch(el, style);
5755
+ if (Object.keys(patch).length > 0) {
5756
+ this.store.update(id, patch);
5757
+ }
5758
+ }
5759
+ this.historyRecorder.commit();
5760
+ }
5510
5761
  getRenderStats() {
5511
5762
  return this.renderLoop.getStats();
5512
5763
  }
@@ -5523,6 +5774,7 @@ var Viewport = class {
5523
5774
  this.renderLoop.stop();
5524
5775
  this.interactMode.destroy();
5525
5776
  this.noteEditor.destroy(this.store);
5777
+ this.arrowLabelEditor.cancel();
5526
5778
  this.historyRecorder.destroy();
5527
5779
  this.wrapper.removeEventListener("pointerdown", this.onTapDown);
5528
5780
  this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
@@ -5608,8 +5860,35 @@ var Viewport = class {
5608
5860
  const hit = this.hitTestWorld(world);
5609
5861
  if (hit?.type === "html") {
5610
5862
  this.interactMode.startInteracting(hit.id);
5863
+ return;
5864
+ }
5865
+ const arrow = this.findArrowAt(world);
5866
+ if (arrow) {
5867
+ this.startArrowLabelEdit(arrow);
5611
5868
  }
5612
5869
  };
5870
+ findArrowAt(world) {
5871
+ const candidates = this.store.queryPoint(world).reverse();
5872
+ for (const el of candidates) {
5873
+ if (el.type === "arrow" && isNearBezier(world, el.from, el.to, el.bend, ARROW_HIT_THRESHOLD)) {
5874
+ return el;
5875
+ }
5876
+ }
5877
+ return void 0;
5878
+ }
5879
+ startArrowLabelEdit(arrow) {
5880
+ this.arrowLabelEditor.startEditing({
5881
+ arrow,
5882
+ layer: this.domLayer,
5883
+ store: this.store,
5884
+ recorder: this.historyRecorder,
5885
+ onDone: () => {
5886
+ this.renderer.setLabelEditingId(null);
5887
+ this.requestRender();
5888
+ }
5889
+ });
5890
+ this.renderer.setLabelEditingId(arrow.id);
5891
+ }
5613
5892
  hitTestWorld(world) {
5614
5893
  const candidates = this.store.queryPoint(world).reverse();
5615
5894
  for (const el of candidates) {
@@ -6122,6 +6401,7 @@ var HANDLE_CURSORS = {
6122
6401
  var SelectTool = class {
6123
6402
  name = "select";
6124
6403
  _selectedIds = [];
6404
+ selectionListeners = /* @__PURE__ */ new Set();
6125
6405
  mode = { type: "idle" };
6126
6406
  lastWorld = { x: 0, y: 0 };
6127
6407
  currentWorld = { x: 0, y: 0 };
@@ -6131,10 +6411,22 @@ var SelectTool = class {
6131
6411
  resizeAspectRatio = 0;
6132
6412
  hoveredId = null;
6133
6413
  get selectedIds() {
6134
- return [...this._selectedIds];
6414
+ return this._selectedIds;
6135
6415
  }
6136
- setSelection(ids) {
6416
+ onSelectionChange(listener) {
6417
+ this.selectionListeners.add(listener);
6418
+ return () => {
6419
+ this.selectionListeners.delete(listener);
6420
+ };
6421
+ }
6422
+ setSelectedIds(ids) {
6423
+ const prev = this._selectedIds;
6424
+ if (prev.length === ids.length && prev.every((id, i) => id === ids[i])) return;
6137
6425
  this._selectedIds = ids;
6426
+ for (const listener of this.selectionListeners) listener();
6427
+ }
6428
+ setSelection(ids) {
6429
+ this.setSelectedIds(ids);
6138
6430
  this.ctx?.requestRender();
6139
6431
  }
6140
6432
  get isMarqueeActive() {
@@ -6144,7 +6436,7 @@ var SelectTool = class {
6144
6436
  this.ctx = ctx;
6145
6437
  }
6146
6438
  onDeactivate(ctx) {
6147
- this._selectedIds = [];
6439
+ this.setSelectedIds([]);
6148
6440
  this.mode = { type: "idle" };
6149
6441
  this.hoveredId = null;
6150
6442
  ctx.setCursor?.("default");
@@ -6195,22 +6487,22 @@ var SelectTool = class {
6195
6487
  const alreadySelected = this._selectedIds.includes(hit.id);
6196
6488
  if (state.shiftKey) {
6197
6489
  if (alreadySelected) {
6198
- this._selectedIds = this._selectedIds.filter((id) => id !== hit.id);
6490
+ this.setSelectedIds(this._selectedIds.filter((id) => id !== hit.id));
6199
6491
  this.mode = { type: "idle" };
6200
6492
  } else {
6201
- this._selectedIds = [...this._selectedIds, hit.id];
6493
+ this.setSelectedIds([...this._selectedIds, hit.id]);
6202
6494
  this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
6203
6495
  }
6204
6496
  } else {
6205
6497
  if (!alreadySelected) {
6206
- this._selectedIds = [hit.id];
6498
+ this.setSelectedIds([hit.id]);
6207
6499
  } else if (this._selectedIds.length > 1) {
6208
6500
  this.pendingSingleSelectId = hit.id;
6209
6501
  }
6210
6502
  this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
6211
6503
  }
6212
6504
  } else {
6213
- this._selectedIds = [];
6505
+ this.setSelectedIds([]);
6214
6506
  this.mode = { type: "marquee", start: world };
6215
6507
  }
6216
6508
  ctx.requestRender();
@@ -6283,12 +6575,12 @@ var SelectTool = class {
6283
6575
  if (this.mode.type === "marquee") {
6284
6576
  const rect = this.getMarqueeRect();
6285
6577
  if (rect) {
6286
- this._selectedIds = this.findElementsInRect(rect, ctx);
6578
+ this.setSelectedIds(this.findElementsInRect(rect, ctx));
6287
6579
  }
6288
6580
  ctx.requestRender();
6289
6581
  }
6290
6582
  if (!this.hasDragged && this.pendingSingleSelectId !== null) {
6291
- this._selectedIds = [this.pendingSingleSelectId];
6583
+ this.setSelectedIds([this.pendingSingleSelectId]);
6292
6584
  }
6293
6585
  this.pendingSingleSelectId = null;
6294
6586
  this.hasDragged = false;
@@ -7510,7 +7802,7 @@ var TemplateTool = class {
7510
7802
  };
7511
7803
 
7512
7804
  // src/index.ts
7513
- var VERSION = "0.26.0";
7805
+ var VERSION = "0.28.0";
7514
7806
  export {
7515
7807
  ArrowTool,
7516
7808
  AutoSave,
@@ -7551,6 +7843,7 @@ export {
7551
7843
  getArrowTangentAngle,
7552
7844
  getBendFromPoint,
7553
7845
  getElementBounds,
7846
+ getElementStyle,
7554
7847
  getElementsBoundingBox,
7555
7848
  getHexCellsInCone,
7556
7849
  getHexCellsInLine,
@@ -7562,6 +7855,7 @@ export {
7562
7855
  smartSnap,
7563
7856
  snapPoint,
7564
7857
  snapToHexCenter,
7858
+ styleToPatch,
7565
7859
  toggleBold,
7566
7860
  toggleItalic,
7567
7861
  toggleStrikethrough,