/** * Public types for `superdoc/ui` (the browser UI controller). * * The controller exposes a single observation pipeline (the **selector * substrate** at `ui.select(...)`) that the domain namespaces * (`ui.toolbar`, `ui.commands`, `ui.comments`, `ui.trackChanges`, * `ui.viewport`, `ui.selection`) are implemented on top of. Consumers * building their own UI typically reach for the domain handles * (`ui.comments.subscribe(...)`, `ui.commands.bold.observe(...)`) * and only drop down to `ui.select` for slices the domain handles * don't expose. * * Lifecycle: `createSuperDocUI({ superdoc })` per editor mount; * `ui.destroy()` on unmount tears down every internal subscription. */ export type EqualityFn = (a: T, b: T) => boolean; export type SelectorFn = (state: TState) => TSlice; /** * A read-only signal. `get()` is synchronous; `subscribe()` invokes the * listener once with the current value, then again whenever the value * changes by the controller's equality function. */ export interface Subscribable { /** Snapshot the current value. */ get(): T; /** * Subscribe to value changes. The listener fires once synchronously * with the current value, then again whenever the value changes. * Returns an unsubscribe function. */ subscribe(listener: (value: T) => void): () => void; } /** * Event names the UI controller (`createSuperDocUI`) subscribes to on * a SuperDoc-like host. Narrower than * `HeadlessToolbarSuperdocHostEvent` (which adds * `formatting-marks-change`); a custom UI host stub only has to * support the three events the UI controller actually consumes. */ export type SuperDocUIHostEvent = 'editorCreate' | 'document-mode-change' | 'zoomChange'; /** * Structural typing for the SuperDoc instance. Keeps the UI controller * loose from the SuperDoc Vue package's specific class type. The * controller only needs an event bus and an `activeEditor` reference. */ export interface SuperDocLike { on?(event: SuperDocUIHostEvent, handler: (...args: unknown[]) => void): unknown; off?(event: SuperDocUIHostEvent, handler: (...args: unknown[]) => void): unknown; activeEditor?: SuperDocEditorLike | null; config?: { documentMode?: 'editing' | 'suggesting' | 'viewing'; }; /** * Optional setter for documentMode. Consumed by `ui.document.setMode` * (SD-2816) and reserved for future `ui.` surfaces (SD-2799) * that move other UI-only commands off the toolbar registry. */ setDocumentMode?(mode: 'editing' | 'suggesting' | 'viewing'): unknown; /** * Optional export bridge. `ui.document.export(options)` forwards * here so consumers wiring an Export DOCX button can call it from * the controller surface instead of pulling the host instance into * their context. The shape mirrors `SuperDoc.export()` from the * superdoc package; declared optional and `unknown`-typed so non * browser test stubs stay valid without a host implementation. */ export?(options?: DocumentExportInput): Promise; } export interface SuperDocEditorLike { on?(event: string, handler: (...args: unknown[]) => void): unknown; off?(event: string, handler: (...args: unknown[]) => void): unknown; emit?(event: string, payload: unknown): void; /** * Minimal editor options surface the controller reads today. * Keep this narrow so test doubles do not need to model the full * editor options bag. */ options?: { replacedFile?: boolean; }; /** * Replace the current document file. Consumed by `ui.document.replaceFile` * to give consumers a typed import path without reaching into the host * instance. Optional in the structural typing so SSR / non-browser * stubs stay valid. */ replaceFile?(file: File): Promise; /** * Converter handle. The controller reads `converter.comments` after * a `replaceFile` to manually re-emit `commentsLoaded` while * SD-2839 is open (Editor short-circuits the event when * `modules.comments: false`). */ converter?: { comments?: unknown[]; }; doc?: { selection?: { current?(input?: { includeText?: boolean; }): { empty: boolean; text?: string; target?: unknown; /** Active mark names at the caret / across the selection. */ activeMarks?: string[]; /** Present after SD-2792; absent on older builds — controller falls back to []. */ activeCommentIds?: string[]; activeChangeIds?: string[]; }; }; /** * Comments member on the Document API. The structural typing * keeps the controller loose from the real `CommentsApi` interface * to allow stub-driven unit tests without pulling in the full * adapter graph; runtime calls forward to the real `editor.doc`. */ comments?: { list?(query?: unknown): unknown; create?(input: unknown, options?: unknown): unknown; patch?(input: unknown, options?: unknown): unknown; delete?(input: unknown, options?: unknown): unknown; }; /** * Tracked-changes member on the Document API. Used by * `ui.trackChanges.*` for accept/reject and the live feed. */ trackChanges?: { list?(query?: unknown): unknown; decide?(input: unknown, options?: unknown): unknown; }; /** * Content-controls (SDT) member on the Document API. Used by * `ui.contentControls.*` for the snapshot list. List signature is * loose to mirror comments / trackChanges; the controller asserts * the concrete `ContentControlsListResult` shape after calling. */ contentControls?: { list?(query?: unknown): unknown; }; /** * Anchored-metadata member on the Document API. Used by * `ui.metadata.*` to look up an entry's resolved range from its * id, and to verify (via `get`) that the id actually maps to a * stored payload before delegating to the SDT-keyed geometry * path — a w:tag on its own can come from a Word-authored * content control with no metadata payload, so the payload side * has to agree. Structurally typed loose for the same * stub-friendly reason as `comments` / `trackChanges` / * `contentControls`; the controller asserts the concrete shapes * after calling. */ metadata?: { get?(input: { id: string; }): unknown | null; resolve?(input: { id: string; }): unknown | null; }; /** * Insert content at a positional target. Surfaces the typed * doc-API signature so custom commands can call * `editor.doc.insert(...)` without a structural cast. The control * surface needs `editor.doc.insert` for the Custom UI custom-command * pattern (Insert clause, AI-generated text); other doc-API * mutation methods stay loose unless a similar use case lands. */ insert?(input: import('../../../document-api/src/index.js').InsertInput, options?: unknown): import('../../../document-api/src/index.js').SDMutationReceipt; }; /** * PresentationEditor handle. Browser-only. The controller calls * `presentationEditor.getEntityRects(target)` from `ui.viewport.getRect` * to look up the painted-DOM rectangles for an entity (comment or * tracked change), and `presentationEditor.getSelectionRects()` / * `getRangeRects(from, to)` from `ui.selection.getRects` / * `ui.selection.getAnchorRect` to anchor floating UI to the painted * selection without consumers reaching for `window.getSelection()` * (which reads from the offscreen ProseMirror DOM and returns the * wrong coordinates). Optional in the structural typing to keep * SSR / non-browser stubs valid. */ presentationEditor?: { getEntityRects?(target: { entityType?: unknown; entityId?: unknown; story?: unknown; }): Array<{ pageIndex: number; left: number; right: number; top: number; bottom: number; width: number; height: number; }>; getSelectionRects?(relativeTo?: HTMLElement): Array<{ pageIndex: number; left: number; right: number; top: number; bottom: number; width: number; height: number; }>; getRangeRects?(from: number, to: number, relativeTo?: HTMLElement): Array<{ pageIndex: number; left: number; right: number; top: number; bottom: number; width: number; height: number; }>; /** * Painted-DOM host element. `ui.viewport.entityAt` reads it to * confirm the hit returned by `document.elementFromPoint` lives * inside this controller's editor — without that scope check, a * page mounting two SuperDoc instances would return entity ids * from the wrong instance. */ visibleHost?: HTMLElement; /** * Coordinate-to-position helper. Consumed by * `ui.viewport.positionAt` to resolve a viewport `(x, y)` to a * caret position in the editor's PM document. */ posAtCoords?(coords: { clientX: number; clientY: number; }): { pos: number; inside: number; } | null; /** * The story locator for the routed editor when the user is * inside a header/footer/footnote/endnote, or `null` when the body * editor is active. `ui.viewport.positionAt` threads this onto the * returned `SelectionPoint` / `SelectionTarget` so consumers passing * the target to `editor.doc.insert` / `replace` route to the right * story instead of falling back to body. */ getActiveStoryLocator?(): import('../../../document-api/src/index.js').StoryLocator | null; } | null; } /** * The unified UI state model. * * Read individual fields via {@link SuperDocUI.select} or pull whole * slices through the domain handles (`ui.selection.subscribe`, * `ui.comments.subscribe`, etc.). Each slice is memoized so a typing * only transaction (which leaves selection / comments / track-changes * unchanged) does not re-fire downstream subscribers. * * Implementation note: the selector substrate recomputes the full * state snapshot on every source event today, then dedups per * subscriber via the equality function. Lazy / incremental * computation is an optimization that does not change the public API. */ export interface SuperDocUIState { /** True when SuperDoc has an active editor mounted. */ ready: boolean; /** Mirror of `superdoc.config.documentMode`. */ documentMode: 'editing' | 'suggesting' | 'viewing' | null; /** * Document-level slice exposed on `state.document` (SD-2816). Sugar * over the top-level `ready` and `documentMode` fields so a single * subscription drives the document-bar / Export button / mode * toggle. Kept minimal: dirty-tracking is a follow-up because * SuperDoc has no host-side dirty primitive today. */ document: DocumentSlice; /** Selection projection. See {@link SelectionSlice}. */ selection: SelectionSlice; /** * Toolbar snapshot — `{ context, commands }`. Sourced from the * internal headless-toolbar instance. Domain consumers normally read * this through `ui.toolbar` (aggregate) or `ui.commands.` * (fine-grained per-command observables). */ toolbar: ToolbarSnapshotSlice; /** * Comments slice. Sourced from `editor.doc.comments.list()` and * cached at the controller level — the list is refreshed on * `commentsUpdate` / `commentsLoaded` events, not recomputed per * `computeState()` call. `activeIds` mirrors * `selection.current().activeCommentIds` so a comment-aware sidebar * can highlight the active card without a separate subscription. */ comments: CommentsSlice; /** * Tracked-changes slice. Items + activeId for the tracked-changes * sidebar pattern. Cached at controller level alongside the comments * slice; refreshes on tracked-change events. */ trackChanges: TrackChangesSlice; /** * Content-controls slice (SD-3157). Same cache + refresh posture as * `comments` and `trackChanges`: items source from * `editor.doc.contentControls.list()`, cached and refreshed on * document transactions; `activeIds` derives from the selection. */ contentControls: ContentControlsSlice; } /** * Toolbar snapshot exposed on `state.toolbar`. Mirrors the headless-toolbar * shape with one widening: every command state carries a `source` field * so consumers can distinguish built-ins from commands registered via * `ui.commands.register(...)` without branching on the id. */ export type ToolbarSnapshotSlice = { context: import('../headless-toolbar/types.js').ToolbarContext | null; /** * Per-command snapshot states, keyed by command id. Returns `undefined` * for ids that are not currently registered (custom commands before * `register` / after `unregister`, typos in built-in ids). Consumers * must guard with `snapshot.commands[id]?.disabled` rather than * indexing directly. */ commands: { [id: string]: UIToolbarCommandState | undefined; }; }; /** * Per-command snapshot entry. `active`/`disabled`/`value` match the * headless-toolbar contract; `source` is the UI-controller addition that * tells consumers whether the command came from the built-in registry or * a `ui.commands.register(...)` call. */ export type UIToolbarCommandState = { active: boolean; disabled: boolean; value?: unknown; source: 'built-in' | 'custom'; }; /** * Snapshot of the editor's current selection — the full * {@link import('../../../document-api/src/index.js').SelectionInfo} projection * mirrored on the controller so a single `ui.select(s => s.selection, * shallowEqual)` subscribe gives consumers everything they need to * drive a floating bubble menu, format toolbar, mention popover, or * "comment here" hint without dipping back into `editor.doc.selection.current()`. */ export interface SelectionSlice { /** True when the selection is empty (cursor only, no range). */ empty: boolean; /** * The selection anchored to text content as a portable * {@link import('../../../document-api/src/index.js').TextTarget}, or `null` when * the selection is not in text (empty document, node selection, no * focus). Multi-segment when the selection spans multiple blocks. * Pass directly to `editor.doc.comments.create({ target })` and to * range-mutation operations like `editor.doc.format.apply`. */ target: import('../../../document-api/src/index.js').TextTarget | null; /** * The same selection in {@link import('../../../document-api/src/index.js').SelectionTarget} * shape — explicit start/end {@link import('../../../document-api/src/index.js').SelectionPoint}s. * Pass directly to `editor.doc.insert({ target })` and to other * point/range operations that accept a SelectionTarget. * * ```ts * const { selectionTarget } = ui.selection.getSnapshot(); * if (selectionTarget) { * editor.doc.insert({ target: selectionTarget, value: 'Hello', type: 'text' }); * } * ``` * * Derived from `target`: `null` when `target` is null; otherwise the * first segment's `blockId` + `range.start` as the start point and * the last segment's `blockId` + `range.end` as the end point. The * derivation lives on the slice so consumers don't have to reach for * a private conversion helper every time they want to insert text at * the cursor. * * Story field caveat: when `target.story` is present, the derivation * preserves it on every {@link import('../../../document-api/src/index.js').SelectionPoint} * and the {@link import('../../../document-api/src/index.js').SelectionTarget} * root, so non-body selections route correctly. Today the selection * resolver does NOT yet stamp `target.story` for non-body surfaces * (header / footer / footnote / endnote); a doc-api follow-up * tracks this. Until it lands, consumers building Custom UI on top of * non-body content should detect the routed surface themselves and * stamp the right `StoryLocator` before passing the target into a * doc-api operation. */ selectionTarget: import('../../../document-api/src/index.js').SelectionTarget | null; /** * Active marks at the caret or across the selection. Names are * ProseMirror mark type names (`'bold'`, `'italic'`, `'link'`). * Drives toolbar active-state rendering. Intersection semantics: a * mark name is included only if every character in the range carries * it (or, when empty, the caret/stored marks). */ activeMarks: string[]; /** * Comment ids whose `commentMark` overlaps the selection (or sits * under the caret when empty). Union semantics: an id is included * when *any* character in the range carries the mark. Use to * highlight the active sidebar card or render a "comment here" hint. * Same array as `state.comments.activeIds` — duplicated for the * single-subscribe ergonomic. */ activeCommentIds: string[]; /** * Tracked-change ids whose mark (`trackInsert` / `trackDelete` / * `trackFormat`) overlaps the selection. Union semantics. Mirrors * `state.trackChanges.activeId` (which picks the first id) for * consumers that want the full set. */ activeChangeIds: string[]; /** * Quoted text of the selection. Always present on the slice; * empty string when the selection is collapsed. Equivalent to * `editor.doc.selection.current({ includeText: true }).text ?? ''`. */ quotedText: string; } /** * Snapshot of the comments collection exposed on `state.comments`. * * Items use the same shape `editor.doc.comments.list()` returns * (`DiscoveryItem`), so consumers that already consume * that contract see no shape mismatch. `activeIds` is a denormalized * convenience driven by `selection.current().activeCommentIds`. */ /** * Content-controls slice exposed on `state.contentControls`. * * Items source from `editor.doc.contentControls.list()` and are * cached at the controller level — the list refreshes on document * transactions, not on every selection update, so typing through * unrelated content doesn't churn the slice. * * `activeIds` and `activeId` track which content controls contain * the caret / selection anchor. Nested SDTs are real (a block SDT * can wrap an inline SDT), so we expose the chain rather than * picking one — consumers can switch on `activeIds[0]` for the * tightest match or walk the chain for context-aware UI. */ export interface ContentControlsSlice { /** Total count from the list result (before pagination, if any). */ total: number; /** Items from `editor.doc.contentControls.list()`. Empty array on error or no editor. */ items: import('../../../document-api/src/index.js').ContentControlsListResult['items']; /** * Content control ids whose painted wrapper contains the current * selection anchor, innermost first. A caret inside an inline SDT * nested in a block SDT surfaces both ids with the inline-SDT id * first. Empty array when the cursor is not inside any SDT. */ activeIds: string[]; /** * Convenience for the common case of "what is the tightest active * content control?". Always equal to `activeIds[0] ?? null`. * Derived, not a separate source of truth — use `activeIds` when * the chain matters. */ activeId: string | null; } /** * Content-controls domain handle exposed on `ui.contentControls`. * Read / observe / look-up only — there are no mutation methods in * v1. Consumers run all mutations through `editor.doc.contentControls.*` * directly, matching the architectural rule that this handle is a UI * surface, not a parallel mutation contract. * * The handle includes `scrollIntoView` via a dedicated model-aware * path. It does NOT widen `ui.viewport.scrollIntoView`: content controls * stay UI-local and out of the Document API address union, mirroring how * `getRect` resolves a content control through a UI-local address. */ export interface ContentControlsHandle { /** Snapshot the current content-controls slice synchronously. */ getSnapshot(): ContentControlsSlice; /** * Subscribe to slice changes. Listener fires once synchronously with * the current snapshot, then again whenever `items`, `activeIds`, * `activeId`, or `total` change (shallow equality). Returns an * unsubscribe. */ subscribe(listener: (event: { snapshot: ContentControlsSlice; }) => void): () => void; /** * Value-shaped alias of {@link subscribe}: listener receives the * snapshot directly. */ observe(listener: (snapshot: ContentControlsSlice) => void): () => void; /** * Look up a single content control by id. Reads from the cached * slice (not a fresh Document API call), so the returned record is * always consistent with what subscribers last saw on the same * snapshot. Returns `null` when the id isn't in the current list. */ get(input: { id: string; }): import('../../../document-api/src/index.js').ContentControlInfo | null; /** * Painter rect for the content control identified by `id`. Sugar * over {@link ViewportHandle.getRect} with a UI-local * `ContentControlViewportAddress` target. Returns the same shape as * the underlying `getRect` (success + `rect` + `rects`, or a * failure reason). */ getRect(input: { id: string; }): ViewportRectResult; /** * Scroll the content control identified by `id` into view. The * control's position is resolved from the document model (not the * painted DOM), so it works even when the control sits on a * not-yet-rendered (virtualized) page — the page is mounted, then * scrolled. Scroll-only: it does not move the selection or place the * caret inside the control. * * Returns the same `ScrollIntoViewOutput` shape as * `ui.viewport.scrollIntoView`: `{ success: true }` once scrolled, or * `{ success: false }` when `id` is empty/unknown or the presentation * layer isn't ready. `block` defaults to `'center'`, `behavior` to * `'smooth'`. * * v1 is body-only: a control inside a header/footer/note story does * not resolve and returns `{ success: false }`. */ scrollIntoView(input: { id: string; block?: 'start' | 'center' | 'end' | 'nearest'; behavior?: 'auto' | 'smooth'; }): Promise; /** * Focus the content control identified by `id`: place the caret inside it * and scroll it into view — the "take me there and let me edit" counterpart * to {@link scrollIntoView} (which is scroll-only). `block` defaults to * `'center'`, `behavior` to `'smooth'`. * * Selection, not mutation: it does NOT bypass lock or document-mode rules. * If the control is locked or the document is read-only, the user can * inspect it, but edits are still blocked by the normal editing rules. * * Resolves to `{ success: false, reason }` only for real navigation * problems — `'invalid-id'` (empty id), `'not-ready'` (no presentation * layer), `'not-found'` (no such control in the body document; v1 is * body-only), or `'not-reachable'` (found, but its page couldn't be * scrolled into view). Lock mode and viewing mode never make it fail. */ focus(input: { id: string; block?: 'start' | 'center' | 'end' | 'nearest'; behavior?: 'auto' | 'smooth'; }): Promise; } /** * Result of {@link ContentControlsHandle.focus}. Fails only for real * navigation problems, never for lock mode or viewing mode (focus is * selection, not mutation). */ export type ContentControlFocusResult = { success: true; } | { success: false; reason: 'invalid-id' | 'not-ready' | 'not-found' | 'not-reachable'; }; /** * Anchored-metadata domain handle exposed on `ui.metadata`. Sugar over * the metadata-id → content-control-id → painter geometry bridge that * custom UI would otherwise compose by hand: callers carry only the * metadata id (the value they passed to `editor.doc.metadata.attach`) * and never see the SDT node id underneath. * * Read / scroll only — there are no mutation methods in v1. All * mutations (`attach` / `update` / `remove`) stay on * `editor.doc.metadata.*`; this handle is a UI surface, not a parallel * mutation contract. * * No `namespace` parameter: `editor.doc.metadata.attach` enforces * globally unique ids within a document (collisions fail with * `INVALID_INPUT`), so the id is sufficient to identify an entry. * No `getRects` either — `getRect`'s success variant already exposes * the per-line `rects[]` array; a second method with the same return * shape would just add API noise. */ export interface MetadataHandle { /** * Painter rect for the anchor identified by metadata `id`. Internally * resolves metadata-id → SDT node-id via the cached content-controls * slice and delegates to {@link ContentControlsHandle.getRect}, so * the success shape and failure reasons match the rest of the * `ui.*.getRect` family exactly. * * Failure mapping: * - empty id → `'invalid-target'` * - unknown id in current document → `'unresolved'` * - SDT exists but not painted (virtualized / pre-paint) → the * reason from `contentControls.getRect` (typically * `'not-mounted'` or `'not-ready'`) is propagated as-is. */ getRect(input: { id: string; }): ViewportRectResult; /** * Scroll the viewport to the anchored span identified by metadata * `id`. Internally calls `editor.doc.metadata.resolve` to get a * `SelectionTarget`, converts it to a `TextTarget` (the shape * `ui.viewport.scrollIntoView` accepts), and forwards * `block`/`behavior` unchanged. Returns the same * `ScrollIntoViewOutput` shape as `ui.viewport.scrollIntoView`; * unknown ids, `nodeEdge` endpoints, and other shapes that can't be * cleanly represented as a `TextTarget` resolve to * `{ success: false }` rather than silently scrolling to an * approximation. */ scrollIntoView(input: { id: string; block?: import('../../../document-api/src/index.js').ScrollIntoViewInput['block']; behavior?: import('../../../document-api/src/index.js').ScrollIntoViewInput['behavior']; }): Promise; } export interface CommentsSlice { /** Total count from the list result (before pagination, if any). */ total: number; /** Items from `editor.doc.comments.list()`. Empty array on error or no editor. */ items: import('../../../document-api/src/index.js').CommentsListResult['items']; /** * Comment IDs whose `commentMark` overlaps the current selection * (or covers the caret when empty). Empty array when the editor's * `selection.current()` predates SD-2792 (no `activeCommentIds` * field) — the controller falls back gracefully. */ activeIds: string[]; } /** * One tracked-change item exposed on `state.trackChanges.items`. * * Mirrors `editor.doc.trackChanges.list()` output (one entry per * change). UIs that want a merged comments-and-changes sidebar * compose their own feed from `ui.comments.items` and * `ui.trackChanges.items`. */ export interface TrackChangesItem { /** Tracked-change id. */ id: string; /** Full change record from `editor.doc.trackChanges.list()`. */ change: import('../../../document-api/src/index.js').TrackChangesListResult['items'][number]; } /** * Snapshot of the tracked-changes feed exposed on `state.trackChanges`. * * `editor.doc.trackChanges.list()` returns items in PM-position order; * `items` mirrors that order so next/previous navigation tracks the * document. */ export interface TrackChangesSlice { /** Tracked changes in document order. */ items: TrackChangesItem[]; /** Convenience count of `items.length`. */ total: number; /** * The currently active change id. Driven by selection * (`activeChangeIds[0]`) plus `ui.trackChanges.next/previous/ * scrollTo` calls. `null` when nothing is focused. */ activeId: string | null; } export interface SuperDocUIOptions { superdoc: SuperDocLike; } export interface SuperDocUI { /** * Subscribe to a slice of the unified UI state. Returns a {@link * Subscribable} that fires whenever the selected slice changes by the * given equality function. * * Default equality is `Object.is`. For object slices, pass * {@link shallowEqual} or a custom equality — otherwise every state * recompute will re-fire your listener. */ select(selector: SelectorFn, equality?: EqualityFn): Subscribable; /** * Aggregate toolbar surface. Mirrors the `HeadlessToolbarController` * shape from `superdoc/headless-toolbar`, sourced from the same * internal controller. Equivalent to subscribing to the toolbar slice * via `ui.select((s) => s.toolbar, ...)` plus a passthrough * `execute` and `getSnapshot`. */ toolbar: ToolbarHandle; /** * Per-command observables and executors — one handle per * {@link import('../headless-toolbar/types.js').PublicToolbarItemId}. * Pattern lifted from CKEditor 5's per-command `Observable`s: each * button binds to its own command's state, so unrelated state * changes don't trigger a re-render. */ commands: CommandsHandle; /** * Comments domain — single subscription + actions surface. Subscribe * to receive snapshot updates (items + activeIds + total); call * action methods to mutate. All mutations route through * `editor.doc.comments.*` (the Document API contract); this handle * exists to give UI consumers a stable surface, not to be a parallel * mutation contract. */ comments: CommentsHandle; /** * Tracked-changes domain. Accept/reject and navigation over the * tracked-change list. Mirrors `editor.doc.trackChanges` for verbs; * `next/previous/scrollTo` are UI-only navigation helpers. UIs that * want a merged comments-and-changes sidebar compose * `ui.comments.items` and `ui.trackChanges.items` themselves. */ trackChanges: TrackChangesHandle; /** * Content-controls (SDT) domain — single subscription + read * surface for chip overlays, citation popovers, and field-aware * side panels. Subscribe to receive snapshot updates (items + * activeIds + total); call `get` / `getRect` for synchronous * lookups. v1 has no mutation methods — `editor.doc.contentControls.*` * is the mutation contract. */ contentControls: ContentControlsHandle; /** * Anchored-metadata domain — read + scroll surface keyed on the * metadata id (= the value passed to `editor.doc.metadata.attach`). * Hides the metadata-id → SDT-node-id bridge so custom UI doesn't * have to compose `useSuperDocContentControls` + a tag → nodeId map * + `ui.contentControls.getRect` itself. v1 has no mutation methods * — `editor.doc.metadata.*` is the mutation contract. */ metadata: MetadataHandle; /** * Selection domain — single subscription + read surface for * floating bubble menus, format toolbars, mention popovers, and * "comment here" hints. The handle is sugar over * `ui.select((s) => s.selection, shallowEqual)` plus a synchronous * `getSnapshot()`; the lower-level selector substrate stays * available for finer-grained slices. * * The slice mirrors `editor.doc.selection.current()` — * `target` (TextTarget | null), `activeMarks`, `activeCommentIds`, * `activeChangeIds`, `quotedText`, `empty` — memoized at the * controller so subscribers don't re-fire on transactions that * leave the projection unchanged. */ selection: SelectionHandle; /** * Viewport domain — geometry queries for sticky-card / floating-toolbar * placement against painted entities and ranges, plus * {@link ViewportHandle.observe} to learn when those rects may have moved. * Browser-only by definition. */ viewport: ViewportHandle; /** * Document domain. Session-level operations a custom toolbar * needs (Export DOCX, document-mode toggle, ready state, unsaved- * changes indicator). Sugar over `state.document` plus passthroughs * to the host SuperDoc instance's `setDocumentMode` / `export` / * `replaceFile`. Lifts the operations that previously forced * consumers to wire a separate "host" hook through their React * context just so a toolbar button could call `superdoc.export(...)`. * The slice's `dirty` flag is transaction-driven and cleared on a * successful `export` or `replaceFile`; see {@link DocumentSlice}. */ document: DocumentHandle; /** * Create a {@link SuperDocUIScope} for collecting subscriptions, * custom-command registrations, and DOM listeners under one * lifecycle. Calling `ui.destroy()` cascades into every live scope * before tearing down the controller's own resources, so a typical * non-React consumer needs only `scope.destroy()` (or just * `ui.destroy()`) to clean up. */ createScope(): SuperDocUIScope; /** * Tear down all internal subscriptions to the editor / SuperDoc * instance / presentation editor, plus every scope created via * {@link SuperDocUI.createScope}. After destroy, no listeners will * fire and `select(...)` should not be called. */ destroy(): void; } /** * Lifecycle helper returned by {@link SuperDocUI.createScope}. * * Collects subscription unsubscribes, custom-command registrations, * and DOM event listeners under a single tear-down call. Calling * `ui.destroy()` automatically destroys every live scope first, so * consumers can either call `scope.destroy()` themselves on unmount / * HMR or rely on the cascade. * * Post-destroy semantics (idempotent: calling `destroy()` twice is * a no-op): * - `add(teardown)` invokes the teardown synchronously. * - `on(target, type, listener)` is a no-op; the listener is never * installed. * - `register(registration)` throws. * - `child()` returns an already-destroyed scope. */ export interface SuperDocUIScope { /** * Add a teardown function. Typically the unsubscribe returned by a * domain handle's `subscribe()` / `observe()` call: * * ```ts * scope.add(ui.commands.bold.observe((state) => render(state))); * scope.add(ui.comments.subscribe(({ snapshot }) => renderList(snapshot))); * ``` * * Calling `add` after `destroy` invokes the teardown immediately: * the canonical caller has already executed the side-effecting * subscribe call, so running the unsubscribe right away matches * what a `try { ... } finally { off(); }` pattern would do. */ add(teardown: () => void): void; /** * Register a custom toolbar command. Returns the full * {@link CustomCommandRegistrationResult} so consumers retain access * to `handle.observe(...)` and `invalidate()`. The scope retains * the `unregister()` callback and runs it when the scope is * destroyed; consumers may still call `result.unregister()` * manually before that, which is idempotent on the registry side. * * Throws when called on a destroyed scope. A register-then-unregister * cycle would still fire the registry's invalidation paths and any * collision-warning hooks, so we surface the lifecycle error * explicitly instead of swallowing it. */ register(registration: CustomCommandRegistration): CustomCommandRegistrationResult; /** * Add a DOM event listener. Calls `target.addEventListener(type, * listener, options)` and queues a `removeEventListener` with the * same arguments for scope teardown. No-op when called on a * destroyed scope. */ on(target: EventTarget, type: string, listener: EventListenerOrEventListenerObject, options?: AddEventListenerOptions | boolean): void; /** * Create a child scope. Destroying the parent destroys every child * first; child scopes share the controller's command registry so * `child.register(...)` registers against the same surface as * `ui.commands.register(...)`. Returns an already-destroyed scope * when called on a destroyed parent. */ child(): SuperDocUIScope; /** * Tear down every collected teardown and child scope. Idempotent. * Errors thrown by individual teardowns are caught and logged to * `console.error`; one failure does not prevent the rest from * running. */ destroy(): void; /** True after {@link destroy} has been called. */ readonly destroyed: boolean; } /** * Document slice exposed on `state.document` and through * {@link DocumentHandle}. * * Mirrors the `ready` / `documentMode` top-level fields as a single * memoized object so a Document Bar / Export button / mode toggle can * subscribe once instead of subscribing to two separate substrate * selectors. */ export interface DocumentSlice { /** True when SuperDoc has an active editor mounted. */ ready: boolean; /** Mirror of `superdoc.config.documentMode`. */ mode: 'editing' | 'suggesting' | 'viewing' | null; /** * True when the document has unsaved changes. Flips to `true` on any * editor transaction that mutates the document (`tr.docChanged`). * Selection-only transactions (cursor moves, range adjustments) do * not flip the flag. * * Cleared back to `false` when: * - `ui.document.export(...)` resolves successfully, or * - `ui.document.replaceFile(...)` swaps the document. * * Undo-to-clean is not tracked: hitting undo until the document * matches its on-open state still reads as dirty. Apps that need * the Word/GDocs-style "no unsaved changes" semantics should layer * their own edit-count diff on top. */ dirty: boolean; } /** * Input shape for {@link DocumentHandle.export}. Mirrors the public * `SuperDoc.export()` signature from the superdoc package; declared * here on the controller so consumers don't have to import the host * type to type their Export button. Every field is optional and the * runtime defaults match `SuperDoc.export()`. */ export interface DocumentExportInput { /** * Output formats. `['docx']` (default) downloads the active document * as DOCX. Multiple formats produce a zip. */ exportType?: string[]; /** * How comments are written to the export. `'external'` (default) * preserves comments as Word-style comment nodes; `'internal'` keeps * them on the SuperDoc internal channel; `'clean'` strips comments * from the export. */ commentsType?: 'internal' | 'external' | 'clean'; /** Override the default document title used as the file name. */ exportedName?: string; /** Additional binary blobs to bundle with the export (zipped). */ additionalFiles?: unknown[]; /** File names paired with `additionalFiles` (same length). */ additionalFileNames?: string[]; /** * When true, accepted/rejected tracked changes are flattened into * the export so the recipient sees the final document instead of * the working copy with revision history. */ isFinalDoc?: boolean; /** * When true (default), the browser triggers a download for the * resulting blob; when false, the blob is returned for the consumer * to handle (upload, preview, attach, etc.). */ triggerDownload?: boolean; /** * Optional CSS color for highlighting form fields in the export. * `null` (default) leaves fields unhighlighted. */ fieldsHighlightColor?: string | null; } /** * Document domain handle exposed on `ui.document`. Snapshot + * subscription mirror the other domain handles; `setMode` and * `export` are imperative passthroughs to the host. Construction is * cheap: every method routes through the controller's existing * substrate / host references, no new caching needed. */ export interface DocumentHandle { /** Snapshot the current document slice synchronously. */ getSnapshot(): DocumentSlice; /** * Subscribe to document-slice changes. Listener fires once * synchronously with the current snapshot, then again whenever * `ready` or `mode` changes by shallow equality. Returns an * unsubscribe. */ subscribe(listener: (event: { snapshot: DocumentSlice; }) => void): () => void; /** * Value-shaped alias of {@link subscribe}: listener receives the * snapshot directly instead of an event wrapper. Matches the * per-command `observe(state => ...)` shape on * {@link CommandHandle.observe}, so a single listener style works * across the whole controller surface. Same emission semantics as * `subscribe`: fires once synchronously, then on shallow-equality * change. Returns an unsubscribe. */ observe(listener: (snapshot: DocumentSlice) => void): () => void; /** * Set the document mode. Routes through `superdoc.setDocumentMode` * which fires the existing `document-mode-change` event and updates * the per-editor mode. No-op when the host stub omits the setter * (e.g. SSR / non-browser test stubs). */ setMode(mode: 'editing' | 'suggesting' | 'viewing'): void; /** * Export the document. Routes through `superdoc.export(options)` * with the same defaults as the host method (DOCX, external * comments, browser-triggered download). Returns the resulting * blob (or zip) when `triggerDownload: false`, or `undefined` * when the download was triggered. Rejects if the host's export * fails; consumers should wrap their toolbar Export button in a * try/catch and surface the error inline. */ export(options?: DocumentExportInput): Promise; /** * Replace the current document file. Routes through * `superdoc.activeEditor.replaceFile(file)` and re-emits * `commentsLoaded` once the swap completes so consumers running * `modules.comments: false` (SD-2839) still see imported comments * refresh in `ui.comments`. Resolves when the swap and the * post-swap event have both fired. Rejects if the host has no * active editor or the engine swap throws. */ replaceFile(file: File): Promise; } /** * Selection domain handle exposed on `ui.selection`. Same shape as * `CommentsHandle` / `TrackChangesHandle`: snapshot + subscription. Mirrors * the full `SelectionInfo` projection through the memoized * `state.selection` slice. */ export interface SelectionHandle { /** Snapshot the current selection slice synchronously. */ getSnapshot(): SelectionSlice; /** * Subscribe to selection slice changes. The listener fires once * with the initial snapshot, then again only when the projected * selection state actually changes (memoized — no re-fire on * typing-only transactions). Returns an unsubscribe. */ subscribe(listener: (event: { snapshot: SelectionSlice; }) => void): () => void; /** * Value-shaped alias of {@link subscribe}: listener receives the * snapshot directly. See {@link DocumentHandle.observe} for why * this exists alongside `subscribe`. */ observe(listener: (snapshot: SelectionSlice) => void): () => void; /** * Capture the current selection as a portable handle. * * The pattern: a sidebar composer or floating menu opens, takes * focus into its own input element, and the editor's selection * visually clears (browser focus moved away). Without this * primitive every consumer reaches for an ad-hoc closure that * snapshots the selection at click-time and races to use it * before focus moves. Capture freezes the portable address * shapes (target / selectionTarget / activeMarks / etc.) so the * consumer can pass `captured.target` or * `captured.selectionTarget` directly into `editor.doc.*` calls * (`comments.create`, `text.replace`, `format.apply`, etc.) when * the composer submits, regardless of where browser focus is. * * Returns `null` when there is no addressable selection (no * editor mounted, selection collapsed in a non-text node, etc.). * The returned handle is a frozen value object, safe to store * on a React ref or in component state across renders. * * Pair with {@link restore} to put the visible selection back when * the composer closes. */ capture(): SelectionCapture | null; /** * Look up the painted rectangles of the current selection (or a * captured one) in viewport coordinates. * * SuperDoc renders the visible page through the layout engine, not * the hidden ProseMirror DOM. `window.getSelection().getRangeAt(0) * .getBoundingClientRect()` reads from the offscreen PM and returns * coordinates that don't match what the user sees — every consumer * who reaches for it ships a broken bubble menu. This method asks * the painter directly so the rects align with what's painted. * * Multi-line selections produce one rect per painted line in * document order. Empty selections, no-editor state, or captures * whose target no longer resolves return `[]`. * * Pass a `SelectionCapture` (from {@link capture}) to query rects * for a frozen selection — useful when a composer has stolen focus * and the editor's live selection is gone but you still want to * position UI relative to where the user originally selected. * * The live path (no capture) handles all surfaces — body, header, * footer, footnote, endnote — because `PresentationEditor` routes * selection-rect lookups through its currently active editor. * * The captured path resolves block ids against the currently routed * editor, so captures taken in a non-body story still produce the * right rects while the user remains in that story (the common case * for a bubble menu or composer that opens a sidebar). When focus * has moved to a different story (or the body) by call time, the * captured block ids no longer resolve and the call returns `[]` * rather than rects from the wrong surface — fully cross-surface * captured rects need a story-keyed lookup that doesn't yet exist * publicly on `PresentationEditor`. */ getRects(capture?: SelectionCapture | null): ViewportRect[]; /** * Single anchor rect for floating UI (bubble menu, link popover, * mention list). Sugar over {@link getRects}: returns the first * line rect when `placement` is `'start'` (default), the last when * `'end'`, or the union bounding box across all line rects when * `'union'`. Returns `null` when there are no rects. */ getAnchorRect(options?: SelectionAnchorRectOptions, capture?: SelectionCapture | null): ViewportRect | null; /** * Inverse of {@link capture}. Set the editor's visible selection to * the range a capture froze. Closes the round-trip a sidebar * composer needs: capture on open, post on submit, restore on close * so the user sees the editor with the same range highlighted. * * Returns a result object rather than `void` because captures go * stale: an edit between capture-time and call-time can move or * delete the captured block, the editor can switch into viewing * mode, or the captured target may have been a non-text selection * with no addressable range. The `reason` discriminator lets * consumers distinguish "the editor hasn't mounted yet" from "the * doc has changed under us" without inspecting state separately. * * Side effect: a successful restore also moves browser focus into * the editor's painted host (via the underlying `setTextSelection` * command). That is the right behavior for the canonical composer * flow — the user submits and expects to keep typing — but it does * mean callers triggering `restore` from contexts where focus * shouldn't move (e.g. a "preview" toggle that should leave focus * on a sidebar control) need to gate the call themselves. * * Cross-surface limitation: a capture taken in a header / footer / * footnote / endnote restores correctly while the user remains in * that story (the routed editor still owns the captured block ids). * Once focus has moved to the body, the routed editor falls back * and the captured non-body block ids no longer resolve there; * `restore` returns `{ success: false, reason: 'stale' }` rather * than placing the selection on the wrong surface. Same posture as * {@link getRects}. */ restore(capture: SelectionCapture): SelectionRestoreResult; } /** * Result of {@link SelectionHandle.restore}. * * `'not-ready'` — no editor mounted (SSR, post-destroy). * `'read-only'` — editor is in viewing mode; selection mutation refused. * `'missing-target'` — capture had no addressable text target. * `'stale'` — captured block ids don't resolve in the current document * (the doc was edited or swapped between capture and restore). */ export type SelectionRestoreResult = { success: true; } | { success: false; reason: 'not-ready' | 'read-only' | 'missing-target' | 'stale'; }; /** * Options for {@link SelectionHandle.getAnchorRect}. */ export interface SelectionAnchorRectOptions { /** * Which line of a multi-line selection to anchor to. * * - `'start'` (default): top-most line. Matches Word / Google Docs * bubble menu placement. * - `'end'`: bottom-most line. Useful when the popover lives below. * - `'union'`: bounding rect across every line. Useful for selection * overlays / shaded backgrounds. */ placement?: 'start' | 'end' | 'union'; } /** * Frozen snapshot returned by {@link SelectionHandle.capture}. * * Same shape as {@link SelectionSlice}; declared as its own type * so consumers can name the captured value in their component * state (`useState(null)`) and so * {@link SelectionHandle.restore} has a stable input type. * * The runtime value is recursively `Object.freeze`d, so assigning * into `captured.target.segments[0].range.start` or * `captured.activeMarks[0]` throws in strict mode. We do NOT * encode that as a `readonly` type because the canonical use case * is passing `captured.target` straight to `editor.doc.*` * operations whose parameters are typed as mutable shapes (the * doc-api doesn't mutate them, but its types don't say `readonly`). * Adding `readonly` here would force a cast at every doc-api * boundary; the runtime guard plus this JSDoc carry the "do not * mutate" contract instead. */ export type SelectionCapture = SelectionSlice; /** * Aggregate toolbar handle exposed on `ui.toolbar`. Compatible with * `HeadlessToolbarController` from `superdoc/headless-toolbar` so the * built-in `SuperToolbar.vue` (and any external consumer using the * standalone controller today) can be migrated without API churn. */ export interface ToolbarHandle { /** Snapshot the current `{ context, commands }` payload synchronously. */ getSnapshot(): ToolbarSnapshotSlice; /** * Subscribe to toolbar snapshot changes. Listener receives an event * with the latest snapshot. Returns an unsubscribe. */ subscribe(listener: (event: { snapshot: ToolbarSnapshotSlice; }) => void): () => void; /** * Value-shaped alias of {@link subscribe}: listener receives the * snapshot directly. See {@link DocumentHandle.observe} for why * this exists alongside `subscribe`. */ observe(listener: (snapshot: ToolbarSnapshotSlice) => void): () => void; /** * Execute a built-in toolbar command. Type-safe payload is enforced * via the existing `ToolbarPayloadMap`. */ execute(...args: import('../headless-toolbar/types.js').ToolbarPayloadMap[Id] extends never ? [id: Id] : [id: Id, payload: import('../headless-toolbar/types.js').ToolbarPayloadMap[Id]]): boolean; } /** * Per-command handle: state observation + execution for a single * toolbar command id. */ export type CommandHandle = { /** * Subscribe to changes in this command's state. The listener fires * once synchronously with the current state, then again whenever the * state changes by shallow equality. Returns unsubscribe. */ observe(listener: (state: ToolbarCommandHandleState) => void): () => void; /** Execute this command. Payload is type-checked per-command. */ execute(...args: import('../headless-toolbar/types.js').ToolbarPayloadMap[Id] extends never ? [] : [payload: import('../headless-toolbar/types.js').ToolbarPayloadMap[Id]]): boolean; }; /** * Stable per-command state shape. `value` is omitted (`undefined`) when * the underlying command has no value (e.g., bold), and typed * per-command via `ToolbarValueMap` otherwise (e.g., `font-size` * resolves to `string | undefined`). */ export type ToolbarCommandHandleState = { active: boolean; disabled: boolean; value: import('../headless-toolbar/types.js').ToolbarValueMap[Id] | undefined; }; /** * Map of every toolbar command id to its handle. Indexed via * `ui.commands.bold.observe(...)` etc. The runtime exposes a Proxy so * any `PublicToolbarItemId` key works without pre-enumerating. * * `register(...)` extends the surface with consumer-defined commands — * see {@link CustomCommandRegistration}. */ export type CommandsHandle = { [Id in import('../headless-toolbar/types.js').PublicToolbarItemId]: CommandHandle; } & { /** * Register a custom toolbar command at runtime so consumers migrating * from TipTap / CKEditor / TinyMCE can wire their own toolbar buttons * (AI Rewrite, Insert Mention, custom workflow actions, etc.) without * forking the built-in registry. * * Returns a {@link CustomCommandRegistration} with three members: * * - `handle`: typed `{ observe, execute }` surface for this command. * Equivalent to `ui.commands[id]` but carries the consumer's payload * and value types — capture the registration to keep that typing. * - `invalidate()`: re-runs `getState` and re-emits the snapshot. * Use when external app state (permissions, AI quota, upload status, * etc.) changes — SuperDoc has no other way to know about it. * Microtask-coalesced; safe to call from any external signal handler * but call it on *bucket* state changes, not per-keystroke. * - `unregister()`: idempotent. Removes the command and tears down its * per-command Subscribable so observers stop firing. * * Built-in collisions are refused by default with a console warning. * Pass `override: true` on the registration to deliberately replace a * built-in (e.g. swap `bold` for a tracked-changes-aware variant). * Custom-vs-custom collisions warn and replace the prior registration. */ register(registration: CustomCommandRegistration): CustomCommandRegistrationResult; /** * Look up a command handle by string id at runtime. * * Returns a {@link DynamicCommandHandle} for any registered id, * built-in (`'bold'`, `'italic'`, etc.) or custom (registered via * {@link CommandsHandle.register}), and `undefined` for unknown ids. * * Use this instead of indexing `ui.commands[id]` when the id is only * known at runtime: a toolbar driven by a `string[]` config, a * keyboard-shortcut router, a plugin loop. Indexing the surface with * a generic `string` type-errors today because the surface mixes * per-command handles with the `register` method, so consumers * otherwise reach for an unsafe `as` cast on every dispatch site. * * The returned handle's `observe` listener receives the full * {@link UIToolbarCommandState} (active / disabled / value / source), * so a single render path can drive built-in *and* custom buttons * uniformly without branching on the id. */ get(id: string): DynamicCommandHandle | undefined; /** * Returns `true` when `id` is currently registered: a built-in * (member of `BUILT_IN_COMMAND_IDS`) or a custom registered via * {@link CommandsHandle.register}. Returns `false` for unknown * strings, including custom ids that have been unregistered. * * Use to validate config-driven toolbars at startup. The runtime * lookup `ui.commands.get(id)` returns `undefined` for unknown ids * silently; `has` makes the check explicit and short. */ has(id: string): boolean; /** * Like {@link CommandsHandle.get} but throws when `id` is not * registered. Use at trusted dispatch sites where an unknown id * indicates a bug, not a user error: keyboard shortcut routers, * tests, internal command pipelines. */ require(id: string): DynamicCommandHandle; /** * Collect the right-click context-menu items contributed by custom * commands, filtered by their `when` predicate and sorted by * `(group, order, registration time)`. Returns `[]` when no * registered command carries a `contextMenu` field or none survives * the predicate. * * The consumer renders the menu themselves. The typical flow: * * ```ts * scope.on(editorHost, 'contextmenu', (event) => { * event.preventDefault(); * // SD-2945: pass the full bundle so predicates filter on the * // same shape handlers receive, and `item.invoke()` fires * // execute with context bound. The legacy `{ entities }` shape * // still works for apps that haven't migrated. * const context = ui.viewport.contextAt({ x: event.clientX, y: event.clientY }); * const items = ui.commands.getContextMenuItems(context); * renderMenu(items, event.clientX, event.clientY, (item) => item.invoke?.()); * }); * ``` * * `entities` defaults to `[]` so menus that aren't point-anchored * (keyboard shortcut, app-bar trigger) still resolve a useful * subset. The current selection slice is read from controller state * automatically. * * Built-in items are NOT in this list: SuperDoc's built-in * context-menu extension still owns Bold / Italic / Copy / Paste * when enabled. This surface exists for apps that disable that * extension (`disableContextMenu: true`) and roll their own menu — * built-in entries belong to the consumer's renderer at that point. */ getContextMenuItems(input?: { entities?: ViewportEntityHit[]; } | ViewportContext): ContextMenuItem[]; }; /** * Type-erased command handle returned from {@link CommandsHandle.get}. * * Bridges built-ins ({@link CommandHandle}) and customs * ({@link CustomCommandHandle}) into one observe/execute surface so * consumers iterating `string[]` ids don't have to branch. The emitted * state carries `source` so a uniform renderer can still distinguish * the two when it wants. * * `execute` accepts an optional `unknown` payload and returns * `boolean | Promise` (built-ins are sync, customs may be * async). Capture the typed registration result for type-safe * payloads. `get(id)` is the dynamic-lookup fallback, not a * replacement for the per-id typing of `ui.commands.bold`. */ export interface DynamicCommandHandle { /** * Subscribe to the command's state. The listener fires once * synchronously with the current state, then again whenever the * state changes by shallow equality. Returns an unsubscribe. * * For ids in the built-in registry that haven't received a * snapshot yet (or whose value has gone stale), the listener is * still called with a deterministic disabled fallback so consumer * code can render without a null check on every emit. */ observe(listener: (state: UIToolbarCommandState) => void): () => void; /** * Execute the command. Forwards to the same dispatch path as * `ui.toolbar.execute(id, payload)` for built-ins and the * registered `execute` handler for customs. * * The payload is `unknown` because `get(id)` erases per-command * payload typing. Pass the value the command expects (e.g. the * `string` for `'font-size'`). The returned Promise resolves to * `false` when a custom command's handler rejects or returns * `false`; built-ins return synchronously. */ execute(payload?: unknown): boolean | Promise; } /** * Input shape for {@link CommandsHandle.register}. * * `getState` is sync and should be cheap (it runs on every snapshot * rebuild). Async work — fetching, uploading, prompting — belongs in * `execute`. If app state changes outside the editor (the app's auth * provider says permissions changed; an AI quota counter ticks down) * call the registration's `invalidate()` to re-derive `getState`. * * Errors thrown from `getState` are caught and the command falls back * to a static `{ active: false, disabled: false }` for that snapshot. * The error is reported via `console.error` once per error message * (not once per snapshot rebuild) so a buggy custom command can't * flood the console or wedge the toolbar. */ export type CustomCommandRegistration = { /** * Command id. Use a namespaced convention like `'company.aiRewrite'` * to avoid future collisions with built-in commands. Collides with a * built-in by default → warns and refuses (pass `override: true` to * replace deliberately). */ id: string; /** * Execute the command. Receives: * * - `payload` (typed per registration), * - the host `superdoc` instance, * - the routed `editor` — the same editor `ui.commands.*` mutations * target. Use `editor.doc.*` for direct Document API access without * reaching `superdoc.activeEditor`. `editor` is `null` before the * editor has reported ready, so guard early, and * - `context` (SD-2945): the {@link ViewportContext} bundle, present * only when the command was invoked via `ContextMenuItem.invoke()` * from a menu opened with `ui.viewport.contextAt(...)`. Lets * right-click handlers act on the click target ("Paste here", * "Comment here") without re-running entityAt / positionAt or * threading payloads. `undefined` for direct * `commands.execute` / `commands.get(id).execute()` calls. * * Return value is normalized to `boolean` for the synchronous result; * async commands return a Promise the runtime awaits internally. */ execute: (args: { payload?: TPayload; superdoc: SuperDocLike; editor: SuperDocEditorLike | null; context?: ViewportContext; }) => boolean | void | Promise; /** * Optional state deriver. Runs on every snapshot rebuild. If omitted, * the command's state stays static at `{ active: false, disabled: false, value: undefined }`. * * `state` is the controller's current `SuperDocUIState` so the * deriver can read `state.selection`, `state.documentMode`, etc. * without needing a separate selector subscription. */ getState?: (args: { state: SuperDocUIState; }) => { active?: boolean; disabled?: boolean; value?: TValue; } | undefined | void; /** * Set to `true` to deliberately replace a built-in command id. Without * this flag, registrations colliding with a built-in are refused with * a console warning. */ override?: boolean; /** * Optional contribution to the right-click context menu. When set, * the command shows up in {@link CommandsHandle.getContextMenuItems} * results (filtered by `when`) so a custom context-menu UI can * render and dispatch it. Consumers using SuperDoc's built-in * context-menu extension keep using that — this surface is for * apps that turn the built-in off (`disableContextMenu`) and roll * their own menu without losing the contribution model. */ contextMenu?: ContextMenuContribution; /** * Optional keyboard shortcut(s) bound to this command. Follows the * ProseMirror / Tiptap convention: `'Mod-K'`, `'Mod-Shift-C'`, * `'Alt-Enter'`. `Mod` is the platform-correct meta key (Cmd on * macOS, Ctrl elsewhere). Pass an array for multiple bindings on * the same command. * * The controller installs a single keydown listener on the editor * host; matched shortcuts dispatch through the same path * `ui.commands.get(id).execute()` uses, so the consumer never has * to wire keyboard plumbing by hand. Shortcuts only fire while * focus is inside the editor, so a Cmd-B in a sidebar input does * not trigger Bold on the document. * * Custom-vs-custom collisions: when two registrations claim the * same shortcut, the later one wins and the controller logs a * warning. Built-in editor keymaps (Bold's Cmd-B, etc.) are owned * by the editor's own keymap plugin and are not in scope for * collision detection — registering `'Mod-B'` will fire alongside * Bold, not in place of it. */ shortcut?: string | string[]; }; /** * Right-click context-menu contribution attached to a custom command. * * The consumer renders the menu themselves; SuperDoc just collects the * items, applies `when`, and sorts. Click handling stays on the * consumer's side and dispatches via `ui.commands.get(id).execute()`. */ export interface ContextMenuContribution { /** Display label for the item. */ label: string; /** * Logical group for sorting. Lets a contribution slot next to * related built-ins. Custom group names are accepted; unknown groups * are placed after the built-in groups in registration order. Built-in * group ids: `'format'`, `'clipboard'`, `'review'`, `'comment'`, * `'link'`. */ group?: string; /** * Sort order within the group. Lower runs earlier. Defaults to `0`; * ties are broken by registration order so the rendered menu is * stable across snapshots. */ order?: number; /** * Predicate scoping the item to specific contexts (the click landed * on a tracked change, the selection is non-empty, etc.). Receives * the entities under the click coordinate (call * {@link ViewportHandle.entityAt} to populate them) and the current * selection slice. Omitted predicate means "always applicable". * * Errors thrown from `when` are caught and the item is hidden for * that query — same posture as `getState` on a custom command. */ when?: (input: ContextMenuWhenInput) => boolean; } /** Input passed to {@link ContextMenuContribution.when}. */ export interface ContextMenuWhenInput { /** * Entities under the right-click point, from * {@link ViewportHandle.entityAt}. Empty array when the consumer * didn't pass entities (e.g. the menu opens from a keyboard shortcut * rather than a click) or when the point is over no painted entity. */ entities: ViewportEntityHit[]; /** Current selection slice. Mirrors `state.selection`. */ selection: SelectionSlice; /** * SD-2945: viewport-relative click point. Present only when the * consumer called `getContextMenuItems(viewport.contextAt({ x, y }))` * (or passed a {@link ViewportContext} directly). Predicates that * only care about entities / selection can keep destructuring the * old two fields; the new ones are additive. */ point?: { x: number; y: number; }; /** * Resolved caret position at the click point, or `null` when the * click is outside the painted host. Present only when the consumer * passed a {@link ViewportContext}. */ position?: ViewportPositionHit | null; /** * `true` when the click point is inside the currently painted * selection rects. Lets predicates distinguish "right-clicked the * selection" from "right-clicked elsewhere" without re-running * geometry. Present only when the consumer passed a * {@link ViewportContext}. */ insideSelection?: boolean; } /** * One item returned by {@link CommandsHandle.getContextMenuItems}. * * The `id` matches a registered custom command; consumers dispatch on * click via `ui.commands.get(item.id).execute()`. `group` and `order` * are surfaced (rather than collapsed) so the consumer's renderer can * insert separators between groups. */ export interface ContextMenuItem { id: string; label: string; group: string; order: number; /** * SD-2945: convenience invoker that fires the registered command's * `execute` with the {@link ViewportContext} bundle bound. Present * only when the items came from * `getContextMenuItems(viewport.contextAt(...))`. The bundle is * captured in the closure so the handler receives the same shape * the predicate filtered on, without the consumer re-threading a * payload at every dispatch site. * * Consumers can still call `ui.commands.get(item.id).execute()` * directly when they don't need context (no behavior change). */ invoke?(): boolean | Promise; } /** Return value from {@link CommandsHandle.register}. */ export type CustomCommandRegistrationResult = { /** * Typed `{ observe, execute }` handle for this registration. Equivalent * to indexing `ui.commands[id]` at runtime, but the captured handle * carries the consumer's `TPayload` / `TValue` types — index access * with a string key cannot. */ handle: CustomCommandHandle; /** * Re-runs `getState` and re-emits the snapshot. Use when external app * state (not editor state) changes. Microtask-coalesced. */ invalidate(): void; /** * Idempotent. Removes the command and tears down per-command * Subscribables. Calling twice is a no-op. */ unregister(): void; }; /** Typed handle returned for a custom registration. */ export type CustomCommandHandle = { observe(listener: (state: CustomCommandHandleState) => void): () => void; execute(...args: TPayload extends void | undefined ? [] : [payload: TPayload]): boolean | Promise; }; /** Stable per-custom-command state shape. */ export type CustomCommandHandleState = { active: boolean; disabled: boolean; value: TValue | undefined; source: 'custom'; }; /** * Comments domain handle exposed on `ui.comments`. The execute * methods are convenience facades over `editor.doc.comments.*` — * they produce identical document mutations to direct doc-API calls. */ export interface CommentsHandle { /** Snapshot the current comments slice synchronously. */ getSnapshot(): CommentsSlice; /** * Subscribe to comments-snapshot changes. Listener fires once * synchronously with the current snapshot, then again whenever * items, activeIds, or total change (shallow equality). * Returns an unsubscribe. */ subscribe(listener: (event: { snapshot: CommentsSlice; }) => void): () => void; /** * Value-shaped alias of {@link subscribe}: listener receives the * snapshot directly. See {@link DocumentHandle.observe} for why * this exists alongside `subscribe`. */ observe(listener: (snapshot: CommentsSlice) => void): () => void; /** * Create a comment anchored to the current selection. Reads the * routed editor's `selection.current().target` and routes through * `editor.doc.comments.create`. Returns the operation receipt. */ createFromSelection(input: { text: string; }): import('../../../document-api/src/index.js').Receipt; /** * Create a comment anchored to a captured selection snapshot. * Use when the live selection is gone by the time the user submits * (the canonical case: a composer textarea takes focus, the editor * loses its visible selection, and `createFromSelection` would see * a null target). Capture the selection at composer-open via * `ui.selection.capture()`, hold it across the user's typing, then * pass it here. Routes through `editor.doc.comments.create` with * the captured `target`. Returns a `NO_OP` receipt when the capture * lacks a positional target. */ createFromCapture(capture: SelectionCapture, input: { text: string; }): import('../../../document-api/src/index.js').Receipt; /** * Post a reply to an existing thread. Routes through * `editor.doc.comments.create({ parentCommentId, text })`; the * reply inherits the parent's anchor, so callers don't pass a * target. The next `useSuperDocComments()` snapshot includes the * reply with `parentCommentId` set, which sidebars can group under * the thread root. * * Returns a `NO_OP` receipt when `text` is empty or whitespace-only, * matching the doc-api's text-required contract for top-level * comments. Returns a failure receipt when the parent id has been * deleted between the time the user opened the reply composer and * pressed Send. */ reply(parentCommentId: string, input: { text: string; }): import('../../../document-api/src/index.js').Receipt; /** Resolve a comment via `editor.doc.comments.patch`. */ resolve(commentId: string): import('../../../document-api/src/index.js').Receipt; /** * Reopen a resolved comment via `editor.doc.comments.patch({ status: * 'active' })`. The doc-api lifecycle inverse shipped in SD-2789; * the call resolves cleanly when the comment exists and is * currently resolved, and returns a failure receipt otherwise. */ reopen(commentId: string): import('../../../document-api/src/index.js').Receipt; /** Delete a comment via `editor.doc.comments.delete`. */ delete(commentId: string): import('../../../document-api/src/index.js').Receipt; /** * Scroll the viewport to the comment's anchor via * `ui.viewport.scrollIntoView({ target: EntityAddress })`. Resolves * to a `{ success: boolean }` receipt. */ scrollTo(commentId: string): Promise; } /** * Tracked-changes domain handle exposed on `ui.trackChanges`. Same * architectural posture as `CommentsHandle`: every mutation routes * through `editor.doc.trackChanges.*` (the Document API contract); * `next` / `previous` / `scrollTo` are UI-only navigation helpers. */ export interface TrackChangesHandle { /** Snapshot the tracked-changes feed synchronously. */ getSnapshot(): TrackChangesSlice; /** * Subscribe to track-changes snapshot updates (items, total, * activeId). Listener fires once synchronously with the current * snapshot, then again whenever the slice changes by shallow * equality. Returns an unsubscribe. */ subscribe(listener: (event: { snapshot: TrackChangesSlice; }) => void): () => void; /** * Value-shaped alias of {@link subscribe}: listener receives the * snapshot directly. See {@link DocumentHandle.observe} for why * this exists alongside `subscribe`. */ observe(listener: (snapshot: TrackChangesSlice) => void): () => void; /** Accept a single tracked change via `trackChanges.decide`. */ accept(changeId: string): import('../../../document-api/src/index.js').Receipt; /** Reject a single tracked change via `trackChanges.decide`. */ reject(changeId: string): import('../../../document-api/src/index.js').Receipt; /** Accept every tracked change via `trackChanges.decide({ scope: 'all' })`. */ acceptAll(): import('../../../document-api/src/index.js').Receipt; /** Reject every tracked change via `trackChanges.decide({ scope: 'all' })`. */ rejectAll(): import('../../../document-api/src/index.js').Receipt; /** * Move `activeId` to the next tracked change in document order. * Wraps to the first item past the last. Returns the new active * id, or `null` when there are no changes. */ next(): string | null; /** * Move `activeId` to the previous tracked change in document order. * Wraps to the last item past the first. Returns the new active id, * or `null` when there are no changes. */ previous(): string | null; /** * Scroll the viewport to the given tracked change and set it as * `activeId`. Routes through * `ui.viewport.scrollIntoView({ target: EntityAddress })`. */ scrollTo(id: string): Promise; } /** * Plain value rectangle in viewport coordinates. Always a snapshot, * never a live `DOMRect`. Coordinates measure from the top-left of * the user's viewport, not the editor host, so consumers can position * fixed/absolute elements directly with the returned `top` / `left`. */ export interface ViewportRect { top: number; left: number; width: number; height: number; /** * Page index of the painted page that contains this rect. Useful * for per-page sidebars or footers that render once per page. */ pageIndex: number; } /** * UI-local address for a content control (SDT) target. Not part of * the Document API's `EntityAddress` union because content controls * are not a Document API navigation primitive (no * `editor.doc.contentControls.navigateTo`). The viewport rect surface * is purely a presentation-layer concern, so the type lives here next * to {@link ViewportGetRectInput}. */ export type ContentControlViewportAddress = { kind: 'entity'; entityType: 'contentControl'; entityId: string; }; /** * Targets accepted by {@link ViewportHandle.getRect}. Extends the * Document API's `EntityAddress` (comment / tracked change) with the * UI-local content-control address. */ export type ViewportEntityAddress = import('../../../document-api/src/index.js').EntityAddress | ContentControlViewportAddress; export interface ViewportGetRectInput { /** * Entity to look up — comment, tracked change, or content control * (SDT) by id. Today `getRect` resolves rects via the painter's * data attributes (`data-comment-ids`, `data-track-change-id`, * `data-sdt-id`) which only stamp entity addresses, not * text-anchored ranges. Text targets (`TextAddress` / `TextTarget`) * are intentionally not in the union: surface should match real * behavior so a typed call site isn't lying about what works at * runtime. They land via a follow-up that adds story-aware text * resolution to the rect helper. */ target: ViewportEntityAddress; } export type ViewportRectResult = { success: true; /** * Primary anchor rect — the first painted occurrence of the * target, suitable as the anchor point for a sidebar card or * floating toolbar. For multi-page / multi-line targets, * `rects` carries the full set in document order. */ rect: ViewportRect; /** Every painted occurrence of the target, in document order. */ rects: ViewportRect[]; /** Page index of the primary anchor (`rect.pageIndex`). */ pageIndex: number; } | { success: false; reason: 'not-ready' /** * Caller-shape error: `target` is missing, has the wrong * `kind`, or refers to an `entityType` the controller does * not handle. Indicates a programming mistake, not a * transient state. */ | 'invalid-target' /** * Target's referenced block / entity is not in the model * (e.g. a stale id from a closed snapshot). Reserved for the * text-anchored paths once they land; the entity-anchored * path returns `not-mounted` for unknown ids since the DOM * lookup can't distinguish "doesn't exist" from "currently * virtualized". */ | 'unresolved' /** * Valid target but currently virtualized / offscreen — the * page or story isn't painted in the DOM. Caller can call * `viewport.scrollIntoView` first to mount it, then retry. * Same posture as the underlying scroll path for non-body * stories on virtualized pages (SD-2750). */ | 'not-mounted'; }; /** * Reason a {@link ViewportHandle.observe} notification fired. `'mixed'` * when more than one change coalesced into the same animation frame. */ export type ViewportGeometryReason = 'layout' | 'zoom' | 'scroll' | 'resize' | 'mixed'; /** * Payload for {@link ViewportHandle.observe}. Intentionally minimal: the * signal means "your cached `getRect()` coordinates may be stale, re-query" - * it carries no geometry. */ export interface ViewportGeometryEvent { reason: ViewportGeometryReason; } export interface ViewportHandle { /** * Look up the painted rectangle(s) of an entity or text range in * viewport coordinates. Synchronous — no DOM mutation required. */ getRect(input: ViewportGetRectInput): ViewportRectResult; /** * Subscribe to viewport geometry invalidation. The listener fires (once * per animation frame, coalesced) after anything that can move painted * rectangles: layout / pagination repaints, zoom, and DOM scroll / resize. * It carries no coordinates — re-query {@link getRect} for the entities you * care about. Returns an unsubscribe. * * This is the single signal overlays should listen to instead of * hand-wiring scroll + resize + layout + zoom (and still missing cases like * reflow and zoom, which fire no scroll event). */ observe(listener: (event: ViewportGeometryEvent) => void): () => void; /** * Scroll the viewport so the target is visible. Browser-only by * definition: drives `presentation.navigateTo()` for entity targets * (story-aware) and `presentation.scrollToPositionAsync()` for text * targets. Lives on `ui.*` rather than `editor.doc.*` because * viewport scroll is a UI side-effect, not a request/response * Document API operation. */ scrollIntoView(input: import('../../../document-api/src/index.js').ScrollIntoViewInput): Promise; /** * Look up entities painted under a viewport coordinate. Used by * right-click menus and hover tooltips to ask "what's at this point?" * without consumers reading `data-track-change-id` / * `data-comment-ids` / `data-sdt-id` off the painted DOM themselves; the * data-attribute layout is an implementation detail of the painter * that consumers shouldn't depend on. * * Returns an ordered array of {@link ViewportEntityHit}, innermost * first. A point can sit inside several entities at once (a tracked * change inside a comment highlight, for example); every match is * surfaced, not just the topmost. Empty array when the point isn't * over any painted entity, when called outside a browser, or when no * editor is mounted. * * Scoped to the controller's own editor: hits are only returned when * the point lands inside this editor's painted host. A page mounting * two SuperDoc instances therefore can't have one controller return * ids from the other's DOM, and post-destroy calls return `[]` * rather than stale ids from cached painted nodes. * * Today the supported entity types are `comment`, `trackedChange`, and * `contentControl` (content controls / SDT fields, whose hit also carries * `scope` and `tag`). `link`, `image`, and `tableCell` are reserved for * follow-ups; adding them is purely additive (new union members), so * callers can `switch` on `hit.type` and the default branch remains * forward compatible. */ entityAt(input: ViewportEntityAtInput): ViewportEntityHit[]; /** * The painted-DOM host element for this controller's editor, or * `null` when no editor is mounted (SSR, post-destroy, before * `onReady` fires). * * Custom UI consumers reach for the host element to scope their * own DOM listeners — `contextmenu`, hover tooltips, drag-and-drop * — to events that originate inside the editor. Without this, * consumers either listen on `document` and filter by a CSS class * they control (fragile, breaks when the wrapper class is renamed) * or pass the editor's container down through their own component * tree (verbose). * * The returned element is the host SuperDoc paints into. The * editor's hidden ProseMirror DOM is appended elsewhere and is not * inside this host — events whose target is in the hidden PM DOM * (most keyboard events after focus moves into the editor) won't * pass `host.contains(target)` checks. For coordinate-based hit * tests use {@link entityAt} or {@link positionAt} instead, both of * which scope correctly across painted-DOM and hidden-DOM events. */ getHost(): HTMLElement | null; /** * Resolve a viewport coordinate to a position in the editor's * document, or `null` when the point is outside the painted host or * no editor is mounted. * * The natural pair to {@link entityAt}: while `entityAt` answers * "what entity is under this point?", `positionAt` answers "what * caret position is under this point?". Right-click menus offering * "Paste here", "Insert clause at this point", or "Add comment at * this point" need this to dispatch their action against the click * coordinate rather than the user's previous selection somewhere * else in the document. * * Returns a {@link ViewportPositionHit} with both the resolved * `point` (a `SelectionPoint` consumers can pass straight to * `editor.doc.insert({ target })` and similar APIs) and the * `target` (a `SelectionTarget` for selection-shaped operations). * The two shapes are derived from the same underlying position, * just packaged differently to match the doc-api method that's * about to consume them. */ positionAt(input: ViewportPositionAtInput): ViewportPositionHit | null; /** * Resolve a viewport `(x, y)` coordinate to the full right-click * context bundle: `entities` under the point, the resolved * `position`, the live `selection`, the `point` itself, and * `insideSelection` (whether the click landed inside the painted * selection rects). * * Composes `entityAt`, `positionAt`, the `selection` slice, and an * AABB hit-test against `selection.getRects()` so consumers building * right-click menus don't reassemble the same shape at every site. * Pass the returned bundle to `getContextMenuItems(context)` so * predicates filter on the same shape handlers receive, and to * `ContextMenuItem.invoke()` so `execute({ context })` can act on * the click target without re-running geometry. * * Always returns a bundle (no `null`) so consumer code can * destructure without null-checking the top-level result; the * inner fields still carry the absent-case defaults each primitive * defines (`entities = []`, `position = null`, * `insideSelection = false`). Non-numeric coordinates coerce to * `(0, 0)` rather than short-circuiting to an empty bundle, since * `(0, 0)` is itself a valid viewport point and may legitimately * sit inside the painted host; pass real coordinates if you want * the result to reflect a specific click. */ contextAt(input: ViewportContextAtInput): ViewportContext; } /** * Input shape for {@link ViewportHandle.contextAt}. Same coordinate * space as `MouseEvent.clientX` / `clientY`. */ export interface ViewportContextAtInput { x: number; y: number; } /** * Input shape for {@link ViewportHandle.positionAt}. Same coordinate * space as `MouseEvent.clientX` / `clientY` and {@link ViewportRect}. */ export interface ViewportPositionAtInput { x: number; y: number; } /** * Resolved caret position returned by {@link ViewportHandle.positionAt}. * * `point` is the {@link import('../../../document-api/src/index.js').SelectionPoint} * shape used by point-anchored doc-api operations (`editor.doc.insert( * { target: { kind: 'selection', start: point, end: point } })` for a * collapsed insert at the click site). * * `target` is the equivalent {@link import('../../../document-api/src/index.js').SelectionTarget} * — a collapsed selection at the click point — for operations that * accept a target shape directly. Same underlying position, two * packagings; consumers pick the shape their downstream call needs. */ export interface ViewportPositionHit { point: import('../../../document-api/src/index.js').SelectionPoint; target: import('../../../document-api/src/index.js').SelectionTarget; } /** * The "what did the user right-click on?" bundle returned by * {@link ViewportHandle.contextAt}. Composes `entityAt`, `positionAt`, * the live selection slice, and an AABB hit-test against the current * selection rects so consumers don't reassemble the same shape at * every register site. * * Threaded into both `ContextMenuContribution.when` (so predicates can * filter on entity / position / selection containment) and the * registered `execute` (via {@link ContextMenuItem.invoke}) so the * handler doesn't redo work the controller already did. */ export interface ViewportContext { /** * The viewport-relative coordinate the consumer asked about. * Echoed back so handlers that anchor floating UI to the click * point don't have to remember it separately. */ point: { x: number; y: number; }; /** * Entities under the click point, ordered innermost-first. Same * shape and ordering {@link ViewportHandle.entityAt} returns * directly. Empty when the click is over no painted entity. */ entities: ViewportEntityHit[]; /** * Resolved caret position at the click point, or `null` when the * point is outside the painted host or no editor is mounted. Same * shape {@link ViewportHandle.positionAt} returns. */ position: ViewportPositionHit | null; /** The live selection slice. Mirrors `state.selection`. */ selection: SelectionSlice; /** * `true` when the click point is inside any of the rects the live * selection currently paints. Distinguishes "right-clicked the * selection itself" (act on the selection) from "right-clicked * elsewhere" (act on the click target). Always `false` for an * empty / collapsed selection. */ insideSelection: boolean; } /** * Input shape for {@link ViewportHandle.entityAt}. Coordinates are * viewport-relative (the same space `MouseEvent.clientX` / * `clientY` produce, and the same space {@link ViewportRect} reports * back), so a `contextmenu` handler can pass `event.clientX` / * `event.clientY` directly. */ export interface ViewportEntityAtInput { x: number; y: number; } /** * One hit returned by {@link ViewportHandle.entityAt}. * * Each entity type lands as its own union member so a `switch` on * `hit.type` with a default branch stays forward compatible. * * `contentControl` hits carry only the fields already stamped on the * painted DOM today: `id`, `scope` (block vs inline), and `tag` (the * ECMA-376 SDT tag). Full property data (alias, controlType, lockMode, * etc.) is not in the hit by design — call the Document API with a * `ContentControlTarget` (`{ kind, nodeType: 'sdt', nodeId }`) when * needed; `nodeId` is the `id` returned in this hit. Keeping the hit * minimal avoids gating viewport reads on metadata plumbing that does * not exist on every property yet. */ export type ViewportEntityHit = { type: 'comment'; id: string; } | { type: 'trackedChange'; id: string; } | { type: 'contentControl'; id: string; scope?: 'block' | 'inline'; tag?: string; }; //# sourceMappingURL=types.d.ts.map