@aznabee/freehand-ui 0.2.1 → 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/react.cjs CHANGED
@@ -356,6 +356,9 @@ function mergeOptions(options = {}) {
356
356
  // `animate: false` opts out, an object tunes speed and gestures
357
357
  animate: normalizeArrowAnimation(config.animate)
358
358
  };
359
+ for (const key of ["color", "strokeWidth", "roughness", "opacity"]) {
360
+ if (config[key] !== void 0) merged.arrow[key] = config[key];
361
+ }
359
362
  }
360
363
  if (merged.decorations === true) {
361
364
  merged.decorations = { count: null, types: null };
@@ -477,14 +480,23 @@ function detectBorderRadius(element) {
477
480
  const fitsTightly = Math.abs(outer.width - inner.width) <= 2 && Math.abs(outer.height - inner.height) <= 2;
478
481
  return fitsTightly ? averageCornerRadius(child) : 0;
479
482
  }
480
- function getElementBounds(element, padding = 0, radiusOverride = null) {
483
+ function screenScaleOf(element) {
484
+ const layoutWidth = (
485
+ /** @type {HTMLElement} */
486
+ element.offsetWidth
487
+ );
488
+ if (!layoutWidth) return 1;
489
+ const scale = element.getBoundingClientRect().width / layoutWidth;
490
+ return Number.isFinite(scale) && scale > 0.01 ? scale : 1;
491
+ }
492
+ function getElementBounds(element, padding = 0, radiusOverride = null, scale = 1) {
481
493
  const rect = element.getBoundingClientRect();
482
494
  const radius = radiusOverride != null ? radiusOverride : detectBorderRadius(element);
483
- const width = rect.width + padding * 2;
484
- const height = rect.height + padding * 2;
495
+ const width = rect.width / scale + padding * 2;
496
+ const height = rect.height / scale + padding * 2;
485
497
  return {
486
- x: rect.left - padding,
487
- y: rect.top - padding,
498
+ x: rect.left / scale - padding,
499
+ y: rect.top / scale - padding,
488
500
  width,
489
501
  height,
490
502
  // Grow the radius with the padding so the doodle stays concentric with the
@@ -1080,7 +1092,24 @@ function boundsOf(points) {
1080
1092
  height: Math.max(...ys) - Math.min(...ys)
1081
1093
  };
1082
1094
  }
1083
- function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
1095
+ function resolveArrowStyle(base, options = {}) {
1096
+ const style = { ...base };
1097
+ if (typeof options.color === "string" && options.color.trim()) {
1098
+ style.color = options.color;
1099
+ }
1100
+ if (Number.isFinite(options.strokeWidth) && options.strokeWidth > 0) {
1101
+ style.strokeWidth = options.strokeWidth;
1102
+ }
1103
+ if (Number.isFinite(options.roughness) && options.roughness >= 0) {
1104
+ style.roughness = options.roughness;
1105
+ }
1106
+ if (Number.isFinite(options.opacity)) {
1107
+ style.opacity = Math.min(1, Math.max(0, options.opacity));
1108
+ }
1109
+ return style;
1110
+ }
1111
+ function drawArrow(svg, rect, arrowOptions, baseStyle, seed, origin = null, context = {}) {
1112
+ const style = resolveArrowStyle(baseStyle, arrowOptions);
1084
1113
  const random = createRandom(seed + 17);
1085
1114
  const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
1086
1115
  const dx = end.x - start.x;
@@ -1200,17 +1229,69 @@ function drawUnderline(svg, x, y, width, style, random) {
1200
1229
  );
1201
1230
  }
1202
1231
  var DEFAULT_WORDS_PER_LINE = 2;
1203
- function wrapWords(text, perLine) {
1232
+ function wrapWords(text, perLine, marks = null) {
1233
+ const source = String(text);
1204
1234
  const lines = [];
1205
- for (const paragraph of String(text).split(/\r?\n/)) {
1206
- const words = paragraph.trim().split(/\s+/).filter(Boolean);
1235
+ let cursor = 0;
1236
+ for (const [index, paragraph] of source.split(/(\r?\n)/).entries()) {
1237
+ const start = cursor;
1238
+ cursor += paragraph.length;
1239
+ if (index % 2 === 1) continue;
1240
+ const words = [...paragraph.matchAll(/\S+/g)].map((m) => ({
1241
+ text: m[0],
1242
+ at: start + m.index
1243
+ }));
1207
1244
  if (!words.length) continue;
1208
1245
  const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
1209
1246
  for (let i = 0; i < words.length; i += size) {
1210
- lines.push(words.slice(i, i + size).join(" "));
1247
+ lines.push(lineOf(words.slice(i, i + size), marks));
1248
+ }
1249
+ }
1250
+ return lines.length ? lines : [lineOf([{ text: source, at: 0 }], marks)];
1251
+ }
1252
+ function lineOf(words, marks) {
1253
+ const text = words.map((w) => w.text).join(" ");
1254
+ const result = [];
1255
+ let previous = -1;
1256
+ words.forEach((word, index) => {
1257
+ const first = marks ? marks[word.at] ?? -1 : -1;
1258
+ if (index > 0) result.push(previous === first ? first : -1);
1259
+ for (let k = 0; k < word.text.length; k++) {
1260
+ result.push(marks ? marks[word.at + k] ?? -1 : -1);
1261
+ }
1262
+ previous = marks ? marks[word.at + word.text.length - 1] ?? -1 : -1;
1263
+ });
1264
+ return { text, marks: result };
1265
+ }
1266
+ function normalizeHighlights(highlight) {
1267
+ const list = Array.isArray(highlight) ? highlight : highlight ? [highlight] : [];
1268
+ return list.map((entry) => typeof entry === "string" ? { text: entry } : entry).filter((entry) => entry && typeof entry.text === "string" && entry.text);
1269
+ }
1270
+ function highlightMarks(text, highlights) {
1271
+ if (!highlights.length) return null;
1272
+ const marks = new Int16Array(text.length).fill(-1);
1273
+ let any = false;
1274
+ highlights.forEach((highlight, index) => {
1275
+ for (let at = text.indexOf(highlight.text); at !== -1; at = text.indexOf(highlight.text, at + highlight.text.length)) {
1276
+ for (let k = at; k < at + highlight.text.length; k++) {
1277
+ if (marks[k] === -1) {
1278
+ marks[k] = index;
1279
+ any = true;
1280
+ }
1281
+ }
1211
1282
  }
1283
+ });
1284
+ return any ? marks : null;
1285
+ }
1286
+ function segmentsOf(line) {
1287
+ const segments = [];
1288
+ for (let i = 0; i < line.text.length; i++) {
1289
+ const highlight = line.marks[i] ?? -1;
1290
+ const last = segments[segments.length - 1];
1291
+ if (last && last.highlight === highlight) last.text += line.text[i];
1292
+ else segments.push({ text: line.text[i], highlight });
1212
1293
  }
1213
- return lines.length ? lines : [String(text)];
1294
+ return segments;
1214
1295
  }
1215
1296
  function measureWidth(element, content, fontSize) {
1216
1297
  try {
@@ -1227,7 +1308,13 @@ function measureWidth(element, content, fontSize) {
1227
1308
  }
1228
1309
  function createNoteText(svg, text, style, options = {}) {
1229
1310
  const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
1230
- const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
1311
+ const highlights = normalizeHighlights(options.highlight);
1312
+ const wrapped = wrapWords(
1313
+ text,
1314
+ options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE,
1315
+ highlightMarks(String(text), highlights)
1316
+ );
1317
+ const lines = wrapped.map((line) => line.text);
1231
1318
  const lineHeight = fontSize * 1.05;
1232
1319
  const rtl = options.rtl ?? isRtlText(text);
1233
1320
  const group = createSvgElement("g");
@@ -1238,7 +1325,7 @@ function createNoteText(svg, text, style, options = {}) {
1238
1325
  textEl.setAttribute("opacity", String(style.opacity));
1239
1326
  textEl.setAttribute("font-size", String(fontSize));
1240
1327
  textEl.setAttribute("font-family", style.fontFamily || HANDWRITTEN_FONT);
1241
- textEl.setAttribute("font-weight", "600");
1328
+ textEl.setAttribute("font-weight", String(style.fontWeight ?? "600"));
1242
1329
  textEl.setAttribute("letter-spacing", "0.4");
1243
1330
  textEl.setAttribute("text-anchor", "start");
1244
1331
  textEl.setAttribute("dominant-baseline", "auto");
@@ -1249,11 +1336,25 @@ function createNoteText(svg, text, style, options = {}) {
1249
1336
  if (options.locale) {
1250
1337
  textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
1251
1338
  }
1252
- const tspans = lines.map((line, index) => {
1339
+ const tspans = wrapped.map((line, index) => {
1253
1340
  const tspan = createSvgElement("tspan");
1254
- tspan.textContent = line;
1255
1341
  tspan.setAttribute("x", "0");
1256
1342
  tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
1343
+ const segments = segmentsOf(line);
1344
+ if (segments.every((segment) => segment.highlight === -1)) {
1345
+ tspan.textContent = line.text;
1346
+ } else {
1347
+ for (const segment of segments) {
1348
+ const run = createSvgElement("tspan");
1349
+ run.textContent = segment.text;
1350
+ const highlight = highlights[segment.highlight];
1351
+ if (highlight?.color) run.setAttribute("fill", highlight.color);
1352
+ if (highlight?.fontWeight != null) {
1353
+ run.setAttribute("font-weight", String(highlight.fontWeight));
1354
+ }
1355
+ tspan.appendChild(run);
1356
+ }
1357
+ }
1257
1358
  textEl.appendChild(tspan);
1258
1359
  return tspan;
1259
1360
  });
@@ -1372,7 +1473,21 @@ function nudgeIntoBand(box, guard, band) {
1372
1473
  if (!dx) return box;
1373
1474
  return { ...box, x: box.x + dx };
1374
1475
  }
1375
- function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
1476
+ function resolveNoteStyle(base, options = {}) {
1477
+ const style = { ...base };
1478
+ if (typeof options.color === "string" && options.color.trim()) {
1479
+ style.color = options.color;
1480
+ }
1481
+ if (Number.isFinite(options.fontSize) && options.fontSize > 0) {
1482
+ style.fontSize = options.fontSize;
1483
+ }
1484
+ if (typeof options.fontWeight === "number" && options.fontWeight > 0 || typeof options.fontWeight === "string" && options.fontWeight.trim()) {
1485
+ style.fontWeight = options.fontWeight;
1486
+ }
1487
+ return style;
1488
+ }
1489
+ function renderAnnotation(svg, rect, noteOptions, baseStyle, seed = 1, layout = {}) {
1490
+ const style = resolveNoteStyle(baseStyle, noteOptions ?? {});
1376
1491
  const { text, locale } = resolveLocalizedText(
1377
1492
  noteOptions?.text,
1378
1493
  noteOptions?.locale
@@ -1383,9 +1498,11 @@ function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {})
1383
1498
  const random = createRandom(seed + 9);
1384
1499
  const note = createNoteText(svg, text, style, {
1385
1500
  wordsPerLine: noteOptions.wordsPerLine,
1386
- locale
1501
+ locale,
1502
+ highlight: noteOptions.highlight
1387
1503
  });
1388
- const tilt = (random() - 0.45) * 7;
1504
+ const randomTilt = (random() - 0.45) * 7;
1505
+ const tilt = Number.isFinite(noteOptions.tilt) ? noteOptions.tilt : randomTilt;
1389
1506
  const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
1390
1507
  const size = {
1391
1508
  width: note.width,
@@ -1857,14 +1974,14 @@ var instances = /* @__PURE__ */ new WeakMap();
1857
1974
  function now() {
1858
1975
  return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1859
1976
  }
1860
- function localBand(originLeft, inset = 8) {
1977
+ function localBand(originLeft, inset = 8, scale = 1) {
1861
1978
  const width = window.innerWidth || document.documentElement.clientWidth || 0;
1862
1979
  return {
1863
- x: -originLeft + inset,
1864
- width: Math.max(0, width - inset * 2)
1980
+ x: -originLeft + inset / scale,
1981
+ width: Math.max(0, width - inset * 2) / scale
1865
1982
  };
1866
1983
  }
1867
- function paddingBox(element) {
1984
+ function paddingBox(element, scale = 1) {
1868
1985
  const rect = element.getBoundingClientRect();
1869
1986
  const style = getComputedStyle(element);
1870
1987
  const left = parseFloat(style.borderLeftWidth) || 0;
@@ -1872,10 +1989,10 @@ function paddingBox(element) {
1872
1989
  const right = parseFloat(style.borderRightWidth) || 0;
1873
1990
  const bottom = parseFloat(style.borderBottomWidth) || 0;
1874
1991
  return {
1875
- left: rect.left + left,
1876
- top: rect.top + top,
1877
- width: Math.max(0, rect.width - left - right),
1878
- height: Math.max(0, rect.height - top - bottom)
1992
+ left: rect.left / scale + left,
1993
+ top: rect.top / scale + top,
1994
+ width: Math.max(0, rect.width / scale - left - right),
1995
+ height: Math.max(0, rect.height / scale - top - bottom)
1879
1996
  };
1880
1997
  }
1881
1998
  var DoodleOverlay = class {
@@ -1964,12 +2081,13 @@ var DoodleOverlay = class {
1964
2081
  const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
1965
2082
  const targets = this.getTargets();
1966
2083
  const elapsed = now() - this.startedAt;
2084
+ const scale = screenScaleOf(this.element);
1967
2085
  const absoluteRects = targets.map(
1968
- (target) => getElementBounds(target, padding, radius)
2086
+ (target) => getElementBounds(target, padding, radius, scale)
1969
2087
  );
1970
2088
  clearSvg(this.svg);
1971
2089
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1972
- const box = paddingBox(this.element);
2090
+ const box = paddingBox(this.element, scale);
1973
2091
  const width = Math.max(1, box.width);
1974
2092
  const height = Math.max(1, box.height);
1975
2093
  const origin = { x: box.left, y: box.top };
@@ -1990,7 +2108,7 @@ var DoodleOverlay = class {
1990
2108
  };
1991
2109
  const noteGroups = [];
1992
2110
  const noteLayout = {
1993
- band: localBand(origin.x),
2111
+ band: localBand(origin.x, 8, scale),
1994
2112
  gap: this.options.arrow ? 40 : 14
1995
2113
  };
1996
2114
  targets.forEach((target, index) => {
package/dist/react.d.ts CHANGED
@@ -14,6 +14,7 @@ export type {
14
14
  DecorationType,
15
15
  LocalizedText,
16
16
  NoteOptions,
17
+ NoteHighlight,
17
18
  ArrowOptions,
18
19
  ArrowAnimationOptions,
19
20
  DecorationOptions,
package/dist/react.js CHANGED
@@ -339,6 +339,9 @@ function mergeOptions(options = {}) {
339
339
  // `animate: false` opts out, an object tunes speed and gestures
340
340
  animate: normalizeArrowAnimation(config.animate)
341
341
  };
342
+ for (const key of ["color", "strokeWidth", "roughness", "opacity"]) {
343
+ if (config[key] !== void 0) merged.arrow[key] = config[key];
344
+ }
342
345
  }
343
346
  if (merged.decorations === true) {
344
347
  merged.decorations = { count: null, types: null };
@@ -460,14 +463,23 @@ function detectBorderRadius(element) {
460
463
  const fitsTightly = Math.abs(outer.width - inner.width) <= 2 && Math.abs(outer.height - inner.height) <= 2;
461
464
  return fitsTightly ? averageCornerRadius(child) : 0;
462
465
  }
463
- function getElementBounds(element, padding = 0, radiusOverride = null) {
466
+ function screenScaleOf(element) {
467
+ const layoutWidth = (
468
+ /** @type {HTMLElement} */
469
+ element.offsetWidth
470
+ );
471
+ if (!layoutWidth) return 1;
472
+ const scale = element.getBoundingClientRect().width / layoutWidth;
473
+ return Number.isFinite(scale) && scale > 0.01 ? scale : 1;
474
+ }
475
+ function getElementBounds(element, padding = 0, radiusOverride = null, scale = 1) {
464
476
  const rect = element.getBoundingClientRect();
465
477
  const radius = radiusOverride != null ? radiusOverride : detectBorderRadius(element);
466
- const width = rect.width + padding * 2;
467
- const height = rect.height + padding * 2;
478
+ const width = rect.width / scale + padding * 2;
479
+ const height = rect.height / scale + padding * 2;
468
480
  return {
469
- x: rect.left - padding,
470
- y: rect.top - padding,
481
+ x: rect.left / scale - padding,
482
+ y: rect.top / scale - padding,
471
483
  width,
472
484
  height,
473
485
  // Grow the radius with the padding so the doodle stays concentric with the
@@ -1063,7 +1075,24 @@ function boundsOf(points) {
1063
1075
  height: Math.max(...ys) - Math.min(...ys)
1064
1076
  };
1065
1077
  }
1066
- function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
1078
+ function resolveArrowStyle(base, options = {}) {
1079
+ const style = { ...base };
1080
+ if (typeof options.color === "string" && options.color.trim()) {
1081
+ style.color = options.color;
1082
+ }
1083
+ if (Number.isFinite(options.strokeWidth) && options.strokeWidth > 0) {
1084
+ style.strokeWidth = options.strokeWidth;
1085
+ }
1086
+ if (Number.isFinite(options.roughness) && options.roughness >= 0) {
1087
+ style.roughness = options.roughness;
1088
+ }
1089
+ if (Number.isFinite(options.opacity)) {
1090
+ style.opacity = Math.min(1, Math.max(0, options.opacity));
1091
+ }
1092
+ return style;
1093
+ }
1094
+ function drawArrow(svg, rect, arrowOptions, baseStyle, seed, origin = null, context = {}) {
1095
+ const style = resolveArrowStyle(baseStyle, arrowOptions);
1067
1096
  const random = createRandom(seed + 17);
1068
1097
  const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
1069
1098
  const dx = end.x - start.x;
@@ -1183,17 +1212,69 @@ function drawUnderline(svg, x, y, width, style, random) {
1183
1212
  );
1184
1213
  }
1185
1214
  var DEFAULT_WORDS_PER_LINE = 2;
1186
- function wrapWords(text, perLine) {
1215
+ function wrapWords(text, perLine, marks = null) {
1216
+ const source = String(text);
1187
1217
  const lines = [];
1188
- for (const paragraph of String(text).split(/\r?\n/)) {
1189
- const words = paragraph.trim().split(/\s+/).filter(Boolean);
1218
+ let cursor = 0;
1219
+ for (const [index, paragraph] of source.split(/(\r?\n)/).entries()) {
1220
+ const start = cursor;
1221
+ cursor += paragraph.length;
1222
+ if (index % 2 === 1) continue;
1223
+ const words = [...paragraph.matchAll(/\S+/g)].map((m) => ({
1224
+ text: m[0],
1225
+ at: start + m.index
1226
+ }));
1190
1227
  if (!words.length) continue;
1191
1228
  const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
1192
1229
  for (let i = 0; i < words.length; i += size) {
1193
- lines.push(words.slice(i, i + size).join(" "));
1230
+ lines.push(lineOf(words.slice(i, i + size), marks));
1231
+ }
1232
+ }
1233
+ return lines.length ? lines : [lineOf([{ text: source, at: 0 }], marks)];
1234
+ }
1235
+ function lineOf(words, marks) {
1236
+ const text = words.map((w) => w.text).join(" ");
1237
+ const result = [];
1238
+ let previous = -1;
1239
+ words.forEach((word, index) => {
1240
+ const first = marks ? marks[word.at] ?? -1 : -1;
1241
+ if (index > 0) result.push(previous === first ? first : -1);
1242
+ for (let k = 0; k < word.text.length; k++) {
1243
+ result.push(marks ? marks[word.at + k] ?? -1 : -1);
1244
+ }
1245
+ previous = marks ? marks[word.at + word.text.length - 1] ?? -1 : -1;
1246
+ });
1247
+ return { text, marks: result };
1248
+ }
1249
+ function normalizeHighlights(highlight) {
1250
+ const list = Array.isArray(highlight) ? highlight : highlight ? [highlight] : [];
1251
+ return list.map((entry) => typeof entry === "string" ? { text: entry } : entry).filter((entry) => entry && typeof entry.text === "string" && entry.text);
1252
+ }
1253
+ function highlightMarks(text, highlights) {
1254
+ if (!highlights.length) return null;
1255
+ const marks = new Int16Array(text.length).fill(-1);
1256
+ let any = false;
1257
+ highlights.forEach((highlight, index) => {
1258
+ for (let at = text.indexOf(highlight.text); at !== -1; at = text.indexOf(highlight.text, at + highlight.text.length)) {
1259
+ for (let k = at; k < at + highlight.text.length; k++) {
1260
+ if (marks[k] === -1) {
1261
+ marks[k] = index;
1262
+ any = true;
1263
+ }
1264
+ }
1194
1265
  }
1266
+ });
1267
+ return any ? marks : null;
1268
+ }
1269
+ function segmentsOf(line) {
1270
+ const segments = [];
1271
+ for (let i = 0; i < line.text.length; i++) {
1272
+ const highlight = line.marks[i] ?? -1;
1273
+ const last = segments[segments.length - 1];
1274
+ if (last && last.highlight === highlight) last.text += line.text[i];
1275
+ else segments.push({ text: line.text[i], highlight });
1195
1276
  }
1196
- return lines.length ? lines : [String(text)];
1277
+ return segments;
1197
1278
  }
1198
1279
  function measureWidth(element, content, fontSize) {
1199
1280
  try {
@@ -1210,7 +1291,13 @@ function measureWidth(element, content, fontSize) {
1210
1291
  }
1211
1292
  function createNoteText(svg, text, style, options = {}) {
1212
1293
  const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
1213
- const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
1294
+ const highlights = normalizeHighlights(options.highlight);
1295
+ const wrapped = wrapWords(
1296
+ text,
1297
+ options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE,
1298
+ highlightMarks(String(text), highlights)
1299
+ );
1300
+ const lines = wrapped.map((line) => line.text);
1214
1301
  const lineHeight = fontSize * 1.05;
1215
1302
  const rtl = options.rtl ?? isRtlText(text);
1216
1303
  const group = createSvgElement("g");
@@ -1221,7 +1308,7 @@ function createNoteText(svg, text, style, options = {}) {
1221
1308
  textEl.setAttribute("opacity", String(style.opacity));
1222
1309
  textEl.setAttribute("font-size", String(fontSize));
1223
1310
  textEl.setAttribute("font-family", style.fontFamily || HANDWRITTEN_FONT);
1224
- textEl.setAttribute("font-weight", "600");
1311
+ textEl.setAttribute("font-weight", String(style.fontWeight ?? "600"));
1225
1312
  textEl.setAttribute("letter-spacing", "0.4");
1226
1313
  textEl.setAttribute("text-anchor", "start");
1227
1314
  textEl.setAttribute("dominant-baseline", "auto");
@@ -1232,11 +1319,25 @@ function createNoteText(svg, text, style, options = {}) {
1232
1319
  if (options.locale) {
1233
1320
  textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
1234
1321
  }
1235
- const tspans = lines.map((line, index) => {
1322
+ const tspans = wrapped.map((line, index) => {
1236
1323
  const tspan = createSvgElement("tspan");
1237
- tspan.textContent = line;
1238
1324
  tspan.setAttribute("x", "0");
1239
1325
  tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
1326
+ const segments = segmentsOf(line);
1327
+ if (segments.every((segment) => segment.highlight === -1)) {
1328
+ tspan.textContent = line.text;
1329
+ } else {
1330
+ for (const segment of segments) {
1331
+ const run = createSvgElement("tspan");
1332
+ run.textContent = segment.text;
1333
+ const highlight = highlights[segment.highlight];
1334
+ if (highlight?.color) run.setAttribute("fill", highlight.color);
1335
+ if (highlight?.fontWeight != null) {
1336
+ run.setAttribute("font-weight", String(highlight.fontWeight));
1337
+ }
1338
+ tspan.appendChild(run);
1339
+ }
1340
+ }
1240
1341
  textEl.appendChild(tspan);
1241
1342
  return tspan;
1242
1343
  });
@@ -1355,7 +1456,21 @@ function nudgeIntoBand(box, guard, band) {
1355
1456
  if (!dx) return box;
1356
1457
  return { ...box, x: box.x + dx };
1357
1458
  }
1358
- function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
1459
+ function resolveNoteStyle(base, options = {}) {
1460
+ const style = { ...base };
1461
+ if (typeof options.color === "string" && options.color.trim()) {
1462
+ style.color = options.color;
1463
+ }
1464
+ if (Number.isFinite(options.fontSize) && options.fontSize > 0) {
1465
+ style.fontSize = options.fontSize;
1466
+ }
1467
+ if (typeof options.fontWeight === "number" && options.fontWeight > 0 || typeof options.fontWeight === "string" && options.fontWeight.trim()) {
1468
+ style.fontWeight = options.fontWeight;
1469
+ }
1470
+ return style;
1471
+ }
1472
+ function renderAnnotation(svg, rect, noteOptions, baseStyle, seed = 1, layout = {}) {
1473
+ const style = resolveNoteStyle(baseStyle, noteOptions ?? {});
1359
1474
  const { text, locale } = resolveLocalizedText(
1360
1475
  noteOptions?.text,
1361
1476
  noteOptions?.locale
@@ -1366,9 +1481,11 @@ function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {})
1366
1481
  const random = createRandom(seed + 9);
1367
1482
  const note = createNoteText(svg, text, style, {
1368
1483
  wordsPerLine: noteOptions.wordsPerLine,
1369
- locale
1484
+ locale,
1485
+ highlight: noteOptions.highlight
1370
1486
  });
1371
- const tilt = (random() - 0.45) * 7;
1487
+ const randomTilt = (random() - 0.45) * 7;
1488
+ const tilt = Number.isFinite(noteOptions.tilt) ? noteOptions.tilt : randomTilt;
1372
1489
  const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
1373
1490
  const size = {
1374
1491
  width: note.width,
@@ -1840,14 +1957,14 @@ var instances = /* @__PURE__ */ new WeakMap();
1840
1957
  function now() {
1841
1958
  return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1842
1959
  }
1843
- function localBand(originLeft, inset = 8) {
1960
+ function localBand(originLeft, inset = 8, scale = 1) {
1844
1961
  const width = window.innerWidth || document.documentElement.clientWidth || 0;
1845
1962
  return {
1846
- x: -originLeft + inset,
1847
- width: Math.max(0, width - inset * 2)
1963
+ x: -originLeft + inset / scale,
1964
+ width: Math.max(0, width - inset * 2) / scale
1848
1965
  };
1849
1966
  }
1850
- function paddingBox(element) {
1967
+ function paddingBox(element, scale = 1) {
1851
1968
  const rect = element.getBoundingClientRect();
1852
1969
  const style = getComputedStyle(element);
1853
1970
  const left = parseFloat(style.borderLeftWidth) || 0;
@@ -1855,10 +1972,10 @@ function paddingBox(element) {
1855
1972
  const right = parseFloat(style.borderRightWidth) || 0;
1856
1973
  const bottom = parseFloat(style.borderBottomWidth) || 0;
1857
1974
  return {
1858
- left: rect.left + left,
1859
- top: rect.top + top,
1860
- width: Math.max(0, rect.width - left - right),
1861
- height: Math.max(0, rect.height - top - bottom)
1975
+ left: rect.left / scale + left,
1976
+ top: rect.top / scale + top,
1977
+ width: Math.max(0, rect.width / scale - left - right),
1978
+ height: Math.max(0, rect.height / scale - top - bottom)
1862
1979
  };
1863
1980
  }
1864
1981
  var DoodleOverlay = class {
@@ -1947,12 +2064,13 @@ var DoodleOverlay = class {
1947
2064
  const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
1948
2065
  const targets = this.getTargets();
1949
2066
  const elapsed = now() - this.startedAt;
2067
+ const scale = screenScaleOf(this.element);
1950
2068
  const absoluteRects = targets.map(
1951
- (target) => getElementBounds(target, padding, radius)
2069
+ (target) => getElementBounds(target, padding, radius, scale)
1952
2070
  );
1953
2071
  clearSvg(this.svg);
1954
2072
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1955
- const box = paddingBox(this.element);
2073
+ const box = paddingBox(this.element, scale);
1956
2074
  const width = Math.max(1, box.width);
1957
2075
  const height = Math.max(1, box.height);
1958
2076
  const origin = { x: box.left, y: box.top };
@@ -1973,7 +2091,7 @@ var DoodleOverlay = class {
1973
2091
  };
1974
2092
  const noteGroups = [];
1975
2093
  const noteLayout = {
1976
- band: localBand(origin.x),
2094
+ band: localBand(origin.x, 8, scale),
1977
2095
  gap: this.options.arrow ? 40 : 14
1978
2096
  };
1979
2097
  targets.forEach((target, index) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aznabee/freehand-ui",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Wrap any existing web element with a beautiful, responsive, hand-drawn doodle layer",
5
5
  "type": "module",
6
6
  "main": "./dist/freehand-ui.cjs",
@@ -36,7 +36,9 @@
36
36
  "build:example-react": "node scripts/build-example-react.js",
37
37
  "dev": "node scripts/build.js && node scripts/build-example-react.js && node scripts/serve.js",
38
38
  "test": "node --test tests/*.test.js",
39
- "prepublishOnly": "npm run build && npm test"
39
+ "prepublishOnly": "npm run build && npm test",
40
+ "typecheck": "tsc --noEmit --strict --skipLibCheck --jsx react-jsx --moduleResolution bundler --module esnext src/index.d.ts src/react.d.ts",
41
+ "check": "npm run typecheck && npm test && npm run build"
40
42
  },
41
43
  "author": "Lakshay Gupta",
42
44
  "repository": {
@@ -76,8 +78,14 @@
76
78
  }
77
79
  },
78
80
  "devDependencies": {
81
+ "@types/react": "^19.3.0",
82
+ "@types/react-dom": "^19.3.0",
79
83
  "esbuild": "^0.25.0",
80
84
  "react": "^19.0.0",
81
- "react-dom": "^19.0.0"
85
+ "react-dom": "^19.0.0",
86
+ "typescript": "^7.0.2"
87
+ },
88
+ "engines": {
89
+ "node": ">=18"
82
90
  }
83
91
  }