Skip to content

feat(examples): drop-in-assessment app on superdoc/ui controller (SD-2671)#3006

Closed
caio-pizzol wants to merge 3 commits into
mainfrom
caio/sd-2671-drop-in-assessment-example-app-adapter-cleanups-frictionmd
Closed

feat(examples): drop-in-assessment app on superdoc/ui controller (SD-2671)#3006
caio-pizzol wants to merge 3 commits into
mainfrom
caio/sd-2671-drop-in-assessment-example-app-adapter-cleanups-frictionmd

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

A small React app (examples/headless/dropin-assessment/) that drives the same custom UI β€” toolbar + activity sidebar with comments + tracked changes β€” against both TipTap and SuperDoc via a shared EditorAdapter interface. The canonical reference for "what does drop-in look like, end-to-end?"

SuperDocAdapter rides the new controller. Toolbar state, selection, comments, review feed, and viewport scroll all flow from createSuperDocUI({ superdoc }). No more direct createHeadlessToolbar use, no more SuperDoc-instance event subscriptions, no more editor.doc.selection.onChange (removed in SD-2795). Comment reopen now works through ui.comments.reopen (SD-2789 closed that gap).

FRICTION.md refreshed. Eight items closed since the original assessment (SD-2789/2790/2791/2792/2793/2796/2802/2803). What's left is ranked: positional target on TrackChangeInfo (S2), virtualized non-body scroll (S10/SD-2750), TC recording vs documentMode (S4), heading/highlight toolbar parity (T2/T3), author runtime updates (S5), docs guide (S1/SD-2669).

Toolbar polish. Bullet/ordered/link buttons replaced their β€’ / 1. / πŸ”— text labels with inline Lucide-style SVGs.

Verified in browser.

  • Loads on the SuperDoc tab without console errors
  • 3 imported comments render in the activity sidebar
  • 3 imported tracked-change insertions render with accept/reject
  • Toolbar buttons reflect mark state, FRICTION-disabled buttons (H, H1, H2) stay disabled

This PR replaces the closed PR #2963 β€” same branch name, rebased onto current main with the SuperDocAdapter rewritten on the controller surface.

Closes SD-2671.

…ion harness (SD-2671)

A working reference app that drives the same custom React UI (toolbar
plus activity sidebar) against TipTap and SuperDoc through a shared
EditorAdapter contract. Toggling the editor in the header swaps the
adapter; the rest of the UI stays put. Every workaround the SuperDoc
adapter takes is annotated `FRICTION:` or `ESCAPE HATCH:` and rolled up
in `FRICTION.md` as a drop-in DX gap.

What this exercises:

- Comments: selection plus `editor.doc.comments.create` with the
  multi-segment `TextTarget` from `editor.doc.selection.current()`,
  list / patch / delete, sidebar to inline-highlight sync against both
  TipTap's `data-comment-id` mark and SuperDoc's `data-comment-ids`
  decorator.
- Tracked changes (SuperDoc only): imported from a Word-authored DOCX,
  rendered alongside comments in one chronological feed, accept/reject
  via `trackChanges.decide`, sidebar-card click scrolls to anchor via
  `editor.doc.ranges.scrollIntoView({ target: EntityAddress })`.
- Selection: `selection.current()` for read, `selection.onChange` for
  live updates. No PM reach-in.
- Toolbar: SuperDoc's `createHeadlessToolbar` 45-command closed union;
  H1/H2/highlight buttons disabled with FRICTION notes for the gaps.
- Export DOCX: SuperDoc routes through `superdoc.export({ ... })` and
  triggers a real download. TipTap surfaces the gap with a visible
  error message rather than shipping a half-faithful exporter.

Contract is editor-agnostic: no PM-shaped types on the public surface.
`SelectionInfo` is `{ hasSelection, empty, quotedText }`; `addComment`
takes only `{ body, authorId }` and the adapter resolves the underlying
target (TextTarget for SuperDoc, PM range cached internally for TipTap).
Consumers copying this contract do not inherit ProseMirror positions.

Honest omissions:

- TipTap ships no built-in tracked changes; the TipTap view shows an
  empty TC panel rather than mock parity. The missing capability is
  the finding.
- HTML/Markdown persistence: out of scope. SuperDoc is DOCX-first.
- TipTap has no native DOCX export; the Export DOCX button raises a
  visible error there. The gap is the finding.
- `addComment` returns `null` on engine failure rather than fabricating
  a placeholder comment. A harness should surface failures, not hide
  them.
- Telemetry explicitly opted out (`telemetry: { enabled: false }`) so
  consumers copying this config don't ship to ingest.superdoc.dev by
  default.

Sample content: `public/sample-review.docx` is a two-page Word memo
with 3 comments plus 3 tracked insertions, two distinct authors, and
exercises multi-page scroll plus multi-author attribution.

Verified end-to-end against published SD-2668 + SD-2670 APIs:
- TipTap to SuperDoc to TipTap toggle leaves no leaked state.
- `selection.current()` returns multi-segment TextTarget for cross-block
  selections.
- `comments.create({ target: TextTarget })` round-trips (3 to 4 items).
- `ranges.scrollIntoView({ target: EntityAddress })` returns
  `{ success: true }` on the new comment id.
- `superdoc.export({ exportType: ['docx'] })` triggers a real DOCX
  download.
- Single Vue warn on unmount is a SuperDoc internal cleanup race
  (filed as SD-2760), not a regression of the example.

Part of the SD-2667 drop-in assessment umbrella. FRICTION.md captures
the remaining DX gaps; SD-2750 (virtualized non-body entity scroll)
and SD-2760 (Vue unmount warn) tracked as separate follow-ups.
…ler (SD-2671)

The example app's SuperDocAdapter previously used createHeadlessToolbar
directly, plus per-event SuperDoc-instance subscriptions ('commentsUpdate',
'trackedChangesUpdate', 'trackChangesLoaded') and the now-removed
editor.doc.selection.onChange. After the SD-2667 umbrella landed
(SD-2790/2791/2793/2794/2795/2796/2789/2792/2802), the canonical
build-your-own-UI surface is `createSuperDocUI({ superdoc })` β€” the
example must consume what we tell migrators to use.

What changed
- `createHeadlessToolbar` swapped for `createSuperDocUI({ superdoc })`.
  Toolbar state, selection state, comments, and the merged review feed
  all flow from `ui.toolbar` / `ui.selection` / `ui.comments` /
  `ui.review`. The four SuperDoc-instance event subscriptions and the
  manual `comments.list()` / `trackChanges.list()` refresh helpers are
  gone β€” the controller's snapshots replace them.
- `editor.doc.selection.onChange` (removed in SD-2795) is no longer
  referenced. Selection updates ride `ui.selection.subscribe`.
- `executeCommand` calls `ui.commands.<id>.execute(payload)`. Index
  access on the typed CommandsHandle goes through an `unknown` cast
  because the proxy's `register` member is incompatible with a uniform
  `Record<string, CommandHandle>` lookup β€” that's the realistic typed
  path documented in SD-2802.
- `scrollToComment` / `scrollToChange` route through
  `ui.comments.scrollTo` / `ui.review.scrollTo` instead of poking
  `editor.doc.ranges.scrollIntoView` directly.
- `acceptChange` / `rejectChange` use `ui.review.accept` / `.reject`.
- `addComment` uses `ui.comments.createFromSelection({ text })` β€”
  the controller reads the routed editor's current TextTarget so the
  adapter no longer needs its own selection-target plumbing.
- `updateComment(..., { resolved: true })` calls `ui.comments.resolve`;
  the previously-FRICTION reopen path now calls `ui.comments.reopen`
  (SD-2789 closed that gap).
- `destroy()` calls `ui.destroy()`; clears every listener set.

Net: 374 line refactor, -245/+182. Smoke-tested in the dev app β€” all
3 comments + 3 tracked-change insertions render correctly on the
SuperDoc tab; toolbar state, accept/reject, resolve, and DOCX export
all work without console errors.

Toolbar icons
- Bullet list, ordered list, and link replaced their `β€’` / `1.` / `πŸ”—`
  text labels with inline Lucide-style SVGs so the example stops
  signalling "demo" and starts looking like a real toolbar consumers
  would build.
…SD-2671)

Eight items closed since the original assessment landed:

  - S3 Closed-toolbar registry (SD-2802)
  - S6 Next/prev navigation helpers (SD-2791 ui.review.next/previous)
  - S8 Active comment / change ids at cursor (SD-2792)
  - S9 ranges.getRect (SD-2793 ui.viewport.getRect)
  - S11 Comment reopen one-way (SD-2789)

Plus the controller foundation itself (SD-2790/2791/2793/2794/2795/
2796/2803). The "Closed since the original assessment" section at the
top now lists every shipped fix with PR numbers so a reader can trace
what changed.

Items still open and ranked:

  - S2 TrackChangeInfo lacks positional target
  - S10 Virtualized non-body entity scroll (SD-2750)
  - S4 TC recording entangled with documentMode (SD-2799 stages move)
  - S5 Author identity init-time only
  - T2/T3/T4 Heading commands / highlight toggle / naming aliases
  - S1 "Build your own UI" docs (SD-2669 in flight)
  - L2 Workspace-dev requires build step
  - S12/S7/L1/T1 Minor DX residue

T1 reframed: the discoverability concern moved from
`superdoc/headless-toolbar` to `superdoc/ui` β€” same shape, but
SD-2803 made the sub-entry tight so consumers no longer pull the
whole editor.
@caio-pizzol caio-pizzol requested a review from a team as a code owner April 29, 2026 17:31
@linear
Copy link
Copy Markdown

linear Bot commented Apr 29, 2026

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7dfc6767dd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +280 to +282
if (m.type === markType && m.attrs.commentId === id) {
tr.removeMark(pos, pos + node.nodeSize, markType);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove only the targeted comment mark

When deleting a comment, this code calls tr.removeMark(..., markType) after matching a specific commentId, but removing by markType clears all comment marks in that text range. Because the custom mark sets excludes: '', overlapping comment marks with different ids can coexist; deleting one thread will also strip other threads' anchors from the same text, corrupting comment highlighting/navigation for the remaining comments.

Useful? React with πŸ‘Β / πŸ‘Ž.

Comment on lines +200 to +202
const newId = receipt.inserted?.[0]?.entityId;
if (!newId) return null;
return this.commentsCache.find((c) => c.id === newId) ?? null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return the created comment after successful insert

After a successful createFromSelection, the adapter immediately looks up the new id in commentsCache, but that cache is only refreshed by the comments subscription callback. In the success path this often returns null even though the comment was inserted, so callers treat successful inserts as failures (e.g., skipping activation/focus behavior tied to the returned comment).

Useful? React with πŸ‘Β / πŸ‘Ž.

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.

πŸ“’ Thoughts on this report? Let us know!

caio-pizzol added a commit that referenced this pull request Apr 29, 2026
Public-facing example showing how a consumer wires their own toolbar,
comments sidebar, review sidebar, and custom toolbar commands to
SuperDoc through createSuperDocUI({ superdoc }).

Replaces the earlier dropin-assessment harness (closed PR #3006). The
TipTap-vs-SuperDoc EditorAdapter shape was useful for internal
validation but teaches the wrong mental model: consumers don't wrap
SuperDoc to make it look like TipTap, they bind their UI to the
controller. The new example reflects that posture.

What it demonstrates

  - <SuperDocEditor> mounted inside a custom three-pane layout
    (contained + hideToolbar) so the wrapper doesn't take over the
    page.
  - Custom toolbar (bold/italic/underline, undo/redo, lists, comment)
    bound to ui.toolbar snapshot + ui.commands.<id>.execute.
  - Comments sidebar bound to ui.comments.subscribe + resolve /
    reopen / scrollTo / createFromSelection.
  - Review sidebar (merged comments + tracked changes feed) bound to
    ui.review.subscribe + accept / reject / next / previous /
    scrollTo.
  - Custom command via ui.commands.register({...}) β€” a legal-tech
    "Insert Clause" button with a hardcoded local clause library.
    Registered from its own component, not at boot.
  - useSuperDocSlice(pickSubscribable, initial) glue hook in
    SuperDocUIProvider.tsx β€” consumers will copy this.

Architecture

  - SuperDocUIProvider holds exactly one controller per app, created
    on the first onReady, destroyed on unmount.
  - Components consume the controller via useSuperDocUI() (returns
    null until the editor is ready) and useSuperDocSlice for typed
    snapshot bindings via ui.select(...).
  - No EditorAdapter abstraction. No TipTap dependency. No UI kit
    dependency. No backend. No AI provider. No direct ProseMirror
    access.

README leads with "build your own UI" and includes a "what this
intentionally does not do" section so the architectural posture is
explicit.

Verified in browser (port 5189):
  - Editor loads sample-review.docx with comments + tracked changes
  - Toolbar reflects active mark state
  - Comments sidebar shows 3 cards with resolve / scroll-to
  - Review tab shows 3 comments + 3 insertion changes with accept /
    reject and previous / next
  - Insert clause menu opens with three clauses
caio-pizzol added a commit that referenced this pull request Apr 29, 2026
Public-facing example showing how a consumer wires their own toolbar,
comments sidebar, review sidebar, and custom toolbar commands to
SuperDoc through createSuperDocUI({ superdoc }).

Replaces the earlier dropin-assessment harness (closed PR #3006). The
TipTap-vs-SuperDoc EditorAdapter shape was useful for internal
validation but teaches the wrong mental model: consumers don't wrap
SuperDoc to make it look like TipTap, they bind their UI to the
controller. The new example reflects that posture.

What it demonstrates

  - <SuperDocEditor> mounted inside a custom three-pane layout
    (contained + hideToolbar) so the wrapper doesn't take over the
    page.
  - Custom toolbar (bold/italic/underline, undo/redo, lists, comment)
    bound to ui.toolbar snapshot + ui.commands.<id>.execute.
  - Comments sidebar bound to ui.comments.subscribe + resolve /
    reopen / scrollTo / createFromSelection.
  - Review sidebar (merged comments + tracked changes feed) bound to
    ui.review.subscribe + accept / reject / next / previous /
    scrollTo.
  - Custom command via ui.commands.register({...}) β€” a legal-tech
    "Insert Clause" button with a hardcoded local clause library.
    Registered from its own component, not at boot.
  - useSuperDocSlice(pickSubscribable, initial) glue hook in
    SuperDocUIProvider.tsx β€” consumers will copy this.

Architecture

  - SuperDocUIProvider holds exactly one controller per app, created
    on the first onReady, destroyed on unmount.
  - Components consume the controller via useSuperDocUI() (returns
    null until the editor is ready) and useSuperDocSlice for typed
    snapshot bindings via ui.select(...).
  - No EditorAdapter abstraction. No TipTap dependency. No UI kit
    dependency. No backend. No AI provider. No direct ProseMirror
    access.

README leads with "build your own UI" and includes a "what this
intentionally does not do" section so the architectural posture is
explicit.

Verified in browser (port 5189):
  - Editor loads sample-review.docx with comments + tracked changes
  - Toolbar reflects active mark state
  - Comments sidebar shows 3 cards with resolve / scroll-to
  - Review tab shows 3 comments + 3 insertion changes with accept /
    reject and previous / next
  - Insert clause menu opens with three clauses
caio-pizzol added a commit that referenced this pull request Apr 30, 2026
Public-facing example showing how a consumer wires their own toolbar,
comments sidebar, review sidebar, and custom toolbar commands to
SuperDoc through createSuperDocUI({ superdoc }).

Replaces the earlier dropin-assessment harness (closed PR #3006). The
TipTap-vs-SuperDoc EditorAdapter shape was useful for internal
validation but teaches the wrong mental model: consumers don't wrap
SuperDoc to make it look like TipTap, they bind their UI to the
controller. The new example reflects that posture.

What it demonstrates

  - <SuperDocEditor> mounted inside a custom three-pane layout
    (contained + hideToolbar) so the wrapper doesn't take over the
    page.
  - Custom toolbar (bold/italic/underline, undo/redo, lists, comment)
    bound to ui.toolbar snapshot + ui.commands.<id>.execute.
  - Comments sidebar bound to ui.comments.subscribe + resolve /
    reopen / scrollTo / createFromSelection.
  - Review sidebar (merged comments + tracked changes feed) bound to
    ui.review.subscribe + accept / reject / next / previous /
    scrollTo.
  - Custom command via ui.commands.register({...}) β€” a legal-tech
    "Insert Clause" button with a hardcoded local clause library.
    Registered from its own component, not at boot.
  - useSuperDocSlice(pickSubscribable, initial) glue hook in
    SuperDocUIProvider.tsx β€” consumers will copy this.

Architecture

  - SuperDocUIProvider holds exactly one controller per app, created
    on the first onReady, destroyed on unmount.
  - Components consume the controller via useSuperDocUI() (returns
    null until the editor is ready) and useSuperDocSlice for typed
    snapshot bindings via ui.select(...).
  - No EditorAdapter abstraction. No TipTap dependency. No UI kit
    dependency. No backend. No AI provider. No direct ProseMirror
    access.

README leads with "build your own UI" and includes a "what this
intentionally does not do" section so the architectural posture is
explicit.

Verified in browser (port 5189):
  - Editor loads sample-review.docx with comments + tracked changes
  - Toolbar reflects active mark state
  - Comments sidebar shows 3 cards with resolve / scroll-to
  - Review tab shows 3 comments + 3 insertion changes with accept /
    reject and previous / next
  - Insert clause menu opens with three clauses
caio-pizzol added a commit that referenced this pull request Apr 30, 2026
…3008)

* feat(examples): build-your-own-ui example app on superdoc/ui (SD-2671)

Public-facing example showing how a consumer wires their own toolbar,
comments sidebar, review sidebar, and custom toolbar commands to
SuperDoc through createSuperDocUI({ superdoc }).

Replaces the earlier dropin-assessment harness (closed PR #3006). The
TipTap-vs-SuperDoc EditorAdapter shape was useful for internal
validation but teaches the wrong mental model: consumers don't wrap
SuperDoc to make it look like TipTap, they bind their UI to the
controller. The new example reflects that posture.

What it demonstrates

  - <SuperDocEditor> mounted inside a custom three-pane layout
    (contained + hideToolbar) so the wrapper doesn't take over the
    page.
  - Custom toolbar (bold/italic/underline, undo/redo, lists, comment)
    bound to ui.toolbar snapshot + ui.commands.<id>.execute.
  - Comments sidebar bound to ui.comments.subscribe + resolve /
    reopen / scrollTo / createFromSelection.
  - Review sidebar (merged comments + tracked changes feed) bound to
    ui.review.subscribe + accept / reject / next / previous /
    scrollTo.
  - Custom command via ui.commands.register({...}) β€” a legal-tech
    "Insert Clause" button with a hardcoded local clause library.
    Registered from its own component, not at boot.
  - useSuperDocSlice(pickSubscribable, initial) glue hook in
    SuperDocUIProvider.tsx β€” consumers will copy this.

Architecture

  - SuperDocUIProvider holds exactly one controller per app, created
    on the first onReady, destroyed on unmount.
  - Components consume the controller via useSuperDocUI() (returns
    null until the editor is ready) and useSuperDocSlice for typed
    snapshot bindings via ui.select(...).
  - No EditorAdapter abstraction. No TipTap dependency. No UI kit
    dependency. No backend. No AI provider. No direct ProseMirror
    access.

README leads with "build your own UI" and includes a "what this
intentionally does not do" section so the architectural posture is
explicit.

Verified in browser (port 5189):
  - Editor loads sample-review.docx with comments + tracked changes
  - Toolbar reflects active mark state
  - Comments sidebar shows 3 cards with resolve / scroll-to
  - Review tab shows 3 comments + 3 insertion changes with accept /
    reject and previous / next
  - Insert clause menu opens with three clauses

* refactor(examples): unified Activity sidebar, public-API custom command, app polish (SD-2671)

Iterates on the build-your-own-ui example based on UX review:

  - Move to `examples/advanced/build-your-own-ui/`. The taxonomy is
    examples/ for narrow how-tos, demos/ for full workflows, labs/
    for internal artifacts. This combines several public primitives
    (toolbar, comments, review, custom command) so it lives under
    examples/advanced/.

  - Replace the dual Comments/Review tabs with a single Activity panel
    rendering the merged ui.review feed. Cards split into Active /
    Resolved sections; resolved comments dim with a strikethrough on
    body text; clicking any card scrolls the editor to its anchor (no
    explicit "Scroll to" button needed).

  - Wire active-card highlight to the document selection. SD-2792
    already exposes activeCommentIds / activeChangeIds on the
    selection slice; the panel watches them and auto-scrolls the
    matching card into view. No extra event needed from the
    controller.

  - Mimic the Google Docs accept-to-resolved trail: when the user
    clicks Accept or Reject on a tracked change, capture a snapshot
    locally before the doc-api call (the change vanishes from the
    live `ui.review` feed once decided) and render it in the
    Resolved section with a "Suggestion accepted/rejected" footer.
    State is component-local; refresh wipes it, which is fine for
    a demo.

  - Replace the toolbar's `window.prompt` comment flow with an
    inline `<CommentComposer>` in the activity panel. The toolbar's
    comment button delegates open-state to App-root via prop; the
    composer captures the current selection target on submit and
    posts via `ui.comments.createFromSelection({ text })`.

  - Custom command (Insert clause) now uses public APIs only:
    routes through `editor.doc.insert({ value, type: 'text', target })`
    instead of `editor.commands.insertContent`. The doc-api expects
    a SelectionTarget (kind: 'selection' with start/end points), so
    the example shows the conversion from `ui.selection`'s
    `TextTarget` inline. Verified end-to-end: selecting a clause
    inserts the paragraph at the cursor.

  - Custom command observes its own state via `reg.handle.observe(...)`
    instead of duplicating the readiness logic locally. The button's
    disabled state now flows from the registered command's snapshot,
    proving custom commands are first-class citizens of `ui.commands`.

  - Fix provider unmount destroy bug. The previous
    `useEffect(() => () => ui?.destroy(), [])` captured the initial
    null. Switched to a ref so the cleanup walks the latest
    controller without re-running the effect on every change.

  - Add `useSuperDocHost()` to the provider for operations not on the
    controller surface (currently: `superdoc.export({...})`). Toolbar
    grows an Export DOCX button so the user can confirm comments,
    tracked-change decisions, and inserted clauses round-trip into
    the downloaded file.

  - Rename in-app title from "SuperDoc β€” Build your own UI" to
    "Contract Review Workspace" so the running app feels like a
    consumer product, not a labeled demo. Explanatory copy stays in
    the README.

  - Fix undo/redo icons (the previous Lucide path data rendered as
    incomplete circles).

Verified in the dev app at port 5189:
  - Toolbar reflects mark state, undo/redo render correctly
  - Inline composer creates comments anchored to the selection
  - Click-card scrolls editor; cursor in document highlights card
  - Accept/reject moves the change to the Resolved section with the
    Google Docs-style audit row
  - Insert clause adds the clause text at the cursor and the
    document remains exportable to DOCX

Closes SD-2802 friction in the example app (custom command is now
purely public-API). Closes the open feedback from the model review.

* refactor(examples): migrate BYO-UI to PR #3010 official APIs (SD-2671)

PR #3010 ships the public surface this example was previously
building glue against:

- `superdoc/ui/react`: official `SuperDocUIProvider` + typed domain
  hooks. Drops the local `lib/SuperDocUIProvider.tsx` (158 lines)
  and the ad-hoc `useSuperDocSlice` calls in every component.
- `state.selection.selectionTarget`: pre-derived SelectionTarget for
  point/range doc-api operations. `InsertClauseButton` drops the
  inline `TextTarget -> SelectionTarget` lift; just passes the slice
  field straight to `editor.doc.insert({ target })`.
- `ui.commands.get(id)`: typed dynamic command lookup. The
  `Toolbar` no longer casts `(ui.commands as Record<string, ...>)[id]`
  to call a string-keyed handle.
- `useSuperDocCommand(id)`: per-button granular subscription. The
  toolbar's built-in buttons each subscribe to ONLY their own
  command's state, instead of a single `useSuperDocSlice` over the
  whole toolbar snapshot.
- `ui.selection.capture()`: addresses the sidebar-composer focus
  loss the example previously punted on. `CommentComposer` now
  freezes the selection at mount and posts against
  `captured.target` instead of a live read that's null while the
  textarea has focus.

The CommentComposer reaches `editor.doc.comments.create` through the
host (`useSuperDocHost`) until `ui.comments.createFromCapture` lands
as a typed action method. The escape-hatch is documented inline.

The example app builds cleanly against the rebased SD-2812 stack.

* fix(superdoc/ui): revert SelectionCapture readonly type (consumer-facing friction)

The DeepReadonly typing forced a cast at every `editor.doc.*` call site
that consumes a captured target β€” the canonical use case for capture.
The runtime deep-freeze plus the existing regression test catch
mutation attempts; the static type signal isn't worth the per-call
cast tax in consumer code.

Walking the BYO UI example exposed this: `editor.doc.comments.create({
target: captured.target })` failed because TextTarget's
`[TextSegment, ...TextSegment[]]` non-empty tuple isn't assignable from
`readonly TextSegment[]`. Forcing every consumer to write
`captured.target as TextTarget` is hostile DX for a public surface.

* fix(examples): reconcile decidedChanges with live review feed (PR #3008 review)

The local `decidedChanges` map was populated when the user clicked
accept / reject but never reconciled against `review.items`. If a
change came back into the live feed (undo of the decision,
collaborator restore, etc.), the same id rendered in BOTH the
Active and Resolved sections with a stale "accepted" / "rejected"
label.

Added an effect keyed on `review.items` that prunes any decided
entry whose id is back in the live feed. Single-pass, no-op fast
path when the prev map is empty or no overlap is found.

* fix(examples): export DOCX preserves imported comments (PR #3008 review)

Setting `modules: { comments: false }` on the React wrapper to hide
SuperDoc's built-in floating-comment UI also short-circuits comment
ingest at the data layer (`packages/superdoc/src/composables/use-document.js`
line 88: when the flag is falsy, `initConversations()` returns []).
The commentsStore stays empty, and `host.export({ commentsType:
'external' })` then writes that empty list into the DOCX, dropping
every imported comment from the round-trip.

The user reported it directly: "default comments and tracked changes
displayed in the editor are not persisted in the export." The bot
review caught the same root cause.

Removed the flag. The default config loads imported comments, the
export round-trips correctly, and the built-in floating UI stays
hidden via the `contained` layout the example already uses.

Tracked changes were already fine (they live as PM marks on the doc
and round-trip through `editor.exportDocx` regardless); this fix
restores the comments half of the round-trip. A follow-up ticket
should add a "hide UI without disabling storage" option so consumers
who really want to suppress the built-in floating UI in non-contained
layouts have a non-destructive switch.

* fix(superdoc): export reads engine comments when UI store is empty (PR #3008 review)

Decouples the DOCX comment-export round-trip from `modules.comments`'s
UI flag. The previous behavior conflated two responsibilities into the
same export path:

  - `Editor.exportDocx({ comments })` already had the right contract:
    `effectiveComments = comments ?? this.converter.comments ?? []`,
    falling back to the engine's imported comments when the caller
    passes `undefined`.
  - `SuperDoc.exportEditorsToDOCX` always passed a defined `comments`
    array (often `[]`), which silently overrode that fallback.

When a consumer set `modules.comments: false` to hide SuperDoc's
built-in floating comment UI, the UI commentsStore stayed empty and
the export wrote `comments: []` to `Editor.exportDocx`, dropping
every comment imported from the source DOCX. Imports survived in
`editor.converter.comments`; the export just refused to read them.

The fix is in the export adapter, not in the import gate. Comments
are now only passed through when the UI store has them OR when
`commentsType: 'clean'` explicitly demands stripping; otherwise pass
`undefined` and let the engine fall back to its own imported set.

Three regression tests pin the boundary:
  - empty UI store + default `commentsType` => `comments: undefined`
    so the engine's converter-comments fallback fires
  - `commentsType: 'clean'` => `comments: []` regardless of UI store
  - non-empty UI store => UI snapshot wins

Restored `modules: { comments: false }` in the BYO UI example to
demonstrate the canonical "hide built-in UI, drive comments through
ui.comments" pattern. Documented the data-vs-UI split inline so a
reader of the example understands why the flag is safe to set now.

A broader architectural cleanup (split `commentsDataEnabled` from
`builtInCommentsUiEnabled`, remove the UI-flag gate from
`Editor.#initComments` and the collaboration sync helpers) is a
follow-up; this commit closes the consumer-visible export bug
without that scope.

* test(superdoc): pin export-comments contract at both layers

Reviewer flagged a real ambiguity in the previous patch: an empty
UI commentsStore meant either "store unhydrated, fall back to
converter.comments" or "store hydrated and user deleted everything,
authoritative empty." The fix now branches on `modules.comments
=== false` to distinguish those, but the regression coverage was
thin. This commit pins the boundary at both layers.

SuperDoc adapter (packages/superdoc/src/core/SuperDoc.test.js):

  1. modules.comments: false + UI store empty -> comments: undefined
     (BYO consumers keep imported comments via engine fallback).
  2. modules.comments: enabled + UI store hydrated and empty ->
     comments: [] (deletion does NOT resurrect imports β€” this is
     the bug the reviewer caught).
  3. UI store has entries -> entries pass through.
  4. commentsType: 'clean' overrides everything -> comments: [].
  5. commentsType: 'clean' even with modules.comments: false ->
     comments: [] (clean must beat the BYO fallback).
  6. Missing commentsStore (race / partial init) -> comments:
     undefined and no throw.

Editor engine (packages/super-editor/src/editors/v1/tests/export/
exportDocx.commentsFallback.test.js):

  1. exportDocx() with no caller comments -> spy on
     `converter.exportToDocx` confirms the engine fell back to
     `converter.comments`.
  2. exportDocx({ comments: [] }) -> spy confirms zero comments
     reached the writer (no `??` -> `||` regression).
  3. exportDocx({ comments: [...] }) -> caller array is the source
     of truth.
  4. converter.comments null/undefined -> resolves to [] without
     throwing.

The Editor-level test set is the contract layer the SuperDoc
adapter relies on. Pinning both is the only way to keep the
"empty means different things in different layers" invariant
honest under future refactors.

* feat(examples): reimport-DOCX button for round-trip testing (SD-2671)

Adds "Reimport DOCX" next to the Export button. The user exports a
DOCX, opens it in Word (or anything that emits OOXML), edits /
comments / accept-rejects there, then reimports the modified file.
The Activity sidebar updates automatically because:

- Tracked changes live as PM marks; `replaceFile` swaps the doc
  and fires `transaction`, which the controller already listens to.
  `ui.review` re-emits its merged feed on the next microtask.
- Imported comments end up in `editor.converter.comments` after
  `replaceFile` runs `#createConverter`. The controller normally
  refreshes its `ui.comments` cache on the editor's `commentsLoaded`
  event, but with `modules.comments: false` (our BYO posture),
  `Editor.#initComments()` short-circuits and never emits. Until
  SD-2839 splits "comment data" from "comment UI" properly, the
  button manually re-emits `commentsLoaded` after `replaceFile`
  resolves so the controller picks up the new converter.comments.

The manual emit is documented inline as a workaround tied to a
specific Linear issue. Once SD-2839 lands, the emit becomes a
no-op (the platform will emit on its own) and can be removed
without changing the example's user-visible behavior.

This unblocks the canonical demo flow: export, edit in Word, see
the changes show up here. Tracked changes round-trip without any
manual help β€” the test from earlier (deleting "ordinary course"
in suggesting mode) demonstrated the export side; this completes
the loop.

* refactor(examples): shorten Toolbar button labels to Import / Export (SD-2671)

* feat(examples): thread comment replies and split paired replacements (SD-2671)

* fix(presentation-editor): honor caller scroll behavior in navigateTo (SD-2671)

* chore(byo-ui): final pass β€” viewport test, README rewrite, EOF whitespace (SD-2671)

* docs(byo-ui): replace em dashes per project style (SD-2671)

* docs(byo-ui): tighten README to brand voice (SD-2671)

* refactor(byo-ui): move to demos and wire smoke test (SD-2671)

* chore(byo-ui): drop unused sample.docx (SD-2671)

* feat(ui): ui.comments.createFromCapture for selection-snapshot composers (SD-2817)

* feat(byo-ui): edit / suggest mode toggle for tracked-change workflow (SD-2671)

* refactor(byo-ui): use exported SuperDoc types end-to-end (SD-2671)

* docs(byo-ui): drop README Types section as internal trivia (SD-2671)

* chore: standardize AGENTS.md as symlink to CLAUDE.md (SD-2671)

* ci(demos): build @superdoc-dev/react before smoke tests (SD-2671)

* refactor(byo-ui): use ui.document for mode toggle and export (SD-2671)

* feat(ui): ui.document.replaceFile + useSuperDocDocument hook (SD-2671)

* feat(byo-ui): inline reply composer on comment cards (SD-2671)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants