@embedpdf/plugin-annotation 3.0.0-next.12 → 3.0.0-next.13

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.
Files changed (39) hide show
  1. package/dist/{capability-Bh_1PL5v.js → capability-B_dkoB1K.js} +502 -547
  2. package/dist/capability-B_dkoB1K.js.map +1 -0
  3. package/dist/{capability-R7dgogJs.cjs → capability-DVx1NmTe.cjs} +504 -573
  4. package/dist/capability-DVx1NmTe.cjs.map +1 -0
  5. package/dist/contract/host.d.cts +1 -1
  6. package/dist/contract/host.d.ts +1 -1
  7. package/dist/contract-CgAcfAE7.d.cts +34 -0
  8. package/dist/contract-CiG2_HBq.d.ts +34 -0
  9. package/dist/contract-DtXE1hjY.js +32 -0
  10. package/dist/contract-DtXE1hjY.js.map +1 -0
  11. package/dist/contract-a4eaQWga.cjs +67 -0
  12. package/dist/contract-a4eaQWga.cjs.map +1 -0
  13. package/dist/contract.cjs +9 -7
  14. package/dist/contract.d.cts +3 -10
  15. package/dist/contract.d.ts +3 -10
  16. package/dist/contract.js +4 -8
  17. package/dist/index.cjs +43 -6
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +3 -3
  20. package/dist/index.d.ts +3 -3
  21. package/dist/index.js +36 -5
  22. package/dist/index.js.map +1 -1
  23. package/dist/internal.cjs +3 -2
  24. package/dist/internal.d.cts +1 -1
  25. package/dist/internal.d.ts +1 -1
  26. package/dist/internal.js +2 -1
  27. package/dist/tools-0NI3b9z8.cjs +536 -0
  28. package/dist/tools-0NI3b9z8.cjs.map +1 -0
  29. package/dist/tools-DBEYx3-c.js +513 -0
  30. package/dist/tools-DBEYx3-c.js.map +1 -0
  31. package/dist/types-BvB1vUKb.js.map +1 -1
  32. package/dist/{types-BAPdbfPT.d.cts → types-D5V0TIwk.d.ts} +82 -11
  33. package/dist/types-Dlhdw2k_.cjs.map +1 -1
  34. package/dist/{types-BgetZ4BK.d.ts → types-ObcPYc3-.d.cts} +82 -11
  35. package/package.json +9 -9
  36. package/dist/capability-Bh_1PL5v.js.map +0 -1
  37. package/dist/capability-R7dgogJs.cjs.map +0 -1
  38. package/dist/contract.cjs.map +0 -1
  39. package/dist/contract.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  const require_seam = require("./seam-6pJeKOOJ.cjs");
2
+ const require_tools = require("./tools-0NI3b9z8.cjs");
2
3
  const require_types = require("./types-Dlhdw2k_.cjs");
3
4
  let _embedpdf_core = require("@embedpdf/core");
4
5
  let _embedpdf_plugin_actions_contract = require("@embedpdf/plugin-actions/contract");
@@ -208,18 +209,387 @@ const ink = {
208
209
  draftExtras: (a) => a.intent === "ink-highlight" ? { intent: a.intent } : {}
209
210
  };
210
211
  //#endregion
212
+ //#region src/rich-text.ts
213
+ /**
214
+ * Rich text POLICY — what the annotation plugin decides about a free-text
215
+ * annotation's rich document, kept pure so every framework's editor glue
216
+ * behaves identically (the mechanics live in `@embedpdf/web`'s binding, the
217
+ * run algebra in the core):
218
+ *
219
+ * • the rich document of any annot (the DTO's, or one synthesised from the
220
+ * plain text + the `/DA` text style for a draft that has no DTO yet)
221
+ * • a flat props patch → the run delta it means for a text RANGE (font,
222
+ * size, colour, bold/italic/underline) and the keys left for the body
223
+ * • what a range reads back for those keys (agree → the value, else mixed)
224
+ * • faces: a DTO font (standard kebab name / registered key) ↔ the face a
225
+ * run names (family + weight + italic) ↔ a CSS family list
226
+ * • the commit rule: a document nothing overrides on a plain annotation
227
+ * commits as `contents`; anything else as `richText`
228
+ */
229
+ /** The props keys a text range takes as run deltas. */
230
+ const RANGE_KEYS = [
231
+ "fontFamily",
232
+ "fontSize",
233
+ "fontColor",
234
+ "bold",
235
+ "italic",
236
+ "underline"
237
+ ];
238
+ /** The engine's standard-14 vocabulary (the `StandardFont` kebab names). */
239
+ const STANDARD_FACES = {
240
+ courier: {
241
+ family: "Courier",
242
+ weight: 400,
243
+ italic: false
244
+ },
245
+ "courier-bold": {
246
+ family: "Courier",
247
+ weight: 700,
248
+ italic: false
249
+ },
250
+ "courier-bold-oblique": {
251
+ family: "Courier",
252
+ weight: 700,
253
+ italic: true
254
+ },
255
+ "courier-oblique": {
256
+ family: "Courier",
257
+ weight: 400,
258
+ italic: true
259
+ },
260
+ helvetica: {
261
+ family: "Helvetica",
262
+ weight: 400,
263
+ italic: false
264
+ },
265
+ "helvetica-bold": {
266
+ family: "Helvetica",
267
+ weight: 700,
268
+ italic: false
269
+ },
270
+ "helvetica-bold-oblique": {
271
+ family: "Helvetica",
272
+ weight: 700,
273
+ italic: true
274
+ },
275
+ "helvetica-oblique": {
276
+ family: "Helvetica",
277
+ weight: 400,
278
+ italic: true
279
+ },
280
+ "times-roman": {
281
+ family: "Times",
282
+ weight: 400,
283
+ italic: false
284
+ },
285
+ "times-bold": {
286
+ family: "Times",
287
+ weight: 700,
288
+ italic: false
289
+ },
290
+ "times-bold-italic": {
291
+ family: "Times",
292
+ weight: 700,
293
+ italic: true
294
+ },
295
+ "times-italic": {
296
+ family: "Times",
297
+ weight: 400,
298
+ italic: true
299
+ },
300
+ symbol: {
301
+ family: "Symbol",
302
+ weight: 400,
303
+ italic: false
304
+ },
305
+ "zapf-dingbats": {
306
+ family: "ZapfDingbats",
307
+ weight: 400,
308
+ italic: false
309
+ }
310
+ };
311
+ /** Family names compare without case, spaces, hyphens, underscores and
312
+ * quotes — the engine's own rule. */
313
+ function familyKey(family) {
314
+ return family.toLowerCase().replace(/[\s\-_'"]/g, "");
315
+ }
316
+ const STANDARD_FAMILY_KEYS = {
317
+ helvetica: "Helvetica",
318
+ arial: "Helvetica",
319
+ arialmt: "Helvetica",
320
+ sansserif: "Helvetica",
321
+ times: "Times",
322
+ timesroman: "Times",
323
+ timesnewroman: "Times",
324
+ timesnewromanpsmt: "Times",
325
+ serif: "Times",
326
+ courier: "Courier",
327
+ couriernew: "Courier",
328
+ couriernewpsmt: "Courier",
329
+ monospace: "Courier",
330
+ symbol: "Symbol",
331
+ zapfdingbats: "ZapfDingbats",
332
+ dingbats: "ZapfDingbats"
333
+ };
334
+ /** Web stacks with the standard families' metrics (what the DOM editor and
335
+ * the vector renderer show for text the engine sets in a standard font). */
336
+ const STANDARD_STACKS = {
337
+ Helvetica: "Helvetica, Arial, sans-serif",
338
+ Times: "\"Times New Roman\", Times, serif",
339
+ Courier: "\"Courier New\", Courier, monospace",
340
+ Symbol: "serif",
341
+ ZapfDingbats: "serif"
342
+ };
343
+ /** The face a DTO font names: a standard font's family/weight/italic, a
344
+ * registered key's identity, else the string itself as a family. */
345
+ function faceForFont(font, fonts) {
346
+ const standard = STANDARD_FACES[font];
347
+ if (standard) return { ...standard };
348
+ const registered = fonts?.().find((f) => f.key === font);
349
+ if (registered) return {
350
+ family: registered.familyName,
351
+ weight: registered.weight,
352
+ italic: registered.italic
353
+ };
354
+ return { family: font };
355
+ }
356
+ /** The DTO font for a face: the registered key whose identity matches (the
357
+ * closest weight, italic first), else the standard kebab name, else the
358
+ * family itself. The inverse of {@link faceForFont}. */
359
+ function fontForFace(face, fonts) {
360
+ const wanted = familyKey(face.family);
361
+ const weight = face.weight ?? 400;
362
+ const italic = face.italic ?? false;
363
+ let best;
364
+ for (const f of fonts?.() ?? []) {
365
+ if (familyKey(f.familyName) !== wanted) continue;
366
+ const score = Math.abs(f.weight - weight) + (f.italic === italic ? 0 : 1e3);
367
+ if (!best || score < best.score) best = {
368
+ key: f.key,
369
+ score
370
+ };
371
+ }
372
+ if (best) return best.key;
373
+ const standard = STANDARD_FAMILY_KEYS[wanted];
374
+ if (standard) {
375
+ const bold = weight >= 600;
376
+ for (const [name, spec] of Object.entries(STANDARD_FACES)) if (spec.family === standard && spec.weight >= 600 === bold && spec.italic === italic) return name;
377
+ for (const [name, spec] of Object.entries(STANDARD_FACES)) if (spec.family === standard) return name;
378
+ }
379
+ return face.family;
380
+ }
381
+ /** The CSS family list for a face family: a standard family's web stack, a
382
+ * registered family's `@font-face` name (its key — see `mountWebFont`),
383
+ * else the family itself with a generic fallback. */
384
+ function cssFontFamilyForFace(family, fonts) {
385
+ const standard = STANDARD_FAMILY_KEYS[familyKey(family)];
386
+ if (standard) return STANDARD_STACKS[standard];
387
+ const wanted = familyKey(family);
388
+ const registered = fonts?.().find((f) => familyKey(f.familyName) === wanted);
389
+ if (registered) return `"${registered.key}"`;
390
+ return `"${family}", sans-serif`;
391
+ }
392
+ /** The CSS family list for a DTO font (a standard kebab name or a key). */
393
+ function cssFontFamilyForFont(font, fonts) {
394
+ const standard = STANDARD_FACES[font];
395
+ if (standard) return STANDARD_STACKS[standard.family];
396
+ return `"${font}", sans-serif`;
397
+ }
398
+ const hex = (css) => css.trim().toUpperCase();
399
+ /** The rich body the `/DA` text style describes (a draft's body before its
400
+ * DTO exists; also the fallback for a DTO without `richText`). */
401
+ function bodyFromTextStyle(t, fonts) {
402
+ const face = faceForFont(t.fontFamily, fonts);
403
+ return {
404
+ family: face.family,
405
+ weight: t.bold ? 700 : face.weight ?? 400,
406
+ italic: t.italic ?? face.italic ?? false,
407
+ size: t.fontSize,
408
+ color: hex(t.fontColor),
409
+ decoration: t.underline ? ["underline"] : [],
410
+ script: "normal",
411
+ letterSpacing: 0,
412
+ horizontalScale: 1,
413
+ align: t.textAlign,
414
+ dir: "ltr"
415
+ };
416
+ }
417
+ /** The annotation's rich document: the DTO's, else one synthesised from
418
+ * its plain text and text style (a draft the engine has not echoed yet). */
419
+ function richDocOf(a, fonts) {
420
+ if (a.data?.subtype === "free-text" && a.data.richText) return a.data.richText;
421
+ return {
422
+ body: bodyFromTextStyle(a.text ?? {
423
+ fontFamily: "helvetica",
424
+ fontSize: 12,
425
+ fontColor: "#000000",
426
+ textAlign: "left"
427
+ }, fonts),
428
+ paragraphs: (0, _embedpdf_core_annotation.paragraphsFromPlainText)(a.data?.contents ?? "")
429
+ };
430
+ }
431
+ /**
432
+ * The write a text edit commits: the rich paragraphs, with paragraph
433
+ * properties equal to the body's stripped (they are inherited, not
434
+ * overrides) and the body omitted — it stays what the props path last
435
+ * wrote. One engine, one path: plain and formatted text alike.
436
+ */
437
+ function textCommitPatch(a, paragraphs, fonts) {
438
+ return { richText: { paragraphs: stripBodyDefaults(paragraphs, richDocOf(a, fonts).body) } };
439
+ }
440
+ /**
441
+ * Paragraph alignment/direction equal to the body's are not overrides: the
442
+ * engine echoes them resolved, the editor renders them as inline styles and
443
+ * serialises them back, and the commit rule must see through that (a plain
444
+ * `/Contents` annotation must not grow an `/RC` from typing).
445
+ */
446
+ function stripBodyDefaults(paragraphs, body) {
447
+ return paragraphs.map((p) => {
448
+ if ((p.align === void 0 || p.align !== body.align) && (p.dir === void 0 || p.dir !== body.dir)) return p;
449
+ const { align, dir, ...rest } = p;
450
+ return {
451
+ ...rest,
452
+ ...align !== void 0 && align !== body.align ? { align } : {},
453
+ ...dir !== void 0 && dir !== body.dir ? { dir } : {}
454
+ };
455
+ });
456
+ }
457
+ /**
458
+ * Split a props patch for an annotation whose editor holds a text RANGE:
459
+ * the run delta the range takes (font → face, size, colour, bold → weight,
460
+ * italic, underline → decoration) and the keys that still go to the body.
461
+ */
462
+ function runDeltaForProps(patch, fonts) {
463
+ const delta = {};
464
+ const rest = {};
465
+ for (const key of Object.keys(patch)) {
466
+ const value = patch[key];
467
+ if (value === void 0) continue;
468
+ switch (key) {
469
+ case "fontFamily": {
470
+ const face = faceForFont(patch.fontFamily, fonts);
471
+ delta.family = face.family;
472
+ if (face.weight !== void 0) delta.weight = face.weight;
473
+ if (face.italic !== void 0) delta.italic = face.italic;
474
+ break;
475
+ }
476
+ case "fontSize":
477
+ delta.size = patch.fontSize;
478
+ break;
479
+ case "fontColor":
480
+ delta.color = hex(patch.fontColor);
481
+ break;
482
+ case "bold":
483
+ delta.weight = patch.bold ? 700 : 400;
484
+ break;
485
+ case "italic":
486
+ delta.italic = patch.italic;
487
+ break;
488
+ case "underline":
489
+ delta.decoration = patch.underline ? ["underline"] : [];
490
+ break;
491
+ default: rest[key] = value;
492
+ }
493
+ }
494
+ return {
495
+ delta,
496
+ rest
497
+ };
498
+ }
499
+ /** The style deltas of every non-empty run a range overlaps. */
500
+ function runsInRange(doc, range) {
501
+ const start = Math.min(range.start, range.end);
502
+ const end = Math.max(range.start, range.end);
503
+ if (end <= start) return [];
504
+ const from = (0, _embedpdf_core_annotation.locateOffset)(doc, start);
505
+ const to = (0, _embedpdf_core_annotation.locateOffset)(doc, end);
506
+ const out = [];
507
+ for (let index = from.paragraph; index <= to.paragraph; index++) {
508
+ const paragraph = doc.paragraphs[index];
509
+ const localStart = index === from.paragraph ? from.offset : 0;
510
+ const localEnd = index === to.paragraph ? to.offset : paragraph.runs.reduce((n, r) => n + r.text.length, 0);
511
+ let pos = 0;
512
+ for (const run of paragraph.runs) {
513
+ const runEnd = pos + run.text.length;
514
+ if (runEnd > localStart && pos < localEnd && run.text.length > 0) out.push(run.style ?? {});
515
+ pos = runEnd;
516
+ }
517
+ }
518
+ return out;
519
+ }
520
+ /** The values a range reads back for the range keys — resolved against the
521
+ * body, one value where every run agrees, `mixed` where they don't. */
522
+ function rangeProps(doc, range, fonts) {
523
+ const runs = runsInRange(doc, range);
524
+ const body = doc.body;
525
+ const resolve = (d) => ({
526
+ family: d.family ?? body.family,
527
+ weight: d.weight ?? body.weight,
528
+ italic: d.italic ?? body.italic,
529
+ size: d.size ?? body.size,
530
+ color: d.color ?? body.color,
531
+ decoration: d.decoration ?? body.decoration
532
+ });
533
+ const styles = (runs.length ? runs : [{}]).map(resolve);
534
+ const values = {};
535
+ const mixed = [];
536
+ const read = (key, of) => {
537
+ const first = of(styles[0]);
538
+ values[key] = first;
539
+ if (styles.some((s) => JSON.stringify(of(s)) !== JSON.stringify(first))) mixed.push(key);
540
+ };
541
+ read("fontFamily", (s) => fontForFace({
542
+ family: s.family,
543
+ weight: body.weight,
544
+ italic: body.italic
545
+ }, fonts));
546
+ read("fontSize", (s) => s.size);
547
+ read("fontColor", (s) => s.color.toLowerCase());
548
+ read("bold", (s) => (s.weight ?? 400) >= 600);
549
+ read("italic", (s) => !!s.italic);
550
+ read("underline", (s) => (s.decoration ?? []).includes("underline"));
551
+ return {
552
+ values,
553
+ mixed
554
+ };
555
+ }
556
+ //#endregion
211
557
  //#region src/repository/kinds/freeText.ts
212
558
  /** Free-text `/DA` fields → content {@link TextStyle}. An absent `fontColor`
213
559
  * falls back to the `/DA` colour — the same rule the CPVT renderer applies. */
214
560
  function textFromDTO(dto) {
561
+ const body = dto.richText?.body;
215
562
  return {
216
563
  fontFamily: dto.fontFamily,
217
564
  fontSize: dto.fontSize,
218
565
  fontColor: require_seam.colorToCss(dto.fontColor ?? dto.color),
219
- textAlign: dto.textAlign
566
+ textAlign: dto.textAlign,
567
+ ...body && body.weight >= 600 ? { bold: true } : {},
568
+ ...body?.italic ? { italic: true } : {},
569
+ ...body?.decoration.includes("underline") ? { underline: true } : {}
220
570
  };
221
571
  }
222
572
  /**
573
+ * A formatting toggle is a BODY change (runs are deltas over it), so it
574
+ * lowers as a rich write carrying the current paragraphs — and every toggle
575
+ * lowers the same COMPLETE body (the current one with the three toggles
576
+ * applied: a partial body means engine defaults, which would reset the
577
+ * size, face and colour), so a patch of several merges cleanly.
578
+ */
579
+ const formattingBody = (a) => {
580
+ const doc = richDocOf(a);
581
+ const t = a.text;
582
+ return { richText: {
583
+ body: {
584
+ ...doc.body,
585
+ weight: t?.bold ? 700 : 400,
586
+ italic: !!t?.italic,
587
+ decoration: t?.underline ? ["underline"] : []
588
+ },
589
+ paragraphs: doc.paragraphs
590
+ } };
591
+ };
592
+ /**
223
593
  * The engine geometry for a callout: the overall `/Rect` (text box ∪ leader ∪
224
594
  * arrow), the `/RD` inset that recovers the text box from it, the `/CL` leader
225
595
  * (`[tip, knee, conn]` with the connection point derived), and the `/LE`
@@ -303,6 +673,11 @@ const freeText = {
303
673
  if (cf) return { ...cf };
304
674
  return boxEmit(a, crop);
305
675
  },
676
+ prop: {
677
+ bold: formattingBody,
678
+ italic: formattingBody,
679
+ underline: formattingBody
680
+ },
306
681
  draftExtras: (a, crop) => ({
307
682
  intent: calloutFields(a, crop) ? "free-text-callout" : "free-text",
308
683
  contents: a.data?.contents ?? ""
@@ -732,18 +1107,6 @@ function createAnnotationHoverFeed(actions, annotOf) {
732
1107
  * the plugin (not the portable core) because the font→CSS stack mapping and the
733
1108
  * engine `Color`→CSS seam are web concerns, shared across every web framework.
734
1109
  */
735
- /** Map a free-text `/DA` font to a CSS font-family. A standard PDF font → a web
736
- * stack with the same metrics; anything else is a registered font key → mount it
737
- * as a `@font-face` of that family (see the framework `mountWebFont` helper). */
738
- const STANDARD_FONT_CSS = {
739
- helvetica: "Helvetica, Arial, sans-serif",
740
- "helvetica-bold": "Helvetica, Arial, sans-serif",
741
- courier: "\"Courier New\", Courier, monospace",
742
- "times-roman": "\"Times New Roman\", Times, serif",
743
- symbol: "serif",
744
- "zapf-dingbats": "serif"
745
- };
746
- const cssFontFor = (font) => STANDARD_FONT_CSS[font] ?? `"${font}", sans-serif`;
747
1110
  /** Project the model's free-text boxes into render-ready {@link TextItem}s — the
748
1111
  * core geometry (`textBoxes`) joined with the DTO-derived CSS. Pure; memoized by
749
1112
  * model identity at the call site so selectors get a stable reference. */
@@ -752,525 +1115,29 @@ function buildTextItems(m, pon, view) {
752
1115
  const a = m.byId[tb.id];
753
1116
  const t = a?.text ?? _embedpdf_core_annotation.initialTextStyle;
754
1117
  const sw = a?.style.strokeWidth ?? 0;
755
- const isCallout = a?.geom.t === "text" && !!a.geom.callout;
1118
+ const doc = a ? richDocOf(a) : null;
756
1119
  return {
757
1120
  id: tb.id,
758
1121
  ref: a?.ref ?? null,
759
1122
  box: tb.box,
760
1123
  contents: a?.data?.contents ?? "",
1124
+ richText: { paragraphs: doc ? stripBodyDefaults(doc.paragraphs, doc.body) : [{ runs: [{ text: "" }] }] },
761
1125
  editing: tb.editing,
762
1126
  ...tb.rot ? { rot: tb.rot } : {},
763
1127
  css: {
764
- fontFamily: cssFontFor(t.fontFamily),
1128
+ fontFamily: cssFontFamilyForFont(t.fontFamily),
765
1129
  fontSize: t.fontSize,
766
- lineHeight: t.fontSize,
767
1130
  color: t.fontColor,
1131
+ fontWeight: t.bold ? 700 : 400,
1132
+ fontStyle: t.italic ? "italic" : "normal",
1133
+ textDecoration: t.underline ? "underline" : "none",
768
1134
  align: t.textAlign,
769
- padding: isCallout ? sw + 2 : sw,
770
- background: a?.style.interiorColor ?? null
1135
+ padding: (0, _embedpdf_core_annotation.textPlateInset)(sw)
771
1136
  }
772
1137
  };
773
1138
  });
774
1139
  }
775
1140
  //#endregion
776
- //#region src/tools.ts
777
- /**
778
- * The tool `armStamp` activates — and the only tool an armed payload survives
779
- * onto (plus the legacy `annotation-stamp` tag, honoured for embedder configs
780
- * written before the tags were unified). Placement consults the armed payload
781
- * BEFORE the active tool's own `source` spec, so a payload must not outlive
782
- * its tool onto a sibling stamp preset: an `image` preset's click has to open
783
- * its own picker, not silently place whatever the stamp panel last armed.
784
- */
785
- const ARMED_STAMP_TOOL_ID = "stamp";
786
- /**
787
- * The creation properties each known authoring kind actually consumes. This is
788
- * deliberately separate from the flat internal patch vocabulary: it gives tool
789
- * configuration precise compile-time and runtime validation while mixed-selection
790
- * edits can keep using `AnnotationPropsPatch`.
791
- */
792
- const TOOL_DEFAULT_KEYS = {
793
- square: [
794
- "color",
795
- "interiorColor",
796
- "opacity",
797
- "strokeWidth",
798
- "border"
799
- ],
800
- circle: [
801
- "color",
802
- "interiorColor",
803
- "opacity",
804
- "strokeWidth",
805
- "border"
806
- ],
807
- line: [
808
- "color",
809
- "interiorColor",
810
- "opacity",
811
- "strokeWidth",
812
- "border",
813
- "lineEndings"
814
- ],
815
- polygon: [
816
- "color",
817
- "interiorColor",
818
- "opacity",
819
- "strokeWidth",
820
- "border"
821
- ],
822
- polyline: [
823
- "color",
824
- "interiorColor",
825
- "opacity",
826
- "strokeWidth",
827
- "border",
828
- "lineEndings"
829
- ],
830
- ink: [
831
- "color",
832
- "opacity",
833
- "strokeWidth",
834
- "blendMode"
835
- ],
836
- "free-text": [
837
- "fontFamily",
838
- "fontSize",
839
- "fontColor",
840
- "textAlign",
841
- "opacity",
842
- "interiorColor",
843
- "color",
844
- "strokeWidth",
845
- "border"
846
- ],
847
- "free-text-callout": [
848
- "fontFamily",
849
- "fontSize",
850
- "fontColor",
851
- "textAlign",
852
- "opacity",
853
- "interiorColor",
854
- "color",
855
- "strokeWidth",
856
- "border",
857
- "lineEndings"
858
- ],
859
- highlight: [
860
- "color",
861
- "opacity",
862
- "blendMode"
863
- ],
864
- underline: [
865
- "color",
866
- "opacity",
867
- "blendMode"
868
- ],
869
- strikeout: [
870
- "color",
871
- "opacity",
872
- "blendMode"
873
- ],
874
- squiggly: [
875
- "color",
876
- "opacity",
877
- "blendMode"
878
- ],
879
- caret: ["color", "opacity"],
880
- redact: [
881
- "color",
882
- "interiorColor",
883
- "opacity",
884
- "fontFamily",
885
- "fontSize",
886
- "fontColor",
887
- "textAlign"
888
- ],
889
- stamp: [],
890
- link: ["link"],
891
- text: [
892
- "icon",
893
- "color",
894
- "opacity"
895
- ],
896
- "file-attachment": [
897
- "icon",
898
- "color",
899
- "opacity"
900
- ],
901
- "widget-text": [
902
- "color",
903
- "interiorColor",
904
- "strokeWidth",
905
- "border",
906
- "fontFamily",
907
- "fontSize",
908
- "fontColor",
909
- "textAlign"
910
- ],
911
- "widget-choice": [
912
- "color",
913
- "interiorColor",
914
- "strokeWidth",
915
- "border",
916
- "fontFamily",
917
- "fontSize",
918
- "fontColor",
919
- "textAlign"
920
- ],
921
- "widget-toggle": [
922
- "color",
923
- "interiorColor",
924
- "strokeWidth",
925
- "border"
926
- ]
927
- };
928
- const DRAW_TAGS = ["annotation-draw", "annotation-edit"];
929
- const MARKUP_TAGS = ["text-select", "annotation-edit"];
930
- /**
931
- * Whether arming this ANNOTATION tool is touch consent (`Tool.touchDirect`):
932
- * drag-CREATE tools — shape/ink draw and text-markup select — take
933
- * single-finger touch wholesale (finger draws/marks, two fingers navigate:
934
- * the drawing-app convention). Click-to-PLACE tools (stamp, note) do not: a
935
- * tap places without any claim, and drags keep scrolling. Derived from the
936
- * gesture tags, so custom registered tools inherit the right behavior. Only
937
- * annotation-owned tools run through this — the built-in `pointer` tool also
938
- * enables `text-select`, but it is registered by the hub, not here.
939
- */
940
- const isTouchDirect = (enables) => enables.has("annotation-draw") || enables.has("text-select");
941
- /**
942
- * The built-in tools — a data mirror of v2's registrations (shapes + lines + ink
943
- * + free text in the draw channel, text markup + caret behind text selection, and
944
- * the click-to-place stamp). Order is display-neutral; the toolbar owns layout.
945
- */
946
- const DEFAULT_TOOLS = [
947
- {
948
- id: "square",
949
- subtype: "square",
950
- cursor: "crosshair",
951
- enables: DRAW_TAGS,
952
- defaults: { strokeWidth: 6 },
953
- clickCreate: {
954
- width: 80,
955
- height: 60
956
- }
957
- },
958
- {
959
- id: "circle",
960
- subtype: "circle",
961
- cursor: "crosshair",
962
- enables: DRAW_TAGS,
963
- defaults: { strokeWidth: 6 },
964
- clickCreate: {
965
- width: 80,
966
- height: 60
967
- }
968
- },
969
- {
970
- id: "line",
971
- subtype: "line",
972
- cursor: "crosshair",
973
- enables: DRAW_TAGS,
974
- defaults: { strokeWidth: 6 },
975
- clickCreate: { length: 80 }
976
- },
977
- {
978
- id: "polygon",
979
- subtype: "polygon",
980
- cursor: "crosshair",
981
- enables: DRAW_TAGS,
982
- defaults: { strokeWidth: 6 }
983
- },
984
- {
985
- id: "polyline",
986
- subtype: "polyline",
987
- cursor: "crosshair",
988
- enables: DRAW_TAGS,
989
- defaults: { strokeWidth: 6 }
990
- },
991
- {
992
- id: "ink",
993
- subtype: "ink",
994
- cursor: "crosshair",
995
- enables: DRAW_TAGS,
996
- defaults: {
997
- color: "#ef4444",
998
- strokeWidth: 6,
999
- blendMode: "normal"
1000
- },
1001
- ink: { groupStrokesMs: 800 }
1002
- },
1003
- {
1004
- id: "ink-highlight",
1005
- extends: "ink",
1006
- intent: "ink-highlight",
1007
- defaults: {
1008
- color: "#ffcd45",
1009
- strokeWidth: 14,
1010
- blendMode: "multiply"
1011
- },
1012
- ink: { straighten: {
1013
- deviationThreshold: .15,
1014
- axisSnapDegrees: 15
1015
- } }
1016
- },
1017
- {
1018
- id: "free-text",
1019
- subtype: "free-text",
1020
- cursor: "crosshair",
1021
- enables: DRAW_TAGS,
1022
- defaults: { fontColor: "#ef4444" },
1023
- upright: true,
1024
- clickCreate: {
1025
- width: 180,
1026
- height: 40,
1027
- anchor: "top-left"
1028
- }
1029
- },
1030
- {
1031
- id: "free-text-callout",
1032
- subtype: "free-text-callout",
1033
- propsKind: "free-text",
1034
- cursor: "crosshair",
1035
- enables: DRAW_TAGS,
1036
- defaults: {
1037
- strokeWidth: 6,
1038
- lineEndings: { end: "open-arrow" }
1039
- },
1040
- upright: true
1041
- },
1042
- {
1043
- id: "highlight",
1044
- subtype: "highlight",
1045
- cursor: "default",
1046
- enables: MARKUP_TAGS,
1047
- defaults: {
1048
- color: "#ffe16a",
1049
- blendMode: "multiply"
1050
- },
1051
- selection: { kind: "markup" }
1052
- },
1053
- {
1054
- id: "redact",
1055
- subtype: "redact",
1056
- cursor: "crosshair",
1057
- enables: [
1058
- "text-select",
1059
- "annotation-draw",
1060
- "annotation-edit"
1061
- ],
1062
- defaults: {
1063
- color: "#e44234",
1064
- interiorColor: "#000000",
1065
- fontColor: "#ffffff",
1066
- opacity: 1
1067
- },
1068
- selection: { kind: "markup" }
1069
- },
1070
- {
1071
- id: "underline",
1072
- subtype: "underline",
1073
- cursor: "default",
1074
- enables: MARKUP_TAGS,
1075
- defaults: { color: "#ef4444" },
1076
- selection: { kind: "markup" }
1077
- },
1078
- {
1079
- id: "strikeout",
1080
- subtype: "strikeout",
1081
- cursor: "default",
1082
- enables: MARKUP_TAGS,
1083
- defaults: { color: "#ef4444" },
1084
- selection: { kind: "markup" }
1085
- },
1086
- {
1087
- id: "squiggly",
1088
- subtype: "squiggly",
1089
- cursor: "default",
1090
- enables: MARKUP_TAGS,
1091
- defaults: { color: "#ef4444" },
1092
- selection: { kind: "markup" }
1093
- },
1094
- {
1095
- id: "insert-text",
1096
- subtype: "caret",
1097
- propsKind: "caret",
1098
- preset: "caret",
1099
- cursor: "default",
1100
- enables: MARKUP_TAGS,
1101
- defaults: { color: "#ef4444" },
1102
- selection: {
1103
- kind: "text-edit",
1104
- operation: "insert"
1105
- }
1106
- },
1107
- {
1108
- id: "replace-text",
1109
- subtype: "strikeout",
1110
- propsKind: "strikeout",
1111
- cursor: "default",
1112
- enables: MARKUP_TAGS,
1113
- defaults: { color: "#ef4444" },
1114
- selection: {
1115
- kind: "text-edit",
1116
- operation: "replace"
1117
- }
1118
- },
1119
- {
1120
- id: ARMED_STAMP_TOOL_ID,
1121
- subtype: "stamp",
1122
- cursor: "copy",
1123
- enables: ["annotation-place", "annotation-edit"],
1124
- source: {
1125
- kind: "prompt",
1126
- accept: "image/png,image/jpeg,application/pdf"
1127
- },
1128
- upright: true,
1129
- ghost: { mode: "footprint" }
1130
- },
1131
- {
1132
- id: "link",
1133
- subtype: "link",
1134
- cursor: "crosshair",
1135
- enables: DRAW_TAGS
1136
- },
1137
- {
1138
- id: "note",
1139
- subtype: "text",
1140
- cursor: "crosshair",
1141
- enables: ["annotation-place", "annotation-edit"],
1142
- defaults: {
1143
- icon: "comment",
1144
- color: "#facc15"
1145
- },
1146
- flags: {
1147
- noZoom: true,
1148
- noRotate: true
1149
- },
1150
- upright: true,
1151
- ghost: { mode: "footprint" }
1152
- },
1153
- {
1154
- id: "attachment",
1155
- subtype: "file-attachment",
1156
- cursor: "copy",
1157
- enables: ["annotation-place", "annotation-edit"],
1158
- source: { kind: "prompt" },
1159
- defaults: {
1160
- icon: "paperclip",
1161
- color: "#facc15"
1162
- },
1163
- flags: {
1164
- noZoom: true,
1165
- noRotate: true
1166
- },
1167
- upright: true,
1168
- ghost: { mode: "footprint" }
1169
- }
1170
- ];
1171
- /** Merge two default patches, `b` over `a`, with line endings merged per side. */
1172
- function mergeDefaults(a, b) {
1173
- if (!a) return b;
1174
- if (!b) return a;
1175
- const merged = {
1176
- ...a,
1177
- ...b
1178
- };
1179
- if (a.lineEndings || b.lineEndings) merged.lineEndings = {
1180
- ...a.lineEndings,
1181
- ...b.lineEndings
1182
- };
1183
- return merged;
1184
- }
1185
- /** Overlay a same-id override onto a base definition (configure a built-in). */
1186
- function mergeDef(base, over) {
1187
- return {
1188
- ...base,
1189
- ...over,
1190
- enables: over.enables ?? base.enables,
1191
- meta: base.meta || over.meta ? {
1192
- ...base.meta,
1193
- ...over.meta
1194
- } : void 0,
1195
- defaults: mergeDefaults(base.defaults, over.defaults),
1196
- flags: base.flags || over.flags ? {
1197
- ...base.flags,
1198
- ...over.flags
1199
- } : void 0,
1200
- ink: base.ink || over.ink ? {
1201
- ...base.ink,
1202
- ...over.ink
1203
- } : void 0
1204
- };
1205
- }
1206
- function validateDefaults(tool) {
1207
- if (!tool.defaults) return;
1208
- const allowed = TOOL_DEFAULT_KEYS[tool.subtype];
1209
- if (!allowed) return;
1210
- const keys = new Set(allowed);
1211
- for (const key of Object.keys(tool.defaults)) if (!keys.has(key)) throw new Error(`[annotation] tool '${tool.id}' does not support default '${key}' for '${tool.subtype}'`);
1212
- }
1213
- /**
1214
- * Resolve {@link DEFAULT_TOOLS} + embedder overrides into the ready-to-use table.
1215
- * A same-id override merges over the built-in; a new id adds a tool; `extends`
1216
- * inherits from an already-defined tool. Keyed by id, so lookups are O(1).
1217
- */
1218
- function buildToolRegistry(overrides = []) {
1219
- const defs = /* @__PURE__ */ new Map();
1220
- for (const d of DEFAULT_TOOLS) defs.set(d.id, d);
1221
- for (const o of overrides) {
1222
- const prev = defs.get(o.id);
1223
- defs.set(o.id, prev ? mergeDef(prev, o) : o);
1224
- }
1225
- const out = /* @__PURE__ */ new Map();
1226
- const resolving = /* @__PURE__ */ new Set();
1227
- const resolve = (id) => {
1228
- const cached = out.get(id);
1229
- if (cached) return cached;
1230
- const def = defs.get(id);
1231
- if (!def) throw new Error(`[annotation] tool '${id}' extends an unknown tool`);
1232
- let base;
1233
- if (def.extends && def.extends !== id && !resolving.has(def.extends)) {
1234
- resolving.add(id);
1235
- base = resolve(def.extends);
1236
- resolving.delete(id);
1237
- }
1238
- const subtype = def.subtype ?? base?.subtype ?? def.id;
1239
- const resolved = {
1240
- id: def.id,
1241
- subtype,
1242
- preset: def.preset ?? def.id,
1243
- propsKind: def.propsKind ?? base?.propsKind ?? subtype,
1244
- cursor: def.cursor ?? base?.cursor ?? "crosshair",
1245
- enables: new Set(def.enables ?? (base ? [...base.enables] : [])),
1246
- defaults: mergeDefaults(base?.defaults, def.defaults),
1247
- flags: base?.flags || def.flags ? {
1248
- ...base?.flags,
1249
- ...def.flags
1250
- } : void 0,
1251
- source: def.source ?? base?.source,
1252
- selection: def.selection ?? base?.selection,
1253
- intent: def.intent ?? base?.intent,
1254
- ink: base?.ink || def.ink ? {
1255
- ...base?.ink,
1256
- ...def.ink
1257
- } : void 0,
1258
- upright: def.upright ?? base?.upright ?? false,
1259
- clickCreate: def.clickCreate ?? base?.clickCreate ?? false,
1260
- ghost: def.ghost ?? base?.ghost ?? false,
1261
- meta: base?.meta || def.meta ? {
1262
- ...base?.meta,
1263
- ...def.meta
1264
- } : void 0
1265
- };
1266
- validateDefaults(resolved);
1267
- out.set(id, resolved);
1268
- return resolved;
1269
- };
1270
- for (const id of defs.keys()) resolve(id);
1271
- return out;
1272
- }
1273
- //#endregion
1274
1141
  //#region src/placement.ts
1275
1142
  /**
1276
1143
  * Per-kind CODE for the click-to-place ICON kinds (note / file attachment)
@@ -1337,10 +1204,13 @@ const TEXT_COMMIT_DEBOUNCE_MS = 250;
1337
1204
  */
1338
1205
  function createAnnotationCapability(ctx, config = {}) {
1339
1206
  const behaviors = [];
1340
- /** Per-annotation debounce timer for the engine `contents` write while typing. */
1207
+ /** Per-annotation debounce timer for the engine text write while typing. */
1341
1208
  const textTimers = /* @__PURE__ */ new Map();
1209
+ /** The registered fonts, for face ↔ key mapping (the local engine's list;
1210
+ * the cloud engine registers none). */
1211
+ const fonts = () => ctx.engine?.fonts?.list() ?? [];
1342
1212
  const configTools = config.tools ?? [];
1343
- const registry = buildToolRegistry(configTools);
1213
+ const registry = require_tools.buildToolRegistry(configTools);
1344
1214
  /** The installed file-picker port (a DOM file dialog, wired by the framework
1345
1215
  * adapter), or null — every click-then-pick tool resolves through this ONE
1346
1216
  * slot. See {@link FilePickerProvider}. */
@@ -1462,10 +1332,18 @@ function createAnnotationCapability(ctx, config = {}) {
1462
1332
  };
1463
1333
  return v;
1464
1334
  };
1335
+ /** The editor's text RANGE inside the annotation being edited, or null
1336
+ * (no editor selection, a bare caret, or a selection left behind by a
1337
+ * previous edit). The one condition that routes the range keys to runs. */
1338
+ const activeTextRange = (m) => {
1339
+ const ts = ctx.getState().textSelection;
1340
+ return ts && m.editing === ts.id && ts.end > ts.start && m.byId[ts.id] ? ts : null;
1341
+ };
1465
1342
  let selPropsCache = null;
1466
1343
  const memoSelectionProps = () => {
1467
1344
  const m = model();
1468
- if (selPropsCache && selPropsCache.model === m) return selPropsCache.v;
1345
+ const range = activeTextRange(m);
1346
+ if (selPropsCache && selPropsCache.model === m && selPropsCache.range === range) return selPropsCache.v;
1469
1347
  const members = m.selected.map((id) => m.byId[id]).filter((a) => !!a);
1470
1348
  const specs = (0, _embedpdf_core_annotation.sharedProps)(members.map((a) => a.subtype));
1471
1349
  const values = {};
@@ -1477,6 +1355,16 @@ function createAnnotationCapability(ctx, config = {}) {
1477
1355
  const firstJson = JSON.stringify(first);
1478
1356
  if (members.some((a) => JSON.stringify(valueOf(a, spec.key)) !== firstJson)) mixed.push(spec.key);
1479
1357
  }
1358
+ if (range && members.length === 1 && members[0].id === range.id) {
1359
+ const rp = rangeProps(richDocOf(members[0], fonts), range, fonts);
1360
+ for (const spec of specs) {
1361
+ if (!RANGE_KEYS.includes(spec.key)) continue;
1362
+ values[spec.key] = rp.values[spec.key];
1363
+ const i = mixed.indexOf(spec.key);
1364
+ if (i >= 0) mixed.splice(i, 1);
1365
+ if (rp.mixed.includes(spec.key)) mixed.push(spec.key);
1366
+ }
1367
+ }
1480
1368
  const v = {
1481
1369
  specs,
1482
1370
  values,
@@ -1484,6 +1372,7 @@ function createAnnotationCapability(ctx, config = {}) {
1484
1372
  };
1485
1373
  selPropsCache = {
1486
1374
  model: m,
1375
+ range,
1487
1376
  v
1488
1377
  };
1489
1378
  return v;
@@ -1625,7 +1514,7 @@ function createAnnotationCapability(ctx, config = {}) {
1625
1514
  ...input.subject !== void 0 ? { subject: input.subject } : {}
1626
1515
  };
1627
1516
  ctx.dispatch({ type: "STAMP_ARM_CHANGED" });
1628
- ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken)?.activateTool(ARMED_STAMP_TOOL_ID);
1517
+ ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken)?.activateTool(require_tools.ARMED_STAMP_TOOL_ID);
1629
1518
  };
1630
1519
  const disarmStamp = () => {
1631
1520
  if (!armedStamp) return;
@@ -1943,6 +1832,34 @@ function createAnnotationCapability(ctx, config = {}) {
1943
1832
  const res = await doc.page(pon).annotations.update(ref, patch);
1944
1833
  syncDTO(res.updated, "vector");
1945
1834
  };
1835
+ const commitText = (ref) => {
1836
+ const key = require_seam.refKey(ref);
1837
+ clearTimeout(textTimers.get(key));
1838
+ textTimers.delete(key);
1839
+ const a = model().byId[key];
1840
+ const pon = ponForRef(ref);
1841
+ if (!a || pon == null) return;
1842
+ const patch = textCommitPatch(a, richDocOf(a, fonts).paragraphs, fonts);
1843
+ ctx.doc?.page(pon).annotations.update(ref, {
1844
+ subtype: "free-text",
1845
+ ...patch
1846
+ }).then(() => {}, () => {});
1847
+ };
1848
+ const scheduleTextCommit = (ref) => {
1849
+ const key = require_seam.refKey(ref);
1850
+ clearTimeout(textTimers.get(key));
1851
+ textTimers.set(key, setTimeout(() => commitText(ref), TEXT_COMMIT_DEBOUNCE_MS));
1852
+ };
1853
+ const flushTextCommits = () => {
1854
+ for (const key of [...textTimers.keys()]) {
1855
+ const a = model().byId[key];
1856
+ if (a?.ref) commitText(a.ref);
1857
+ else {
1858
+ clearTimeout(textTimers.get(key));
1859
+ textTimers.delete(key);
1860
+ }
1861
+ }
1862
+ };
1946
1863
  function apply(msg) {
1947
1864
  const [next, effects] = (0, _embedpdf_core_annotation.update)(model(), msg);
1948
1865
  ctx.dispatch({
@@ -2559,6 +2476,36 @@ function createAnnotationCapability(ctx, config = {}) {
2559
2476
  if (patch.flags) out.flags = patch.flags;
2560
2477
  return out;
2561
2478
  };
2479
+ const updateSelection = (patch) => {
2480
+ const m = model();
2481
+ const range = activeTextRange(m);
2482
+ if (range) {
2483
+ const a = m.byId[range.id];
2484
+ const { delta, rest } = runDeltaForProps(patch, fonts);
2485
+ if (Object.keys(delta).length) {
2486
+ const next = (0, _embedpdf_core_annotation.applyStyleToRange)({ paragraphs: richDocOf(a, fonts).paragraphs }, range, delta);
2487
+ apply({
2488
+ t: "setRichText",
2489
+ id: range.id,
2490
+ doc: next
2491
+ });
2492
+ if (a.ref) scheduleTextCommit(a.ref);
2493
+ }
2494
+ if (Object.keys(rest).length) {
2495
+ flushTextCommits();
2496
+ apply({
2497
+ t: "setProps",
2498
+ patch: rest
2499
+ });
2500
+ }
2501
+ return;
2502
+ }
2503
+ if (m.editing) flushTextCommits();
2504
+ apply({
2505
+ t: "setProps",
2506
+ patch
2507
+ });
2508
+ };
2562
2509
  return {
2563
2510
  create: async (pon, draft) => {
2564
2511
  const doc = ctx.doc;
@@ -2617,14 +2564,14 @@ function createAnnotationCapability(ctx, config = {}) {
2617
2564
  tool: (id) => registry.get(id) ?? null,
2618
2565
  toolSubtype: (id) => registry.get(id)?.subtype ?? id,
2619
2566
  registerTool: (def) => {
2620
- const resolved = buildToolRegistry([...configTools, def]).get(def.id);
2567
+ const resolved = require_tools.buildToolRegistry([...configTools, def]).get(def.id);
2621
2568
  if (!resolved) throw new Error(`[annotation] could not resolve tool '${def.id}'`);
2622
2569
  registry.set(resolved.id, resolved);
2623
2570
  const un = ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken)?.registerTool({
2624
2571
  id: resolved.id,
2625
2572
  cursor: resolved.cursor,
2626
2573
  enables: resolved.enables,
2627
- touchDirect: isTouchDirect(resolved.enables)
2574
+ touchDirect: require_tools.isTouchDirect(resolved.enables)
2628
2575
  });
2629
2576
  if (resolved.defaults) apply({
2630
2577
  t: "setDefaults",
@@ -2637,10 +2584,11 @@ function createAnnotationCapability(ctx, config = {}) {
2637
2584
  };
2638
2585
  },
2639
2586
  update: (ref, patch) => updateOne(ref, patch),
2640
- updateSelection: (patch) => apply({
2641
- t: "setProps",
2642
- patch
2643
- }),
2587
+ updateSelection,
2588
+ toggleTextFormat: (format) => {
2589
+ const current = memoSelectionProps().values[format];
2590
+ updateSelection({ [format]: !current });
2591
+ },
2644
2592
  updateSelectionFlags: (patch) => apply({
2645
2593
  t: "setFlags",
2646
2594
  patch
@@ -2979,30 +2927,37 @@ function createAnnotationCapability(ctx, config = {}) {
2979
2927
  id: require_seam.refKey(ref),
2980
2928
  text
2981
2929
  });
2982
- const key = require_seam.refKey(ref);
2983
- clearTimeout(textTimers.get(key));
2984
- textTimers.set(key, setTimeout(() => {
2985
- textTimers.delete(key);
2986
- const pon = ponForRef(ref);
2987
- if (pon != null) ctx.doc?.page(pon).annotations.update(ref, {
2988
- subtype: "free-text",
2989
- contents: text
2990
- }).then(() => {}, () => {});
2991
- }, TEXT_COMMIT_DEBOUNCE_MS));
2930
+ scheduleTextCommit(ref);
2931
+ },
2932
+ setRichText: (ref, doc) => {
2933
+ apply({
2934
+ t: "setRichText",
2935
+ id: require_seam.refKey(ref),
2936
+ doc: { paragraphs: doc.paragraphs }
2937
+ });
2938
+ scheduleTextCommit(ref);
2939
+ },
2940
+ setTextSelection: (ref, range) => {
2941
+ const id = require_seam.refKey(ref);
2942
+ const prev = ctx.getState().textSelection;
2943
+ const next = range ? {
2944
+ id,
2945
+ start: range.start,
2946
+ end: range.end
2947
+ } : null;
2948
+ if (prev === null === (next === null) && (!prev || !next || prev.id === next.id && prev.start === next.start && prev.end === next.end)) return;
2949
+ ctx.dispatch({
2950
+ type: "SET_TEXT_SELECTION",
2951
+ selection: next
2952
+ });
2992
2953
  },
2954
+ cssFontFamily: (family) => cssFontFamilyForFace(family, fonts),
2993
2955
  endTextEdit: () => {
2994
- for (const t of textTimers.values()) clearTimeout(t);
2995
- const id = model().editing;
2996
- const a = id ? model().byId[id] : null;
2997
- if (a?.ref) {
2998
- const pon = ponForRef(a.ref);
2999
- const text = a.data?.contents ?? "";
3000
- if (pon != null) ctx.doc?.page(pon).annotations.update(a.ref, {
3001
- subtype: "free-text",
3002
- contents: text
3003
- }).then(() => {}, () => {});
3004
- }
3005
- textTimers.clear();
2956
+ flushTextCommits();
2957
+ if (ctx.getState().textSelection) ctx.dispatch({
2958
+ type: "SET_TEXT_SELECTION",
2959
+ selection: null
2960
+ });
3006
2961
  apply({ t: "endTextEdit" });
3007
2962
  },
3008
2963
  commitScriptEffects: async (entries) => {
@@ -3081,24 +3036,6 @@ function createAnnotationCapability(ctx, config = {}) {
3081
3036
  };
3082
3037
  }
3083
3038
  //#endregion
3084
- Object.defineProperty(exports, "ARMED_STAMP_TOOL_ID", {
3085
- enumerable: true,
3086
- get: function() {
3087
- return ARMED_STAMP_TOOL_ID;
3088
- }
3089
- });
3090
- Object.defineProperty(exports, "DEFAULT_TOOLS", {
3091
- enumerable: true,
3092
- get: function() {
3093
- return DEFAULT_TOOLS;
3094
- }
3095
- });
3096
- Object.defineProperty(exports, "buildToolRegistry", {
3097
- enumerable: true,
3098
- get: function() {
3099
- return buildToolRegistry;
3100
- }
3101
- });
3102
3039
  Object.defineProperty(exports, "createAnnotationCapability", {
3103
3040
  enumerable: true,
3104
3041
  get: function() {
@@ -3111,12 +3048,6 @@ Object.defineProperty(exports, "fromDTO", {
3111
3048
  return fromDTO;
3112
3049
  }
3113
3050
  });
3114
- Object.defineProperty(exports, "isTouchDirect", {
3115
- enumerable: true,
3116
- get: function() {
3117
- return isTouchDirect;
3118
- }
3119
- });
3120
3051
  Object.defineProperty(exports, "toCreateDraft", {
3121
3052
  enumerable: true,
3122
3053
  get: function() {
@@ -3130,4 +3061,4 @@ Object.defineProperty(exports, "toPatch", {
3130
3061
  }
3131
3062
  });
3132
3063
 
3133
- //# sourceMappingURL=capability-R7dgogJs.cjs.map
3064
+ //# sourceMappingURL=capability-DVx1NmTe.cjs.map