@fieldnotes/core 0.27.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/README.md CHANGED
@@ -441,6 +441,14 @@ new Viewport(container, {
441
441
  new PencilTool({ color: '#ff0000', width: 3, smoothing: 1.5 });
442
442
  new EraserTool({ radius: 30 });
443
443
  new ArrowTool({ color: '#333', width: 2 });
444
+ ```
445
+
446
+ ### Arrow Labels
447
+
448
+ Arrows support an optional `label` string, rendered as a pill at the curve midpoint. Pass it at creation or double-click an arrow on the canvas to add or edit the label inline.
449
+
450
+ ```typescript
451
+ createArrow({ from: { x: 0, y: 0 }, to: { x: 200, y: 0 }, label: 'depends on' });
444
452
  new NoteTool({ backgroundColor: '#fff9c4', size: { w: 200, h: 150 } });
445
453
  new ImageTool({ size: { w: 400, h: 300 } });
446
454
  ```
package/dist/index.cjs CHANGED
@@ -2739,6 +2739,7 @@ function drawHexPath(ctx, cx, cy, cellSize, orientation) {
2739
2739
  var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
2740
2740
  var ARROWHEAD_LENGTH = 12;
2741
2741
  var ARROWHEAD_ANGLE = Math.PI / 6;
2742
+ var ARROW_LABEL_FONT_SIZE = 14;
2742
2743
  var ElementRenderer = class {
2743
2744
  store = null;
2744
2745
  imageCache = /* @__PURE__ */ new Map();
@@ -2749,6 +2750,7 @@ var ElementRenderer = class {
2749
2750
  hexTileCache = null;
2750
2751
  hexTileCacheKey = "";
2751
2752
  gridBoundsOverride = null;
2753
+ labelEditingId = null;
2752
2754
  setStore(store) {
2753
2755
  this.store = store;
2754
2756
  }
@@ -2767,6 +2769,9 @@ var ElementRenderer = class {
2767
2769
  setGridBoundsOverride(bounds) {
2768
2770
  this.gridBoundsOverride = bounds;
2769
2771
  }
2772
+ setLabelEditingId(id) {
2773
+ this.labelEditingId = id;
2774
+ }
2770
2775
  isDomElement(element) {
2771
2776
  return DOM_ELEMENT_TYPES.has(element.type);
2772
2777
  }
@@ -2843,6 +2848,28 @@ var ElementRenderer = class {
2843
2848
  ctx.stroke();
2844
2849
  this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
2845
2850
  ctx.restore();
2851
+ this.renderArrowLabel(ctx, arrow);
2852
+ }
2853
+ renderArrowLabel(ctx, arrow) {
2854
+ if (!arrow.label || arrow.label.length === 0) return;
2855
+ if (arrow.id === this.labelEditingId) return;
2856
+ const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
2857
+ ctx.save();
2858
+ ctx.font = `${ARROW_LABEL_FONT_SIZE}px system-ui, sans-serif`;
2859
+ const metrics = ctx.measureText(arrow.label);
2860
+ const padX = 6;
2861
+ const padY = 4;
2862
+ const w = metrics.width + padX * 2;
2863
+ const h = ARROW_LABEL_FONT_SIZE + padY * 2;
2864
+ ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
2865
+ ctx.beginPath();
2866
+ ctx.roundRect(mid.x - w / 2, mid.y - h / 2, w, h, 4);
2867
+ ctx.fill();
2868
+ ctx.fillStyle = "#1a1a1a";
2869
+ ctx.textAlign = "center";
2870
+ ctx.textBaseline = "middle";
2871
+ ctx.fillText(arrow.label, mid.x, mid.y);
2872
+ ctx.restore();
2846
2873
  }
2847
2874
  renderArrowhead(ctx, arrow, tip, angle) {
2848
2875
  ctx.beginPath();
@@ -3253,6 +3280,7 @@ function createArrow(input) {
3253
3280
  };
3254
3281
  if (input.fromBinding) result.fromBinding = input.fromBinding;
3255
3282
  if (input.toBinding) result.toBinding = input.toBinding;
3283
+ if (input.label !== void 0) result.label = input.label;
3256
3284
  return result;
3257
3285
  }
3258
3286
  function createImage(input) {
@@ -3716,6 +3744,84 @@ var NoteEditor = class {
3716
3744
  }
3717
3745
  };
3718
3746
 
3747
+ // src/elements/arrow-label-editor.ts
3748
+ var ArrowLabelEditor = class {
3749
+ input = null;
3750
+ done = false;
3751
+ get isEditing() {
3752
+ return this.input !== null;
3753
+ }
3754
+ startEditing(start) {
3755
+ if (this.input) this.cleanup();
3756
+ const { arrow, layer, store, recorder, onDone } = start;
3757
+ const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
3758
+ const input = document.createElement("input");
3759
+ input.type = "text";
3760
+ input.value = arrow.label ?? "";
3761
+ Object.assign(input.style, {
3762
+ position: "absolute",
3763
+ left: `${mid.x}px`,
3764
+ top: `${mid.y}px`,
3765
+ transform: "translate(-50%, -50%)",
3766
+ // domLayer is pointer-events:none; the input must opt back in to receive taps/clicks.
3767
+ pointerEvents: "auto",
3768
+ font: "14px system-ui, sans-serif",
3769
+ padding: "2px 6px",
3770
+ border: "1px solid #2196F3",
3771
+ borderRadius: "4px",
3772
+ background: "#ffffff",
3773
+ color: "#1a1a1a",
3774
+ outline: "none",
3775
+ minWidth: "40px"
3776
+ });
3777
+ this.done = false;
3778
+ const commit = () => {
3779
+ if (this.done) return;
3780
+ this.done = true;
3781
+ const next = input.value.trim() || void 0;
3782
+ if (next !== arrow.label) {
3783
+ recorder.begin();
3784
+ store.update(arrow.id, { label: next });
3785
+ recorder.commit();
3786
+ }
3787
+ this.cleanup();
3788
+ onDone();
3789
+ };
3790
+ const cancel = () => {
3791
+ if (this.done) return;
3792
+ this.done = true;
3793
+ this.cleanup();
3794
+ onDone();
3795
+ };
3796
+ input.addEventListener("keydown", (e) => {
3797
+ if (e.key === "Enter") {
3798
+ e.preventDefault();
3799
+ commit();
3800
+ } else if (e.key === "Escape") {
3801
+ e.preventDefault();
3802
+ cancel();
3803
+ }
3804
+ e.stopPropagation();
3805
+ });
3806
+ input.addEventListener("blur", commit);
3807
+ layer.appendChild(input);
3808
+ this.input = input;
3809
+ input.focus();
3810
+ input.select();
3811
+ }
3812
+ /** Abort any in-progress edit without committing (e.g. on viewport teardown). */
3813
+ cancel() {
3814
+ this.done = true;
3815
+ this.cleanup();
3816
+ }
3817
+ cleanup() {
3818
+ if (this.input) {
3819
+ this.input.remove();
3820
+ this.input = null;
3821
+ }
3822
+ }
3823
+ };
3824
+
3719
3825
  // src/tools/tool-manager.ts
3720
3826
  var ToolManager = class {
3721
3827
  tools = /* @__PURE__ */ new Map();
@@ -5325,6 +5431,7 @@ function getElementStyle(element) {
5325
5431
 
5326
5432
  // src/canvas/viewport.ts
5327
5433
  var EMPTY_IDS = [];
5434
+ var ARROW_HIT_THRESHOLD = 10;
5328
5435
  function noop() {
5329
5436
  }
5330
5437
  function sharedValue(values) {
@@ -5366,6 +5473,7 @@ var Viewport = class {
5366
5473
  placeholder: options.placeholder
5367
5474
  });
5368
5475
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
5476
+ this.arrowLabelEditor = new ArrowLabelEditor();
5369
5477
  this.noteEditor.setHistoryHooks(
5370
5478
  () => this.historyRecorder.begin(),
5371
5479
  () => this.historyRecorder.commit()
@@ -5492,6 +5600,7 @@ var Viewport = class {
5492
5600
  background;
5493
5601
  renderer;
5494
5602
  noteEditor;
5603
+ arrowLabelEditor;
5495
5604
  historyRecorder;
5496
5605
  toolContext;
5497
5606
  marginViewport;
@@ -5746,6 +5855,7 @@ var Viewport = class {
5746
5855
  this.renderLoop.stop();
5747
5856
  this.interactMode.destroy();
5748
5857
  this.noteEditor.destroy(this.store);
5858
+ this.arrowLabelEditor.cancel();
5749
5859
  this.historyRecorder.destroy();
5750
5860
  this.wrapper.removeEventListener("pointerdown", this.onTapDown);
5751
5861
  this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
@@ -5831,8 +5941,35 @@ var Viewport = class {
5831
5941
  const hit = this.hitTestWorld(world);
5832
5942
  if (hit?.type === "html") {
5833
5943
  this.interactMode.startInteracting(hit.id);
5944
+ return;
5945
+ }
5946
+ const arrow = this.findArrowAt(world);
5947
+ if (arrow) {
5948
+ this.startArrowLabelEdit(arrow);
5834
5949
  }
5835
5950
  };
5951
+ findArrowAt(world) {
5952
+ const candidates = this.store.queryPoint(world).reverse();
5953
+ for (const el of candidates) {
5954
+ if (el.type === "arrow" && isNearBezier(world, el.from, el.to, el.bend, ARROW_HIT_THRESHOLD)) {
5955
+ return el;
5956
+ }
5957
+ }
5958
+ return void 0;
5959
+ }
5960
+ startArrowLabelEdit(arrow) {
5961
+ this.arrowLabelEditor.startEditing({
5962
+ arrow,
5963
+ layer: this.domLayer,
5964
+ store: this.store,
5965
+ recorder: this.historyRecorder,
5966
+ onDone: () => {
5967
+ this.renderer.setLabelEditingId(null);
5968
+ this.requestRender();
5969
+ }
5970
+ });
5971
+ this.renderer.setLabelEditingId(arrow.id);
5972
+ }
5836
5973
  hitTestWorld(world) {
5837
5974
  const candidates = this.store.queryPoint(world).reverse();
5838
5975
  for (const el of candidates) {
@@ -7746,7 +7883,7 @@ var TemplateTool = class {
7746
7883
  };
7747
7884
 
7748
7885
  // src/index.ts
7749
- var VERSION = "0.27.0";
7886
+ var VERSION = "0.28.0";
7750
7887
  // Annotate the CommonJS export names for ESM import in node:
7751
7888
  0 && (module.exports = {
7752
7889
  ArrowTool,