@aznabee/freehand-ui 0.2.0 → 0.3.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.d.ts CHANGED
@@ -28,7 +28,25 @@ export type DecorationType =
28
28
  */
29
29
  export type LocalizedText = string | Record<string, string>;
30
30
 
31
+ /**
32
+ * Words of a note drawn in a colour (and weight) of their own. Every
33
+ * occurrence of `text` in the note is painted, including one that wraps onto
34
+ * the next line. Where two highlights overlap, the earlier one wins.
35
+ */
36
+ export interface NoteHighlight {
37
+ /** The words to find, exactly as written in the note. */
38
+ text: string;
39
+ /** Any CSS colour. Omit to keep the note's own colour. */
40
+ color?: string;
41
+ /** e.g. `700`. Omit to keep the note's own weight. */
42
+ fontWeight?: number | string;
43
+ }
44
+
31
45
  export interface NoteOptions {
46
+ /**
47
+ * What the note says. A line break (`\n`) starts a new line; see
48
+ * `wordsPerLine` for how the rest is wrapped.
49
+ */
32
50
  text: LocalizedText;
33
51
  /**
34
52
  * Which side of the element the note sits on. Always honoured - a note that
@@ -49,6 +67,27 @@ export interface NoteOptions {
49
67
  * visitor's own languages. Only meaningful when `text` is a map.
50
68
  */
51
69
  locale?: string | string[] | null;
70
+ /**
71
+ * Colour of the handwriting and its underline. Defaults to the doodle's own
72
+ * `color`.
73
+ */
74
+ color?: string;
75
+ /** Size of the handwriting, px. Default `19`. */
76
+ fontSize?: number;
77
+ /** Weight of the handwriting. Default `600`. */
78
+ fontWeight?: number | string;
79
+ /**
80
+ * Rotate the note by exactly this many degrees (negative leans it up to the
81
+ * right). Omit for a small random tilt, different for each note.
82
+ */
83
+ tilt?: number;
84
+ /**
85
+ * Words per line. Default `2`. Use `0` to break only where the text has a
86
+ * line break (`\n`).
87
+ */
88
+ wordsPerLine?: number;
89
+ /** Paint some of the words in another colour, e.g. `{ text: "Game Mode!", color: "#ffc83d" }`. */
90
+ highlight?: NoteHighlight | NoteHighlight[];
52
91
  }
53
92
 
54
93
  export interface ArrowAnimationOptions {
@@ -85,6 +124,14 @@ export interface ArrowOptions {
85
124
  /** `"edge"` lands just outside the frame; `"center"` points into it. */
86
125
  to?: DoodlePosition | "edge";
87
126
  style?: "curved" | "straight" | "dotted" | "looped";
127
+ /** Colour of the arrow. Defaults to the doodle's own `color`. */
128
+ color?: string;
129
+ /** Thickness of the arrow's line, px. Defaults to the doodle's `strokeWidth`. */
130
+ strokeWidth?: number;
131
+ /** How shaky the pen is. Defaults to the doodle's `roughness`. */
132
+ roughness?: number;
133
+ /** `0`-`1`. Defaults to the doodle's `opacity`. */
134
+ opacity?: number;
88
135
  /**
89
136
  * Motion for the arrow. On by default - the shaft draws itself on and the
90
137
  * whole mark leans toward its target. `false` draws it static. Respects
package/dist/native.cjs CHANGED
@@ -1,8 +1,6 @@
1
- var __create = Object.create;
2
1
  var __defProp = Object.defineProperty;
3
2
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getProtoOf = Object.getPrototypeOf;
6
4
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
5
  var __export = (target, all) => {
8
6
  for (var name in all)
@@ -16,14 +14,6 @@ var __copyProps = (to, from, except, desc) => {
16
14
  }
17
15
  return to;
18
16
  };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
- // If the importer is in node compatibility mode or this is not an ESM
21
- // file that has been converted to a CommonJS file using a Babel-
22
- // compatible transform (i.e. "__esModule" has not been set), then set
23
- // "default" to the CommonJS "module.exports" for node compatibility.
24
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
- mod
26
- ));
27
17
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
18
 
29
19
  // src/native/react-native.js
@@ -36,7 +26,7 @@ __export(react_native_exports, {
36
26
  module.exports = __toCommonJS(react_native_exports);
37
27
  var import_react = require("react");
38
28
  var import_react_native = require("react-native");
39
- var import_react_native_svg = __toESM(require("react-native-svg"), 1);
29
+ var import_react_native_svg = require("react-native-svg");
40
30
 
41
31
  // src/animation.js
42
32
  var STYLE_MARKER = "data-freehand-animation";
@@ -352,6 +342,9 @@ function mergeOptions(options = {}) {
352
342
  // `animate: false` opts out, an object tunes speed and gestures
353
343
  animate: normalizeArrowAnimation(config.animate)
354
344
  };
345
+ for (const key of ["color", "strokeWidth", "roughness", "opacity"]) {
346
+ if (config[key] !== void 0) merged.arrow[key] = config[key];
347
+ }
355
348
  }
356
349
  if (merged.decorations === true) {
357
350
  merged.decorations = { count: null, types: null };
@@ -1279,7 +1272,24 @@ function boundsOf(points) {
1279
1272
  height: Math.max(...ys) - Math.min(...ys)
1280
1273
  };
1281
1274
  }
1282
- function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
1275
+ function resolveArrowStyle(base, options = {}) {
1276
+ const style = { ...base };
1277
+ if (typeof options.color === "string" && options.color.trim()) {
1278
+ style.color = options.color;
1279
+ }
1280
+ if (Number.isFinite(options.strokeWidth) && options.strokeWidth > 0) {
1281
+ style.strokeWidth = options.strokeWidth;
1282
+ }
1283
+ if (Number.isFinite(options.roughness) && options.roughness >= 0) {
1284
+ style.roughness = options.roughness;
1285
+ }
1286
+ if (Number.isFinite(options.opacity)) {
1287
+ style.opacity = Math.min(1, Math.max(0, options.opacity));
1288
+ }
1289
+ return style;
1290
+ }
1291
+ function drawArrow(svg, rect, arrowOptions, baseStyle, seed, origin = null, context = {}) {
1292
+ const style = resolveArrowStyle(baseStyle, arrowOptions);
1283
1293
  const random = createRandom(seed + 17);
1284
1294
  const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
1285
1295
  const dx = end.x - start.x;
@@ -1399,17 +1409,69 @@ function drawUnderline(svg, x, y, width, style, random) {
1399
1409
  );
1400
1410
  }
1401
1411
  var DEFAULT_WORDS_PER_LINE = 2;
1402
- function wrapWords(text, perLine) {
1412
+ function wrapWords(text, perLine, marks = null) {
1413
+ const source = String(text);
1403
1414
  const lines = [];
1404
- for (const paragraph of String(text).split(/\r?\n/)) {
1405
- const words = paragraph.trim().split(/\s+/).filter(Boolean);
1415
+ let cursor = 0;
1416
+ for (const [index, paragraph] of source.split(/(\r?\n)/).entries()) {
1417
+ const start = cursor;
1418
+ cursor += paragraph.length;
1419
+ if (index % 2 === 1) continue;
1420
+ const words = [...paragraph.matchAll(/\S+/g)].map((m) => ({
1421
+ text: m[0],
1422
+ at: start + m.index
1423
+ }));
1406
1424
  if (!words.length) continue;
1407
1425
  const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
1408
1426
  for (let i = 0; i < words.length; i += size) {
1409
- lines.push(words.slice(i, i + size).join(" "));
1427
+ lines.push(lineOf(words.slice(i, i + size), marks));
1410
1428
  }
1411
1429
  }
1412
- return lines.length ? lines : [String(text)];
1430
+ return lines.length ? lines : [lineOf([{ text: source, at: 0 }], marks)];
1431
+ }
1432
+ function lineOf(words, marks) {
1433
+ const text = words.map((w) => w.text).join(" ");
1434
+ const result = [];
1435
+ let previous = -1;
1436
+ words.forEach((word, index) => {
1437
+ const first = marks ? marks[word.at] ?? -1 : -1;
1438
+ if (index > 0) result.push(previous === first ? first : -1);
1439
+ for (let k = 0; k < word.text.length; k++) {
1440
+ result.push(marks ? marks[word.at + k] ?? -1 : -1);
1441
+ }
1442
+ previous = marks ? marks[word.at + word.text.length - 1] ?? -1 : -1;
1443
+ });
1444
+ return { text, marks: result };
1445
+ }
1446
+ function normalizeHighlights(highlight) {
1447
+ const list = Array.isArray(highlight) ? highlight : highlight ? [highlight] : [];
1448
+ return list.map((entry) => typeof entry === "string" ? { text: entry } : entry).filter((entry) => entry && typeof entry.text === "string" && entry.text);
1449
+ }
1450
+ function highlightMarks(text, highlights) {
1451
+ if (!highlights.length) return null;
1452
+ const marks = new Int16Array(text.length).fill(-1);
1453
+ let any = false;
1454
+ highlights.forEach((highlight, index) => {
1455
+ for (let at = text.indexOf(highlight.text); at !== -1; at = text.indexOf(highlight.text, at + highlight.text.length)) {
1456
+ for (let k = at; k < at + highlight.text.length; k++) {
1457
+ if (marks[k] === -1) {
1458
+ marks[k] = index;
1459
+ any = true;
1460
+ }
1461
+ }
1462
+ }
1463
+ });
1464
+ return any ? marks : null;
1465
+ }
1466
+ function segmentsOf(line) {
1467
+ const segments = [];
1468
+ for (let i = 0; i < line.text.length; i++) {
1469
+ const highlight = line.marks[i] ?? -1;
1470
+ const last = segments[segments.length - 1];
1471
+ if (last && last.highlight === highlight) last.text += line.text[i];
1472
+ else segments.push({ text: line.text[i], highlight });
1473
+ }
1474
+ return segments;
1413
1475
  }
1414
1476
  function measureWidth(element, content, fontSize) {
1415
1477
  try {
@@ -1426,7 +1488,13 @@ function measureWidth(element, content, fontSize) {
1426
1488
  }
1427
1489
  function createNoteText(svg, text, style, options = {}) {
1428
1490
  const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
1429
- const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
1491
+ const highlights = normalizeHighlights(options.highlight);
1492
+ const wrapped = wrapWords(
1493
+ text,
1494
+ options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE,
1495
+ highlightMarks(String(text), highlights)
1496
+ );
1497
+ const lines = wrapped.map((line) => line.text);
1430
1498
  const lineHeight = fontSize * 1.05;
1431
1499
  const rtl = options.rtl ?? isRtlText(text);
1432
1500
  const group = createSvgElement("g");
@@ -1437,7 +1505,7 @@ function createNoteText(svg, text, style, options = {}) {
1437
1505
  textEl.setAttribute("opacity", String(style.opacity));
1438
1506
  textEl.setAttribute("font-size", String(fontSize));
1439
1507
  textEl.setAttribute("font-family", style.fontFamily || HANDWRITTEN_FONT);
1440
- textEl.setAttribute("font-weight", "600");
1508
+ textEl.setAttribute("font-weight", String(style.fontWeight ?? "600"));
1441
1509
  textEl.setAttribute("letter-spacing", "0.4");
1442
1510
  textEl.setAttribute("text-anchor", "start");
1443
1511
  textEl.setAttribute("dominant-baseline", "auto");
@@ -1448,11 +1516,25 @@ function createNoteText(svg, text, style, options = {}) {
1448
1516
  if (options.locale) {
1449
1517
  textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
1450
1518
  }
1451
- const tspans = lines.map((line, index) => {
1519
+ const tspans = wrapped.map((line, index) => {
1452
1520
  const tspan = createSvgElement("tspan");
1453
- tspan.textContent = line;
1454
1521
  tspan.setAttribute("x", "0");
1455
1522
  tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
1523
+ const segments = segmentsOf(line);
1524
+ if (segments.every((segment) => segment.highlight === -1)) {
1525
+ tspan.textContent = line.text;
1526
+ } else {
1527
+ for (const segment of segments) {
1528
+ const run = createSvgElement("tspan");
1529
+ run.textContent = segment.text;
1530
+ const highlight = highlights[segment.highlight];
1531
+ if (highlight?.color) run.setAttribute("fill", highlight.color);
1532
+ if (highlight?.fontWeight != null) {
1533
+ run.setAttribute("font-weight", String(highlight.fontWeight));
1534
+ }
1535
+ tspan.appendChild(run);
1536
+ }
1537
+ }
1456
1538
  textEl.appendChild(tspan);
1457
1539
  return tspan;
1458
1540
  });
@@ -1555,7 +1637,21 @@ function nudgeIntoBand(box, guard, band) {
1555
1637
  if (!dx) return box;
1556
1638
  return { ...box, x: box.x + dx };
1557
1639
  }
1558
- function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
1640
+ function resolveNoteStyle(base, options = {}) {
1641
+ const style = { ...base };
1642
+ if (typeof options.color === "string" && options.color.trim()) {
1643
+ style.color = options.color;
1644
+ }
1645
+ if (Number.isFinite(options.fontSize) && options.fontSize > 0) {
1646
+ style.fontSize = options.fontSize;
1647
+ }
1648
+ if (typeof options.fontWeight === "number" && options.fontWeight > 0 || typeof options.fontWeight === "string" && options.fontWeight.trim()) {
1649
+ style.fontWeight = options.fontWeight;
1650
+ }
1651
+ return style;
1652
+ }
1653
+ function renderAnnotation(svg, rect, noteOptions, baseStyle, seed = 1, layout = {}) {
1654
+ const style = resolveNoteStyle(baseStyle, noteOptions ?? {});
1559
1655
  const { text, locale } = resolveLocalizedText(
1560
1656
  noteOptions?.text,
1561
1657
  noteOptions?.locale
@@ -1566,9 +1662,11 @@ function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {})
1566
1662
  const random = createRandom(seed + 9);
1567
1663
  const note = createNoteText(svg, text, style, {
1568
1664
  wordsPerLine: noteOptions.wordsPerLine,
1569
- locale
1665
+ locale,
1666
+ highlight: noteOptions.highlight
1570
1667
  });
1571
- const tilt = (random() - 0.45) * 7;
1668
+ const randomTilt = (random() - 0.45) * 7;
1669
+ const tilt = Number.isFinite(noteOptions.tilt) ? noteOptions.tilt : randomTilt;
1572
1670
  const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
1573
1671
  const size = {
1574
1672
  width: note.width,
@@ -2166,7 +2264,16 @@ function blurOf(node) {
2166
2264
  }
2167
2265
  function renderNode(node, key, context) {
2168
2266
  const props = { key, ...propsOf(node) };
2169
- if (node.tagName === "tspan") return (0, import_react.createElement)(import_react_native_svg.TSpan, props, node.textContent);
2267
+ if (node.tagName === "tspan") {
2268
+ if (node.children.length) {
2269
+ return (0, import_react.createElement)(
2270
+ import_react_native_svg.TSpan,
2271
+ props,
2272
+ ...node.children.map((child, index) => renderNode(child, index, context))
2273
+ );
2274
+ }
2275
+ return (0, import_react.createElement)(import_react_native_svg.TSpan, props, node.textContent);
2276
+ }
2170
2277
  if (node.tagName === "circle" && blurOf(node) > 0) {
2171
2278
  const id = `${context.prefix}-glow-${context.defs.length}`;
2172
2279
  const color = props.fill;
@@ -2229,7 +2336,7 @@ function LayerSvg({ nodes, bounds, clock, prefix }) {
2229
2336
  const context = { defs: [], clock, prefix };
2230
2337
  const content = nodes.map((node, index) => renderNode(node, index, context));
2231
2338
  return (0, import_react.createElement)(
2232
- import_react_native_svg.default,
2339
+ import_react_native_svg.Svg,
2233
2340
  {
2234
2341
  style: import_react_native.StyleSheet.absoluteFill,
2235
2342
  width: bounds.width,
package/dist/native.d.ts CHANGED
@@ -8,6 +8,7 @@ export type {
8
8
  DecorationType,
9
9
  LocalizedText,
10
10
  NoteOptions,
11
+ NoteHighlight,
11
12
  ArrowOptions,
12
13
  ArrowAnimationOptions,
13
14
  DecorationOptions,
package/dist/native.js CHANGED
@@ -17,7 +17,8 @@ import {
17
17
  View,
18
18
  useWindowDimensions
19
19
  } from "react-native";
20
- import Svg, {
20
+ import {
21
+ Svg,
21
22
  Circle,
22
23
  Defs,
23
24
  G,
@@ -342,6 +343,9 @@ function mergeOptions(options = {}) {
342
343
  // `animate: false` opts out, an object tunes speed and gestures
343
344
  animate: normalizeArrowAnimation(config.animate)
344
345
  };
346
+ for (const key of ["color", "strokeWidth", "roughness", "opacity"]) {
347
+ if (config[key] !== void 0) merged.arrow[key] = config[key];
348
+ }
345
349
  }
346
350
  if (merged.decorations === true) {
347
351
  merged.decorations = { count: null, types: null };
@@ -1269,7 +1273,24 @@ function boundsOf(points) {
1269
1273
  height: Math.max(...ys) - Math.min(...ys)
1270
1274
  };
1271
1275
  }
1272
- function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
1276
+ function resolveArrowStyle(base, options = {}) {
1277
+ const style = { ...base };
1278
+ if (typeof options.color === "string" && options.color.trim()) {
1279
+ style.color = options.color;
1280
+ }
1281
+ if (Number.isFinite(options.strokeWidth) && options.strokeWidth > 0) {
1282
+ style.strokeWidth = options.strokeWidth;
1283
+ }
1284
+ if (Number.isFinite(options.roughness) && options.roughness >= 0) {
1285
+ style.roughness = options.roughness;
1286
+ }
1287
+ if (Number.isFinite(options.opacity)) {
1288
+ style.opacity = Math.min(1, Math.max(0, options.opacity));
1289
+ }
1290
+ return style;
1291
+ }
1292
+ function drawArrow(svg, rect, arrowOptions, baseStyle, seed, origin = null, context = {}) {
1293
+ const style = resolveArrowStyle(baseStyle, arrowOptions);
1273
1294
  const random = createRandom(seed + 17);
1274
1295
  const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
1275
1296
  const dx = end.x - start.x;
@@ -1389,17 +1410,69 @@ function drawUnderline(svg, x, y, width, style, random) {
1389
1410
  );
1390
1411
  }
1391
1412
  var DEFAULT_WORDS_PER_LINE = 2;
1392
- function wrapWords(text, perLine) {
1413
+ function wrapWords(text, perLine, marks = null) {
1414
+ const source = String(text);
1393
1415
  const lines = [];
1394
- for (const paragraph of String(text).split(/\r?\n/)) {
1395
- const words = paragraph.trim().split(/\s+/).filter(Boolean);
1416
+ let cursor = 0;
1417
+ for (const [index, paragraph] of source.split(/(\r?\n)/).entries()) {
1418
+ const start = cursor;
1419
+ cursor += paragraph.length;
1420
+ if (index % 2 === 1) continue;
1421
+ const words = [...paragraph.matchAll(/\S+/g)].map((m) => ({
1422
+ text: m[0],
1423
+ at: start + m.index
1424
+ }));
1396
1425
  if (!words.length) continue;
1397
1426
  const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
1398
1427
  for (let i = 0; i < words.length; i += size) {
1399
- lines.push(words.slice(i, i + size).join(" "));
1428
+ lines.push(lineOf(words.slice(i, i + size), marks));
1400
1429
  }
1401
1430
  }
1402
- return lines.length ? lines : [String(text)];
1431
+ return lines.length ? lines : [lineOf([{ text: source, at: 0 }], marks)];
1432
+ }
1433
+ function lineOf(words, marks) {
1434
+ const text = words.map((w) => w.text).join(" ");
1435
+ const result = [];
1436
+ let previous = -1;
1437
+ words.forEach((word, index) => {
1438
+ const first = marks ? marks[word.at] ?? -1 : -1;
1439
+ if (index > 0) result.push(previous === first ? first : -1);
1440
+ for (let k = 0; k < word.text.length; k++) {
1441
+ result.push(marks ? marks[word.at + k] ?? -1 : -1);
1442
+ }
1443
+ previous = marks ? marks[word.at + word.text.length - 1] ?? -1 : -1;
1444
+ });
1445
+ return { text, marks: result };
1446
+ }
1447
+ function normalizeHighlights(highlight) {
1448
+ const list = Array.isArray(highlight) ? highlight : highlight ? [highlight] : [];
1449
+ return list.map((entry) => typeof entry === "string" ? { text: entry } : entry).filter((entry) => entry && typeof entry.text === "string" && entry.text);
1450
+ }
1451
+ function highlightMarks(text, highlights) {
1452
+ if (!highlights.length) return null;
1453
+ const marks = new Int16Array(text.length).fill(-1);
1454
+ let any = false;
1455
+ highlights.forEach((highlight, index) => {
1456
+ for (let at = text.indexOf(highlight.text); at !== -1; at = text.indexOf(highlight.text, at + highlight.text.length)) {
1457
+ for (let k = at; k < at + highlight.text.length; k++) {
1458
+ if (marks[k] === -1) {
1459
+ marks[k] = index;
1460
+ any = true;
1461
+ }
1462
+ }
1463
+ }
1464
+ });
1465
+ return any ? marks : null;
1466
+ }
1467
+ function segmentsOf(line) {
1468
+ const segments = [];
1469
+ for (let i = 0; i < line.text.length; i++) {
1470
+ const highlight = line.marks[i] ?? -1;
1471
+ const last = segments[segments.length - 1];
1472
+ if (last && last.highlight === highlight) last.text += line.text[i];
1473
+ else segments.push({ text: line.text[i], highlight });
1474
+ }
1475
+ return segments;
1403
1476
  }
1404
1477
  function measureWidth(element, content, fontSize) {
1405
1478
  try {
@@ -1416,7 +1489,13 @@ function measureWidth(element, content, fontSize) {
1416
1489
  }
1417
1490
  function createNoteText(svg, text, style, options = {}) {
1418
1491
  const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
1419
- const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
1492
+ const highlights = normalizeHighlights(options.highlight);
1493
+ const wrapped = wrapWords(
1494
+ text,
1495
+ options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE,
1496
+ highlightMarks(String(text), highlights)
1497
+ );
1498
+ const lines = wrapped.map((line) => line.text);
1420
1499
  const lineHeight = fontSize * 1.05;
1421
1500
  const rtl = options.rtl ?? isRtlText(text);
1422
1501
  const group = createSvgElement("g");
@@ -1427,7 +1506,7 @@ function createNoteText(svg, text, style, options = {}) {
1427
1506
  textEl.setAttribute("opacity", String(style.opacity));
1428
1507
  textEl.setAttribute("font-size", String(fontSize));
1429
1508
  textEl.setAttribute("font-family", style.fontFamily || HANDWRITTEN_FONT);
1430
- textEl.setAttribute("font-weight", "600");
1509
+ textEl.setAttribute("font-weight", String(style.fontWeight ?? "600"));
1431
1510
  textEl.setAttribute("letter-spacing", "0.4");
1432
1511
  textEl.setAttribute("text-anchor", "start");
1433
1512
  textEl.setAttribute("dominant-baseline", "auto");
@@ -1438,11 +1517,25 @@ function createNoteText(svg, text, style, options = {}) {
1438
1517
  if (options.locale) {
1439
1518
  textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
1440
1519
  }
1441
- const tspans = lines.map((line, index) => {
1520
+ const tspans = wrapped.map((line, index) => {
1442
1521
  const tspan = createSvgElement("tspan");
1443
- tspan.textContent = line;
1444
1522
  tspan.setAttribute("x", "0");
1445
1523
  tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
1524
+ const segments = segmentsOf(line);
1525
+ if (segments.every((segment) => segment.highlight === -1)) {
1526
+ tspan.textContent = line.text;
1527
+ } else {
1528
+ for (const segment of segments) {
1529
+ const run = createSvgElement("tspan");
1530
+ run.textContent = segment.text;
1531
+ const highlight = highlights[segment.highlight];
1532
+ if (highlight?.color) run.setAttribute("fill", highlight.color);
1533
+ if (highlight?.fontWeight != null) {
1534
+ run.setAttribute("font-weight", String(highlight.fontWeight));
1535
+ }
1536
+ tspan.appendChild(run);
1537
+ }
1538
+ }
1446
1539
  textEl.appendChild(tspan);
1447
1540
  return tspan;
1448
1541
  });
@@ -1545,7 +1638,21 @@ function nudgeIntoBand(box, guard, band) {
1545
1638
  if (!dx) return box;
1546
1639
  return { ...box, x: box.x + dx };
1547
1640
  }
1548
- function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
1641
+ function resolveNoteStyle(base, options = {}) {
1642
+ const style = { ...base };
1643
+ if (typeof options.color === "string" && options.color.trim()) {
1644
+ style.color = options.color;
1645
+ }
1646
+ if (Number.isFinite(options.fontSize) && options.fontSize > 0) {
1647
+ style.fontSize = options.fontSize;
1648
+ }
1649
+ if (typeof options.fontWeight === "number" && options.fontWeight > 0 || typeof options.fontWeight === "string" && options.fontWeight.trim()) {
1650
+ style.fontWeight = options.fontWeight;
1651
+ }
1652
+ return style;
1653
+ }
1654
+ function renderAnnotation(svg, rect, noteOptions, baseStyle, seed = 1, layout = {}) {
1655
+ const style = resolveNoteStyle(baseStyle, noteOptions ?? {});
1549
1656
  const { text, locale } = resolveLocalizedText(
1550
1657
  noteOptions?.text,
1551
1658
  noteOptions?.locale
@@ -1556,9 +1663,11 @@ function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {})
1556
1663
  const random = createRandom(seed + 9);
1557
1664
  const note = createNoteText(svg, text, style, {
1558
1665
  wordsPerLine: noteOptions.wordsPerLine,
1559
- locale
1666
+ locale,
1667
+ highlight: noteOptions.highlight
1560
1668
  });
1561
- const tilt = (random() - 0.45) * 7;
1669
+ const randomTilt = (random() - 0.45) * 7;
1670
+ const tilt = Number.isFinite(noteOptions.tilt) ? noteOptions.tilt : randomTilt;
1562
1671
  const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
1563
1672
  const size = {
1564
1673
  width: note.width,
@@ -2156,7 +2265,16 @@ function blurOf(node) {
2156
2265
  }
2157
2266
  function renderNode(node, key, context) {
2158
2267
  const props = { key, ...propsOf(node) };
2159
- if (node.tagName === "tspan") return h(TSpan, props, node.textContent);
2268
+ if (node.tagName === "tspan") {
2269
+ if (node.children.length) {
2270
+ return h(
2271
+ TSpan,
2272
+ props,
2273
+ ...node.children.map((child, index) => renderNode(child, index, context))
2274
+ );
2275
+ }
2276
+ return h(TSpan, props, node.textContent);
2277
+ }
2160
2278
  if (node.tagName === "circle" && blurOf(node) > 0) {
2161
2279
  const id = `${context.prefix}-glow-${context.defs.length}`;
2162
2280
  const color = props.fill;