@stll/folio-react 0.1.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/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { t as containedHandler } from "./contained-handler-4uiqUVRn.js";
2
- import { i as cn, n as FolioUIProvider, r as useFolioUI, t as DEFAULT_COMPONENTS } from "./folio-ui-o_rftArH.js";
2
+ import { i as cn, n as FolioUIProvider, r as useFolioUI, t as DEFAULT_COMPONENTS } from "./folio-ui-CA59webC.js";
3
3
  import { i as findInDocument, o as scrollToMatch, t as useFindReplace$1 } from "./useFindReplace-MZ8wi31A.js";
4
+ import { t as messages_exports } from "./messages.js";
4
5
  import * as React$1 from "react";
5
- import { Component, Suspense, cloneElement, createContext, isValidElement, lazy, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
+ import React, { Component, Suspense, cloneElement, createContext, isValidElement, lazy, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
7
  import { AlertCircleIcon, AlertTriangleIcon, AlignCenterIcon, AlignJustifyIcon, AlignLeftIcon, AlignRightIcon, BaselineIcon, BoldIcon, CheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ClipboardCopyIcon, EyeIcon, FileIcon, GlobeIcon, ImageIcon, IndentDecreaseIcon, IndentIncreaseIcon, InfoIcon, ItalicIcon, LinkIcon, ListIcon, ListOrderedIcon, MoreHorizontalIcon, PaintbrushIcon, PenLineIcon, PencilIcon, PilcrowIcon, Redo2Icon, RulerIcon, SeparatorHorizontalIcon, SquarePenIcon, StickyNoteIcon, TableIcon, TableOfContentsIcon, TypeIcon, UnderlineIcon, Undo2Icon, UnlinkIcon, XIcon } from "lucide-react";
7
- import { useTranslations } from "use-intl";
8
+ import { IntlProvider, useTranslations } from "use-intl";
8
9
  import { applyFolioAIEditOperations, applyFolioAIEditOperations as applyFolioAIEditOperations$1, createFolioAIEditSnapshot, createFolioAIEditSnapshot as createFolioAIEditSnapshot$1, diffWordSegments, hashFolioAIBlockText, normalizeFolioAIBlockText } from "@stll/folio-core/ai-edits";
9
10
  import { normalizeBaseDirection } from "@stll/folio-core/docx/normalizeBaseDirection";
10
11
  import { getCachedNumberingMap } from "@stll/folio-core/docx/numberingParser";
@@ -62,20 +63,21 @@ import { recordLayoutPhase } from "@stll/folio-core/layout-engine/layoutInstrume
62
63
  import { LayoutPainter } from "@stll/folio-core/layout-painter";
63
64
  import { PAINTER_PAINTED_EVENT, findPageShellForPmPos } from "@stll/folio-core/layout-painter/renderPage";
64
65
  import { LayoutSelectionGate } from "@stll/folio-core/paged-layout/LayoutSelectionGate";
65
- import { collectRunFontsAtPmPositions, measureContentLeft, projectRangesToRects } from "@stll/folio-core/paged-layout/rangeProjection";
66
+ import { collectRunFontsAtPmPositions, measureDirectiveGutter, projectRangesToRects } from "@stll/folio-core/paged-layout/rangeProjection";
66
67
  import { isReadOnlyEditKey } from "@stll/folio-core/paged-layout/readOnlyEditAttempt";
67
68
  import { getPageScrollTarget, prefersReducedMotionBehavior } from "@stll/folio-core/paged-layout/scrollNavigation";
68
69
  import { PAGES_CONTAINER_CLASS, scrollFolioPositionIntoView, scrollPagesToPmPosition } from "@stll/folio-core/paged-layout/scrollToPmPosition";
70
+ import { flashParagraphFragmentsByParaId } from "@stll/folio-core/paged-layout/paragraphFlash";
69
71
  import { DEFAULT_PAGE_HEIGHT_PX, getMargins, getPageSize, twipsToPixels as twipsToPixels$1 } from "@stll/folio-core/paged-layout/sectionGeometry";
70
72
  import { resizeColumnPair } from "@stll/folio-core/paged-layout/tableColumnResize";
71
73
  import { tableInsertButtonOffset } from "@stll/folio-core/paged-layout/tableInsertButtonGeometry";
72
74
  import { getTransactionDirtyRange } from "@stll/folio-core/paged-layout/transactionDirtyRange";
75
+ import { findParagraphByParaId, findStartPosForParaId } from "@stll/folio-core/prosemirror/utils/findParagraphByParaId";
73
76
  import { expectFontFamilyMarkAttrs, expectImageAttrs, expectTableAttrs, expectTableCellAttrs, mergeImageAttrs, mergeTableAttrs, mergeTableCellAttrs, mergeTableRowAttrs } from "@stll/folio-core/prosemirror/attrs";
74
77
  import { extractEmbeddedFonts } from "@stll/folio-core/fonts/embeddedFonts";
75
78
  import { panic } from "better-result";
76
79
  import { collectRemoteSelections, createHiddenEditorManager, createHiddenEditorState } from "@stll/folio-core/controller/hiddenEditorManager";
77
80
  import { createFolioAIEditSnapshot as createFolioAIEditSnapshot$2 } from "@stll/folio-core/ai-edits/snapshot";
78
- import { findParagraphByParaId } from "@stll/folio-core/prosemirror/utils/findParagraphByParaId";
79
81
  import { deriveBlockId, getFolioParaIdFromBlockId, getFolioParaIdFromBlockId as getFolioParaIdFromBlockId$1, getSequentialFolioBlockIdIndex, isFolioBlockId, isSequentialFolioBlockId } from "@stll/folio-core/types/block-id";
80
82
  import { CONTENT_CONTROL_WIDGET_EVENT_NAME, dispatchDatePick, dispatchDropdownPick } from "@stll/folio-core/prosemirror/plugins/contentControlWidgets";
81
83
  import { ErrorManager } from "@stll/folio-core/managers/ErrorManager";
@@ -91,6 +93,7 @@ import { applyImagePosition, applyImageProperties, applyImageTransform, applyIma
91
93
  import { classifyEditorKeydown, deleteSelectedTable, isFocusInInputLike, isMacPlatform } from "@stll/folio-core/managers/editorShortcuts";
92
94
  import { getScrollTopForZoomAnchor, getViewportCenterZoomAnchorForZoomChange } from "@stll/folio-core/paged-layout/zoomScrollAnchor";
93
95
  import { redo, undo } from "prosemirror-history";
96
+ import { createRoot } from "react-dom/client";
94
97
  import { createEmptyDocument } from "@stll/folio-core/utils/createDocument";
95
98
  import { createDocx } from "@stll/folio-core/docx/rezip";
96
99
  import { DEFAULT_AI_SUGGESTION_PRESETS } from "@stll/folio-core/ai-suggestions/types";
@@ -1447,63 +1450,213 @@ const overlayStyles = {
1447
1450
  };
1448
1451
  const BLOCK_OPENERS = /* @__PURE__ */ new Set(["if", "each"]);
1449
1452
  const BLOCK_CLOSERS = /* @__PURE__ */ new Set(["endif", "endeach"]);
1450
- const RAIL_BASE_GUTTER = 12;
1453
+ const RAIL_WIDTH = 2.5;
1454
+ /** Horizontal gap between adjacent nesting depths' rails. */
1451
1455
  const RAIL_DEPTH_GAP = 5;
1452
- const RAIL_WIDTH = 2;
1456
+ /** Minimum clear space between the innermost (deepest) rail and the text edge. */
1457
+ const RAIL_TEXT_CLEARANCE = 6;
1458
+ /** Keep the outermost rail this far off the physical page-left edge. */
1459
+ const RAIL_EDGE_PAD = 2;
1460
+ /** Drop per-page segments thinner than this (sub-pixel slivers at a page seam). */
1461
+ const RAIL_SEGMENT_MIN_HEIGHT = 2;
1462
+ const bandKindOf = (kind) => kind === "each" ? "each" : "if";
1453
1463
  /**
1454
- * Pair opener/closer block directives with a stack so each conditional/loop
1455
- * region knows its top, bottom, and nesting depth (for the gutter rail).
1456
- * Unbalanced directives are skipped. The rail's horizontal position comes from
1457
- * the stable `contentLeft` anchor (not the opener's re-projected rect), so it
1458
- * stays put while text above reflows; only its vertical span tracks the rects.
1464
+ * Deepest nesting level whose rail still fits inside the left margin with a
1465
+ * `RAIL_TEXT_CLEARANCE` gap to the text and a `RAIL_EDGE_PAD` gap to the page
1466
+ * edge. Depths past this reuse the deepest offset, so a 5-level nest can never
1467
+ * march a rail into the text column. Pure function of the measured margin width.
1459
1468
  */
1460
- const computeBands = (groups, contentLeft) => {
1461
- const blocks = groups.filter((g) => g.range.block && (BLOCK_OPENERS.has(g.range.kind) || BLOCK_CLOSERS.has(g.range.kind))).sort((a, b) => a.range.from - b.range.from);
1462
- const bands = [];
1469
+ const railBudgetDepth = (marginWidth) => {
1470
+ const usable = marginWidth - RAIL_TEXT_CLEARANCE - RAIL_WIDTH - RAIL_EDGE_PAD;
1471
+ if (usable <= 0) return 0;
1472
+ return Math.floor(usable / RAIL_DEPTH_GAP);
1473
+ };
1474
+ /**
1475
+ * Left edge (container space) of a depth-`depth` rail. The deepest rail the
1476
+ * budget allows sits `RAIL_TEXT_CLEARANCE + RAIL_WIDTH` left of the text edge;
1477
+ * shallower (outer) blocks fan further left into the margin — nested blocks read
1478
+ * as concentric brackets `[ [ [ text ] ] ]`. Depths beyond the budget clamp to
1479
+ * the deepest offset.
1480
+ */
1481
+ const railXForDepth = (depth, contentLeft, marginWidth) => {
1482
+ const budget = railBudgetDepth(marginWidth);
1483
+ const capped = Math.min(Math.max(0, depth), budget);
1484
+ return contentLeft - RAIL_TEXT_CLEARANCE - RAIL_WIDTH - (budget - capped) * RAIL_DEPTH_GAP;
1485
+ };
1486
+ /**
1487
+ * Clip a band's vertical span to each page's body-content band so a rail is
1488
+ * drawn as one rounded segment per page and never paints across the inter-page
1489
+ * gap, header, or footer. With no page bands (geometry not ready) the whole span
1490
+ * is returned as a single segment so rails still show. Pure math.
1491
+ */
1492
+ const segmentBandByPages = (band, pageBands) => {
1493
+ if (pageBands.length === 0) return band.bottom - band.top >= RAIL_SEGMENT_MIN_HEIGHT ? [band] : [];
1494
+ const segments = [];
1495
+ for (const page of pageBands) {
1496
+ const top = Math.max(band.top, page.top);
1497
+ const bottom = Math.min(band.bottom, page.bottom);
1498
+ if (bottom - top >= RAIL_SEGMENT_MIN_HEIGHT) segments.push({
1499
+ top,
1500
+ bottom
1501
+ });
1502
+ }
1503
+ return segments;
1504
+ };
1505
+ /**
1506
+ * Pair block openers with their closers via a kind-aware stack, so both chips of
1507
+ * a pair share a `blockId`, the rail knows its opener→closer PM span, and each
1508
+ * pairing carries the `depth` recorded in this very walk (never a second,
1509
+ * possibly-divergent depth pass). A closer matches the nearest opener of the same
1510
+ * family ({{/if}} ⇒ {{#if}}, {{/each}} ⇒ {{#each}}) and drops any still-open
1511
+ * openers nested above it; a closer with no matching opener is ignored. This keeps
1512
+ * a mid-edit / unbalanced template from pairing a {{/if}} with an {{#each}}.
1513
+ * Inline (block:false) markers are excluded. Pure function of the ranges.
1514
+ */
1515
+ const pairBlockRanges = (ranges) => {
1516
+ const blocks = ranges.filter((r) => r.block && (BLOCK_OPENERS.has(r.kind) || BLOCK_CLOSERS.has(r.kind))).slice().sort((a, b) => a.from - b.from);
1517
+ const pairings = [];
1463
1518
  const stack = [];
1464
- for (const group of blocks) {
1465
- if (BLOCK_OPENERS.has(group.range.kind)) {
1519
+ for (const range of blocks) {
1520
+ if (BLOCK_OPENERS.has(range.kind)) {
1466
1521
  stack.push({
1467
- group,
1522
+ range,
1468
1523
  depth: stack.length
1469
1524
  });
1470
1525
  continue;
1471
1526
  }
1472
- const open = stack.pop();
1473
- const openerRect = open?.group.rects[0];
1474
- const closerRect = group.rects[0];
1475
- if (!open || !openerRect || !closerRect) continue;
1476
- const railBase = contentLeft ?? openerRect.x;
1477
- bands.push({
1527
+ const wantOpener = range.kind === "endif" ? "if" : "each";
1528
+ let matched;
1529
+ let matchIdx = -1;
1530
+ for (let i = stack.length - 1; i >= 0; i -= 1) {
1531
+ const entry = stack[i];
1532
+ if (entry?.range.kind === wantOpener) {
1533
+ matched = entry;
1534
+ matchIdx = i;
1535
+ break;
1536
+ }
1537
+ }
1538
+ if (!matched) continue;
1539
+ stack.length = matchIdx;
1540
+ pairings.push({
1541
+ blockId: matched.range.from,
1542
+ kind: bandKindOf(matched.range.kind),
1543
+ depth: matched.depth,
1544
+ openerFrom: matched.range.from,
1545
+ closerFrom: range.from,
1546
+ closerTo: range.to,
1547
+ openerExpr: matched.range.expr
1548
+ });
1549
+ }
1550
+ return pairings;
1551
+ };
1552
+ /** Hover hint for a closer chip: what it closes, e.g. `/if · hasVerdicts`. */
1553
+ const closerHintLabel = (kind, openerExpr) => {
1554
+ const head = kind === "each" ? "/each" : "/if";
1555
+ return openerExpr ? `${head} · ${openerExpr}` : head;
1556
+ };
1557
+ /**
1558
+ * Block id of the innermost pairing whose opener→closer span contains `caretPos`
1559
+ * (largest opener position wins), or null. Drives the caret emphasis.
1560
+ */
1561
+ const innermostBlockAt = (pairings, caretPos) => {
1562
+ if (caretPos === null) return null;
1563
+ let bestBlockId = null;
1564
+ let bestOpener = Number.NEGATIVE_INFINITY;
1565
+ for (const pairing of pairings) if (caretPos >= pairing.openerFrom && caretPos <= pairing.closerTo && pairing.openerFrom > bestOpener) {
1566
+ bestOpener = pairing.openerFrom;
1567
+ bestBlockId = pairing.blockId;
1568
+ }
1569
+ return bestBlockId;
1570
+ };
1571
+ /**
1572
+ * Assemble the per-page rail segments for every balanced block: look up
1573
+ * opener/closer rects by `from`, place the rail by the pairing's own budgeted
1574
+ * depth, and clip its span to each page. Depth comes from the pairing (same walk
1575
+ * that matched opener→closer), so a rail's indentation and its span always agree.
1576
+ * O(pairings); all geometry comes from the props.
1577
+ */
1578
+ const computeRailBands = (pairings, groups, gutter) => {
1579
+ const groupByFrom = new Map(groups.map((group) => [group.range.from, group]));
1580
+ const bands = [];
1581
+ for (const pairing of pairings) {
1582
+ const openerRect = groupByFrom.get(pairing.openerFrom)?.rects[0];
1583
+ const closerRect = groupByFrom.get(pairing.closerFrom)?.rects[0];
1584
+ if (!openerRect || !closerRect) continue;
1585
+ const segments = segmentBandByPages({
1478
1586
  top: openerRect.y,
1479
- bottom: closerRect.y + closerRect.height,
1480
- railX: railBase - RAIL_BASE_GUTTER + open.depth * RAIL_DEPTH_GAP
1587
+ bottom: closerRect.y + closerRect.height
1588
+ }, gutter.pageBands);
1589
+ if (segments.length === 0) continue;
1590
+ bands.push({
1591
+ blockId: pairing.blockId,
1592
+ kind: pairing.kind,
1593
+ railX: railXForDepth(pairing.depth, gutter.contentLeft, gutter.marginWidth),
1594
+ segments
1481
1595
  });
1482
1596
  }
1483
1597
  return bands;
1484
1598
  };
1485
- const TemplateDirectivesOverlay = ({ groups, contentLeft }) => {
1599
+ const blockIdFromTarget = (target) => {
1600
+ if (!(target instanceof HTMLElement)) return null;
1601
+ const raw = target.dataset["folioBlockId"];
1602
+ return raw === void 0 ? null : Number(raw);
1603
+ };
1604
+ const TemplateDirectivesOverlay = ({ groups, gutter, caretPos, ranges }) => {
1605
+ const [hoveredBlockId, setHoveredBlockId] = useState(null);
1486
1606
  if (groups.length === 0) return null;
1607
+ const pairings = pairBlockRanges(ranges);
1608
+ const railBands = gutter ? computeRailBands(pairings, groups, gutter) : [];
1609
+ const blockIdByFrom = /* @__PURE__ */ new Map();
1610
+ const closerLabelByFrom = /* @__PURE__ */ new Map();
1611
+ for (const pairing of pairings) {
1612
+ blockIdByFrom.set(pairing.openerFrom, pairing.blockId);
1613
+ blockIdByFrom.set(pairing.closerFrom, pairing.blockId);
1614
+ closerLabelByFrom.set(pairing.closerFrom, closerHintLabel(pairing.kind, pairing.openerExpr));
1615
+ }
1616
+ const caretBlockId = innermostBlockAt(pairings, caretPos);
1617
+ const isActive = (blockId) => blockId === hoveredBlockId || blockId === caretBlockId;
1618
+ const handlePointerOver = (event) => {
1619
+ const blockId = blockIdFromTarget(event.target);
1620
+ if (blockId !== null) setHoveredBlockId(blockId);
1621
+ };
1622
+ const handlePointerOut = (event) => {
1623
+ const from = blockIdFromTarget(event.target);
1624
+ if (from === null) return;
1625
+ const to = blockIdFromTarget(event.relatedTarget);
1626
+ if (to === from) return;
1627
+ setHoveredBlockId((current) => current === from ? to : current);
1628
+ };
1487
1629
  return /* @__PURE__ */ jsxs("div", {
1488
1630
  style: overlayStyles,
1489
1631
  "data-folio-template-directives-overlay": "",
1490
- children: [computeBands(groups, contentLeft).map((band, bandIdx) => /* @__PURE__ */ jsx("div", {
1491
- className: "folio-template-band-rail",
1632
+ onPointerOver: handlePointerOver,
1633
+ onPointerOut: handlePointerOut,
1634
+ children: [railBands.flatMap((band) => band.segments.map((segment, segmentIdx) => /* @__PURE__ */ jsx("div", {
1635
+ className: `folio-template-band-rail folio-template-band-rail--${band.kind}${isActive(band.blockId) ? " folio-template-band-rail--active" : ""}`,
1636
+ "data-folio-block-id": band.blockId,
1492
1637
  style: {
1493
1638
  left: band.railX,
1494
- top: band.top,
1639
+ top: segment.top,
1495
1640
  width: RAIL_WIDTH,
1496
- height: Math.max(0, band.bottom - band.top)
1497
- }
1498
- }, `rail:${bandIdx}`)), groups.flatMap(({ range, rects }, groupIdx) => rects.map((rect, idx) => /* @__PURE__ */ jsx("span", {
1499
- className: `folio-template-directive folio-template-directive--${range.kind}`,
1500
- style: {
1501
- left: rect.x - 1,
1502
- top: rect.y + 1,
1503
- width: rect.width + 2,
1504
- height: Math.max(0, rect.height - 2)
1505
- }
1506
- }, `d:${groupIdx}:${idx}`)))]
1641
+ height: Math.max(0, segment.bottom - segment.top)
1642
+ }
1643
+ }, `rail:${band.blockId}:${segmentIdx}`))), groups.flatMap(({ range, rects }, groupIdx) => {
1644
+ const blockId = range.block ? blockIdByFrom.get(range.from) : void 0;
1645
+ const isBlockChip = range.block && (BLOCK_OPENERS.has(range.kind) || BLOCK_CLOSERS.has(range.kind));
1646
+ const closerLabel = range.block ? closerLabelByFrom.get(range.from) : void 0;
1647
+ const active = blockId !== void 0 && isActive(blockId);
1648
+ return rects.map((rect, idx) => /* @__PURE__ */ jsx("span", {
1649
+ className: `folio-template-directive folio-template-directive--${range.kind}${isBlockChip ? " folio-template-directive--block" : ""}${active ? " folio-template-directive--active" : ""}`,
1650
+ ...blockId !== void 0 ? { "data-folio-block-id": blockId } : {},
1651
+ ...closerLabel !== void 0 ? { title: closerLabel } : {},
1652
+ style: {
1653
+ left: rect.x - 1,
1654
+ top: rect.y + 1,
1655
+ width: rect.width + 2,
1656
+ height: Math.max(0, rect.height - 2)
1657
+ }
1658
+ }, `d:${groupIdx}:${idx}`));
1659
+ })]
1507
1660
  });
1508
1661
  };
1509
1662
  //#endregion
@@ -2561,7 +2714,8 @@ function PagedEditor(props) {
2561
2714
  const [autocompleteText, setAutocompleteText] = useState("");
2562
2715
  const [autocompleteIsStreaming, setAutocompleteIsStreaming] = useState(false);
2563
2716
  const [directiveRectGroups, setDirectiveRectGroups] = useState([]);
2564
- const [directiveContentLeft, setDirectiveContentLeft] = useState(null);
2717
+ const [directiveGutter, setDirectiveGutter] = useState(null);
2718
+ const [directiveCaretPos, setDirectiveCaretPos] = useState(null);
2565
2719
  const directivesRef = useRef([]);
2566
2720
  const directivesOverlayRequestSeqRef = useRef(0);
2567
2721
  const templatePreviewRef = useRef({
@@ -2948,6 +3102,7 @@ function PagedEditor(props) {
2948
3102
  const requestSeq = selectionOverlayRequestSeqRef.current + 1;
2949
3103
  selectionOverlayRequestSeqRef.current = requestSeq;
2950
3104
  const isCurrentRequest = () => selectionOverlayRequestSeqRef.current === requestSeq;
3105
+ if (directivesRef.current.length > 0) setDirectiveCaretPos(state.selection.head);
2951
3106
  onSelectionChangeRef.current?.(from, to);
2952
3107
  folioEmitterRef.current.emit("selectionChange", {
2953
3108
  from,
@@ -3252,10 +3407,10 @@ function PagedEditor(props) {
3252
3407
  const pagesContainer = pagesContainerRef.current;
3253
3408
  if (ranges.length === 0 || !pagesContainer) {
3254
3409
  setDirectiveRectGroups([]);
3255
- setDirectiveContentLeft(null);
3410
+ setDirectiveGutter(null);
3256
3411
  return;
3257
3412
  }
3258
- setDirectiveContentLeft(measureContentLeft(pagesContainer, zoom));
3413
+ setDirectiveGutter(measureDirectiveGutter(pagesContainer, zoom));
3259
3414
  (async () => {
3260
3415
  const projected = await projectRangesToRects(ranges, {
3261
3416
  pagesContainer,
@@ -3569,6 +3724,24 @@ function PagedEditor(props) {
3569
3724
  inline: "nearest"
3570
3725
  });
3571
3726
  }, [layout, scrollToPositionImpl]);
3727
+ const scrollToParaIdImpl = useCallback((paraId, options) => {
3728
+ const state = hiddenPMRef.current?.getState();
3729
+ if (!state) return false;
3730
+ const startPos = findStartPosForParaId(state.doc, paraId);
3731
+ if (startPos == null || startPos < 0) return false;
3732
+ scrollToPositionImpl(startPos);
3733
+ requestAnimationFrame(() => {
3734
+ if (options?.highlight) {
3735
+ const pages = pagesContainerRef.current;
3736
+ if (pages) flashParagraphFragmentsByParaId(pages, paraId, options.highlight);
3737
+ }
3738
+ const targetNode = state.doc.nodeAt(startPos);
3739
+ const inner = targetNode?.isTextblock === true ? Math.min(startPos + 1 + targetNode.content.size, state.doc.content.size) : Math.min(startPos + 1, state.doc.content.size);
3740
+ hiddenPMRef.current?.setSelection(inner);
3741
+ hiddenPMRef.current?.focus();
3742
+ });
3743
+ return true;
3744
+ }, [scrollToPositionImpl]);
3572
3745
  const focusHiddenEditor = useCallback(() => {
3573
3746
  if (readOnly) {
3574
3747
  containerRef.current?.focus({ preventScroll: true });
@@ -4989,6 +5162,7 @@ function PagedEditor(props) {
4989
5162
  },
4990
5163
  scrollToPosition: scrollToPositionImpl,
4991
5164
  scrollToPage: scrollToPageImpl,
5165
+ scrollToParaId: scrollToParaIdImpl,
4992
5166
  getPageNumberForPmPos(pmPos) {
4993
5167
  const container = pagesContainerRef.current;
4994
5168
  if (!container) return null;
@@ -5028,6 +5202,7 @@ function PagedEditor(props) {
5028
5202
  ensureHiddenEditorView,
5029
5203
  folioEditor,
5030
5204
  scrollToPageImpl,
5205
+ scrollToParaIdImpl,
5031
5206
  scrollToPositionImpl
5032
5207
  ]);
5033
5208
  useEffect(() => {
@@ -5144,8 +5319,10 @@ function PagedEditor(props) {
5144
5319
  }),
5145
5320
  /* @__PURE__ */ jsx(AISuggestionRectsOverlay, { groups: aiSuggestionRectGroups }),
5146
5321
  showTemplateDirectives && /* @__PURE__ */ jsx(TemplateDirectivesOverlay, {
5147
- contentLeft: directiveContentLeft,
5148
- groups: directiveRectGroups
5322
+ gutter: directiveGutter,
5323
+ groups: directiveRectGroups,
5324
+ caretPos: directiveCaretPos,
5325
+ ranges: directivesRef.current
5149
5326
  }),
5150
5327
  /* @__PURE__ */ jsx(SelectionOverlay, {
5151
5328
  selectionRects: hfEditMode ? EMPTY_SELECTION_RECTS : selectionRects,
@@ -6238,30 +6415,34 @@ function ContentControlWidgetsOverlay({ getEditorView }) {
6238
6415
  if (liveView && value.length > 0) dispatchDatePick(liveView, open.pmPos, value);
6239
6416
  close();
6240
6417
  };
6241
- return createPortal(/* @__PURE__ */ jsxs("div", {
6242
- role: "dialog",
6243
- "aria-label": open.kind === "dropdown" ? t("contentControlDropdownAriaLabel") : t("contentControlDateAriaLabel"),
6244
- style,
6245
- className: "bg-popover text-popover-foreground min-w-[10rem] rounded-md border p-1 shadow-md",
6246
- children: [open.kind === "dropdown" && /* @__PURE__ */ jsx("div", {
6247
- role: "menu",
6248
- className: "flex flex-col",
6249
- children: open.items.length === 0 ? /* @__PURE__ */ jsx("div", {
6250
- className: "text-muted-foreground px-2 py-1 text-sm",
6251
- children: t("contentControlDropdownNoOptions")
6252
- }) : open.items.map((item) => /* @__PURE__ */ jsx(MenuItem, {
6253
- onClick: () => onDropdownPick(item.value),
6254
- children: item.displayText
6255
- }, `${item.value}::${item.displayText}`))
6256
- }), open.kind === "date" && /* @__PURE__ */ jsx(DatePickerPopover, {
6257
- clearLabel: t("clearDate"),
6258
- defaultOpen: true,
6259
- onChange: (value) => {
6260
- if (value) onDatePick(value);
6261
- },
6262
- showIcon: false,
6263
- value: null
6264
- })]
6418
+ return createPortal(/* @__PURE__ */ jsx("div", {
6419
+ className: "folio-root",
6420
+ style: { display: "contents" },
6421
+ children: /* @__PURE__ */ jsxs("div", {
6422
+ role: "dialog",
6423
+ "aria-label": open.kind === "dropdown" ? t("contentControlDropdownAriaLabel") : t("contentControlDateAriaLabel"),
6424
+ style,
6425
+ className: "bg-popover text-popover-foreground min-w-[10rem] rounded-md border p-1 shadow-md",
6426
+ children: [open.kind === "dropdown" && /* @__PURE__ */ jsx("div", {
6427
+ role: "menu",
6428
+ className: "flex flex-col",
6429
+ children: open.items.length === 0 ? /* @__PURE__ */ jsx("div", {
6430
+ className: "text-muted-foreground px-2 py-1 text-sm",
6431
+ children: t("contentControlDropdownNoOptions")
6432
+ }) : open.items.map((item) => /* @__PURE__ */ jsx(MenuItem, {
6433
+ onClick: () => onDropdownPick(item.value),
6434
+ children: item.displayText
6435
+ }, `${item.value}::${item.displayText}`))
6436
+ }), open.kind === "date" && /* @__PURE__ */ jsx(DatePickerPopover, {
6437
+ clearLabel: t("clearDate"),
6438
+ defaultOpen: true,
6439
+ onChange: (value) => {
6440
+ if (value) onDatePick(value);
6441
+ },
6442
+ showIcon: false,
6443
+ value: null
6444
+ })]
6445
+ })
6265
6446
  }), document.body);
6266
6447
  }
6267
6448
  //#endregion
@@ -6345,12 +6526,12 @@ const DocumentOutline = ({ headings, scrollContainerRef, topOffset, docSize, onH
6345
6526
  };
6346
6527
  //#endregion
6347
6528
  //#region src/components/DocxEditorDialogs.tsx
6348
- const FindReplaceDialog = lazy(() => import("./FindReplaceDialog-CG1VKA_g.js").then((m) => ({ default: m.FindReplaceDialog })));
6349
- const TablePropertiesDialog = lazy(() => import("./TablePropertiesDialog-mQMnGfHu.js").then((m) => ({ default: m.TablePropertiesDialog })));
6350
- const ImagePositionDialog = lazy(() => import("./ImagePositionDialog-BQnOnDf4.js").then((m) => ({ default: m.ImagePositionDialog })));
6351
- const ImagePropertiesDialog = lazy(() => import("./ImagePropertiesDialog-Dn4N7qZv.js").then((m) => ({ default: m.ImagePropertiesDialog })));
6352
- const FootnotePropertiesDialog = lazy(() => import("./FootnotePropertiesDialog-MuW4D3gt.js").then((m) => ({ default: m.FootnotePropertiesDialog })));
6353
- const PageSetupDialog = lazy(() => import("./PageSetupDialog-CxJUEkoC.js").then((m) => ({ default: m.PageSetupDialog })));
6529
+ const FindReplaceDialog = lazy(() => import("./FindReplaceDialog-EFjmqEbg.js").then((m) => ({ default: m.FindReplaceDialog })));
6530
+ const TablePropertiesDialog = lazy(() => import("./TablePropertiesDialog-ZmzjSqME.js").then((m) => ({ default: m.TablePropertiesDialog })));
6531
+ const ImagePositionDialog = lazy(() => import("./ImagePositionDialog-FGLCOf0Q.js").then((m) => ({ default: m.ImagePositionDialog })));
6532
+ const ImagePropertiesDialog = lazy(() => import("./ImagePropertiesDialog-Baokui4M.js").then((m) => ({ default: m.ImagePropertiesDialog })));
6533
+ const FootnotePropertiesDialog = lazy(() => import("./FootnotePropertiesDialog-VfF7SQo2.js").then((m) => ({ default: m.FootnotePropertiesDialog })));
6534
+ const PageSetupDialog = lazy(() => import("./PageSetupDialog-Df2HjHiu.js").then((m) => ({ default: m.PageSetupDialog })));
6354
6535
  function DocxEditorDialogs({ findReplace, tableProperties, imagePosition, imageProperties, pageSetup, footnoteProperties }) {
6355
6536
  return /* @__PURE__ */ jsxs(Suspense, {
6356
6537
  fallback: null,
@@ -6672,7 +6853,7 @@ function ToolbarButton({ active = false, disabled = false, title, onClick, onDou
6672
6853
  };
6673
6854
  const button = /* @__PURE__ */ jsx("button", {
6674
6855
  type: "button",
6675
- className: cn("flex h-8 w-8 shrink-0 items-center justify-center rounded-md", "transition-colors duration-100", "-webkit-font-smoothing-antialiased", "text-[var(--doc-text-muted)] hover:bg-[var(--doc-primary-light)] hover:text-[var(--doc-text)]", active && "bg-[var(--doc-primary-light)] text-[var(--doc-text)] hover:bg-[var(--doc-primary-light)]", disabled && "cursor-not-allowed text-[var(--doc-text-subtle)] opacity-[0.16] disabled:hover:bg-transparent disabled:hover:text-[var(--doc-text-subtle)]", className),
6856
+ className: cn("flex h-8 w-8 shrink-0 items-center justify-center rounded-md", "transition-colors duration-100", "antialiased", "text-[var(--doc-text-muted)] hover:bg-[var(--doc-primary-light)] hover:text-[var(--doc-text)]", active && "bg-[var(--doc-primary-light)] text-[var(--doc-text)] hover:bg-[var(--doc-primary-light)]", disabled && "cursor-not-allowed text-[var(--doc-text-subtle)] opacity-[0.16] disabled:hover:bg-transparent disabled:hover:text-[var(--doc-text-subtle)]", className),
6676
6857
  onMouseDown: handleMouseDown,
6677
6858
  onClick: disabled ? void 0 : onClick,
6678
6859
  onDoubleClick: disabled ? void 0 : onDoubleClick,
@@ -9691,21 +9872,32 @@ const getBuiltinTableStyle = (styleId) => BUILTIN_STYLES.find((s) => s.id === st
9691
9872
  * (`replaceComments`/`updateComments`) stay in the component because they
9692
9873
  * depend on its document-build pipeline.
9693
9874
  */
9694
- function useFolioComments({ doc, autoOpenReviewSidebar, anchorPositions, editorContentRef }) {
9875
+ function useFolioComments({ doc, autoOpenReviewSidebar, anchorPositions, editorContentRef, commentsProp, onCommentsChange }) {
9695
9876
  const [showCommentsSidebar, setShowCommentsSidebar] = useState(false);
9696
9877
  const [visibleCommentAuthors, setVisibleCommentAuthors] = useState(null);
9697
9878
  const [activeCommentId, setActiveCommentId] = useState(null);
9698
- const [comments, setComments] = useState([]);
9879
+ const [internalComments, setInternalComments] = useState([]);
9880
+ const isControlledComments = commentsProp !== void 0;
9881
+ const comments = isControlledComments ? commentsProp : internalComments;
9699
9882
  const commentsDirtyRef = useRef(false);
9700
- const commentsRef = useRef([]);
9883
+ const commentsRef = useRef(comments);
9701
9884
  commentsRef.current = comments;
9885
+ const onCommentsChangeRef = useRef(onCommentsChange);
9886
+ onCommentsChangeRef.current = onCommentsChange;
9887
+ const setComments = useCallback((next) => {
9888
+ const resolved = typeof next === "function" ? next(commentsRef.current) : next;
9889
+ if (resolved === commentsRef.current) return;
9890
+ commentsRef.current = resolved;
9891
+ if (!isControlledComments) setInternalComments(resolved);
9892
+ onCommentsChangeRef.current?.(resolved);
9893
+ }, [isControlledComments]);
9702
9894
  const [isAddingComment, setIsAddingComment] = useState(false);
9703
9895
  const [commentSelectionRange, setCommentSelectionRange] = useState(null);
9704
9896
  const [addCommentYPosition, setAddCommentYPosition] = useState(null);
9705
9897
  const [floatingCommentBtn, setFloatingCommentBtn] = useState(null);
9706
9898
  const commentsLoadedRef = useRef(false);
9707
9899
  useEffect(() => {
9708
- if (commentsLoadedRef.current) return;
9900
+ if (isControlledComments || commentsLoadedRef.current) return;
9709
9901
  if (!doc) return;
9710
9902
  const bodyComments = doc.package.document.comments;
9711
9903
  if (bodyComments && bodyComments.length > 0) {
@@ -9715,7 +9907,12 @@ function useFolioComments({ doc, autoOpenReviewSidebar, anchorPositions, editorC
9715
9907
  if (autoOpenReviewSidebar) setShowCommentsSidebar(true);
9716
9908
  commentsLoadedRef.current = true;
9717
9909
  }
9718
- }, [autoOpenReviewSidebar, doc]);
9910
+ }, [
9911
+ autoOpenReviewSidebar,
9912
+ doc,
9913
+ isControlledComments,
9914
+ setComments
9915
+ ]);
9719
9916
  const commentAuthors = useMemo(() => {
9720
9917
  const seen = /* @__PURE__ */ new Set();
9721
9918
  const authors = [];
@@ -9801,6 +9998,7 @@ function useFolioComments({ doc, autoOpenReviewSidebar, anchorPositions, editorC
9801
9998
  return {
9802
9999
  comments,
9803
10000
  setComments,
10001
+ isControlledComments,
9804
10002
  commentsRef,
9805
10003
  commentsDirtyRef,
9806
10004
  commentsLoadedRef,
@@ -9836,7 +10034,7 @@ function useFolioComments({ doc, autoOpenReviewSidebar, anchorPositions, editorC
9836
10034
  * - Loading states
9837
10035
  */
9838
10036
  const CommentsSidebar = lazy(() => import("./CommentsSidebar-BsYOInaX.js").then((m) => ({ default: m.CommentsSidebar })));
9839
- const TextContextMenu = lazy(() => import("./TextContextMenu-DzTbp2sn.js").then((m) => ({ default: m.TextContextMenu })));
10037
+ const TextContextMenu = lazy(() => import("./TextContextMenu-CG0V37wi.js").then((m) => ({ default: m.TextContextMenu })));
9840
10038
  const loadAttemptSelectiveSave = async () => {
9841
10039
  const { attemptSelectiveSave } = await import("@stll/folio-core/docx/selectiveSave");
9842
10040
  return attemptSelectiveSave;
@@ -9915,7 +10113,7 @@ function areActiveTrackedChangesEqual(a, b) {
9915
10113
  /**
9916
10114
  * DocxEditor - Complete DOCX editor component
9917
10115
  */
9918
- function DocxEditor({ ref, documentBuffer, document: initialDocument, documentKey, onSave, author = "User", onChange, onSelectionChange, onSelectionTextChange, onError, onFontsLoaded: onFontsLoadedCallback, fontFamilies, fonts, theme, showToolbar = true, showZoomControl = true, showReviewControls = true, showHeaderFooterEditing = true, showMarginGuides: _showMarginGuides = false, marginGuideColor: _marginGuideColor, showRuler: showRulerProp = false, rulerUnit = "inch", initialZoom = 1, enableWheelZoom = true, readOnly: readOnlyProp = false, autoOpenReviewSidebar = true, components, toolbarExtra, className = "", style, placeholder, loadingIndicator, preserveDocumentWhileLoading = false, initialScrollTop, onScrollTopChange, showOutline: showOutlineProp = true, onPrint, onInsertImage, onInsertTable, showTableInsert = true, onInsertPageBreak, onInsertTOC, onCopy: _onCopy, onCut: _onCut, onPaste: _onPaste, mode: modeProp, onModeChange, onReadonlyEditAttempt, onCompatibilityChange, onEditorViewReady, onAnonymizationMatchesChange, onAnonymizationTermClick, selectedAnonymizationCanonical = null, anonymizationSelectionSeq, showTemplateDirectives = false, onSlashMenuChange, onSlashMenuKeyAction, customContextMenuItems, onCustomContextAction, collaboration, featureFlags, onSelectiveSaveTripwire }) {
10116
+ function DocxEditor({ ref, documentBuffer, document: initialDocument, documentKey, onSave, author = "User", onChange, onSelectionChange, onSelectionTextChange, onError, onFontsLoaded: onFontsLoadedCallback, fontFamilies, fonts, theme, showToolbar = true, showZoomControl = true, showReviewControls = true, showHeaderFooterEditing = true, showMarginGuides: _showMarginGuides = false, marginGuideColor: _marginGuideColor, showRuler: showRulerProp = false, rulerUnit = "inch", initialZoom = 1, enableWheelZoom = true, readOnly: readOnlyProp = false, autoOpenReviewSidebar = true, components, toolbarExtra, className = "", style, placeholder, loadingIndicator, preserveDocumentWhileLoading = false, initialScrollTop, onScrollTopChange, showOutline: showOutlineProp = true, onPrint, onInsertImage, onInsertTable, showTableInsert = true, onInsertPageBreak, onInsertTOC, onCopy: _onCopy, onCut: _onCut, onPaste: _onPaste, mode: modeProp, onModeChange, onReadonlyEditAttempt, onCompatibilityChange, onEditorViewReady, onAnonymizationMatchesChange, onAnonymizationTermClick, selectedAnonymizationCanonical = null, anonymizationSelectionSeq, showTemplateDirectives = false, onSlashMenuChange, onSlashMenuKeyAction, customContextMenuItems, onCustomContextAction, comments: commentsProp, onCommentsChange, collaboration, featureFlags, onSelectiveSaveTripwire }) {
9919
10117
  const t = useTranslations("folio");
9920
10118
  useEffect(() => {
9921
10119
  const onClipboardReadError = () => {
@@ -10129,7 +10327,9 @@ function DocxEditor({ ref, documentBuffer, document: initialDocument, documentKe
10129
10327
  doc: history.state,
10130
10328
  autoOpenReviewSidebar,
10131
10329
  anchorPositions,
10132
- editorContentRef
10330
+ editorContentRef,
10331
+ commentsProp,
10332
+ onCommentsChange
10133
10333
  });
10134
10334
  const styleResolverCacheRef = useRef(null);
10135
10335
  const getCachedStyleResolver = useCallback((styles) => {
@@ -11390,6 +11590,7 @@ function DocxEditor({ ref, documentBuffer, document: initialDocument, documentKe
11390
11590
  scrollToPage: (pageNumber) => {
11391
11591
  pagedEditorRef.current?.scrollToPage(pageNumber);
11392
11592
  },
11593
+ scrollToParaId: (paraId, options) => pagedEditorRef.current?.scrollToParaId(paraId, options) ?? false,
11393
11594
  openPrintPreview: handleDirectPrint,
11394
11595
  print: handleDirectPrint,
11395
11596
  loadDocument: loadParsedDocument,
@@ -12318,4 +12519,100 @@ function DocxEditor({ ref, documentBuffer, document: initialDocument, documentKe
12318
12519
  });
12319
12520
  }
12320
12521
  //#endregion
12321
- export { AutocompleteCaretOverlay, DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, DocxEditor, FolioUIProvider, FormattingBar, ZOOM_PRESETS, ZoomControl, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applySuggestions, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildPositionalText, clampZoom, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, deriveBlockId, diffWordSegments, finishAutocompleteSuggestion, formatZoom, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getFolioCaretViewportRect, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, insertImageFromFile, insertPageBreakInView, insertTableInView, insertTableOfContentsInView, isFolioBlockId, isSequentialFolioBlockId, isSuggestionStale, normalizeFolioAIBlockText, parseZoom, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setFocusedSuggestionMeta, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult, useWheelZoom };
12522
+ //#region src/renderAsync.tsx
12523
+ /**
12524
+ * Imperative API for mounting a DOCX editor into a DOM element.
12525
+ *
12526
+ * Returns a framework-agnostic {@link EditorHandle} extended with zoom and
12527
+ * scroll helpers. Wraps the editor in `IntlProvider` so `use-intl` works
12528
+ * without extra host setup.
12529
+ *
12530
+ * ```ts
12531
+ * import { renderAsync } from "@stll/folio-react";
12532
+ * import "@stll/folio-react/standalone.css";
12533
+ *
12534
+ * const editor = await renderAsync(buffer, document.getElementById("host")!, {
12535
+ * readOnly: false,
12536
+ * showToolbar: true,
12537
+ * });
12538
+ *
12539
+ * const blob = await editor.save();
12540
+ * editor.destroy();
12541
+ * ```
12542
+ */
12543
+ /**
12544
+ * Render a DOCX editor into a container element.
12545
+ *
12546
+ * Resolves once the document has parsed and the first `onChange` fires.
12547
+ * Rejects if `onError` runs before that milestone.
12548
+ */
12549
+ const renderAsync = (input, container, options = {}) => {
12550
+ const { locale = "en", ...editorOptions } = options;
12551
+ return new Promise((resolve, reject) => {
12552
+ const ref = React.createRef();
12553
+ let root = null;
12554
+ let settled = false;
12555
+ try {
12556
+ root = createRoot(container);
12557
+ } catch (error) {
12558
+ reject(error);
12559
+ return;
12560
+ }
12561
+ const handle = {
12562
+ save: async () => {
12563
+ const buffer = await (ref.current?.save() ?? Promise.resolve(null));
12564
+ if (!buffer) return null;
12565
+ return new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" });
12566
+ },
12567
+ getDocument: () => ref.current?.getDocument() ?? null,
12568
+ focus: () => {
12569
+ ref.current?.focus();
12570
+ },
12571
+ setZoom: (zoom) => {
12572
+ ref.current?.setZoom(zoom);
12573
+ },
12574
+ scrollToPosition: (pmPos) => {
12575
+ ref.current?.getEditorRef()?.scrollToPosition(pmPos);
12576
+ },
12577
+ scrollToPage: (pageNumber) => {
12578
+ ref.current?.scrollToPage(pageNumber);
12579
+ },
12580
+ destroy: () => {
12581
+ if (!settled) {
12582
+ settled = true;
12583
+ reject(/* @__PURE__ */ new Error("Editor was destroyed before mounting completed."));
12584
+ }
12585
+ root?.unmount();
12586
+ root = null;
12587
+ }
12588
+ };
12589
+ const element = /* @__PURE__ */ jsx(IntlProvider, {
12590
+ locale,
12591
+ messages: (0, messages_exports.getFolioMessages)(locale),
12592
+ children: /* @__PURE__ */ jsx(DocxEditor, {
12593
+ ...editorOptions,
12594
+ documentBuffer: input,
12595
+ onError: (error) => {
12596
+ editorOptions["onError"]?.(error);
12597
+ if (!settled) {
12598
+ settled = true;
12599
+ root?.unmount();
12600
+ root = null;
12601
+ reject(error);
12602
+ }
12603
+ },
12604
+ onChange: (doc) => {
12605
+ editorOptions["onChange"]?.(doc);
12606
+ if (!settled) {
12607
+ settled = true;
12608
+ resolve(handle);
12609
+ }
12610
+ },
12611
+ ref
12612
+ })
12613
+ });
12614
+ root.render(element);
12615
+ });
12616
+ };
12617
+ //#endregion
12618
+ export { AutocompleteCaretOverlay, DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, DocxEditor, FolioUIProvider, FormattingBar, ZOOM_PRESETS, ZoomControl, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applySuggestions, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildPositionalText, clampZoom, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, deriveBlockId, diffWordSegments, finishAutocompleteSuggestion, formatZoom, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getFolioCaretViewportRect, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, insertImageFromFile, insertPageBreakInView, insertTableInView, insertTableOfContentsInView, isFolioBlockId, isSequentialFolioBlockId, isSuggestionStale, normalizeFolioAIBlockText, parseZoom, renderAsync, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setFocusedSuggestionMeta, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult, useWheelZoom };