@fieldnotes/core 0.9.0 → 0.11.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
@@ -27,6 +27,8 @@ __export(index_exports, {
27
27
  BatchCommand: () => BatchCommand,
28
28
  Camera: () => Camera,
29
29
  CreateLayerCommand: () => CreateLayerCommand,
30
+ DEFAULT_FONT_SIZE_PRESETS: () => DEFAULT_FONT_SIZE_PRESETS,
31
+ DEFAULT_NOTE_FONT_SIZE: () => DEFAULT_NOTE_FONT_SIZE,
30
32
  ElementRenderer: () => ElementRenderer,
31
33
  ElementStore: () => ElementStore,
32
34
  EraserTool: () => EraserTool,
@@ -40,6 +42,7 @@ __export(index_exports, {
40
42
  MeasureTool: () => MeasureTool,
41
43
  NoteEditor: () => NoteEditor,
42
44
  NoteTool: () => NoteTool,
45
+ NoteToolbar: () => NoteToolbar,
43
46
  PencilTool: () => PencilTool,
44
47
  Quadtree: () => Quadtree,
45
48
  RemoveElementCommand: () => RemoveElementCommand,
@@ -70,6 +73,7 @@ __export(index_exports, {
70
73
  exportState: () => exportState,
71
74
  findBindTarget: () => findBindTarget,
72
75
  findBoundArrows: () => findBoundArrows,
76
+ getActiveFormats: () => getActiveFormats,
73
77
  getArrowBounds: () => getArrowBounds,
74
78
  getArrowControlPoint: () => getArrowControlPoint,
75
79
  getArrowMidpoint: () => getArrowMidpoint,
@@ -86,9 +90,15 @@ __export(index_exports, {
86
90
  isBindable: () => isBindable,
87
91
  isNearBezier: () => isNearBezier,
88
92
  parseState: () => parseState,
93
+ sanitizeNoteHtml: () => sanitizeNoteHtml,
94
+ setFontSize: () => setFontSize,
89
95
  smartSnap: () => smartSnap,
90
96
  snapPoint: () => snapPoint,
91
97
  snapToHexCenter: () => snapToHexCenter,
98
+ toggleBold: () => toggleBold,
99
+ toggleItalic: () => toggleItalic,
100
+ toggleStrikethrough: () => toggleStrikethrough,
101
+ toggleUnderline: () => toggleUnderline,
92
102
  unbindArrow: () => unbindArrow,
93
103
  updateBoundArrow: () => updateBoundArrow
94
104
  });
@@ -265,6 +275,120 @@ var Quadtree = class {
265
275
  }
266
276
  };
267
277
 
278
+ // src/elements/note-sanitizer.ts
279
+ var BOLD_TAGS = /* @__PURE__ */ new Set(["b", "strong"]);
280
+ var ITALIC_TAGS = /* @__PURE__ */ new Set(["i", "em"]);
281
+ var UNDERLINE_TAGS = /* @__PURE__ */ new Set(["u"]);
282
+ var STRIKE_TAGS = /* @__PURE__ */ new Set(["s", "strike", "del"]);
283
+ var BLOCK_TAGS = /* @__PURE__ */ new Set(["div"]);
284
+ function parseStyledRuns(html, baseFontSize) {
285
+ if (!html) return [];
286
+ const doc = new DOMParser().parseFromString(html, "text/html");
287
+ const runs = [];
288
+ const baseStyle = {
289
+ bold: false,
290
+ italic: false,
291
+ underline: false,
292
+ strikethrough: false,
293
+ fontSize: baseFontSize
294
+ };
295
+ walkNodes(doc.body, baseStyle, runs);
296
+ return runs;
297
+ }
298
+ function walkNodes(node, style, runs) {
299
+ for (const child of Array.from(node.childNodes)) {
300
+ if (child.nodeType === Node.TEXT_NODE) {
301
+ const text = child.textContent ?? "";
302
+ if (text) {
303
+ runs.push({ text, ...style });
304
+ }
305
+ continue;
306
+ }
307
+ if (child.nodeType !== Node.ELEMENT_NODE) continue;
308
+ const el = child;
309
+ const tag = el.tagName.toLowerCase();
310
+ if (tag === "br") {
311
+ runs.push({ text: "\n", ...style });
312
+ continue;
313
+ }
314
+ if (BLOCK_TAGS.has(tag) && runs.length > 0) {
315
+ const lastRun = runs[runs.length - 1];
316
+ if (lastRun && !lastRun.text.endsWith("\n")) {
317
+ runs.push({ text: "\n", ...style });
318
+ }
319
+ }
320
+ const childStyle = { ...style };
321
+ if (BOLD_TAGS.has(tag)) childStyle.bold = true;
322
+ if (ITALIC_TAGS.has(tag)) childStyle.italic = true;
323
+ if (UNDERLINE_TAGS.has(tag)) childStyle.underline = true;
324
+ if (STRIKE_TAGS.has(tag)) childStyle.strikethrough = true;
325
+ if (tag === "span") {
326
+ const fontSize = el.style.fontSize;
327
+ if (fontSize) {
328
+ childStyle.fontSize = parseInt(fontSize, 10) || style.fontSize;
329
+ }
330
+ }
331
+ walkNodes(el, childStyle, runs);
332
+ }
333
+ }
334
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
335
+ "b",
336
+ "strong",
337
+ "i",
338
+ "em",
339
+ "u",
340
+ "s",
341
+ "strike",
342
+ "del",
343
+ "span",
344
+ "br",
345
+ "div"
346
+ ]);
347
+ function sanitizeNoteHtml(html) {
348
+ if (!html) return "";
349
+ const doc = new DOMParser().parseFromString(html, "text/html");
350
+ sanitizeNode(doc.body);
351
+ return doc.body.innerHTML;
352
+ }
353
+ function sanitizeNode(node) {
354
+ const children = Array.from(node.childNodes);
355
+ for (const child of children) {
356
+ if (child.nodeType === Node.TEXT_NODE) continue;
357
+ if (child.nodeType !== Node.ELEMENT_NODE) {
358
+ child.remove();
359
+ continue;
360
+ }
361
+ const el = child;
362
+ const tag = el.tagName.toLowerCase();
363
+ if (!ALLOWED_TAGS.has(tag)) {
364
+ const fragment = document.createDocumentFragment();
365
+ while (el.firstChild) {
366
+ fragment.appendChild(el.firstChild);
367
+ }
368
+ node.replaceChild(fragment, el);
369
+ sanitizeNode(node);
370
+ return;
371
+ }
372
+ sanitizeAttributes(el, tag);
373
+ sanitizeNode(el);
374
+ }
375
+ }
376
+ function sanitizeAttributes(el, tag) {
377
+ const attrs = Array.from(el.attributes);
378
+ for (const attr of attrs) {
379
+ if (tag === "span" && attr.name === "style") {
380
+ const fontSize = el.style.fontSize;
381
+ if (fontSize) {
382
+ el.setAttribute("style", `font-size: ${fontSize};`);
383
+ } else {
384
+ el.removeAttribute("style");
385
+ }
386
+ continue;
387
+ }
388
+ el.removeAttribute(attr.name);
389
+ }
390
+ }
391
+
268
392
  // src/core/state-serializer.ts
269
393
  var CURRENT_VERSION = 2;
270
394
  function exportState(elements, camera, layers = []) {
@@ -327,7 +451,17 @@ function validateState(data) {
327
451
  ];
328
452
  }
329
453
  }
330
- var VALID_TYPES = /* @__PURE__ */ new Set(["stroke", "note", "arrow", "image", "html", "text", "shape", "grid"]);
454
+ var VALID_TYPES = /* @__PURE__ */ new Set([
455
+ "stroke",
456
+ "note",
457
+ "arrow",
458
+ "image",
459
+ "html",
460
+ "text",
461
+ "shape",
462
+ "grid",
463
+ "template"
464
+ ]);
331
465
  function validateElement(el) {
332
466
  if (!el || typeof el !== "object") {
333
467
  throw new Error("Invalid element: expected an object");
@@ -377,6 +511,9 @@ function migrateElement(obj) {
377
511
  if (obj["type"] === "note" && typeof obj["textColor"] !== "string") {
378
512
  obj["textColor"] = "#000000";
379
513
  }
514
+ if (obj["type"] === "note" && typeof obj["text"] === "string") {
515
+ obj["text"] = sanitizeNoteHtml(obj["text"]);
516
+ }
380
517
  }
381
518
 
382
519
  // src/core/snap.ts
@@ -2286,7 +2423,359 @@ var ElementRenderer = class {
2286
2423
  }
2287
2424
  };
2288
2425
 
2426
+ // src/elements/create-id.ts
2427
+ var counter = 0;
2428
+ function createId(prefix) {
2429
+ return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
2430
+ }
2431
+
2432
+ // src/elements/element-factory.ts
2433
+ var DEFAULT_NOTE_FONT_SIZE = 18;
2434
+ function createStroke(input) {
2435
+ return {
2436
+ id: createId("stroke"),
2437
+ type: "stroke",
2438
+ position: input.position ?? { x: 0, y: 0 },
2439
+ zIndex: input.zIndex ?? 0,
2440
+ locked: input.locked ?? false,
2441
+ layerId: input.layerId ?? "",
2442
+ points: input.points,
2443
+ color: input.color ?? "#000000",
2444
+ width: input.width ?? 2,
2445
+ opacity: input.opacity ?? 1
2446
+ };
2447
+ }
2448
+ function createNote(input) {
2449
+ return {
2450
+ id: createId("note"),
2451
+ type: "note",
2452
+ position: input.position,
2453
+ zIndex: input.zIndex ?? 0,
2454
+ locked: input.locked ?? false,
2455
+ layerId: input.layerId ?? "",
2456
+ size: input.size ?? { w: 200, h: 100 },
2457
+ text: input.text ?? "",
2458
+ backgroundColor: input.backgroundColor ?? "#ffeb3b",
2459
+ textColor: input.textColor ?? "#000000",
2460
+ fontSize: input.fontSize ?? DEFAULT_NOTE_FONT_SIZE
2461
+ };
2462
+ }
2463
+ function createArrow(input) {
2464
+ const bend = input.bend ?? 0;
2465
+ const result = {
2466
+ id: createId("arrow"),
2467
+ type: "arrow",
2468
+ position: input.position ?? { x: 0, y: 0 },
2469
+ zIndex: input.zIndex ?? 0,
2470
+ locked: input.locked ?? false,
2471
+ layerId: input.layerId ?? "",
2472
+ from: input.from,
2473
+ to: input.to,
2474
+ bend,
2475
+ color: input.color ?? "#000000",
2476
+ width: input.width ?? 2,
2477
+ cachedControlPoint: getArrowControlPoint(input.from, input.to, bend)
2478
+ };
2479
+ if (input.fromBinding) result.fromBinding = input.fromBinding;
2480
+ if (input.toBinding) result.toBinding = input.toBinding;
2481
+ return result;
2482
+ }
2483
+ function createImage(input) {
2484
+ return {
2485
+ id: createId("image"),
2486
+ type: "image",
2487
+ position: input.position,
2488
+ zIndex: input.zIndex ?? 0,
2489
+ locked: input.locked ?? false,
2490
+ layerId: input.layerId ?? "",
2491
+ size: input.size,
2492
+ src: input.src
2493
+ };
2494
+ }
2495
+ function createHtmlElement(input) {
2496
+ const el = {
2497
+ id: createId("html"),
2498
+ type: "html",
2499
+ position: input.position,
2500
+ zIndex: input.zIndex ?? 0,
2501
+ locked: input.locked ?? false,
2502
+ layerId: input.layerId ?? "",
2503
+ size: input.size
2504
+ };
2505
+ if (input.domId) el.domId = input.domId;
2506
+ return el;
2507
+ }
2508
+ function createShape(input) {
2509
+ return {
2510
+ id: createId("shape"),
2511
+ type: "shape",
2512
+ position: input.position,
2513
+ zIndex: input.zIndex ?? 0,
2514
+ locked: input.locked ?? false,
2515
+ layerId: input.layerId ?? "",
2516
+ shape: input.shape ?? "rectangle",
2517
+ size: input.size,
2518
+ strokeColor: input.strokeColor ?? "#000000",
2519
+ strokeWidth: input.strokeWidth ?? 2,
2520
+ fillColor: input.fillColor ?? "none"
2521
+ };
2522
+ }
2523
+ function createGrid(input) {
2524
+ return {
2525
+ id: createId("grid"),
2526
+ type: "grid",
2527
+ position: input.position ?? { x: 0, y: 0 },
2528
+ zIndex: input.zIndex ?? 0,
2529
+ locked: input.locked ?? false,
2530
+ layerId: input.layerId ?? "",
2531
+ gridType: input.gridType ?? "square",
2532
+ hexOrientation: input.hexOrientation ?? "pointy",
2533
+ cellSize: input.cellSize ?? 40,
2534
+ strokeColor: input.strokeColor ?? "#000000",
2535
+ strokeWidth: input.strokeWidth ?? 1,
2536
+ opacity: input.opacity ?? 1
2537
+ };
2538
+ }
2539
+ function createText(input) {
2540
+ return {
2541
+ id: createId("text"),
2542
+ type: "text",
2543
+ position: input.position,
2544
+ zIndex: input.zIndex ?? 0,
2545
+ locked: input.locked ?? false,
2546
+ layerId: input.layerId ?? "",
2547
+ size: input.size ?? { w: 200, h: 28 },
2548
+ text: input.text ?? "",
2549
+ fontSize: input.fontSize ?? 16,
2550
+ color: input.color ?? "#1a1a1a",
2551
+ textAlign: input.textAlign ?? "left"
2552
+ };
2553
+ }
2554
+ function createTemplate(input) {
2555
+ return {
2556
+ id: createId("template"),
2557
+ type: "template",
2558
+ position: input.position,
2559
+ zIndex: input.zIndex ?? 0,
2560
+ locked: input.locked ?? false,
2561
+ layerId: input.layerId ?? "",
2562
+ templateShape: input.templateShape,
2563
+ radius: input.radius,
2564
+ angle: input.angle ?? 0,
2565
+ fillColor: input.fillColor ?? "rgba(255, 87, 34, 0.2)",
2566
+ strokeColor: input.strokeColor ?? "#FF5722",
2567
+ strokeWidth: input.strokeWidth ?? 2,
2568
+ opacity: input.opacity ?? 0.6,
2569
+ feetPerCell: input.feetPerCell,
2570
+ radiusFeet: input.radiusFeet
2571
+ };
2572
+ }
2573
+
2574
+ // src/elements/note-formatting.ts
2575
+ function toggleBold() {
2576
+ document.execCommand("bold");
2577
+ }
2578
+ function toggleItalic() {
2579
+ document.execCommand("italic");
2580
+ }
2581
+ function toggleUnderline() {
2582
+ document.execCommand("underline");
2583
+ }
2584
+ function toggleStrikethrough() {
2585
+ document.execCommand("strikeThrough");
2586
+ }
2587
+ function setFontSize(size) {
2588
+ const sel = window.getSelection();
2589
+ if (!sel || sel.rangeCount === 0) return;
2590
+ const range = sel.getRangeAt(0);
2591
+ if (range.collapsed) return;
2592
+ const span = document.createElement("span");
2593
+ span.style.fontSize = `${size}px`;
2594
+ try {
2595
+ range.surroundContents(span);
2596
+ } catch {
2597
+ span.appendChild(range.extractContents());
2598
+ range.insertNode(span);
2599
+ }
2600
+ }
2601
+ function getActiveFormats() {
2602
+ const query = (cmd) => {
2603
+ try {
2604
+ return document.queryCommandState(cmd);
2605
+ } catch {
2606
+ return false;
2607
+ }
2608
+ };
2609
+ return {
2610
+ bold: query("bold"),
2611
+ italic: query("italic"),
2612
+ underline: query("underline"),
2613
+ strikethrough: query("strikeThrough")
2614
+ };
2615
+ }
2616
+
2617
+ // src/elements/note-toolbar.ts
2618
+ var TOOLBAR_HEIGHT = 32;
2619
+ var TOOLBAR_GAP = 4;
2620
+ var FORMAT_BUTTONS = [
2621
+ { label: "B", format: "bold", command: "bold" },
2622
+ { label: "I", format: "italic", command: "italic" },
2623
+ { label: "U", format: "underline", command: "underline" },
2624
+ { label: "S", format: "strikethrough", command: "strikeThrough" }
2625
+ ];
2626
+ var DEFAULT_FONT_SIZE_PRESETS = [
2627
+ { label: "Small", size: 14 },
2628
+ { label: "Normal", size: 18 },
2629
+ { label: "Large", size: 24 },
2630
+ { label: "Heading", size: 32 }
2631
+ ];
2632
+ var NoteToolbar = class {
2633
+ el = null;
2634
+ anchor = null;
2635
+ selectionListener = null;
2636
+ fontSizePresets;
2637
+ constructor(fontSizePresets) {
2638
+ this.fontSizePresets = fontSizePresets ?? DEFAULT_FONT_SIZE_PRESETS;
2639
+ }
2640
+ show(anchor) {
2641
+ this.hide();
2642
+ this.anchor = anchor;
2643
+ this.el = this.createToolbarElement();
2644
+ document.body.appendChild(this.el);
2645
+ this.positionToolbar(anchor);
2646
+ this.selectionListener = () => this.updateActiveStates();
2647
+ document.addEventListener("selectionchange", this.selectionListener);
2648
+ }
2649
+ hide() {
2650
+ if (this.selectionListener) {
2651
+ document.removeEventListener("selectionchange", this.selectionListener);
2652
+ this.selectionListener = null;
2653
+ }
2654
+ if (this.el) {
2655
+ this.el.remove();
2656
+ this.el = null;
2657
+ }
2658
+ this.anchor = null;
2659
+ }
2660
+ getElement() {
2661
+ return this.el;
2662
+ }
2663
+ updatePosition(anchor) {
2664
+ if (this.el) {
2665
+ this.positionToolbar(anchor);
2666
+ }
2667
+ }
2668
+ createToolbarElement() {
2669
+ const toolbar = document.createElement("div");
2670
+ toolbar.dataset["noteToolbar"] = "";
2671
+ Object.assign(toolbar.style, {
2672
+ position: "fixed",
2673
+ display: "flex",
2674
+ alignItems: "center",
2675
+ gap: "2px",
2676
+ padding: "2px 4px",
2677
+ background: "#fff",
2678
+ border: "1px solid #ccc",
2679
+ borderRadius: "4px",
2680
+ boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
2681
+ zIndex: "10000",
2682
+ height: `${TOOLBAR_HEIGHT}px`,
2683
+ userSelect: "none"
2684
+ });
2685
+ for (const btn of FORMAT_BUTTONS) {
2686
+ toolbar.appendChild(this.createFormatButton(btn));
2687
+ }
2688
+ toolbar.appendChild(this.createFontSizeSelect());
2689
+ return toolbar;
2690
+ }
2691
+ createFormatButton(config) {
2692
+ const btn = document.createElement("button");
2693
+ btn.dataset["format"] = config.format;
2694
+ btn.textContent = config.label;
2695
+ Object.assign(btn.style, {
2696
+ border: "1px solid transparent",
2697
+ borderRadius: "3px",
2698
+ background: "none",
2699
+ cursor: "pointer",
2700
+ padding: "2px 6px",
2701
+ fontSize: "13px",
2702
+ fontWeight: config.format === "bold" ? "bold" : "normal",
2703
+ fontStyle: config.format === "italic" ? "italic" : "normal",
2704
+ textDecoration: config.format === "underline" ? "underline" : config.format === "strikethrough" ? "line-through" : "none",
2705
+ minWidth: "24px",
2706
+ height: "24px",
2707
+ lineHeight: "24px"
2708
+ });
2709
+ btn.addEventListener("pointerdown", (e) => {
2710
+ e.preventDefault();
2711
+ document.execCommand(config.command);
2712
+ this.updateActiveStates();
2713
+ });
2714
+ return btn;
2715
+ }
2716
+ createFontSizeSelect() {
2717
+ const select = document.createElement("select");
2718
+ Object.assign(select.style, {
2719
+ border: "1px solid #ccc",
2720
+ borderRadius: "3px",
2721
+ background: "#fff",
2722
+ cursor: "pointer",
2723
+ padding: "2px",
2724
+ fontSize: "12px",
2725
+ height: "24px",
2726
+ marginLeft: "4px"
2727
+ });
2728
+ for (const preset of this.fontSizePresets) {
2729
+ const option = document.createElement("option");
2730
+ option.value = String(preset.size);
2731
+ option.textContent = preset.label;
2732
+ select.appendChild(option);
2733
+ }
2734
+ select.value = String(DEFAULT_NOTE_FONT_SIZE);
2735
+ select.addEventListener("pointerdown", (e) => {
2736
+ e.stopPropagation();
2737
+ });
2738
+ select.addEventListener("change", () => {
2739
+ setFontSize(Number(select.value));
2740
+ this.updateActiveStates();
2741
+ this.anchor?.focus();
2742
+ });
2743
+ return select;
2744
+ }
2745
+ positionToolbar(anchor) {
2746
+ if (!this.el) return;
2747
+ const rect = anchor.getBoundingClientRect();
2748
+ const toolbarWidth = this.el.offsetWidth || 200;
2749
+ let top = rect.top - TOOLBAR_HEIGHT - TOOLBAR_GAP;
2750
+ if (top < 0) {
2751
+ top = rect.bottom + TOOLBAR_GAP;
2752
+ }
2753
+ let left = rect.left + (rect.width - toolbarWidth) / 2;
2754
+ left = Math.max(4, left);
2755
+ Object.assign(this.el.style, {
2756
+ top: `${top}px`,
2757
+ left: `${left}px`
2758
+ });
2759
+ }
2760
+ updateActiveStates() {
2761
+ if (!this.el) return;
2762
+ const active = getActiveFormats();
2763
+ for (const config of FORMAT_BUTTONS) {
2764
+ const btn = this.el.querySelector(`[data-format="${config.format}"]`);
2765
+ if (!btn) continue;
2766
+ const isActive = active[config.format] ?? false;
2767
+ btn.style.background = isActive ? "#e0e0e0" : "none";
2768
+ btn.style.borderColor = isActive ? "#bbb" : "transparent";
2769
+ }
2770
+ }
2771
+ };
2772
+
2289
2773
  // src/elements/note-editor.ts
2774
+ var FORMAT_SHORTCUTS = {
2775
+ b: toggleBold,
2776
+ i: toggleItalic,
2777
+ u: toggleUnderline
2778
+ };
2290
2779
  var NoteEditor = class {
2291
2780
  editingId = null;
2292
2781
  editingNode = null;
@@ -2295,6 +2784,10 @@ var NoteEditor = class {
2295
2784
  pointerHandler = null;
2296
2785
  pendingEditId = null;
2297
2786
  onStopCallback = null;
2787
+ toolbar;
2788
+ constructor(options) {
2789
+ this.toolbar = options?.toolbar === false ? null : new NoteToolbar(options?.fontSizePresets);
2790
+ }
2298
2791
  get isEditing() {
2299
2792
  return this.editingId !== null;
2300
2793
  }
@@ -2319,13 +2812,6 @@ var NoteEditor = class {
2319
2812
  stopEditing(store) {
2320
2813
  this.pendingEditId = null;
2321
2814
  if (!this.editingId || !this.editingNode) return;
2322
- const text = this.editingNode.textContent ?? "";
2323
- store.update(this.editingId, { text });
2324
- this.editingNode.contentEditable = "false";
2325
- Object.assign(this.editingNode.style, {
2326
- userSelect: "none",
2327
- cursor: "default"
2328
- });
2329
2815
  if (this.blurHandler) {
2330
2816
  this.editingNode.removeEventListener("blur", this.blurHandler);
2331
2817
  }
@@ -2335,6 +2821,14 @@ var NoteEditor = class {
2335
2821
  if (this.pointerHandler) {
2336
2822
  this.editingNode.removeEventListener("pointerdown", this.pointerHandler);
2337
2823
  }
2824
+ const text = sanitizeNoteHtml(this.editingNode.innerHTML);
2825
+ store.update(this.editingId, { text });
2826
+ this.editingNode.contentEditable = "false";
2827
+ Object.assign(this.editingNode.style, {
2828
+ userSelect: "none",
2829
+ cursor: "default"
2830
+ });
2831
+ this.toolbar?.hide();
2338
2832
  if (this.editingId && this.onStopCallback) {
2339
2833
  this.onStopCallback(this.editingId);
2340
2834
  }
@@ -2350,6 +2844,11 @@ var NoteEditor = class {
2350
2844
  this.stopEditing(store);
2351
2845
  }
2352
2846
  }
2847
+ updateToolbarPosition() {
2848
+ if (this.editingNode) {
2849
+ this.toolbar?.updatePosition(this.editingNode);
2850
+ }
2851
+ }
2353
2852
  activateEditing(node, elementId, store) {
2354
2853
  this.editingId = elementId;
2355
2854
  this.editingNode = node;
@@ -2368,8 +2867,21 @@ var NoteEditor = class {
2368
2867
  selection.removeAllRanges();
2369
2868
  selection.addRange(range);
2370
2869
  }
2371
- this.blurHandler = () => this.stopEditing(store);
2870
+ this.toolbar?.show(node);
2871
+ this.blurHandler = (e) => {
2872
+ const related = e.relatedTarget;
2873
+ if (related && this.toolbar?.getElement()?.contains(related)) return;
2874
+ this.stopEditing(store);
2875
+ };
2372
2876
  this.keyHandler = (e) => {
2877
+ if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
2878
+ const action = FORMAT_SHORTCUTS[e.key.toLowerCase()];
2879
+ if (action) {
2880
+ e.preventDefault();
2881
+ action();
2882
+ return;
2883
+ }
2884
+ }
2373
2885
  if (e.key === "Escape") {
2374
2886
  node.blur();
2375
2887
  }
@@ -2621,150 +3133,86 @@ var HistoryRecorder = class {
2621
3133
  }
2622
3134
  };
2623
3135
 
2624
- // src/elements/create-id.ts
2625
- var counter = 0;
2626
- function createId(prefix) {
2627
- return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
2628
- }
2629
-
2630
- // src/elements/element-factory.ts
2631
- function createStroke(input) {
2632
- return {
2633
- id: createId("stroke"),
2634
- type: "stroke",
2635
- position: input.position ?? { x: 0, y: 0 },
2636
- zIndex: input.zIndex ?? 0,
2637
- locked: input.locked ?? false,
2638
- layerId: input.layerId ?? "",
2639
- points: input.points,
2640
- color: input.color ?? "#000000",
2641
- width: input.width ?? 2,
2642
- opacity: input.opacity ?? 1
2643
- };
2644
- }
2645
- function createNote(input) {
2646
- return {
2647
- id: createId("note"),
2648
- type: "note",
2649
- position: input.position,
2650
- zIndex: input.zIndex ?? 0,
2651
- locked: input.locked ?? false,
2652
- layerId: input.layerId ?? "",
2653
- size: input.size ?? { w: 200, h: 100 },
2654
- text: input.text ?? "",
2655
- backgroundColor: input.backgroundColor ?? "#ffeb3b",
2656
- textColor: input.textColor ?? "#000000"
2657
- };
2658
- }
2659
- function createArrow(input) {
2660
- const bend = input.bend ?? 0;
2661
- const result = {
2662
- id: createId("arrow"),
2663
- type: "arrow",
2664
- position: input.position ?? { x: 0, y: 0 },
2665
- zIndex: input.zIndex ?? 0,
2666
- locked: input.locked ?? false,
2667
- layerId: input.layerId ?? "",
2668
- from: input.from,
2669
- to: input.to,
2670
- bend,
2671
- color: input.color ?? "#000000",
2672
- width: input.width ?? 2,
2673
- cachedControlPoint: getArrowControlPoint(input.from, input.to, bend)
2674
- };
2675
- if (input.fromBinding) result.fromBinding = input.fromBinding;
2676
- if (input.toBinding) result.toBinding = input.toBinding;
2677
- return result;
2678
- }
2679
- function createImage(input) {
2680
- return {
2681
- id: createId("image"),
2682
- type: "image",
2683
- position: input.position,
2684
- zIndex: input.zIndex ?? 0,
2685
- locked: input.locked ?? false,
2686
- layerId: input.layerId ?? "",
2687
- size: input.size,
2688
- src: input.src
2689
- };
2690
- }
2691
- function createHtmlElement(input) {
2692
- const el = {
2693
- id: createId("html"),
2694
- type: "html",
2695
- position: input.position,
2696
- zIndex: input.zIndex ?? 0,
2697
- locked: input.locked ?? false,
2698
- layerId: input.layerId ?? "",
2699
- size: input.size
2700
- };
2701
- if (input.domId) el.domId = input.domId;
2702
- return el;
2703
- }
2704
- function createShape(input) {
2705
- return {
2706
- id: createId("shape"),
2707
- type: "shape",
2708
- position: input.position,
2709
- zIndex: input.zIndex ?? 0,
2710
- locked: input.locked ?? false,
2711
- layerId: input.layerId ?? "",
2712
- shape: input.shape ?? "rectangle",
2713
- size: input.size,
2714
- strokeColor: input.strokeColor ?? "#000000",
2715
- strokeWidth: input.strokeWidth ?? 2,
2716
- fillColor: input.fillColor ?? "none"
2717
- };
2718
- }
2719
- function createGrid(input) {
2720
- return {
2721
- id: createId("grid"),
2722
- type: "grid",
2723
- position: input.position ?? { x: 0, y: 0 },
2724
- zIndex: input.zIndex ?? 0,
2725
- locked: input.locked ?? false,
2726
- layerId: input.layerId ?? "",
2727
- gridType: input.gridType ?? "square",
2728
- hexOrientation: input.hexOrientation ?? "pointy",
2729
- cellSize: input.cellSize ?? 40,
2730
- strokeColor: input.strokeColor ?? "#000000",
2731
- strokeWidth: input.strokeWidth ?? 1,
2732
- opacity: input.opacity ?? 1
2733
- };
3136
+ // src/canvas/note-canvas-renderer.ts
3137
+ function renderNoteOnCanvas(ctx, note) {
3138
+ const { x, y } = note.position;
3139
+ const { w, h } = note.size;
3140
+ const r = 4;
3141
+ const pad = 8;
3142
+ const baseFontSize = note.fontSize ?? DEFAULT_NOTE_FONT_SIZE;
3143
+ ctx.save();
3144
+ ctx.fillStyle = note.backgroundColor;
3145
+ ctx.beginPath();
3146
+ ctx.moveTo(x + r, y);
3147
+ ctx.lineTo(x + w - r, y);
3148
+ ctx.arcTo(x + w, y, x + w, y + r, r);
3149
+ ctx.lineTo(x + w, y + h - r);
3150
+ ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
3151
+ ctx.lineTo(x + r, y + h);
3152
+ ctx.arcTo(x, y + h, x, y + h - r, r);
3153
+ ctx.lineTo(x, y + r);
3154
+ ctx.arcTo(x, y, x + r, y, r);
3155
+ ctx.closePath();
3156
+ ctx.fill();
3157
+ if (note.text) {
3158
+ ctx.fillStyle = note.textColor;
3159
+ const runs = parseStyledRuns(note.text, baseFontSize);
3160
+ renderStyledRuns(ctx, runs, x + pad, y + pad, w - pad * 2);
3161
+ }
3162
+ ctx.restore();
2734
3163
  }
2735
- function createText(input) {
2736
- return {
2737
- id: createId("text"),
2738
- type: "text",
2739
- position: input.position,
2740
- zIndex: input.zIndex ?? 0,
2741
- locked: input.locked ?? false,
2742
- layerId: input.layerId ?? "",
2743
- size: input.size ?? { w: 200, h: 28 },
2744
- text: input.text ?? "",
2745
- fontSize: input.fontSize ?? 16,
2746
- color: input.color ?? "#1a1a1a",
2747
- textAlign: input.textAlign ?? "left"
2748
- };
3164
+ function buildFontString(run) {
3165
+ const style = run.italic ? "italic" : "normal";
3166
+ const weight = run.bold ? "bold" : "normal";
3167
+ return `${style} ${weight} ${run.fontSize}px system-ui, sans-serif`;
2749
3168
  }
2750
- function createTemplate(input) {
2751
- return {
2752
- id: createId("template"),
2753
- type: "template",
2754
- position: input.position,
2755
- zIndex: input.zIndex ?? 0,
2756
- locked: input.locked ?? false,
2757
- layerId: input.layerId ?? "",
2758
- templateShape: input.templateShape,
2759
- radius: input.radius,
2760
- angle: input.angle ?? 0,
2761
- fillColor: input.fillColor ?? "rgba(255, 87, 34, 0.2)",
2762
- strokeColor: input.strokeColor ?? "#FF5722",
2763
- strokeWidth: input.strokeWidth ?? 2,
2764
- opacity: input.opacity ?? 0.6,
2765
- feetPerCell: input.feetPerCell,
2766
- radiusFeet: input.radiusFeet
2767
- };
3169
+ function renderStyledRuns(ctx, runs, startX, startY, maxWidth) {
3170
+ ctx.textBaseline = "top";
3171
+ let cursorX = startX;
3172
+ let cursorY = startY;
3173
+ let lineHeight = 0;
3174
+ for (const run of runs) {
3175
+ ctx.font = buildFontString(run);
3176
+ const runLineHeight = run.fontSize * 1.3;
3177
+ lineHeight = Math.max(lineHeight, runLineHeight);
3178
+ const words = run.text.split(/(\n| )/);
3179
+ for (const word of words) {
3180
+ if (word === "\n") {
3181
+ cursorX = startX;
3182
+ cursorY += lineHeight;
3183
+ lineHeight = runLineHeight;
3184
+ continue;
3185
+ }
3186
+ if (word === " ") {
3187
+ const spaceWidth = ctx.measureText(" ").width;
3188
+ if (cursorX + spaceWidth > startX + maxWidth && cursorX > startX) {
3189
+ cursorX = startX;
3190
+ cursorY += lineHeight;
3191
+ lineHeight = runLineHeight;
3192
+ } else {
3193
+ cursorX += spaceWidth;
3194
+ }
3195
+ continue;
3196
+ }
3197
+ if (!word) continue;
3198
+ const metrics = ctx.measureText(word);
3199
+ if (cursorX + metrics.width > startX + maxWidth && cursorX > startX) {
3200
+ cursorX = startX;
3201
+ cursorY += lineHeight;
3202
+ lineHeight = runLineHeight;
3203
+ }
3204
+ ctx.fillText(word, cursorX, cursorY);
3205
+ if (run.underline) {
3206
+ const underY = cursorY + run.fontSize + 1;
3207
+ ctx.fillRect(cursorX, underY, metrics.width, 1);
3208
+ }
3209
+ if (run.strikethrough) {
3210
+ const strikeY = cursorY + run.fontSize * 0.55;
3211
+ ctx.fillRect(cursorX, strikeY, metrics.width, 1);
3212
+ }
3213
+ cursorX += metrics.width;
3214
+ }
3215
+ }
2768
3216
  }
2769
3217
 
2770
3218
  // src/canvas/export-image.ts
@@ -2843,33 +3291,6 @@ function computeBounds(elements, padding) {
2843
3291
  h: maxY - minY + padding * 2
2844
3292
  };
2845
3293
  }
2846
- function renderNoteOnCanvas(ctx, note) {
2847
- const { x, y } = note.position;
2848
- const { w, h } = note.size;
2849
- const r = 4;
2850
- const pad = 8;
2851
- ctx.save();
2852
- ctx.fillStyle = note.backgroundColor;
2853
- ctx.beginPath();
2854
- ctx.moveTo(x + r, y);
2855
- ctx.lineTo(x + w - r, y);
2856
- ctx.arcTo(x + w, y, x + w, y + r, r);
2857
- ctx.lineTo(x + w, y + h - r);
2858
- ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
2859
- ctx.lineTo(x + r, y + h);
2860
- ctx.arcTo(x, y + h, x, y + h - r, r);
2861
- ctx.lineTo(x, y + r);
2862
- ctx.arcTo(x, y, x + r, y, r);
2863
- ctx.closePath();
2864
- ctx.fill();
2865
- if (note.text) {
2866
- ctx.fillStyle = note.textColor;
2867
- ctx.font = "14px system-ui, sans-serif";
2868
- ctx.textBaseline = "top";
2869
- wrapText(ctx, note.text, x + pad, y + pad, w - pad * 2, 18);
2870
- }
2871
- ctx.restore();
2872
- }
2873
3294
  function renderTextOnCanvas(ctx, text) {
2874
3295
  if (!text.text) return;
2875
3296
  ctx.save();
@@ -2894,25 +3315,6 @@ function renderTextOnCanvas(ctx, text) {
2894
3315
  }
2895
3316
  ctx.restore();
2896
3317
  }
2897
- function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
2898
- const words = text.split(" ");
2899
- let line = "";
2900
- let offsetY = 0;
2901
- for (const word of words) {
2902
- const testLine = line ? `${line} ${word}` : word;
2903
- const metrics = ctx.measureText(testLine);
2904
- if (metrics.width > maxWidth && line) {
2905
- ctx.fillText(line, x, y + offsetY);
2906
- line = word;
2907
- offsetY += lineHeight;
2908
- } else {
2909
- line = testLine;
2910
- }
2911
- }
2912
- if (line) {
2913
- ctx.fillText(line, x, y + offsetY);
2914
- }
2915
- }
2916
3318
  function renderGridForBounds(ctx, grid, bounds) {
2917
3319
  const visibleBounds = {
2918
3320
  minX: bounds.x,
@@ -3311,13 +3713,13 @@ var DomNodeManager = class {
3311
3713
  padding: "8px",
3312
3714
  borderRadius: "4px",
3313
3715
  boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
3314
- fontSize: "14px",
3716
+ fontSize: `${element.fontSize ?? DEFAULT_NOTE_FONT_SIZE}px`,
3315
3717
  overflow: "hidden",
3316
3718
  cursor: "default",
3317
3719
  userSelect: "none",
3318
3720
  wordWrap: "break-word"
3319
3721
  });
3320
- node.textContent = element.text || "";
3722
+ node.innerHTML = element.text || "";
3321
3723
  node.addEventListener("dblclick", (e) => {
3322
3724
  e.stopPropagation();
3323
3725
  const id = node.dataset["elementId"];
@@ -3325,11 +3727,13 @@ var DomNodeManager = class {
3325
3727
  });
3326
3728
  }
3327
3729
  if (!this.isEditingElement(element.id)) {
3328
- if (node.textContent !== element.text) {
3329
- node.textContent = element.text || "";
3730
+ const text = element.text || "";
3731
+ if (node.innerHTML !== text) {
3732
+ node.innerHTML = text;
3330
3733
  }
3331
3734
  node.style.backgroundColor = element.backgroundColor;
3332
3735
  node.style.color = element.textColor;
3736
+ node.style.fontSize = `${element.fontSize ?? DEFAULT_NOTE_FONT_SIZE}px`;
3333
3737
  }
3334
3738
  }
3335
3739
  if (element.type === "html" && !node.dataset["initialized"]) {
@@ -3760,7 +4164,10 @@ var Viewport = class {
3760
4164
  this.renderLoop.markAllLayersDirty();
3761
4165
  this.requestRender();
3762
4166
  });
3763
- this.noteEditor = new NoteEditor();
4167
+ this.noteEditor = new NoteEditor({
4168
+ fontSizePresets: options.fontSizePresets,
4169
+ toolbar: options.toolbar
4170
+ });
3764
4171
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
3765
4172
  this.history = new HistoryStack();
3766
4173
  this.historyRecorder = new HistoryRecorder(this.store, this.history);
@@ -3816,6 +4223,7 @@ var Viewport = class {
3816
4223
  });
3817
4224
  this.unsubCamera = this.camera.onChange(() => {
3818
4225
  this.applyCameraTransform();
4226
+ this.noteEditor.updateToolbarPosition();
3819
4227
  this.requestRender();
3820
4228
  });
3821
4229
  this.unsubStore = [
@@ -3880,6 +4288,7 @@ var Viewport = class {
3880
4288
  renderLoop;
3881
4289
  domNodeManager;
3882
4290
  interactMode;
4291
+ gridChangeListeners = /* @__PURE__ */ new Set();
3883
4292
  get ctx() {
3884
4293
  return this.canvasEl.getContext("2d");
3885
4294
  }
@@ -3984,6 +4393,22 @@ var Viewport = class {
3984
4393
  this.historyRecorder.commit();
3985
4394
  this.requestRender();
3986
4395
  }
4396
+ getGridInfo() {
4397
+ const grid = this.store.getElementsByType("grid")[0];
4398
+ if (!grid) return null;
4399
+ return {
4400
+ gridType: grid.gridType,
4401
+ hexOrientation: grid.hexOrientation,
4402
+ cellSize: grid.cellSize,
4403
+ cellRadius: grid.gridType === "hex" ? grid.cellSize : grid.cellSize / 2
4404
+ };
4405
+ }
4406
+ onGridChange(listener) {
4407
+ this.gridChangeListeners.add(listener);
4408
+ return () => {
4409
+ this.gridChangeListeners.delete(listener);
4410
+ };
4411
+ }
3987
4412
  getRenderStats() {
3988
4413
  return this.renderLoop.getStats();
3989
4414
  }
@@ -4184,6 +4609,13 @@ var Viewport = class {
4184
4609
  this.toolContext.gridType = void 0;
4185
4610
  this.toolContext.hexOrientation = void 0;
4186
4611
  }
4612
+ this.notifyGridChangeListeners();
4613
+ }
4614
+ notifyGridChangeListeners() {
4615
+ const info = this.getGridInfo();
4616
+ for (const listener of this.gridChangeListeners) {
4617
+ listener(info);
4618
+ }
4187
4619
  }
4188
4620
  observeResize() {
4189
4621
  if (typeof ResizeObserver === "undefined") return;
@@ -5165,17 +5597,20 @@ var NoteTool = class {
5165
5597
  backgroundColor;
5166
5598
  textColor;
5167
5599
  size;
5600
+ fontSize;
5168
5601
  optionListeners = /* @__PURE__ */ new Set();
5169
5602
  constructor(options = {}) {
5170
5603
  this.backgroundColor = options.backgroundColor ?? "#ffeb3b";
5171
5604
  this.textColor = options.textColor ?? "#000000";
5172
5605
  this.size = options.size ?? { w: 200, h: 100 };
5606
+ this.fontSize = options.fontSize ?? DEFAULT_NOTE_FONT_SIZE;
5173
5607
  }
5174
5608
  getOptions() {
5175
5609
  return {
5176
5610
  backgroundColor: this.backgroundColor,
5177
5611
  textColor: this.textColor,
5178
- size: { ...this.size }
5612
+ size: { ...this.size },
5613
+ fontSize: this.fontSize
5179
5614
  };
5180
5615
  }
5181
5616
  onOptionsChange(listener) {
@@ -5186,6 +5621,7 @@ var NoteTool = class {
5186
5621
  if (options.backgroundColor !== void 0) this.backgroundColor = options.backgroundColor;
5187
5622
  if (options.textColor !== void 0) this.textColor = options.textColor;
5188
5623
  if (options.size !== void 0) this.size = options.size;
5624
+ if (options.fontSize !== void 0) this.fontSize = options.fontSize;
5189
5625
  this.notifyOptionsChange();
5190
5626
  }
5191
5627
  notifyOptionsChange() {
@@ -5203,6 +5639,7 @@ var NoteTool = class {
5203
5639
  size: { ...this.size },
5204
5640
  backgroundColor: this.backgroundColor,
5205
5641
  textColor: this.textColor,
5642
+ fontSize: this.fontSize,
5206
5643
  layerId: ctx.activeLayerId ?? ""
5207
5644
  });
5208
5645
  ctx.store.add(note);
@@ -5870,7 +6307,7 @@ var UpdateLayerCommand = class {
5870
6307
  };
5871
6308
 
5872
6309
  // src/index.ts
5873
- var VERSION = "0.9.0";
6310
+ var VERSION = "0.11.0";
5874
6311
  // Annotate the CommonJS export names for ESM import in node:
5875
6312
  0 && (module.exports = {
5876
6313
  AddElementCommand,
@@ -5880,6 +6317,8 @@ var VERSION = "0.9.0";
5880
6317
  BatchCommand,
5881
6318
  Camera,
5882
6319
  CreateLayerCommand,
6320
+ DEFAULT_FONT_SIZE_PRESETS,
6321
+ DEFAULT_NOTE_FONT_SIZE,
5883
6322
  ElementRenderer,
5884
6323
  ElementStore,
5885
6324
  EraserTool,
@@ -5893,6 +6332,7 @@ var VERSION = "0.9.0";
5893
6332
  MeasureTool,
5894
6333
  NoteEditor,
5895
6334
  NoteTool,
6335
+ NoteToolbar,
5896
6336
  PencilTool,
5897
6337
  Quadtree,
5898
6338
  RemoveElementCommand,
@@ -5923,6 +6363,7 @@ var VERSION = "0.9.0";
5923
6363
  exportState,
5924
6364
  findBindTarget,
5925
6365
  findBoundArrows,
6366
+ getActiveFormats,
5926
6367
  getArrowBounds,
5927
6368
  getArrowControlPoint,
5928
6369
  getArrowMidpoint,
@@ -5939,9 +6380,15 @@ var VERSION = "0.9.0";
5939
6380
  isBindable,
5940
6381
  isNearBezier,
5941
6382
  parseState,
6383
+ sanitizeNoteHtml,
6384
+ setFontSize,
5942
6385
  smartSnap,
5943
6386
  snapPoint,
5944
6387
  snapToHexCenter,
6388
+ toggleBold,
6389
+ toggleItalic,
6390
+ toggleStrikethrough,
6391
+ toggleUnderline,
5945
6392
  unbindArrow,
5946
6393
  updateBoundArrow
5947
6394
  });