@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.js CHANGED
@@ -169,6 +169,120 @@ var Quadtree = class {
169
169
  }
170
170
  };
171
171
 
172
+ // src/elements/note-sanitizer.ts
173
+ var BOLD_TAGS = /* @__PURE__ */ new Set(["b", "strong"]);
174
+ var ITALIC_TAGS = /* @__PURE__ */ new Set(["i", "em"]);
175
+ var UNDERLINE_TAGS = /* @__PURE__ */ new Set(["u"]);
176
+ var STRIKE_TAGS = /* @__PURE__ */ new Set(["s", "strike", "del"]);
177
+ var BLOCK_TAGS = /* @__PURE__ */ new Set(["div"]);
178
+ function parseStyledRuns(html, baseFontSize) {
179
+ if (!html) return [];
180
+ const doc = new DOMParser().parseFromString(html, "text/html");
181
+ const runs = [];
182
+ const baseStyle = {
183
+ bold: false,
184
+ italic: false,
185
+ underline: false,
186
+ strikethrough: false,
187
+ fontSize: baseFontSize
188
+ };
189
+ walkNodes(doc.body, baseStyle, runs);
190
+ return runs;
191
+ }
192
+ function walkNodes(node, style, runs) {
193
+ for (const child of Array.from(node.childNodes)) {
194
+ if (child.nodeType === Node.TEXT_NODE) {
195
+ const text = child.textContent ?? "";
196
+ if (text) {
197
+ runs.push({ text, ...style });
198
+ }
199
+ continue;
200
+ }
201
+ if (child.nodeType !== Node.ELEMENT_NODE) continue;
202
+ const el = child;
203
+ const tag = el.tagName.toLowerCase();
204
+ if (tag === "br") {
205
+ runs.push({ text: "\n", ...style });
206
+ continue;
207
+ }
208
+ if (BLOCK_TAGS.has(tag) && runs.length > 0) {
209
+ const lastRun = runs[runs.length - 1];
210
+ if (lastRun && !lastRun.text.endsWith("\n")) {
211
+ runs.push({ text: "\n", ...style });
212
+ }
213
+ }
214
+ const childStyle = { ...style };
215
+ if (BOLD_TAGS.has(tag)) childStyle.bold = true;
216
+ if (ITALIC_TAGS.has(tag)) childStyle.italic = true;
217
+ if (UNDERLINE_TAGS.has(tag)) childStyle.underline = true;
218
+ if (STRIKE_TAGS.has(tag)) childStyle.strikethrough = true;
219
+ if (tag === "span") {
220
+ const fontSize = el.style.fontSize;
221
+ if (fontSize) {
222
+ childStyle.fontSize = parseInt(fontSize, 10) || style.fontSize;
223
+ }
224
+ }
225
+ walkNodes(el, childStyle, runs);
226
+ }
227
+ }
228
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
229
+ "b",
230
+ "strong",
231
+ "i",
232
+ "em",
233
+ "u",
234
+ "s",
235
+ "strike",
236
+ "del",
237
+ "span",
238
+ "br",
239
+ "div"
240
+ ]);
241
+ function sanitizeNoteHtml(html) {
242
+ if (!html) return "";
243
+ const doc = new DOMParser().parseFromString(html, "text/html");
244
+ sanitizeNode(doc.body);
245
+ return doc.body.innerHTML;
246
+ }
247
+ function sanitizeNode(node) {
248
+ const children = Array.from(node.childNodes);
249
+ for (const child of children) {
250
+ if (child.nodeType === Node.TEXT_NODE) continue;
251
+ if (child.nodeType !== Node.ELEMENT_NODE) {
252
+ child.remove();
253
+ continue;
254
+ }
255
+ const el = child;
256
+ const tag = el.tagName.toLowerCase();
257
+ if (!ALLOWED_TAGS.has(tag)) {
258
+ const fragment = document.createDocumentFragment();
259
+ while (el.firstChild) {
260
+ fragment.appendChild(el.firstChild);
261
+ }
262
+ node.replaceChild(fragment, el);
263
+ sanitizeNode(node);
264
+ return;
265
+ }
266
+ sanitizeAttributes(el, tag);
267
+ sanitizeNode(el);
268
+ }
269
+ }
270
+ function sanitizeAttributes(el, tag) {
271
+ const attrs = Array.from(el.attributes);
272
+ for (const attr of attrs) {
273
+ if (tag === "span" && attr.name === "style") {
274
+ const fontSize = el.style.fontSize;
275
+ if (fontSize) {
276
+ el.setAttribute("style", `font-size: ${fontSize};`);
277
+ } else {
278
+ el.removeAttribute("style");
279
+ }
280
+ continue;
281
+ }
282
+ el.removeAttribute(attr.name);
283
+ }
284
+ }
285
+
172
286
  // src/core/state-serializer.ts
173
287
  var CURRENT_VERSION = 2;
174
288
  function exportState(elements, camera, layers = []) {
@@ -231,7 +345,17 @@ function validateState(data) {
231
345
  ];
232
346
  }
233
347
  }
234
- var VALID_TYPES = /* @__PURE__ */ new Set(["stroke", "note", "arrow", "image", "html", "text", "shape", "grid"]);
348
+ var VALID_TYPES = /* @__PURE__ */ new Set([
349
+ "stroke",
350
+ "note",
351
+ "arrow",
352
+ "image",
353
+ "html",
354
+ "text",
355
+ "shape",
356
+ "grid",
357
+ "template"
358
+ ]);
235
359
  function validateElement(el) {
236
360
  if (!el || typeof el !== "object") {
237
361
  throw new Error("Invalid element: expected an object");
@@ -281,6 +405,9 @@ function migrateElement(obj) {
281
405
  if (obj["type"] === "note" && typeof obj["textColor"] !== "string") {
282
406
  obj["textColor"] = "#000000";
283
407
  }
408
+ if (obj["type"] === "note" && typeof obj["text"] === "string") {
409
+ obj["text"] = sanitizeNoteHtml(obj["text"]);
410
+ }
284
411
  }
285
412
 
286
413
  // src/core/snap.ts
@@ -2190,7 +2317,359 @@ var ElementRenderer = class {
2190
2317
  }
2191
2318
  };
2192
2319
 
2320
+ // src/elements/create-id.ts
2321
+ var counter = 0;
2322
+ function createId(prefix) {
2323
+ return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
2324
+ }
2325
+
2326
+ // src/elements/element-factory.ts
2327
+ var DEFAULT_NOTE_FONT_SIZE = 18;
2328
+ function createStroke(input) {
2329
+ return {
2330
+ id: createId("stroke"),
2331
+ type: "stroke",
2332
+ position: input.position ?? { x: 0, y: 0 },
2333
+ zIndex: input.zIndex ?? 0,
2334
+ locked: input.locked ?? false,
2335
+ layerId: input.layerId ?? "",
2336
+ points: input.points,
2337
+ color: input.color ?? "#000000",
2338
+ width: input.width ?? 2,
2339
+ opacity: input.opacity ?? 1
2340
+ };
2341
+ }
2342
+ function createNote(input) {
2343
+ return {
2344
+ id: createId("note"),
2345
+ type: "note",
2346
+ position: input.position,
2347
+ zIndex: input.zIndex ?? 0,
2348
+ locked: input.locked ?? false,
2349
+ layerId: input.layerId ?? "",
2350
+ size: input.size ?? { w: 200, h: 100 },
2351
+ text: input.text ?? "",
2352
+ backgroundColor: input.backgroundColor ?? "#ffeb3b",
2353
+ textColor: input.textColor ?? "#000000",
2354
+ fontSize: input.fontSize ?? DEFAULT_NOTE_FONT_SIZE
2355
+ };
2356
+ }
2357
+ function createArrow(input) {
2358
+ const bend = input.bend ?? 0;
2359
+ const result = {
2360
+ id: createId("arrow"),
2361
+ type: "arrow",
2362
+ position: input.position ?? { x: 0, y: 0 },
2363
+ zIndex: input.zIndex ?? 0,
2364
+ locked: input.locked ?? false,
2365
+ layerId: input.layerId ?? "",
2366
+ from: input.from,
2367
+ to: input.to,
2368
+ bend,
2369
+ color: input.color ?? "#000000",
2370
+ width: input.width ?? 2,
2371
+ cachedControlPoint: getArrowControlPoint(input.from, input.to, bend)
2372
+ };
2373
+ if (input.fromBinding) result.fromBinding = input.fromBinding;
2374
+ if (input.toBinding) result.toBinding = input.toBinding;
2375
+ return result;
2376
+ }
2377
+ function createImage(input) {
2378
+ return {
2379
+ id: createId("image"),
2380
+ type: "image",
2381
+ position: input.position,
2382
+ zIndex: input.zIndex ?? 0,
2383
+ locked: input.locked ?? false,
2384
+ layerId: input.layerId ?? "",
2385
+ size: input.size,
2386
+ src: input.src
2387
+ };
2388
+ }
2389
+ function createHtmlElement(input) {
2390
+ const el = {
2391
+ id: createId("html"),
2392
+ type: "html",
2393
+ position: input.position,
2394
+ zIndex: input.zIndex ?? 0,
2395
+ locked: input.locked ?? false,
2396
+ layerId: input.layerId ?? "",
2397
+ size: input.size
2398
+ };
2399
+ if (input.domId) el.domId = input.domId;
2400
+ return el;
2401
+ }
2402
+ function createShape(input) {
2403
+ return {
2404
+ id: createId("shape"),
2405
+ type: "shape",
2406
+ position: input.position,
2407
+ zIndex: input.zIndex ?? 0,
2408
+ locked: input.locked ?? false,
2409
+ layerId: input.layerId ?? "",
2410
+ shape: input.shape ?? "rectangle",
2411
+ size: input.size,
2412
+ strokeColor: input.strokeColor ?? "#000000",
2413
+ strokeWidth: input.strokeWidth ?? 2,
2414
+ fillColor: input.fillColor ?? "none"
2415
+ };
2416
+ }
2417
+ function createGrid(input) {
2418
+ return {
2419
+ id: createId("grid"),
2420
+ type: "grid",
2421
+ position: input.position ?? { x: 0, y: 0 },
2422
+ zIndex: input.zIndex ?? 0,
2423
+ locked: input.locked ?? false,
2424
+ layerId: input.layerId ?? "",
2425
+ gridType: input.gridType ?? "square",
2426
+ hexOrientation: input.hexOrientation ?? "pointy",
2427
+ cellSize: input.cellSize ?? 40,
2428
+ strokeColor: input.strokeColor ?? "#000000",
2429
+ strokeWidth: input.strokeWidth ?? 1,
2430
+ opacity: input.opacity ?? 1
2431
+ };
2432
+ }
2433
+ function createText(input) {
2434
+ return {
2435
+ id: createId("text"),
2436
+ type: "text",
2437
+ position: input.position,
2438
+ zIndex: input.zIndex ?? 0,
2439
+ locked: input.locked ?? false,
2440
+ layerId: input.layerId ?? "",
2441
+ size: input.size ?? { w: 200, h: 28 },
2442
+ text: input.text ?? "",
2443
+ fontSize: input.fontSize ?? 16,
2444
+ color: input.color ?? "#1a1a1a",
2445
+ textAlign: input.textAlign ?? "left"
2446
+ };
2447
+ }
2448
+ function createTemplate(input) {
2449
+ return {
2450
+ id: createId("template"),
2451
+ type: "template",
2452
+ position: input.position,
2453
+ zIndex: input.zIndex ?? 0,
2454
+ locked: input.locked ?? false,
2455
+ layerId: input.layerId ?? "",
2456
+ templateShape: input.templateShape,
2457
+ radius: input.radius,
2458
+ angle: input.angle ?? 0,
2459
+ fillColor: input.fillColor ?? "rgba(255, 87, 34, 0.2)",
2460
+ strokeColor: input.strokeColor ?? "#FF5722",
2461
+ strokeWidth: input.strokeWidth ?? 2,
2462
+ opacity: input.opacity ?? 0.6,
2463
+ feetPerCell: input.feetPerCell,
2464
+ radiusFeet: input.radiusFeet
2465
+ };
2466
+ }
2467
+
2468
+ // src/elements/note-formatting.ts
2469
+ function toggleBold() {
2470
+ document.execCommand("bold");
2471
+ }
2472
+ function toggleItalic() {
2473
+ document.execCommand("italic");
2474
+ }
2475
+ function toggleUnderline() {
2476
+ document.execCommand("underline");
2477
+ }
2478
+ function toggleStrikethrough() {
2479
+ document.execCommand("strikeThrough");
2480
+ }
2481
+ function setFontSize(size) {
2482
+ const sel = window.getSelection();
2483
+ if (!sel || sel.rangeCount === 0) return;
2484
+ const range = sel.getRangeAt(0);
2485
+ if (range.collapsed) return;
2486
+ const span = document.createElement("span");
2487
+ span.style.fontSize = `${size}px`;
2488
+ try {
2489
+ range.surroundContents(span);
2490
+ } catch {
2491
+ span.appendChild(range.extractContents());
2492
+ range.insertNode(span);
2493
+ }
2494
+ }
2495
+ function getActiveFormats() {
2496
+ const query = (cmd) => {
2497
+ try {
2498
+ return document.queryCommandState(cmd);
2499
+ } catch {
2500
+ return false;
2501
+ }
2502
+ };
2503
+ return {
2504
+ bold: query("bold"),
2505
+ italic: query("italic"),
2506
+ underline: query("underline"),
2507
+ strikethrough: query("strikeThrough")
2508
+ };
2509
+ }
2510
+
2511
+ // src/elements/note-toolbar.ts
2512
+ var TOOLBAR_HEIGHT = 32;
2513
+ var TOOLBAR_GAP = 4;
2514
+ var FORMAT_BUTTONS = [
2515
+ { label: "B", format: "bold", command: "bold" },
2516
+ { label: "I", format: "italic", command: "italic" },
2517
+ { label: "U", format: "underline", command: "underline" },
2518
+ { label: "S", format: "strikethrough", command: "strikeThrough" }
2519
+ ];
2520
+ var DEFAULT_FONT_SIZE_PRESETS = [
2521
+ { label: "Small", size: 14 },
2522
+ { label: "Normal", size: 18 },
2523
+ { label: "Large", size: 24 },
2524
+ { label: "Heading", size: 32 }
2525
+ ];
2526
+ var NoteToolbar = class {
2527
+ el = null;
2528
+ anchor = null;
2529
+ selectionListener = null;
2530
+ fontSizePresets;
2531
+ constructor(fontSizePresets) {
2532
+ this.fontSizePresets = fontSizePresets ?? DEFAULT_FONT_SIZE_PRESETS;
2533
+ }
2534
+ show(anchor) {
2535
+ this.hide();
2536
+ this.anchor = anchor;
2537
+ this.el = this.createToolbarElement();
2538
+ document.body.appendChild(this.el);
2539
+ this.positionToolbar(anchor);
2540
+ this.selectionListener = () => this.updateActiveStates();
2541
+ document.addEventListener("selectionchange", this.selectionListener);
2542
+ }
2543
+ hide() {
2544
+ if (this.selectionListener) {
2545
+ document.removeEventListener("selectionchange", this.selectionListener);
2546
+ this.selectionListener = null;
2547
+ }
2548
+ if (this.el) {
2549
+ this.el.remove();
2550
+ this.el = null;
2551
+ }
2552
+ this.anchor = null;
2553
+ }
2554
+ getElement() {
2555
+ return this.el;
2556
+ }
2557
+ updatePosition(anchor) {
2558
+ if (this.el) {
2559
+ this.positionToolbar(anchor);
2560
+ }
2561
+ }
2562
+ createToolbarElement() {
2563
+ const toolbar = document.createElement("div");
2564
+ toolbar.dataset["noteToolbar"] = "";
2565
+ Object.assign(toolbar.style, {
2566
+ position: "fixed",
2567
+ display: "flex",
2568
+ alignItems: "center",
2569
+ gap: "2px",
2570
+ padding: "2px 4px",
2571
+ background: "#fff",
2572
+ border: "1px solid #ccc",
2573
+ borderRadius: "4px",
2574
+ boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
2575
+ zIndex: "10000",
2576
+ height: `${TOOLBAR_HEIGHT}px`,
2577
+ userSelect: "none"
2578
+ });
2579
+ for (const btn of FORMAT_BUTTONS) {
2580
+ toolbar.appendChild(this.createFormatButton(btn));
2581
+ }
2582
+ toolbar.appendChild(this.createFontSizeSelect());
2583
+ return toolbar;
2584
+ }
2585
+ createFormatButton(config) {
2586
+ const btn = document.createElement("button");
2587
+ btn.dataset["format"] = config.format;
2588
+ btn.textContent = config.label;
2589
+ Object.assign(btn.style, {
2590
+ border: "1px solid transparent",
2591
+ borderRadius: "3px",
2592
+ background: "none",
2593
+ cursor: "pointer",
2594
+ padding: "2px 6px",
2595
+ fontSize: "13px",
2596
+ fontWeight: config.format === "bold" ? "bold" : "normal",
2597
+ fontStyle: config.format === "italic" ? "italic" : "normal",
2598
+ textDecoration: config.format === "underline" ? "underline" : config.format === "strikethrough" ? "line-through" : "none",
2599
+ minWidth: "24px",
2600
+ height: "24px",
2601
+ lineHeight: "24px"
2602
+ });
2603
+ btn.addEventListener("pointerdown", (e) => {
2604
+ e.preventDefault();
2605
+ document.execCommand(config.command);
2606
+ this.updateActiveStates();
2607
+ });
2608
+ return btn;
2609
+ }
2610
+ createFontSizeSelect() {
2611
+ const select = document.createElement("select");
2612
+ Object.assign(select.style, {
2613
+ border: "1px solid #ccc",
2614
+ borderRadius: "3px",
2615
+ background: "#fff",
2616
+ cursor: "pointer",
2617
+ padding: "2px",
2618
+ fontSize: "12px",
2619
+ height: "24px",
2620
+ marginLeft: "4px"
2621
+ });
2622
+ for (const preset of this.fontSizePresets) {
2623
+ const option = document.createElement("option");
2624
+ option.value = String(preset.size);
2625
+ option.textContent = preset.label;
2626
+ select.appendChild(option);
2627
+ }
2628
+ select.value = String(DEFAULT_NOTE_FONT_SIZE);
2629
+ select.addEventListener("pointerdown", (e) => {
2630
+ e.stopPropagation();
2631
+ });
2632
+ select.addEventListener("change", () => {
2633
+ setFontSize(Number(select.value));
2634
+ this.updateActiveStates();
2635
+ this.anchor?.focus();
2636
+ });
2637
+ return select;
2638
+ }
2639
+ positionToolbar(anchor) {
2640
+ if (!this.el) return;
2641
+ const rect = anchor.getBoundingClientRect();
2642
+ const toolbarWidth = this.el.offsetWidth || 200;
2643
+ let top = rect.top - TOOLBAR_HEIGHT - TOOLBAR_GAP;
2644
+ if (top < 0) {
2645
+ top = rect.bottom + TOOLBAR_GAP;
2646
+ }
2647
+ let left = rect.left + (rect.width - toolbarWidth) / 2;
2648
+ left = Math.max(4, left);
2649
+ Object.assign(this.el.style, {
2650
+ top: `${top}px`,
2651
+ left: `${left}px`
2652
+ });
2653
+ }
2654
+ updateActiveStates() {
2655
+ if (!this.el) return;
2656
+ const active = getActiveFormats();
2657
+ for (const config of FORMAT_BUTTONS) {
2658
+ const btn = this.el.querySelector(`[data-format="${config.format}"]`);
2659
+ if (!btn) continue;
2660
+ const isActive = active[config.format] ?? false;
2661
+ btn.style.background = isActive ? "#e0e0e0" : "none";
2662
+ btn.style.borderColor = isActive ? "#bbb" : "transparent";
2663
+ }
2664
+ }
2665
+ };
2666
+
2193
2667
  // src/elements/note-editor.ts
2668
+ var FORMAT_SHORTCUTS = {
2669
+ b: toggleBold,
2670
+ i: toggleItalic,
2671
+ u: toggleUnderline
2672
+ };
2194
2673
  var NoteEditor = class {
2195
2674
  editingId = null;
2196
2675
  editingNode = null;
@@ -2199,6 +2678,10 @@ var NoteEditor = class {
2199
2678
  pointerHandler = null;
2200
2679
  pendingEditId = null;
2201
2680
  onStopCallback = null;
2681
+ toolbar;
2682
+ constructor(options) {
2683
+ this.toolbar = options?.toolbar === false ? null : new NoteToolbar(options?.fontSizePresets);
2684
+ }
2202
2685
  get isEditing() {
2203
2686
  return this.editingId !== null;
2204
2687
  }
@@ -2223,13 +2706,6 @@ var NoteEditor = class {
2223
2706
  stopEditing(store) {
2224
2707
  this.pendingEditId = null;
2225
2708
  if (!this.editingId || !this.editingNode) return;
2226
- const text = this.editingNode.textContent ?? "";
2227
- store.update(this.editingId, { text });
2228
- this.editingNode.contentEditable = "false";
2229
- Object.assign(this.editingNode.style, {
2230
- userSelect: "none",
2231
- cursor: "default"
2232
- });
2233
2709
  if (this.blurHandler) {
2234
2710
  this.editingNode.removeEventListener("blur", this.blurHandler);
2235
2711
  }
@@ -2239,6 +2715,14 @@ var NoteEditor = class {
2239
2715
  if (this.pointerHandler) {
2240
2716
  this.editingNode.removeEventListener("pointerdown", this.pointerHandler);
2241
2717
  }
2718
+ const text = sanitizeNoteHtml(this.editingNode.innerHTML);
2719
+ store.update(this.editingId, { text });
2720
+ this.editingNode.contentEditable = "false";
2721
+ Object.assign(this.editingNode.style, {
2722
+ userSelect: "none",
2723
+ cursor: "default"
2724
+ });
2725
+ this.toolbar?.hide();
2242
2726
  if (this.editingId && this.onStopCallback) {
2243
2727
  this.onStopCallback(this.editingId);
2244
2728
  }
@@ -2254,6 +2738,11 @@ var NoteEditor = class {
2254
2738
  this.stopEditing(store);
2255
2739
  }
2256
2740
  }
2741
+ updateToolbarPosition() {
2742
+ if (this.editingNode) {
2743
+ this.toolbar?.updatePosition(this.editingNode);
2744
+ }
2745
+ }
2257
2746
  activateEditing(node, elementId, store) {
2258
2747
  this.editingId = elementId;
2259
2748
  this.editingNode = node;
@@ -2272,8 +2761,21 @@ var NoteEditor = class {
2272
2761
  selection.removeAllRanges();
2273
2762
  selection.addRange(range);
2274
2763
  }
2275
- this.blurHandler = () => this.stopEditing(store);
2764
+ this.toolbar?.show(node);
2765
+ this.blurHandler = (e) => {
2766
+ const related = e.relatedTarget;
2767
+ if (related && this.toolbar?.getElement()?.contains(related)) return;
2768
+ this.stopEditing(store);
2769
+ };
2276
2770
  this.keyHandler = (e) => {
2771
+ if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
2772
+ const action = FORMAT_SHORTCUTS[e.key.toLowerCase()];
2773
+ if (action) {
2774
+ e.preventDefault();
2775
+ action();
2776
+ return;
2777
+ }
2778
+ }
2277
2779
  if (e.key === "Escape") {
2278
2780
  node.blur();
2279
2781
  }
@@ -2525,150 +3027,86 @@ var HistoryRecorder = class {
2525
3027
  }
2526
3028
  };
2527
3029
 
2528
- // src/elements/create-id.ts
2529
- var counter = 0;
2530
- function createId(prefix) {
2531
- return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
2532
- }
2533
-
2534
- // src/elements/element-factory.ts
2535
- function createStroke(input) {
2536
- return {
2537
- id: createId("stroke"),
2538
- type: "stroke",
2539
- position: input.position ?? { x: 0, y: 0 },
2540
- zIndex: input.zIndex ?? 0,
2541
- locked: input.locked ?? false,
2542
- layerId: input.layerId ?? "",
2543
- points: input.points,
2544
- color: input.color ?? "#000000",
2545
- width: input.width ?? 2,
2546
- opacity: input.opacity ?? 1
2547
- };
2548
- }
2549
- function createNote(input) {
2550
- return {
2551
- id: createId("note"),
2552
- type: "note",
2553
- position: input.position,
2554
- zIndex: input.zIndex ?? 0,
2555
- locked: input.locked ?? false,
2556
- layerId: input.layerId ?? "",
2557
- size: input.size ?? { w: 200, h: 100 },
2558
- text: input.text ?? "",
2559
- backgroundColor: input.backgroundColor ?? "#ffeb3b",
2560
- textColor: input.textColor ?? "#000000"
2561
- };
2562
- }
2563
- function createArrow(input) {
2564
- const bend = input.bend ?? 0;
2565
- const result = {
2566
- id: createId("arrow"),
2567
- type: "arrow",
2568
- position: input.position ?? { x: 0, y: 0 },
2569
- zIndex: input.zIndex ?? 0,
2570
- locked: input.locked ?? false,
2571
- layerId: input.layerId ?? "",
2572
- from: input.from,
2573
- to: input.to,
2574
- bend,
2575
- color: input.color ?? "#000000",
2576
- width: input.width ?? 2,
2577
- cachedControlPoint: getArrowControlPoint(input.from, input.to, bend)
2578
- };
2579
- if (input.fromBinding) result.fromBinding = input.fromBinding;
2580
- if (input.toBinding) result.toBinding = input.toBinding;
2581
- return result;
2582
- }
2583
- function createImage(input) {
2584
- return {
2585
- id: createId("image"),
2586
- type: "image",
2587
- position: input.position,
2588
- zIndex: input.zIndex ?? 0,
2589
- locked: input.locked ?? false,
2590
- layerId: input.layerId ?? "",
2591
- size: input.size,
2592
- src: input.src
2593
- };
2594
- }
2595
- function createHtmlElement(input) {
2596
- const el = {
2597
- id: createId("html"),
2598
- type: "html",
2599
- position: input.position,
2600
- zIndex: input.zIndex ?? 0,
2601
- locked: input.locked ?? false,
2602
- layerId: input.layerId ?? "",
2603
- size: input.size
2604
- };
2605
- if (input.domId) el.domId = input.domId;
2606
- return el;
2607
- }
2608
- function createShape(input) {
2609
- return {
2610
- id: createId("shape"),
2611
- type: "shape",
2612
- position: input.position,
2613
- zIndex: input.zIndex ?? 0,
2614
- locked: input.locked ?? false,
2615
- layerId: input.layerId ?? "",
2616
- shape: input.shape ?? "rectangle",
2617
- size: input.size,
2618
- strokeColor: input.strokeColor ?? "#000000",
2619
- strokeWidth: input.strokeWidth ?? 2,
2620
- fillColor: input.fillColor ?? "none"
2621
- };
2622
- }
2623
- function createGrid(input) {
2624
- return {
2625
- id: createId("grid"),
2626
- type: "grid",
2627
- position: input.position ?? { x: 0, y: 0 },
2628
- zIndex: input.zIndex ?? 0,
2629
- locked: input.locked ?? false,
2630
- layerId: input.layerId ?? "",
2631
- gridType: input.gridType ?? "square",
2632
- hexOrientation: input.hexOrientation ?? "pointy",
2633
- cellSize: input.cellSize ?? 40,
2634
- strokeColor: input.strokeColor ?? "#000000",
2635
- strokeWidth: input.strokeWidth ?? 1,
2636
- opacity: input.opacity ?? 1
2637
- };
3030
+ // src/canvas/note-canvas-renderer.ts
3031
+ function renderNoteOnCanvas(ctx, note) {
3032
+ const { x, y } = note.position;
3033
+ const { w, h } = note.size;
3034
+ const r = 4;
3035
+ const pad = 8;
3036
+ const baseFontSize = note.fontSize ?? DEFAULT_NOTE_FONT_SIZE;
3037
+ ctx.save();
3038
+ ctx.fillStyle = note.backgroundColor;
3039
+ ctx.beginPath();
3040
+ ctx.moveTo(x + r, y);
3041
+ ctx.lineTo(x + w - r, y);
3042
+ ctx.arcTo(x + w, y, x + w, y + r, r);
3043
+ ctx.lineTo(x + w, y + h - r);
3044
+ ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
3045
+ ctx.lineTo(x + r, y + h);
3046
+ ctx.arcTo(x, y + h, x, y + h - r, r);
3047
+ ctx.lineTo(x, y + r);
3048
+ ctx.arcTo(x, y, x + r, y, r);
3049
+ ctx.closePath();
3050
+ ctx.fill();
3051
+ if (note.text) {
3052
+ ctx.fillStyle = note.textColor;
3053
+ const runs = parseStyledRuns(note.text, baseFontSize);
3054
+ renderStyledRuns(ctx, runs, x + pad, y + pad, w - pad * 2);
3055
+ }
3056
+ ctx.restore();
2638
3057
  }
2639
- function createText(input) {
2640
- return {
2641
- id: createId("text"),
2642
- type: "text",
2643
- position: input.position,
2644
- zIndex: input.zIndex ?? 0,
2645
- locked: input.locked ?? false,
2646
- layerId: input.layerId ?? "",
2647
- size: input.size ?? { w: 200, h: 28 },
2648
- text: input.text ?? "",
2649
- fontSize: input.fontSize ?? 16,
2650
- color: input.color ?? "#1a1a1a",
2651
- textAlign: input.textAlign ?? "left"
2652
- };
3058
+ function buildFontString(run) {
3059
+ const style = run.italic ? "italic" : "normal";
3060
+ const weight = run.bold ? "bold" : "normal";
3061
+ return `${style} ${weight} ${run.fontSize}px system-ui, sans-serif`;
2653
3062
  }
2654
- function createTemplate(input) {
2655
- return {
2656
- id: createId("template"),
2657
- type: "template",
2658
- position: input.position,
2659
- zIndex: input.zIndex ?? 0,
2660
- locked: input.locked ?? false,
2661
- layerId: input.layerId ?? "",
2662
- templateShape: input.templateShape,
2663
- radius: input.radius,
2664
- angle: input.angle ?? 0,
2665
- fillColor: input.fillColor ?? "rgba(255, 87, 34, 0.2)",
2666
- strokeColor: input.strokeColor ?? "#FF5722",
2667
- strokeWidth: input.strokeWidth ?? 2,
2668
- opacity: input.opacity ?? 0.6,
2669
- feetPerCell: input.feetPerCell,
2670
- radiusFeet: input.radiusFeet
2671
- };
3063
+ function renderStyledRuns(ctx, runs, startX, startY, maxWidth) {
3064
+ ctx.textBaseline = "top";
3065
+ let cursorX = startX;
3066
+ let cursorY = startY;
3067
+ let lineHeight = 0;
3068
+ for (const run of runs) {
3069
+ ctx.font = buildFontString(run);
3070
+ const runLineHeight = run.fontSize * 1.3;
3071
+ lineHeight = Math.max(lineHeight, runLineHeight);
3072
+ const words = run.text.split(/(\n| )/);
3073
+ for (const word of words) {
3074
+ if (word === "\n") {
3075
+ cursorX = startX;
3076
+ cursorY += lineHeight;
3077
+ lineHeight = runLineHeight;
3078
+ continue;
3079
+ }
3080
+ if (word === " ") {
3081
+ const spaceWidth = ctx.measureText(" ").width;
3082
+ if (cursorX + spaceWidth > startX + maxWidth && cursorX > startX) {
3083
+ cursorX = startX;
3084
+ cursorY += lineHeight;
3085
+ lineHeight = runLineHeight;
3086
+ } else {
3087
+ cursorX += spaceWidth;
3088
+ }
3089
+ continue;
3090
+ }
3091
+ if (!word) continue;
3092
+ const metrics = ctx.measureText(word);
3093
+ if (cursorX + metrics.width > startX + maxWidth && cursorX > startX) {
3094
+ cursorX = startX;
3095
+ cursorY += lineHeight;
3096
+ lineHeight = runLineHeight;
3097
+ }
3098
+ ctx.fillText(word, cursorX, cursorY);
3099
+ if (run.underline) {
3100
+ const underY = cursorY + run.fontSize + 1;
3101
+ ctx.fillRect(cursorX, underY, metrics.width, 1);
3102
+ }
3103
+ if (run.strikethrough) {
3104
+ const strikeY = cursorY + run.fontSize * 0.55;
3105
+ ctx.fillRect(cursorX, strikeY, metrics.width, 1);
3106
+ }
3107
+ cursorX += metrics.width;
3108
+ }
3109
+ }
2672
3110
  }
2673
3111
 
2674
3112
  // src/canvas/export-image.ts
@@ -2747,33 +3185,6 @@ function computeBounds(elements, padding) {
2747
3185
  h: maxY - minY + padding * 2
2748
3186
  };
2749
3187
  }
2750
- function renderNoteOnCanvas(ctx, note) {
2751
- const { x, y } = note.position;
2752
- const { w, h } = note.size;
2753
- const r = 4;
2754
- const pad = 8;
2755
- ctx.save();
2756
- ctx.fillStyle = note.backgroundColor;
2757
- ctx.beginPath();
2758
- ctx.moveTo(x + r, y);
2759
- ctx.lineTo(x + w - r, y);
2760
- ctx.arcTo(x + w, y, x + w, y + r, r);
2761
- ctx.lineTo(x + w, y + h - r);
2762
- ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
2763
- ctx.lineTo(x + r, y + h);
2764
- ctx.arcTo(x, y + h, x, y + h - r, r);
2765
- ctx.lineTo(x, y + r);
2766
- ctx.arcTo(x, y, x + r, y, r);
2767
- ctx.closePath();
2768
- ctx.fill();
2769
- if (note.text) {
2770
- ctx.fillStyle = note.textColor;
2771
- ctx.font = "14px system-ui, sans-serif";
2772
- ctx.textBaseline = "top";
2773
- wrapText(ctx, note.text, x + pad, y + pad, w - pad * 2, 18);
2774
- }
2775
- ctx.restore();
2776
- }
2777
3188
  function renderTextOnCanvas(ctx, text) {
2778
3189
  if (!text.text) return;
2779
3190
  ctx.save();
@@ -2798,25 +3209,6 @@ function renderTextOnCanvas(ctx, text) {
2798
3209
  }
2799
3210
  ctx.restore();
2800
3211
  }
2801
- function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
2802
- const words = text.split(" ");
2803
- let line = "";
2804
- let offsetY = 0;
2805
- for (const word of words) {
2806
- const testLine = line ? `${line} ${word}` : word;
2807
- const metrics = ctx.measureText(testLine);
2808
- if (metrics.width > maxWidth && line) {
2809
- ctx.fillText(line, x, y + offsetY);
2810
- line = word;
2811
- offsetY += lineHeight;
2812
- } else {
2813
- line = testLine;
2814
- }
2815
- }
2816
- if (line) {
2817
- ctx.fillText(line, x, y + offsetY);
2818
- }
2819
- }
2820
3212
  function renderGridForBounds(ctx, grid, bounds) {
2821
3213
  const visibleBounds = {
2822
3214
  minX: bounds.x,
@@ -3215,13 +3607,13 @@ var DomNodeManager = class {
3215
3607
  padding: "8px",
3216
3608
  borderRadius: "4px",
3217
3609
  boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
3218
- fontSize: "14px",
3610
+ fontSize: `${element.fontSize ?? DEFAULT_NOTE_FONT_SIZE}px`,
3219
3611
  overflow: "hidden",
3220
3612
  cursor: "default",
3221
3613
  userSelect: "none",
3222
3614
  wordWrap: "break-word"
3223
3615
  });
3224
- node.textContent = element.text || "";
3616
+ node.innerHTML = element.text || "";
3225
3617
  node.addEventListener("dblclick", (e) => {
3226
3618
  e.stopPropagation();
3227
3619
  const id = node.dataset["elementId"];
@@ -3229,11 +3621,13 @@ var DomNodeManager = class {
3229
3621
  });
3230
3622
  }
3231
3623
  if (!this.isEditingElement(element.id)) {
3232
- if (node.textContent !== element.text) {
3233
- node.textContent = element.text || "";
3624
+ const text = element.text || "";
3625
+ if (node.innerHTML !== text) {
3626
+ node.innerHTML = text;
3234
3627
  }
3235
3628
  node.style.backgroundColor = element.backgroundColor;
3236
3629
  node.style.color = element.textColor;
3630
+ node.style.fontSize = `${element.fontSize ?? DEFAULT_NOTE_FONT_SIZE}px`;
3237
3631
  }
3238
3632
  }
3239
3633
  if (element.type === "html" && !node.dataset["initialized"]) {
@@ -3664,7 +4058,10 @@ var Viewport = class {
3664
4058
  this.renderLoop.markAllLayersDirty();
3665
4059
  this.requestRender();
3666
4060
  });
3667
- this.noteEditor = new NoteEditor();
4061
+ this.noteEditor = new NoteEditor({
4062
+ fontSizePresets: options.fontSizePresets,
4063
+ toolbar: options.toolbar
4064
+ });
3668
4065
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
3669
4066
  this.history = new HistoryStack();
3670
4067
  this.historyRecorder = new HistoryRecorder(this.store, this.history);
@@ -3720,6 +4117,7 @@ var Viewport = class {
3720
4117
  });
3721
4118
  this.unsubCamera = this.camera.onChange(() => {
3722
4119
  this.applyCameraTransform();
4120
+ this.noteEditor.updateToolbarPosition();
3723
4121
  this.requestRender();
3724
4122
  });
3725
4123
  this.unsubStore = [
@@ -3784,6 +4182,7 @@ var Viewport = class {
3784
4182
  renderLoop;
3785
4183
  domNodeManager;
3786
4184
  interactMode;
4185
+ gridChangeListeners = /* @__PURE__ */ new Set();
3787
4186
  get ctx() {
3788
4187
  return this.canvasEl.getContext("2d");
3789
4188
  }
@@ -3888,6 +4287,22 @@ var Viewport = class {
3888
4287
  this.historyRecorder.commit();
3889
4288
  this.requestRender();
3890
4289
  }
4290
+ getGridInfo() {
4291
+ const grid = this.store.getElementsByType("grid")[0];
4292
+ if (!grid) return null;
4293
+ return {
4294
+ gridType: grid.gridType,
4295
+ hexOrientation: grid.hexOrientation,
4296
+ cellSize: grid.cellSize,
4297
+ cellRadius: grid.gridType === "hex" ? grid.cellSize : grid.cellSize / 2
4298
+ };
4299
+ }
4300
+ onGridChange(listener) {
4301
+ this.gridChangeListeners.add(listener);
4302
+ return () => {
4303
+ this.gridChangeListeners.delete(listener);
4304
+ };
4305
+ }
3891
4306
  getRenderStats() {
3892
4307
  return this.renderLoop.getStats();
3893
4308
  }
@@ -4088,6 +4503,13 @@ var Viewport = class {
4088
4503
  this.toolContext.gridType = void 0;
4089
4504
  this.toolContext.hexOrientation = void 0;
4090
4505
  }
4506
+ this.notifyGridChangeListeners();
4507
+ }
4508
+ notifyGridChangeListeners() {
4509
+ const info = this.getGridInfo();
4510
+ for (const listener of this.gridChangeListeners) {
4511
+ listener(info);
4512
+ }
4091
4513
  }
4092
4514
  observeResize() {
4093
4515
  if (typeof ResizeObserver === "undefined") return;
@@ -5069,17 +5491,20 @@ var NoteTool = class {
5069
5491
  backgroundColor;
5070
5492
  textColor;
5071
5493
  size;
5494
+ fontSize;
5072
5495
  optionListeners = /* @__PURE__ */ new Set();
5073
5496
  constructor(options = {}) {
5074
5497
  this.backgroundColor = options.backgroundColor ?? "#ffeb3b";
5075
5498
  this.textColor = options.textColor ?? "#000000";
5076
5499
  this.size = options.size ?? { w: 200, h: 100 };
5500
+ this.fontSize = options.fontSize ?? DEFAULT_NOTE_FONT_SIZE;
5077
5501
  }
5078
5502
  getOptions() {
5079
5503
  return {
5080
5504
  backgroundColor: this.backgroundColor,
5081
5505
  textColor: this.textColor,
5082
- size: { ...this.size }
5506
+ size: { ...this.size },
5507
+ fontSize: this.fontSize
5083
5508
  };
5084
5509
  }
5085
5510
  onOptionsChange(listener) {
@@ -5090,6 +5515,7 @@ var NoteTool = class {
5090
5515
  if (options.backgroundColor !== void 0) this.backgroundColor = options.backgroundColor;
5091
5516
  if (options.textColor !== void 0) this.textColor = options.textColor;
5092
5517
  if (options.size !== void 0) this.size = options.size;
5518
+ if (options.fontSize !== void 0) this.fontSize = options.fontSize;
5093
5519
  this.notifyOptionsChange();
5094
5520
  }
5095
5521
  notifyOptionsChange() {
@@ -5107,6 +5533,7 @@ var NoteTool = class {
5107
5533
  size: { ...this.size },
5108
5534
  backgroundColor: this.backgroundColor,
5109
5535
  textColor: this.textColor,
5536
+ fontSize: this.fontSize,
5110
5537
  layerId: ctx.activeLayerId ?? ""
5111
5538
  });
5112
5539
  ctx.store.add(note);
@@ -5774,7 +6201,7 @@ var UpdateLayerCommand = class {
5774
6201
  };
5775
6202
 
5776
6203
  // src/index.ts
5777
- var VERSION = "0.9.0";
6204
+ var VERSION = "0.11.0";
5778
6205
  export {
5779
6206
  AddElementCommand,
5780
6207
  ArrowTool,
@@ -5783,6 +6210,8 @@ export {
5783
6210
  BatchCommand,
5784
6211
  Camera,
5785
6212
  CreateLayerCommand,
6213
+ DEFAULT_FONT_SIZE_PRESETS,
6214
+ DEFAULT_NOTE_FONT_SIZE,
5786
6215
  ElementRenderer,
5787
6216
  ElementStore,
5788
6217
  EraserTool,
@@ -5796,6 +6225,7 @@ export {
5796
6225
  MeasureTool,
5797
6226
  NoteEditor,
5798
6227
  NoteTool,
6228
+ NoteToolbar,
5799
6229
  PencilTool,
5800
6230
  Quadtree,
5801
6231
  RemoveElementCommand,
@@ -5826,6 +6256,7 @@ export {
5826
6256
  exportState,
5827
6257
  findBindTarget,
5828
6258
  findBoundArrows,
6259
+ getActiveFormats,
5829
6260
  getArrowBounds,
5830
6261
  getArrowControlPoint,
5831
6262
  getArrowMidpoint,
@@ -5842,9 +6273,15 @@ export {
5842
6273
  isBindable,
5843
6274
  isNearBezier,
5844
6275
  parseState,
6276
+ sanitizeNoteHtml,
6277
+ setFontSize,
5845
6278
  smartSnap,
5846
6279
  snapPoint,
5847
6280
  snapToHexCenter,
6281
+ toggleBold,
6282
+ toggleItalic,
6283
+ toggleStrikethrough,
6284
+ toggleUnderline,
5848
6285
  unbindArrow,
5849
6286
  updateBoundArrow
5850
6287
  };