@stll/folio-react 0.1.0 → 0.2.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 { n as getFolioMessages } from "./messages-heGr9XLo.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";
@@ -66,17 +67,18 @@ import { collectRunFontsAtPmPositions, measureContentLeft, projectRangesToRects
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
- import { deriveBlockId, getFolioParaIdFromBlockId, getFolioParaIdFromBlockId as getFolioParaIdFromBlockId$1, isFolioBlockId, isSequentialFolioBlockId } from "@stll/folio-core/types/block-id";
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";
82
84
  import { ContextMenuManager } from "@stll/folio-core/managers/ContextMenuManager";
@@ -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";
@@ -623,7 +626,8 @@ const AutocompleteCaretOverlay = ({ caret, text, isStreaming }) => {
623
626
  position: "absolute",
624
627
  left: caret.x,
625
628
  top: caret.y,
626
- lineHeight: `${caret.lineHeight}px`
629
+ lineHeight: `${caret.lineHeight}px`,
630
+ ...caret.maxWidth !== void 0 ? { maxWidth: `${caret.maxWidth}px` } : {}
627
631
  },
628
632
  children: [text, /* @__PURE__ */ jsx("span", {
629
633
  className: isStreaming ? "folio-autocomplete-caret folio-autocomplete-caret--streaming" : "folio-autocomplete-caret",
@@ -1849,6 +1853,13 @@ const TRANSACTION_LAYOUT_DEBOUNCE_MS = 32;
1849
1853
  const TRANSACTION_LAYOUT_MAX_DELAY_MS = 96;
1850
1854
  /** Keep the visual caret hidden briefly while typed content relayouts. */
1851
1855
  const SELECTION_REVEAL_AFTER_INPUT_DELAY = 120;
1856
+ /**
1857
+ * Readability floor for the autocomplete ghost width: with the caret at the
1858
+ * right content boundary, an unfloored width plus `overflow-wrap: anywhere`
1859
+ * would wrap the suggestion into a one-character-wide vertical column. Near
1860
+ * the margin the ghost overflows slightly past it instead.
1861
+ */
1862
+ const AUTOCOMPLETE_GHOST_MIN_WIDTH = 100;
1852
1863
  const EMPTY_PLUGINS = [];
1853
1864
  const EMPTY_TEMPLATE_PREVIEW_ENTRIES = [];
1854
1865
  const EMPTY_AI_SUGGESTIONS = [];
@@ -3219,10 +3230,13 @@ function PagedEditor(props) {
3219
3230
  const pageOffsetX = (pageRect.left - overlayRect.left) / zoom;
3220
3231
  const pageOffsetY = (pageRect.top - overlayRect.top) / zoom;
3221
3232
  if (!isCurrentRequest()) return;
3233
+ const caretPage = layout.pages.at(caret.pageIndex);
3234
+ const maxWidth = caretPage ? Math.max(AUTOCOMPLETE_GHOST_MIN_WIDTH, caretPage.size.w - caretPage.margins.right - caret.x) : void 0;
3222
3235
  setAutocompleteCaret({
3223
3236
  x: caret.x + pageOffsetX,
3224
3237
  y: caret.y + pageOffsetY,
3225
- lineHeight: caret.height
3238
+ lineHeight: caret.height,
3239
+ maxWidth
3226
3240
  });
3227
3241
  setAutocompleteText(current.text);
3228
3242
  setAutocompleteIsStreaming(current.status === "streaming");
@@ -3558,6 +3572,24 @@ function PagedEditor(props) {
3558
3572
  inline: "nearest"
3559
3573
  });
3560
3574
  }, [layout, scrollToPositionImpl]);
3575
+ const scrollToParaIdImpl = useCallback((paraId, options) => {
3576
+ const state = hiddenPMRef.current?.getState();
3577
+ if (!state) return false;
3578
+ const startPos = findStartPosForParaId(state.doc, paraId);
3579
+ if (startPos == null || startPos < 0) return false;
3580
+ scrollToPositionImpl(startPos);
3581
+ requestAnimationFrame(() => {
3582
+ if (options?.highlight) {
3583
+ const pages = pagesContainerRef.current;
3584
+ if (pages) flashParagraphFragmentsByParaId(pages, paraId, options.highlight);
3585
+ }
3586
+ const targetNode = state.doc.nodeAt(startPos);
3587
+ const inner = targetNode?.isTextblock === true ? Math.min(startPos + 1 + targetNode.content.size, state.doc.content.size) : Math.min(startPos + 1, state.doc.content.size);
3588
+ hiddenPMRef.current?.setSelection(inner);
3589
+ hiddenPMRef.current?.focus();
3590
+ });
3591
+ return true;
3592
+ }, [scrollToPositionImpl]);
3561
3593
  const focusHiddenEditor = useCallback(() => {
3562
3594
  if (readOnly) {
3563
3595
  containerRef.current?.focus({ preventScroll: true });
@@ -4978,6 +5010,7 @@ function PagedEditor(props) {
4978
5010
  },
4979
5011
  scrollToPosition: scrollToPositionImpl,
4980
5012
  scrollToPage: scrollToPageImpl,
5013
+ scrollToParaId: scrollToParaIdImpl,
4981
5014
  getPageNumberForPmPos(pmPos) {
4982
5015
  const container = pagesContainerRef.current;
4983
5016
  if (!container) return null;
@@ -5017,6 +5050,7 @@ function PagedEditor(props) {
5017
5050
  ensureHiddenEditorView,
5018
5051
  folioEditor,
5019
5052
  scrollToPageImpl,
5053
+ scrollToParaIdImpl,
5020
5054
  scrollToPositionImpl
5021
5055
  ]);
5022
5056
  useEffect(() => {
@@ -5891,11 +5925,32 @@ const resolveFolioAIBlockRange = ({ blockId, doc, snapshot }) => {
5891
5925
  to: liveRange.to
5892
5926
  };
5893
5927
  }
5894
- const anchor = (snapshot ?? createFolioAIEditSnapshot$2(doc)).anchors[blockId];
5928
+ const resolvedSnapshot = snapshot ?? createFolioAIEditSnapshot$2(doc);
5929
+ const anchor = resolvedSnapshot.anchors[blockId] ?? resolveSequentialBlockAnchor(blockId, resolvedSnapshot);
5895
5930
  if (!anchor) return null;
5896
5931
  return clampRangeToDocSize(doc.content.size, anchor);
5897
5932
  };
5898
5933
  /**
5934
+ * Resolve a `seq-NNNN` fallback id by document position.
5935
+ *
5936
+ * A sequential id is only minted for a paragraph the source DOCX left
5937
+ * without a `w14:paraId`. The live editor's `ParaIdAllocator` fills
5938
+ * that gap with a fresh random hex paraId, so a snapshot of the live
5939
+ * document keys the block by that hex and never reproduces the
5940
+ * server's `seq-NNNN`: the direct anchor lookup misses, and a
5941
+ * paraId-based live lookup can't match either (no node carries a
5942
+ * `seq-` paraId). The seq number is the block's 1-based position in
5943
+ * the same non-empty-block walk the server extractor and
5944
+ * `createFolioAIEditSnapshot` share, so it indexes the snapshot's
5945
+ * ordered `blocks` directly.
5946
+ */
5947
+ const resolveSequentialBlockAnchor = (blockId, snapshot) => {
5948
+ const index = getSequentialFolioBlockIdIndex(blockId);
5949
+ if (index === null) return;
5950
+ const block = snapshot.blocks.at(index - 1);
5951
+ return block ? snapshot.anchors[block.id] : void 0;
5952
+ };
5953
+ /**
5899
5954
  * Clamp a `{from, to}` pair so both endpoints fit inside a document of
5900
5955
  * `docSize` (in PM content positions). Block-boundary snapshots and stale
5901
5956
  * range data sometimes produce a `to` one past the last inline position;
@@ -6206,30 +6261,34 @@ function ContentControlWidgetsOverlay({ getEditorView }) {
6206
6261
  if (liveView && value.length > 0) dispatchDatePick(liveView, open.pmPos, value);
6207
6262
  close();
6208
6263
  };
6209
- return createPortal(/* @__PURE__ */ jsxs("div", {
6210
- role: "dialog",
6211
- "aria-label": open.kind === "dropdown" ? t("contentControlDropdownAriaLabel") : t("contentControlDateAriaLabel"),
6212
- style,
6213
- className: "bg-popover text-popover-foreground min-w-[10rem] rounded-md border p-1 shadow-md",
6214
- children: [open.kind === "dropdown" && /* @__PURE__ */ jsx("div", {
6215
- role: "menu",
6216
- className: "flex flex-col",
6217
- children: open.items.length === 0 ? /* @__PURE__ */ jsx("div", {
6218
- className: "text-muted-foreground px-2 py-1 text-sm",
6219
- children: t("contentControlDropdownNoOptions")
6220
- }) : open.items.map((item) => /* @__PURE__ */ jsx(MenuItem, {
6221
- onClick: () => onDropdownPick(item.value),
6222
- children: item.displayText
6223
- }, `${item.value}::${item.displayText}`))
6224
- }), open.kind === "date" && /* @__PURE__ */ jsx(DatePickerPopover, {
6225
- clearLabel: t("clearDate"),
6226
- defaultOpen: true,
6227
- onChange: (value) => {
6228
- if (value) onDatePick(value);
6229
- },
6230
- showIcon: false,
6231
- value: null
6232
- })]
6264
+ return createPortal(/* @__PURE__ */ jsx("div", {
6265
+ className: "folio-root",
6266
+ style: { display: "contents" },
6267
+ children: /* @__PURE__ */ jsxs("div", {
6268
+ role: "dialog",
6269
+ "aria-label": open.kind === "dropdown" ? t("contentControlDropdownAriaLabel") : t("contentControlDateAriaLabel"),
6270
+ style,
6271
+ className: "bg-popover text-popover-foreground min-w-[10rem] rounded-md border p-1 shadow-md",
6272
+ children: [open.kind === "dropdown" && /* @__PURE__ */ jsx("div", {
6273
+ role: "menu",
6274
+ className: "flex flex-col",
6275
+ children: open.items.length === 0 ? /* @__PURE__ */ jsx("div", {
6276
+ className: "text-muted-foreground px-2 py-1 text-sm",
6277
+ children: t("contentControlDropdownNoOptions")
6278
+ }) : open.items.map((item) => /* @__PURE__ */ jsx(MenuItem, {
6279
+ onClick: () => onDropdownPick(item.value),
6280
+ children: item.displayText
6281
+ }, `${item.value}::${item.displayText}`))
6282
+ }), open.kind === "date" && /* @__PURE__ */ jsx(DatePickerPopover, {
6283
+ clearLabel: t("clearDate"),
6284
+ defaultOpen: true,
6285
+ onChange: (value) => {
6286
+ if (value) onDatePick(value);
6287
+ },
6288
+ showIcon: false,
6289
+ value: null
6290
+ })]
6291
+ })
6233
6292
  }), document.body);
6234
6293
  }
6235
6294
  //#endregion
@@ -6313,12 +6372,12 @@ const DocumentOutline = ({ headings, scrollContainerRef, topOffset, docSize, onH
6313
6372
  };
6314
6373
  //#endregion
6315
6374
  //#region src/components/DocxEditorDialogs.tsx
6316
- const FindReplaceDialog = lazy(() => import("./FindReplaceDialog-CG1VKA_g.js").then((m) => ({ default: m.FindReplaceDialog })));
6317
- const TablePropertiesDialog = lazy(() => import("./TablePropertiesDialog-mQMnGfHu.js").then((m) => ({ default: m.TablePropertiesDialog })));
6318
- const ImagePositionDialog = lazy(() => import("./ImagePositionDialog-BQnOnDf4.js").then((m) => ({ default: m.ImagePositionDialog })));
6319
- const ImagePropertiesDialog = lazy(() => import("./ImagePropertiesDialog-Dn4N7qZv.js").then((m) => ({ default: m.ImagePropertiesDialog })));
6320
- const FootnotePropertiesDialog = lazy(() => import("./FootnotePropertiesDialog-MuW4D3gt.js").then((m) => ({ default: m.FootnotePropertiesDialog })));
6321
- const PageSetupDialog = lazy(() => import("./PageSetupDialog-CxJUEkoC.js").then((m) => ({ default: m.PageSetupDialog })));
6375
+ const FindReplaceDialog = lazy(() => import("./FindReplaceDialog-EFjmqEbg.js").then((m) => ({ default: m.FindReplaceDialog })));
6376
+ const TablePropertiesDialog = lazy(() => import("./TablePropertiesDialog-ZmzjSqME.js").then((m) => ({ default: m.TablePropertiesDialog })));
6377
+ const ImagePositionDialog = lazy(() => import("./ImagePositionDialog-FGLCOf0Q.js").then((m) => ({ default: m.ImagePositionDialog })));
6378
+ const ImagePropertiesDialog = lazy(() => import("./ImagePropertiesDialog-Baokui4M.js").then((m) => ({ default: m.ImagePropertiesDialog })));
6379
+ const FootnotePropertiesDialog = lazy(() => import("./FootnotePropertiesDialog-VfF7SQo2.js").then((m) => ({ default: m.FootnotePropertiesDialog })));
6380
+ const PageSetupDialog = lazy(() => import("./PageSetupDialog-Df2HjHiu.js").then((m) => ({ default: m.PageSetupDialog })));
6322
6381
  function DocxEditorDialogs({ findReplace, tableProperties, imagePosition, imageProperties, pageSetup, footnoteProperties }) {
6323
6382
  return /* @__PURE__ */ jsxs(Suspense, {
6324
6383
  fallback: null,
@@ -6640,7 +6699,7 @@ function ToolbarButton({ active = false, disabled = false, title, onClick, onDou
6640
6699
  };
6641
6700
  const button = /* @__PURE__ */ jsx("button", {
6642
6701
  type: "button",
6643
- 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),
6702
+ 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),
6644
6703
  onMouseDown: handleMouseDown,
6645
6704
  onClick: disabled ? void 0 : onClick,
6646
6705
  onDoubleClick: disabled ? void 0 : onDoubleClick,
@@ -9659,21 +9718,32 @@ const getBuiltinTableStyle = (styleId) => BUILTIN_STYLES.find((s) => s.id === st
9659
9718
  * (`replaceComments`/`updateComments`) stay in the component because they
9660
9719
  * depend on its document-build pipeline.
9661
9720
  */
9662
- function useFolioComments({ doc, autoOpenReviewSidebar, anchorPositions, editorContentRef }) {
9721
+ function useFolioComments({ doc, autoOpenReviewSidebar, anchorPositions, editorContentRef, commentsProp, onCommentsChange }) {
9663
9722
  const [showCommentsSidebar, setShowCommentsSidebar] = useState(false);
9664
9723
  const [visibleCommentAuthors, setVisibleCommentAuthors] = useState(null);
9665
9724
  const [activeCommentId, setActiveCommentId] = useState(null);
9666
- const [comments, setComments] = useState([]);
9725
+ const [internalComments, setInternalComments] = useState([]);
9726
+ const isControlledComments = commentsProp !== void 0;
9727
+ const comments = isControlledComments ? commentsProp : internalComments;
9667
9728
  const commentsDirtyRef = useRef(false);
9668
- const commentsRef = useRef([]);
9729
+ const commentsRef = useRef(comments);
9669
9730
  commentsRef.current = comments;
9731
+ const onCommentsChangeRef = useRef(onCommentsChange);
9732
+ onCommentsChangeRef.current = onCommentsChange;
9733
+ const setComments = useCallback((next) => {
9734
+ const resolved = typeof next === "function" ? next(commentsRef.current) : next;
9735
+ if (resolved === commentsRef.current) return;
9736
+ commentsRef.current = resolved;
9737
+ if (!isControlledComments) setInternalComments(resolved);
9738
+ onCommentsChangeRef.current?.(resolved);
9739
+ }, [isControlledComments]);
9670
9740
  const [isAddingComment, setIsAddingComment] = useState(false);
9671
9741
  const [commentSelectionRange, setCommentSelectionRange] = useState(null);
9672
9742
  const [addCommentYPosition, setAddCommentYPosition] = useState(null);
9673
9743
  const [floatingCommentBtn, setFloatingCommentBtn] = useState(null);
9674
9744
  const commentsLoadedRef = useRef(false);
9675
9745
  useEffect(() => {
9676
- if (commentsLoadedRef.current) return;
9746
+ if (isControlledComments || commentsLoadedRef.current) return;
9677
9747
  if (!doc) return;
9678
9748
  const bodyComments = doc.package.document.comments;
9679
9749
  if (bodyComments && bodyComments.length > 0) {
@@ -9683,7 +9753,12 @@ function useFolioComments({ doc, autoOpenReviewSidebar, anchorPositions, editorC
9683
9753
  if (autoOpenReviewSidebar) setShowCommentsSidebar(true);
9684
9754
  commentsLoadedRef.current = true;
9685
9755
  }
9686
- }, [autoOpenReviewSidebar, doc]);
9756
+ }, [
9757
+ autoOpenReviewSidebar,
9758
+ doc,
9759
+ isControlledComments,
9760
+ setComments
9761
+ ]);
9687
9762
  const commentAuthors = useMemo(() => {
9688
9763
  const seen = /* @__PURE__ */ new Set();
9689
9764
  const authors = [];
@@ -9769,6 +9844,7 @@ function useFolioComments({ doc, autoOpenReviewSidebar, anchorPositions, editorC
9769
9844
  return {
9770
9845
  comments,
9771
9846
  setComments,
9847
+ isControlledComments,
9772
9848
  commentsRef,
9773
9849
  commentsDirtyRef,
9774
9850
  commentsLoadedRef,
@@ -9804,7 +9880,7 @@ function useFolioComments({ doc, autoOpenReviewSidebar, anchorPositions, editorC
9804
9880
  * - Loading states
9805
9881
  */
9806
9882
  const CommentsSidebar = lazy(() => import("./CommentsSidebar-BsYOInaX.js").then((m) => ({ default: m.CommentsSidebar })));
9807
- const TextContextMenu = lazy(() => import("./TextContextMenu-DzTbp2sn.js").then((m) => ({ default: m.TextContextMenu })));
9883
+ const TextContextMenu = lazy(() => import("./TextContextMenu-CG0V37wi.js").then((m) => ({ default: m.TextContextMenu })));
9808
9884
  const loadAttemptSelectiveSave = async () => {
9809
9885
  const { attemptSelectiveSave } = await import("@stll/folio-core/docx/selectiveSave");
9810
9886
  return attemptSelectiveSave;
@@ -9883,7 +9959,7 @@ function areActiveTrackedChangesEqual(a, b) {
9883
9959
  /**
9884
9960
  * DocxEditor - Complete DOCX editor component
9885
9961
  */
9886
- 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 }) {
9962
+ 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 }) {
9887
9963
  const t = useTranslations("folio");
9888
9964
  useEffect(() => {
9889
9965
  const onClipboardReadError = () => {
@@ -10097,7 +10173,9 @@ function DocxEditor({ ref, documentBuffer, document: initialDocument, documentKe
10097
10173
  doc: history.state,
10098
10174
  autoOpenReviewSidebar,
10099
10175
  anchorPositions,
10100
- editorContentRef
10176
+ editorContentRef,
10177
+ commentsProp,
10178
+ onCommentsChange
10101
10179
  });
10102
10180
  const styleResolverCacheRef = useRef(null);
10103
10181
  const getCachedStyleResolver = useCallback((styles) => {
@@ -11358,6 +11436,7 @@ function DocxEditor({ ref, documentBuffer, document: initialDocument, documentKe
11358
11436
  scrollToPage: (pageNumber) => {
11359
11437
  pagedEditorRef.current?.scrollToPage(pageNumber);
11360
11438
  },
11439
+ scrollToParaId: (paraId, options) => pagedEditorRef.current?.scrollToParaId(paraId, options) ?? false,
11361
11440
  openPrintPreview: handleDirectPrint,
11362
11441
  print: handleDirectPrint,
11363
11442
  loadDocument: loadParsedDocument,
@@ -12286,4 +12365,100 @@ function DocxEditor({ ref, documentBuffer, document: initialDocument, documentKe
12286
12365
  });
12287
12366
  }
12288
12367
  //#endregion
12289
- 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 };
12368
+ //#region src/renderAsync.tsx
12369
+ /**
12370
+ * Imperative API for mounting a DOCX editor into a DOM element.
12371
+ *
12372
+ * Returns a framework-agnostic {@link EditorHandle} extended with zoom and
12373
+ * scroll helpers. Wraps the editor in `IntlProvider` so `use-intl` works
12374
+ * without extra host setup.
12375
+ *
12376
+ * ```ts
12377
+ * import { renderAsync } from "@stll/folio-react";
12378
+ * import "@stll/folio-react/standalone.css";
12379
+ *
12380
+ * const editor = await renderAsync(buffer, document.getElementById("host")!, {
12381
+ * readOnly: false,
12382
+ * showToolbar: true,
12383
+ * });
12384
+ *
12385
+ * const blob = await editor.save();
12386
+ * editor.destroy();
12387
+ * ```
12388
+ */
12389
+ /**
12390
+ * Render a DOCX editor into a container element.
12391
+ *
12392
+ * Resolves once the document has parsed and the first `onChange` fires.
12393
+ * Rejects if `onError` runs before that milestone.
12394
+ */
12395
+ const renderAsync = (input, container, options = {}) => {
12396
+ const { locale = "en", ...editorOptions } = options;
12397
+ return new Promise((resolve, reject) => {
12398
+ const ref = React.createRef();
12399
+ let root = null;
12400
+ let settled = false;
12401
+ try {
12402
+ root = createRoot(container);
12403
+ } catch (error) {
12404
+ reject(error);
12405
+ return;
12406
+ }
12407
+ const handle = {
12408
+ save: async () => {
12409
+ const buffer = await (ref.current?.save() ?? Promise.resolve(null));
12410
+ if (!buffer) return null;
12411
+ return new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" });
12412
+ },
12413
+ getDocument: () => ref.current?.getDocument() ?? null,
12414
+ focus: () => {
12415
+ ref.current?.focus();
12416
+ },
12417
+ setZoom: (zoom) => {
12418
+ ref.current?.setZoom(zoom);
12419
+ },
12420
+ scrollToPosition: (pmPos) => {
12421
+ ref.current?.getEditorRef()?.scrollToPosition(pmPos);
12422
+ },
12423
+ scrollToPage: (pageNumber) => {
12424
+ ref.current?.scrollToPage(pageNumber);
12425
+ },
12426
+ destroy: () => {
12427
+ if (!settled) {
12428
+ settled = true;
12429
+ reject(/* @__PURE__ */ new Error("Editor was destroyed before mounting completed."));
12430
+ }
12431
+ root?.unmount();
12432
+ root = null;
12433
+ }
12434
+ };
12435
+ const element = /* @__PURE__ */ jsx(IntlProvider, {
12436
+ locale,
12437
+ messages: getFolioMessages(locale),
12438
+ children: /* @__PURE__ */ jsx(DocxEditor, {
12439
+ ...editorOptions,
12440
+ documentBuffer: input,
12441
+ onError: (error) => {
12442
+ editorOptions["onError"]?.(error);
12443
+ if (!settled) {
12444
+ settled = true;
12445
+ root?.unmount();
12446
+ root = null;
12447
+ reject(error);
12448
+ }
12449
+ },
12450
+ onChange: (doc) => {
12451
+ editorOptions["onChange"]?.(doc);
12452
+ if (!settled) {
12453
+ settled = true;
12454
+ resolve(handle);
12455
+ }
12456
+ },
12457
+ ref
12458
+ })
12459
+ });
12460
+ root.render(element);
12461
+ });
12462
+ };
12463
+ //#endregion
12464
+ 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 };