feat(ui): add ui.document.dirty for unsaved-changes indicators (SD-2667)#3040
feat(ui): add ui.document.dirty for unsaved-changes indicators (SD-2667)#3040caio-pizzol merged 3 commits intomainfrom
Conversation
DocumentSlice gains a dirty: boolean field driven by editor transactions. Flips to true on any transaction with tr.docChanged; clears on a successful ui.document.export(...) or ui.document.replaceFile(). Selection-only transactions don't move it. - Internal flag tracked alongside documentMemo so a flag flip busts the memo without re-allocating on typing-only events. - Editor swap (replaceFile, document switch) resets dirty to false in attachEditorListeners so the new document opens clean. - Rejected export() leaves dirty alone; the consumer can retry. - Undo-to-clean is intentionally out of scope β apps that need Word/GDocs 'no unsaved changes' semantics layer their own edit-count diff on top. - Hook EMPTY_DOCUMENT fallback updated. - 7 new tests covering: starts false; flips on docChanged; stays false on selection-only; subscribers re-fire on flip; export clears; rejected export preserves; replaceFile clears.
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9e260a5e75
βΉοΈ 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".
Codecov Reportβ All modified and coverable lines are covered by tests. π’ Thoughts on this report? Let us know! |
Wires the new ui.document.dirty flag into the demo's Export button so the docs page has a concrete reference for the pattern. The button shows a small amber dot and a tooltip suffix when dirty is true, then clears once export() resolves.
attachEditorListeners() runs on both editorCreate (new document) and on activeSurfaceChange (body β header / footer / footnote routing within the same document). Resetting dirty in that helper meant a user could edit the body, click into a header, and lose the unsaved-changes signal even though nothing was saved. Move the reset out of attachEditorListeners and into a dedicated editorCreate handler. ui.document.replaceFile() still clears dirty explicitly (defensive). New test covers the editorCreate path. Caught by chatgpt-codex on PR #3040 review.
β¦, #3040 Three workarounds drop now that the controller surface covers them: - comments.mdx: post a reply via ui.comments.reply(parentId, { text }) instead of useSuperDocHost() + editor.doc.comments.create. - custom-commands.mdx: drop the structural cast on superdoc.activeEditor; use the typed editor argument that execute({ payload, superdoc, editor }) now provides. Pattern B's chain-command cast narrowed and called out. - document-control.mdx: drop the transaction-listener snippet; use useSuperDocDocument().dirty directly. Includes the export-button example pattern. Trade-offs trimmed accordingly. API reference updated: useSuperDocDocument now returns dirty; ui.commands.register() execute receives editor; ui.comments.reply listed in the comments handle block.
β¦, #3040 Three workarounds drop now that the controller surface covers them: - comments.mdx: post a reply via ui.comments.reply(parentId, { text }) instead of useSuperDocHost() + editor.doc.comments.create. - custom-commands.mdx: drop the structural cast on superdoc.activeEditor; use the typed editor argument that execute({ payload, superdoc, editor }) now provides. Pattern B's chain-command cast narrowed and called out. - document-control.mdx: drop the transaction-listener snippet; use useSuperDocDocument().dirty directly. Includes the export-button example pattern. Trade-offs trimmed accordingly. API reference updated: useSuperDocDocument now returns dirty; ui.commands.register() execute receives editor; ui.comments.reply listed in the comments handle block.
|
π This PR is included in @superdoc-dev/mcp v0.3.0-next.26 The release is available on GitHub release |
|
π This PR is included in vscode-ext v2.3.0-next.71 |
|
π This PR is included in @superdoc-dev/react v1.2.0-next.69 The release is available on GitHub release |
|
π This PR is included in superdoc v1.30.0-next.28 The release is available on GitHub release |
|
π This PR is included in superdoc-cli v0.8.0-next.44 The release is available on GitHub release |
|
π This PR is included in superdoc-sdk v1.8.0-next.30 |
β¦667) (#3043) The JSDoc on SuperDocUI.document still claimed unsaved-changes was intentionally not on the slice. That landed in #3040 as DocumentSlice.dirty. The stale JSDoc misled at least one external reviewer into flagging the docs as documenting a non-existent field; update the JSDoc so the source is accurate.
β¦odules (SD-2669) (#3034) * docs(byo-ui): add Build Your Own UI section nav and stub pages (SD-2669) Adds a new top-level "Build Your Own UI" group between Modules and Solutions in docs.json, plus 10 stub pages mapping one-to-one to controller domains: overview, react-setup, toolbar-and-commands, custom-commands, comments, review, selection-and-viewport, document-control, reference-demo, api-reference. The stub pages include frontmatter, a one-line value statement, and a Note callout pointing at SD-2669 for status. Stubs land first so cross-doc callouts on the existing Modules pages can link to stable URLs while the content is being written. Overview is filled in to anchor the layer model (Document API / superdoc/ui / built-in modules) and link to every page in the section. * docs(byo-ui): align overview and stubs with brand voice (SD-2669) Rewrites the overview to lead with one sentence, the layer model table, and a 15-line code snippet showing useSuperDocCommand bound to a button. Adds icons to the CardGroup so the section index reads at a glance. Sweeps em dashes from every stub. Brand voice rules ban them in favor of hyphens, periods, or split sentences. Adds superdoc/ui and superdoc/ui/react to the docs import allowlist so future pages in this section pass the import validator. Mintlify validate passes. * docs(byo-ui): write 8 BYO-UI section pages (SD-2669) Fills in: - React setup: provider, onReady, hooks table, common pitfalls - Toolbar and commands: per-button binding, payloads, built-in id table - Custom commands: register, getState, execute, override, namespacing - Comments: list, add from selection, capture-based composer, threads - Selection and viewport: selection slice, capture, scrollIntoView, getRect - Document control: setMode (Edit/Suggest), export, replaceFile - Reference demo: what it covers, how to read it, what it deliberately doesn't do - API reference: handwritten skeleton mirroring page order review.mdx and api-reference.mdx will be revisited after the ui.review β ui.trackChanges rename lands as a separate PR. All pages follow brand voice rules: code-first, short sentences, no em dashes, public APIs only. Trade-offs called out per page. Mintlify validate passes; import validator green. * docs(byo-ui): rename Review page to Track changes, drop merged-feed surface Mirrors the controller-side rename in #3029 / superdoc 1.30.1: 'ui.review' is gone and 'ui.trackChanges' takes its place. - Rename review.mdx β track-changes.mdx and rewrite for the tracked-changes-only surface (items, total, activeId). - Document the merged comments + changes feed as a consumer-side composition pattern with useSuperDocComments + useSuperDocTrackChanges. - Update docs.json nav, overview card grid, react-setup hook table, api-reference handle + types, and reference-demo coverage table. * docs: cross-link built-in modules to Build Your Own UI Adds <Tip> callouts on the four pages a consumer hits before they realize there's a custom-UI path: modules/comments, modules/track-changes, modules/toolbar/headless, and core/react/overview. Each one routes them to the relevant Build Your Own UI page so they don't fight the built-in module before discovering useSuperDocComments / useSuperDocTrackChanges / useSuperDocCommand. * docs(byo-ui): address review feedback on canonical pages - Custom commands: rewrite execute example to use the public ui.selection.getSnapshot().selectionTarget shape (was passing the wrong target type to editor.doc.insert). Lead with a pure-additive pattern; mark the activeEditor reach as a known escape hatch. - Custom commands: split override into two patterns. Pattern A (sibling + dispatch both) is the default. Pattern B (true override) now actually toggles the mark instead of silently discarding it. - Comments: de-emphasize the reply pattern. Group-by-parent shape is public; posting a reply is filed as a follow-up and points to the reference demo as a temporary workaround. - Track changes: move the merged-feed pattern out of the canonical page and into reference-demo as 'one app-level composition'. Keeps the controller surface tight. - Drop SSO-gated linear.app URLs from public-facing docs; keep ticket IDs as plain text. * docs(byo-ui): replace ticket references with copy-safe workarounds Public docs shouldn't promise specific follow-up tickets readers can't see. Each former 'tracked under SD-XXXX' note now describes either how the surface actually works today or a workaround that ships. - comments: post replies via useSuperDocHost() + editor.doc.comments.create (full snippet, not just a pointer to the demo). - custom-commands: drop the 'when SD-2844 lands' framing; the scoped structural cast is the pattern. - selection-and-viewport / track-changes: explain that non-body entities snap because story activation mounts the surface synchronously, and show how to layer your own smooth scroll if needed. - reference-demo: show window.getSelection() rect lookup as the path for floating menus today. - document-control: full transaction-listener snippet for a dirty indicator. * docs(byo-ui): use the typed controller APIs that landed in #3035, #3039, #3040 Three workarounds drop now that the controller surface covers them: - comments.mdx: post a reply via ui.comments.reply(parentId, { text }) instead of useSuperDocHost() + editor.doc.comments.create. - custom-commands.mdx: drop the structural cast on superdoc.activeEditor; use the typed editor argument that execute({ payload, superdoc, editor }) now provides. Pattern B's chain-command cast narrowed and called out. - document-control.mdx: drop the transaction-listener snippet; use useSuperDocDocument().dirty directly. Includes the export-button example pattern. Trade-offs trimmed accordingly. API reference updated: useSuperDocDocument now returns dirty; ui.commands.register() execute receives editor; ui.comments.reply listed in the comments handle block. * docs(byo-ui): address codex review on PR #3034 - api-reference.mdx: ui.toolbar.execute(id, payload?) was signature syntax, not valid TS. Split into two valid call forms with prose. - core/react/overview.mdx: drop the <Tip> block per AGENTS.md rule ('Don't add Tips, Warnings, or deep explanations in overview pages'). Cross-link kept as plain inline paragraph. * docs(byo-ui): drop em dashes per brand style * docs(byo-ui): correct toolbar ids and comment scrollTo scope Two factual fixes flagged in PR #3034 review: - toolbar-and-commands.mdx: the built-in command id table listed several ids that aren't on PublicToolbarItemId. strike β strikethrough, color β text-color, highlight β highlight-color, and the entire 'Block' row (heading-1/-2/-3, paragraph) is dropped because no block-level commands exist on this surface today. Replaced the table with the actual id set grouped by domain. - comments.mdx: the trade-off note claimed ui.comments.scrollTo is story-aware. CommentAddress is body-scoped in the contract β only TrackedChangeAddress carries a story field β so the call doesn't navigate to header/footer/note comments. Replaced with an honest description and pointer to ui.viewport.scrollIntoView for non-body cases. * refactor: rename build-your-own-ui to bring-your-own-ui (SD-2669) The BYO acronym is conventional for 'Bring Your Own X' (BYOK, BYOC, BYOD). It also reads more accurately to what consumers actually do: they bring their existing React app + design system and slot SuperDoc behind it, rather than building a UI from scratch. - Section title in docs.json: 'Build Your Own UI' becomes 'Bring Your Own UI'. - URL slugs: /build-your-own-ui/* becomes /bring-your-own-ui/*. - apps/docs/build-your-own-ui/ renamed to apps/docs/bring-your-own-ui/ (10 mdx pages, all internal links updated). - demos/build-your-own-ui/ renamed to demos/bring-your-own-ui/. Demo's package.json name, README heading, the docs reference-demo links, the playwright port-map key, and the ci-demos.yml matrix entry all follow. - Cross-link callouts on /modules/comments, /modules/track-changes, /modules/toolbar/headless, /core/react/overview reworded. * docs: integrate Bring Your Own UI into the broader docs (SD-2669) Fixes stale correctness issues and lays out the layer model so existing docs route customers to the right path. Stage 1 β correctness fixes on BYO pages - bring-your-own-ui/reference-demo: drop the 'demo reaches through useSuperDocHost()' prose; replies and custom commands now use the typed surface after #3035 and #3039 shipped. - bring-your-own-ui/custom-commands: Pattern B (override) reframed as an advanced escape hatch with a Warning, since override truly replaces and consumers shouldn't trust the built-in to still run. - modules/track-changes: drop 'merged comments + changes feed' from the BYO callout; the merged-feed pattern moved to reference-demo as an app-level composition. Stage 2 β toolbar IA cleanup - modules/toolbar/overview: replace the two-path table with a three-path decision: Built-in / Bring Your Own UI / Headless toolbar. Recommendation: new React apps reach for BYO first; headless stays supported for non-React and existing integrations. - modules/toolbar/headless: promote the BYO redirect from a Tip to a 'Using React?' section with the supported-vs-recommended framing. - modules/toolbar/examples: drop the React + shadcn and React + MUI examples; redirect React readers to BYO. Vue / Svelte / vanilla examples stay. Stage 3 β onboarding links - getting-started/frameworks/react: add BYO card to Next steps. - getting-started/quickstart: add BYO link inline under the React scenario plus a card in 'What's next'. Stage 4 β authoring guidance + LLM context - AGENTS.md: new 'Layer model' section with explicit recommendation order (Document API > superdoc/ui > Modules > Core). API naming marks superdoc.activeEditor.commands.* as legacy/compat. Source file table extended with the doc-api contract and superdoc/ui. - llms.txt: add Bring Your Own UI link plus a Layer Model section so agents recommend the right surface. Stage 5 β disambiguate track-changes pages without URL renames - modules/track-changes: title 'Track changes module', sidebarTitle 'Track changes module'. - bring-your-own-ui/track-changes: title 'Custom track changes UI', sidebarTitle stays 'Track changes' for nav consistency. * docs: route live-state events to BYO UI; expand llms-full layer model - core/superdoc/events.mdx: leading Note tells React consumers to prefer superdoc/ui subscriptions over manual superdoc.on(...) loops for live UI state. Lifecycle / integration / analytics events stay the documented use case. - llms-full.txt: new Layer Model section after Architecture so agents recommend Document API for mutations, superdoc/ui for custom React UI, and modules for built-in UI. Lists the four common anti-patterns to steer clear of in customer code. * docs: route legacy paths to typed surfaces (SD-2669) - modules/comments: lead the 'API methods' section with a Note that steers new code to editor.doc.comments.* (mutations) or ui.comments.* (React UI). The activeEditor.commands.* docs stay for backwards compatibility but no longer present as the recommended path. - core/supereditor/overview: was framing SuperEditor as 'Custom UI implementation' and claimed Document API was 'coming soon'. Both outdated. Rewrote 'When to use' as a layer-picker that points custom React UI at Bring Your Own UI, document mutations at the Document API, and SuperEditor at the genuine low-level cases (engine internals, custom extensions, server-side headless). * docs: add BYO UI pointers in core methods + doc-api migration guide - core/superdoc/methods: lead the Comments-methods section and the activeEditor property docs with a Note steering new code to ui.comments.* / editor.doc.* / ui.commands.*. addCommentsList / removeCommentsList stay supported for built-in sidebar users; activeEditor.commands stays for backwards compat. - guides/migration/document-api: append a 'Driving custom React UI' section. Migrators dropping editor.commands/state/view from doc mutations also benefit from migrating the UI side to typed hooks in the same pass. Points at the BYO UI overview and the demo. * docs: route remaining built-in callouts to typed surfaces - getting-started/import-export: post-init insertContent example steers new code to editor.doc.insert; chain command stays for apps already wired to activeEditor.commands.* - modules/toolbar/built-in: top-of-page Tip routes React readers to Bring Your Own UI first; headless stays as the non-React alternative. * docs: add four Mermaid diagrams to anchor BYO UI conceptually - bring-your-own-ui/overview: layer-model diagram showing how Your UI β superdoc/ui/react β superdoc/ui β Document API β engine, with built-in modules as a sibling path also routed through Document API. - bring-your-own-ui/react-setup: provider-tree diagram showing one provider, one controller, many components subscribing per slice. - bring-your-own-ui/comments: capture-sequence diagram for the textarea- composer flow (select, click Comment, capture, focus moves, submit posts via createFromCapture). - getting-started/quickstart: pick-your-surface decision flow at the top of the page so visitors route to AI agents / backend / built-in editor / Bring Your Own UI without reading the section headings. * docs(byo-ui): drop the three BYO Mermaid diagrams Felt too much for the BYO surface. Removing the layer-model, provider-tree, and capture-sequence diagrams. Visual story for BYO will land via media assets and embedded demos in a follow-up, not diagrams. Quickstart 'pick your surface' diagram stays β it's site-wide orientation, not BYO-specific. * docs(byo-ui): add embedded BYO UI demo on overview page New snippet at apps/docs/snippets/components/byo-ui-demo.jsx renders a compact 'workspace slice' driven by superdoc/ui: - Custom toolbar (B / I / Comment / Edit-Suggest / Export) wired to ui.commands observables, ui.document.setMode, ui.document.export. - Activity sidebar shows live ui.comments and ui.trackChanges with Resolve / Accept / Reject actions. Empty state with hints when no comments or changes exist. - Selection-capture composer for new comments via ui.selection.capture() + ui.comments.createFromCapture(). - Try-it checklist above the workspace. Loading: lazy behind a 'Launch interactive demo' button so the 5 MB SuperDoc UMD only loads on user gesture. Editor comes from the existing UMD pattern (window.SuperDoc); controller comes from a dynamic import() of superdoc/ui's ESM entry. The embed bridges controller observables into local React state instead of using superdoc/ui/react β that bundle imports React as a bare ESM specifier, which is fragile in Mintlify's runtime. Caption text points React app users at the typed hooks. Pinned to superdoc@1.30.1 (deterministic; floats forward only when we update the snippet). v1 ships overview only; per-domain variants (comments, track-changes, custom-command) come in follow-ups. * docs(byo-ui): revert embed from overview, hits Mintlify runtime issues Two failures on the deployed preview: 1. MDX runtime can't resolve capital-letter JSX references inside a snippet ('Expected component Workspace to be defined'). The helper functions inside byo-ui-demo.jsx need to be inlined into one big component, no nested <Workspace />, <ToolButton />, etc. 2. The SuperDoc UMD throws 'Identifier fs has already been declared' on injection, suggesting it's being loaded twice. Likely a baseUrl mismatch with the existing SuperDocEditor snippet on extension pages β different URL strings dodge the dedup querySelector. Per the agreed fallback plan, don't block #3034 on the embed. The snippet file stays at apps/docs/snippets/components/byo-ui-demo.jsx for the follow-up PR where we can debug Mintlify's runtime quirks without delaying the docs launch. * docs(byo-ui): tighten sidebar β group toolbar, drop reference-demo, drop embed snippet Sidebar IA cleanup. Customer-facing surface goes from 10 entries to 8 (one of which expands to 2 sub-pages): - Group 'Toolbar and commands' + 'Custom commands' under a 'Toolbar and commands' parent. URLs unchanged; sidebarTitle on each page set to 'Built-in' / 'Custom' so the sub-entries don't collide with the group label. Page titles ('Toolbar and commands', 'Custom commands') stay verbose at the top of each page. - Drop the 'Reference demo' page. Most of it duplicated content that belongs in the demo's own README. The merged-feed pattern, the 'deliberately doesn't do' list, and the three takeaways for your own UI all moved to demos/bring-your-own-ui/README.md. Overview gets a one-line callout pointing at the demo on GitHub. - 'Document control' page sidebarTitle becomes 'Document' β matches the one-word pattern of Comments / Track changes (each named after its ui.* slice). - Drop apps/docs/snippets/components/byo-ui-demo.jsx. The embed work is captured in SD-2871 and isn't blocking the docs launch. Final sidebar: Overview / React setup / Toolbar and commands (Built-in, Custom) / Comments / Track changes / Selection and viewport / Document / API reference * docs(quickstart): hide mermaid pan/zoom controls on the surface picker actions={false} disables the Mintlify-injected zoom/pan widget on the 'Pick your surface' diagram. The diagram is small and one-glance readable; the controls add visual noise without buying anything.
`DocumentSlice` gains a typed `dirty: boolean` field. Apps wiring a Save / Export button or a "you have unsaved changes" indicator drop their host-listening boilerplate.
```tsx
const { ready, mode, dirty } = useSuperDocDocument();
return Save;
```
Semantics chosen:
Implementation notes:
Verified: 164/164 super-editor UI tests; superdoc + @superdoc-dev/react builds clean; BYO-UI demo builds clean. 7 new tests cover: starts false, flips on docChanged, stays false on selection-only, subscribers re-fire on flip, export clears, rejected export preserves, replaceFile clears.
Once merged, the BYO-UI docs PR (#3034) drops the `useSuperDocHost()` transaction-listener snippet from `document-control.mdx` in favor of `useSuperDocDocument().dirty`.