@fieldnotes/core 0.9.0 → 0.10.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 = [
@@ -5069,17 +5467,20 @@ var NoteTool = class {
5069
5467
  backgroundColor;
5070
5468
  textColor;
5071
5469
  size;
5470
+ fontSize;
5072
5471
  optionListeners = /* @__PURE__ */ new Set();
5073
5472
  constructor(options = {}) {
5074
5473
  this.backgroundColor = options.backgroundColor ?? "#ffeb3b";
5075
5474
  this.textColor = options.textColor ?? "#000000";
5076
5475
  this.size = options.size ?? { w: 200, h: 100 };
5476
+ this.fontSize = options.fontSize ?? DEFAULT_NOTE_FONT_SIZE;
5077
5477
  }
5078
5478
  getOptions() {
5079
5479
  return {
5080
5480
  backgroundColor: this.backgroundColor,
5081
5481
  textColor: this.textColor,
5082
- size: { ...this.size }
5482
+ size: { ...this.size },
5483
+ fontSize: this.fontSize
5083
5484
  };
5084
5485
  }
5085
5486
  onOptionsChange(listener) {
@@ -5090,6 +5491,7 @@ var NoteTool = class {
5090
5491
  if (options.backgroundColor !== void 0) this.backgroundColor = options.backgroundColor;
5091
5492
  if (options.textColor !== void 0) this.textColor = options.textColor;
5092
5493
  if (options.size !== void 0) this.size = options.size;
5494
+ if (options.fontSize !== void 0) this.fontSize = options.fontSize;
5093
5495
  this.notifyOptionsChange();
5094
5496
  }
5095
5497
  notifyOptionsChange() {
@@ -5107,6 +5509,7 @@ var NoteTool = class {
5107
5509
  size: { ...this.size },
5108
5510
  backgroundColor: this.backgroundColor,
5109
5511
  textColor: this.textColor,
5512
+ fontSize: this.fontSize,
5110
5513
  layerId: ctx.activeLayerId ?? ""
5111
5514
  });
5112
5515
  ctx.store.add(note);
@@ -5774,7 +6177,7 @@ var UpdateLayerCommand = class {
5774
6177
  };
5775
6178
 
5776
6179
  // src/index.ts
5777
- var VERSION = "0.9.0";
6180
+ var VERSION = "0.10.0";
5778
6181
  export {
5779
6182
  AddElementCommand,
5780
6183
  ArrowTool,
@@ -5783,6 +6186,8 @@ export {
5783
6186
  BatchCommand,
5784
6187
  Camera,
5785
6188
  CreateLayerCommand,
6189
+ DEFAULT_FONT_SIZE_PRESETS,
6190
+ DEFAULT_NOTE_FONT_SIZE,
5786
6191
  ElementRenderer,
5787
6192
  ElementStore,
5788
6193
  EraserTool,
@@ -5796,6 +6201,7 @@ export {
5796
6201
  MeasureTool,
5797
6202
  NoteEditor,
5798
6203
  NoteTool,
6204
+ NoteToolbar,
5799
6205
  PencilTool,
5800
6206
  Quadtree,
5801
6207
  RemoveElementCommand,
@@ -5826,6 +6232,7 @@ export {
5826
6232
  exportState,
5827
6233
  findBindTarget,
5828
6234
  findBoundArrows,
6235
+ getActiveFormats,
5829
6236
  getArrowBounds,
5830
6237
  getArrowControlPoint,
5831
6238
  getArrowMidpoint,
@@ -5842,9 +6249,15 @@ export {
5842
6249
  isBindable,
5843
6250
  isNearBezier,
5844
6251
  parseState,
6252
+ sanitizeNoteHtml,
6253
+ setFontSize,
5845
6254
  smartSnap,
5846
6255
  snapPoint,
5847
6256
  snapToHexCenter,
6257
+ toggleBold,
6258
+ toggleItalic,
6259
+ toggleStrikethrough,
6260
+ toggleUnderline,
5848
6261
  unbindArrow,
5849
6262
  updateBoundArrow
5850
6263
  };