@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.cjs CHANGED
@@ -59,6 +59,7 @@ __export(index_exports, {
59
59
  getArrowTangentAngle: () => getArrowTangentAngle,
60
60
  getBendFromPoint: () => getBendFromPoint,
61
61
  getElementBounds: () => getElementBounds,
62
+ getElementStyle: () => getElementStyle,
62
63
  getElementsBoundingBox: () => getElementsBoundingBox,
63
64
  getHexCellsInCone: () => getHexCellsInCone,
64
65
  getHexCellsInLine: () => getHexCellsInLine,
@@ -70,6 +71,7 @@ __export(index_exports, {
70
71
  smartSnap: () => smartSnap,
71
72
  snapPoint: () => snapPoint,
72
73
  snapToHexCenter: () => snapToHexCenter,
74
+ styleToPatch: () => styleToPatch,
73
75
  toggleBold: () => toggleBold,
74
76
  toggleItalic: () => toggleItalic,
75
77
  toggleStrikethrough: () => toggleStrikethrough,
@@ -2737,6 +2739,7 @@ function drawHexPath(ctx, cx, cy, cellSize, orientation) {
2737
2739
  var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
2738
2740
  var ARROWHEAD_LENGTH = 12;
2739
2741
  var ARROWHEAD_ANGLE = Math.PI / 6;
2742
+ var ARROW_LABEL_FONT_SIZE = 14;
2740
2743
  var ElementRenderer = class {
2741
2744
  store = null;
2742
2745
  imageCache = /* @__PURE__ */ new Map();
@@ -2747,6 +2750,7 @@ var ElementRenderer = class {
2747
2750
  hexTileCache = null;
2748
2751
  hexTileCacheKey = "";
2749
2752
  gridBoundsOverride = null;
2753
+ labelEditingId = null;
2750
2754
  setStore(store) {
2751
2755
  this.store = store;
2752
2756
  }
@@ -2765,6 +2769,9 @@ var ElementRenderer = class {
2765
2769
  setGridBoundsOverride(bounds) {
2766
2770
  this.gridBoundsOverride = bounds;
2767
2771
  }
2772
+ setLabelEditingId(id) {
2773
+ this.labelEditingId = id;
2774
+ }
2768
2775
  isDomElement(element) {
2769
2776
  return DOM_ELEMENT_TYPES.has(element.type);
2770
2777
  }
@@ -2841,6 +2848,28 @@ var ElementRenderer = class {
2841
2848
  ctx.stroke();
2842
2849
  this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
2843
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();
2844
2873
  }
2845
2874
  renderArrowhead(ctx, arrow, tip, angle) {
2846
2875
  ctx.beginPath();
@@ -3251,6 +3280,7 @@ function createArrow(input) {
3251
3280
  };
3252
3281
  if (input.fromBinding) result.fromBinding = input.fromBinding;
3253
3282
  if (input.toBinding) result.toBinding = input.toBinding;
3283
+ if (input.label !== void 0) result.label = input.label;
3254
3284
  return result;
3255
3285
  }
3256
3286
  function createImage(input) {
@@ -3714,6 +3744,84 @@ var NoteEditor = class {
3714
3744
  }
3715
3745
  };
3716
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
+
3717
3825
  // src/tools/tool-manager.ts
3718
3826
  var ToolManager = class {
3719
3827
  tools = /* @__PURE__ */ new Map();
@@ -5234,7 +5342,104 @@ var MarginViewport = class {
5234
5342
  }
5235
5343
  };
5236
5344
 
5345
+ // src/elements/element-style.ts
5346
+ function styleToPatch(element, style) {
5347
+ const { color, fillColor, strokeWidth, opacity, fontSize } = style;
5348
+ switch (element.type) {
5349
+ case "stroke":
5350
+ return {
5351
+ ...color !== void 0 ? { color } : {},
5352
+ ...strokeWidth !== void 0 ? { width: strokeWidth } : {},
5353
+ ...opacity !== void 0 ? { opacity } : {}
5354
+ };
5355
+ case "arrow":
5356
+ return {
5357
+ ...color !== void 0 ? { color } : {},
5358
+ ...strokeWidth !== void 0 ? { width: strokeWidth } : {}
5359
+ };
5360
+ case "shape":
5361
+ return {
5362
+ ...color !== void 0 ? { strokeColor: color } : {},
5363
+ ...fillColor !== void 0 ? { fillColor } : {},
5364
+ ...strokeWidth !== void 0 ? { strokeWidth } : {}
5365
+ };
5366
+ case "text":
5367
+ return {
5368
+ ...color !== void 0 ? { color } : {},
5369
+ ...fontSize !== void 0 ? { fontSize } : {}
5370
+ };
5371
+ case "note":
5372
+ return {
5373
+ ...color !== void 0 ? { textColor: color } : {},
5374
+ ...fillColor !== void 0 ? { backgroundColor: fillColor } : {},
5375
+ ...fontSize !== void 0 ? { fontSize } : {}
5376
+ };
5377
+ case "grid":
5378
+ return {
5379
+ ...color !== void 0 ? { strokeColor: color } : {},
5380
+ ...strokeWidth !== void 0 ? { strokeWidth } : {},
5381
+ ...opacity !== void 0 ? { opacity } : {}
5382
+ };
5383
+ case "template":
5384
+ return {
5385
+ ...color !== void 0 ? { strokeColor: color } : {},
5386
+ ...fillColor !== void 0 ? { fillColor } : {},
5387
+ ...strokeWidth !== void 0 ? { strokeWidth } : {},
5388
+ ...opacity !== void 0 ? { opacity } : {}
5389
+ };
5390
+ default:
5391
+ return {};
5392
+ }
5393
+ }
5394
+ function getElementStyle(element) {
5395
+ switch (element.type) {
5396
+ case "stroke":
5397
+ return { color: element.color, strokeWidth: element.width, opacity: element.opacity };
5398
+ case "arrow":
5399
+ return { color: element.color, strokeWidth: element.width };
5400
+ case "shape":
5401
+ return {
5402
+ color: element.strokeColor,
5403
+ fillColor: element.fillColor,
5404
+ strokeWidth: element.strokeWidth
5405
+ };
5406
+ case "text":
5407
+ return { color: element.color, fontSize: element.fontSize };
5408
+ case "note":
5409
+ return {
5410
+ color: element.textColor,
5411
+ fillColor: element.backgroundColor,
5412
+ ...element.fontSize !== void 0 ? { fontSize: element.fontSize } : {}
5413
+ };
5414
+ case "grid":
5415
+ return {
5416
+ color: element.strokeColor,
5417
+ strokeWidth: element.strokeWidth,
5418
+ opacity: element.opacity
5419
+ };
5420
+ case "template":
5421
+ return {
5422
+ color: element.strokeColor,
5423
+ fillColor: element.fillColor,
5424
+ strokeWidth: element.strokeWidth,
5425
+ opacity: element.opacity
5426
+ };
5427
+ default:
5428
+ return {};
5429
+ }
5430
+ }
5431
+
5237
5432
  // src/canvas/viewport.ts
5433
+ var EMPTY_IDS = [];
5434
+ var ARROW_HIT_THRESHOLD = 10;
5435
+ function noop() {
5436
+ }
5437
+ function sharedValue(values) {
5438
+ const present = values.filter((v) => v !== void 0);
5439
+ if (present.length === 0) return void 0;
5440
+ const first = present[0];
5441
+ return present.every((v) => v === first) ? first : void 0;
5442
+ }
5238
5443
  var Viewport = class {
5239
5444
  constructor(container, options = {}) {
5240
5445
  this.container = container;
@@ -5268,6 +5473,7 @@ var Viewport = class {
5268
5473
  placeholder: options.placeholder
5269
5474
  });
5270
5475
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
5476
+ this.arrowLabelEditor = new ArrowLabelEditor();
5271
5477
  this.noteEditor.setHistoryHooks(
5272
5478
  () => this.historyRecorder.begin(),
5273
5479
  () => this.historyRecorder.commit()
@@ -5394,6 +5600,7 @@ var Viewport = class {
5394
5600
  background;
5395
5601
  renderer;
5396
5602
  noteEditor;
5603
+ arrowLabelEditor;
5397
5604
  historyRecorder;
5398
5605
  toolContext;
5399
5606
  marginViewport;
@@ -5586,6 +5793,52 @@ var Viewport = class {
5586
5793
  this.gridChangeListeners.delete(listener);
5587
5794
  };
5588
5795
  }
5796
+ getSelectTool() {
5797
+ return this.toolManager.getTool("select");
5798
+ }
5799
+ getSelectedIds() {
5800
+ return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
5801
+ }
5802
+ onSelectionChange(listener) {
5803
+ const tool = this.getSelectTool();
5804
+ return tool ? tool.onSelectionChange(listener) : noop;
5805
+ }
5806
+ getSelectionStyle() {
5807
+ const ids = this.getSelectedIds();
5808
+ if (ids.length === 0) return null;
5809
+ const styles = [];
5810
+ for (const id of ids) {
5811
+ const el = this.store.getById(id);
5812
+ if (el) styles.push(getElementStyle(el));
5813
+ }
5814
+ if (styles.length === 0) return null;
5815
+ const result = {};
5816
+ const color = sharedValue(styles.map((s) => s.color));
5817
+ if (color !== void 0) result.color = color;
5818
+ const fillColor = sharedValue(styles.map((s) => s.fillColor));
5819
+ if (fillColor !== void 0) result.fillColor = fillColor;
5820
+ const strokeWidth = sharedValue(styles.map((s) => s.strokeWidth));
5821
+ if (strokeWidth !== void 0) result.strokeWidth = strokeWidth;
5822
+ const opacity = sharedValue(styles.map((s) => s.opacity));
5823
+ if (opacity !== void 0) result.opacity = opacity;
5824
+ const fontSize = sharedValue(styles.map((s) => s.fontSize));
5825
+ if (fontSize !== void 0) result.fontSize = fontSize;
5826
+ return result;
5827
+ }
5828
+ applyStyleToSelection(style) {
5829
+ const ids = this.getSelectedIds();
5830
+ if (ids.length === 0) return;
5831
+ this.historyRecorder.begin();
5832
+ for (const id of ids) {
5833
+ const el = this.store.getById(id);
5834
+ if (!el) continue;
5835
+ const patch = styleToPatch(el, style);
5836
+ if (Object.keys(patch).length > 0) {
5837
+ this.store.update(id, patch);
5838
+ }
5839
+ }
5840
+ this.historyRecorder.commit();
5841
+ }
5589
5842
  getRenderStats() {
5590
5843
  return this.renderLoop.getStats();
5591
5844
  }
@@ -5602,6 +5855,7 @@ var Viewport = class {
5602
5855
  this.renderLoop.stop();
5603
5856
  this.interactMode.destroy();
5604
5857
  this.noteEditor.destroy(this.store);
5858
+ this.arrowLabelEditor.cancel();
5605
5859
  this.historyRecorder.destroy();
5606
5860
  this.wrapper.removeEventListener("pointerdown", this.onTapDown);
5607
5861
  this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
@@ -5687,8 +5941,35 @@ var Viewport = class {
5687
5941
  const hit = this.hitTestWorld(world);
5688
5942
  if (hit?.type === "html") {
5689
5943
  this.interactMode.startInteracting(hit.id);
5944
+ return;
5945
+ }
5946
+ const arrow = this.findArrowAt(world);
5947
+ if (arrow) {
5948
+ this.startArrowLabelEdit(arrow);
5690
5949
  }
5691
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
+ }
5692
5973
  hitTestWorld(world) {
5693
5974
  const candidates = this.store.queryPoint(world).reverse();
5694
5975
  for (const el of candidates) {
@@ -6201,6 +6482,7 @@ var HANDLE_CURSORS = {
6201
6482
  var SelectTool = class {
6202
6483
  name = "select";
6203
6484
  _selectedIds = [];
6485
+ selectionListeners = /* @__PURE__ */ new Set();
6204
6486
  mode = { type: "idle" };
6205
6487
  lastWorld = { x: 0, y: 0 };
6206
6488
  currentWorld = { x: 0, y: 0 };
@@ -6210,10 +6492,22 @@ var SelectTool = class {
6210
6492
  resizeAspectRatio = 0;
6211
6493
  hoveredId = null;
6212
6494
  get selectedIds() {
6213
- return [...this._selectedIds];
6495
+ return this._selectedIds;
6214
6496
  }
6215
- setSelection(ids) {
6497
+ onSelectionChange(listener) {
6498
+ this.selectionListeners.add(listener);
6499
+ return () => {
6500
+ this.selectionListeners.delete(listener);
6501
+ };
6502
+ }
6503
+ setSelectedIds(ids) {
6504
+ const prev = this._selectedIds;
6505
+ if (prev.length === ids.length && prev.every((id, i) => id === ids[i])) return;
6216
6506
  this._selectedIds = ids;
6507
+ for (const listener of this.selectionListeners) listener();
6508
+ }
6509
+ setSelection(ids) {
6510
+ this.setSelectedIds(ids);
6217
6511
  this.ctx?.requestRender();
6218
6512
  }
6219
6513
  get isMarqueeActive() {
@@ -6223,7 +6517,7 @@ var SelectTool = class {
6223
6517
  this.ctx = ctx;
6224
6518
  }
6225
6519
  onDeactivate(ctx) {
6226
- this._selectedIds = [];
6520
+ this.setSelectedIds([]);
6227
6521
  this.mode = { type: "idle" };
6228
6522
  this.hoveredId = null;
6229
6523
  ctx.setCursor?.("default");
@@ -6274,22 +6568,22 @@ var SelectTool = class {
6274
6568
  const alreadySelected = this._selectedIds.includes(hit.id);
6275
6569
  if (state.shiftKey) {
6276
6570
  if (alreadySelected) {
6277
- this._selectedIds = this._selectedIds.filter((id) => id !== hit.id);
6571
+ this.setSelectedIds(this._selectedIds.filter((id) => id !== hit.id));
6278
6572
  this.mode = { type: "idle" };
6279
6573
  } else {
6280
- this._selectedIds = [...this._selectedIds, hit.id];
6574
+ this.setSelectedIds([...this._selectedIds, hit.id]);
6281
6575
  this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
6282
6576
  }
6283
6577
  } else {
6284
6578
  if (!alreadySelected) {
6285
- this._selectedIds = [hit.id];
6579
+ this.setSelectedIds([hit.id]);
6286
6580
  } else if (this._selectedIds.length > 1) {
6287
6581
  this.pendingSingleSelectId = hit.id;
6288
6582
  }
6289
6583
  this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
6290
6584
  }
6291
6585
  } else {
6292
- this._selectedIds = [];
6586
+ this.setSelectedIds([]);
6293
6587
  this.mode = { type: "marquee", start: world };
6294
6588
  }
6295
6589
  ctx.requestRender();
@@ -6362,12 +6656,12 @@ var SelectTool = class {
6362
6656
  if (this.mode.type === "marquee") {
6363
6657
  const rect = this.getMarqueeRect();
6364
6658
  if (rect) {
6365
- this._selectedIds = this.findElementsInRect(rect, ctx);
6659
+ this.setSelectedIds(this.findElementsInRect(rect, ctx));
6366
6660
  }
6367
6661
  ctx.requestRender();
6368
6662
  }
6369
6663
  if (!this.hasDragged && this.pendingSingleSelectId !== null) {
6370
- this._selectedIds = [this.pendingSingleSelectId];
6664
+ this.setSelectedIds([this.pendingSingleSelectId]);
6371
6665
  }
6372
6666
  this.pendingSingleSelectId = null;
6373
6667
  this.hasDragged = false;
@@ -7589,7 +7883,7 @@ var TemplateTool = class {
7589
7883
  };
7590
7884
 
7591
7885
  // src/index.ts
7592
- var VERSION = "0.26.0";
7886
+ var VERSION = "0.28.0";
7593
7887
  // Annotate the CommonJS export names for ESM import in node:
7594
7888
  0 && (module.exports = {
7595
7889
  ArrowTool,
@@ -7631,6 +7925,7 @@ var VERSION = "0.26.0";
7631
7925
  getArrowTangentAngle,
7632
7926
  getBendFromPoint,
7633
7927
  getElementBounds,
7928
+ getElementStyle,
7634
7929
  getElementsBoundingBox,
7635
7930
  getHexCellsInCone,
7636
7931
  getHexCellsInLine,
@@ -7642,6 +7937,7 @@ var VERSION = "0.26.0";
7642
7937
  smartSnap,
7643
7938
  snapPoint,
7644
7939
  snapToHexCenter,
7940
+ styleToPatch,
7645
7941
  toggleBold,
7646
7942
  toggleItalic,
7647
7943
  toggleStrikethrough,