Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions .github/workflows/ci-demos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,11 @@ jobs:
- contract-templates
- custom-ui
- docx-from-html
- docxtemplater
- fields
- fields-source
- grading-papers
# - html-editor # broken: imports unpublished superdoc/super-editor/style.css subpath
- linked-sections
- loading-from-json
- nextjs-ssr
# - replace-content # broken: runtime nextSibling error in SuperDoc
- text-selection
- toolbar
steps:
- name: Restore workspace
uses: actions/cache/restore@v4
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/document-api/migration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ superdoc.on('editorCreate', ({ editor }) => {

## Driving custom React UI

If your migration is also moving the UI side off legacy chains, the destination is `superdoc/ui/react`. Replace `superdoc.on('editor-update', ...)` loops and `useState(superdoc.activeEditor.commands.X)` patterns with the typed hooks: `useSuperDocCommand`, `useSuperDocSelection`, `useSuperDocComments`, `useSuperDocTrackChanges`, `useSuperDocDocument`. See [Custom UI](/editor/custom-ui/overview) for the full surface and the [reference workspace on GitHub](https://github.com/superdoc-dev/superdoc/tree/main/demos/custom-ui).
If your migration is also moving the UI side off legacy chains, the destination is `superdoc/ui/react`. Replace `superdoc.on('editor-update', ...)` loops and `useState(superdoc.activeEditor.commands.X)` patterns with the typed hooks: `useSuperDocCommand`, `useSuperDocSelection`, `useSuperDocComments`, `useSuperDocTrackChanges`, `useSuperDocDocument`. See [Custom UI](/editor/custom-ui/overview) for the full surface and the [reference workspace on GitHub](https://github.com/superdoc-dev/superdoc/tree/main/demos/editor/custom-ui).

## Full reference

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/editor/custom-ui/context-menu.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ If you'd rather suppress the native menu in the empty case too, call `event.prev

## Worked example

The reference workspace at [`demos/custom-ui`](https://github.com/superdoc-dev/superdoc/tree/main/demos/custom-ui) wires the full pattern end-to-end. The four registrations below mirror the demo's `ContextMenuRegistrations.tsx`. They cover the three subjects the menu can act on: an entity, the selection, or the click point.
The reference workspace at [`demos/editor/custom-ui`](https://github.com/superdoc-dev/superdoc/tree/main/demos/editor/custom-ui) wires the full pattern end-to-end. The four registrations below mirror the demo's `ContextMenuRegistrations.tsx`. They cover the three subjects the menu can act on: an entity, the selection, or the click point.

<CodeGroup>

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/editor/custom-ui/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,4 @@ The controller surfaces this split directly. The toolbar reads `state.selection`

## Worked example

The [reference workspace on GitHub](https://github.com/superdoc-dev/superdoc/tree/main/demos/custom-ui) ships a full app on these surfaces: toolbar, custom command with keyboard shortcut, floating bubble menu, right-click context menu, comments sidebar with reply threads, tracked-change review, selection capture / restore, DOCX export and reimport.
The [reference workspace on GitHub](https://github.com/superdoc-dev/superdoc/tree/main/demos/editor/custom-ui) ships a full app on these surfaces: toolbar, custom command with keyboard shortcut, floating bubble menu, right-click context menu, comments sidebar with reply threads, tracked-change review, selection capture / restore, DOCX export and reimport.
22 changes: 18 additions & 4 deletions demos/__tests__/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineConfig, devices } from '@playwright/test';
import { existsSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { existsSync, readFileSync } from 'node:fs';
import { resolve, dirname, relative } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
Expand All @@ -11,8 +11,22 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
// when running this suite locally without an explicit DEMO override.
const demo = process.env.DEMO || 'custom-ui';

// Demos are flat: demos/<name>/
const demoPath = `../${demo}`;
// Resolve the demo's working directory via the manifest. Old paths under
// demos/<name>/ may now be shim READMEs; manifest sourcePath is the source
// of truth post-SD-3217.
const manifestPath = resolve(__dirname, '../manifest.json');
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Array<{
id: string;
sourcePath?: string | null;
sourceRepo?: string;
}>;
const entry = manifest.find((e) => e.id === demo);
const sourcePath = entry?.sourceRepo === 'superdoc-dev/superdoc' ? entry?.sourcePath : null;
if (!sourcePath) {
throw new Error(`DEMO="${demo}" not found in demos/manifest.json or is not a local demo`);
}
const repoRoot = resolve(__dirname, '../..');
const demoPath = relative(__dirname, resolve(repoRoot, sourcePath));

// Port mapping for non-Vite demos (these use their framework's default port)
const portMap: Record<string, number> = {
Expand Down
5 changes: 5 additions & 0 deletions demos/chrome-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Moved to demos/editor/integrations/chrome-extension

The Chrome extension demo moved to [`demos/editor/integrations/chrome-extension`](../editor/integrations/chrome-extension).

It now sits under `demos/editor/integrations/` to reflect that it integrates SuperDoc into a host browser surface, rather than being its own product workflow.
111 changes: 3 additions & 108 deletions demos/custom-ui/README.md
Original file line number Diff line number Diff line change
@@ -1,110 +1,5 @@
# SuperDoc Custom UI demo
# Moved to demos/editor/custom-ui

A reference workspace built on the `superdoc/ui/react` surface. The headline use case is **source-grounded citations**: insert a mock RAG-generated draft, see anchored citation highlights with hover previews, navigate from a sources panel, edit or remove citations. Wrapped in a full editor workspace: custom toolbar, comment threads, tracked-change review, custom commands, and DOCX round-trip.
The custom-UI reference workspace moved to [`demos/editor/custom-ui`](../editor/custom-ui).

See the [Custom UI docs](https://docs.superdoc.dev/editor/custom-ui/overview) for the conceptual guide, and the upcoming [source-grounded citations feature page](https://docs.superdoc.dev/document-api/features/anchored-metadata) for the citation story.

This demo shows how the pieces compose in a real product, not a single-concept recipe. Read it alongside the docs above when you're wiring your own toolbar, sources panel, or citation overlay.

## Run

Prerequisites: Node 20+, pnpm 9+, run from inside the SuperDoc monorepo.

```bash
pnpm install
pnpm --filter superdoc run build
pnpm --filter @superdoc-dev/react run build
pnpm --filter custom-ui run dev
```

Open http://localhost:5189.

## What you can do here

- Open the **Sources** tab and click **Insert sample cited draft**. A mocked RAG-generated paragraph is inserted at the end of the document; each cited span is anchored with `editor.doc.metadata.attach` and rendered with a highlight overlay.
- Hover a citation highlight to see the source's display text, locator, provider, and confidence. The popover reads the payload via `ui.viewport.entityAt` + `metadata.get`.
- Click **Scroll to** in the sources panel to navigate to a cited span. Uses `ui.metadata.scrollIntoView({ id })`.
- Click **Edit** on a citation to change `displayText`, `locator`, or `excerpt`. Calls `editor.doc.metadata.update`.
- Click **Remove** to strip the anchor and payload. Calls `editor.doc.metadata.remove`.
- Click toolbar buttons (bold, italic, lists, undo, redo) wired through `useSuperDocCommand`.
- Insert a custom clause registered with `ui.commands.register`. The button works, and so does its keyboard shortcut `Mod-Shift-C`, declared on the registration rather than wired in a separate keydown listener.
- Switch between Edit and Suggest. In Suggest, every edit lands as a tracked change.
- Select text and watch the floating bubble menu appear next to the selection (anchored via `ui.selection.getAnchorRect()`, not `window.getSelection()`).
- Right-click on a tracked change, comment, inside a selection, or on plain text. The menu adapts to the click target: Accept / Reject / Resolve on entities, Copy / Comment on a selection, Insert clause here on plain caret-only text.
- Add a comment. The composer captures the selection on open, posts on submit, and restores the visible range on close so the user keeps their place.
- Accept or reject tracked changes. Decided ones move to a Resolved section.
- Export the doc, edit it in Word, click Import, watch the activity feed update.

## Source-grounded citations

The demo composes anchored citation pointers on top of `editor.doc.metadata.*` and `ui.metadata.*`:

| Layer | What it does | Code |
| --- | --- | --- |
| **Mock RAG output** | Pre-canned text + per-citation payloads (sourceId, displayText, locator, excerpt, confidence). Stand-in for a real generation pipeline. | `mockDraft.ts` |
| **Insert + attach** | Inserts the text via `editor.doc.insert`, then computes a `SelectionTarget` for each cited span and calls `editor.doc.metadata.attach` per citation. | `GenerateDraftButton.tsx`, `useCitations.ts` |
| **Sources panel** | Lists citations grouped by `sourceId`. Scroll-to navigation uses `ui.metadata.scrollIntoView({ id })`. Edit form calls `editor.doc.metadata.update`. | `CitationsPanel.tsx` |
| **Highlight overlay** | Renders one absolute-positioned rectangle per painted line of each cited span. Rects come from `ui.metadata.getRect({ id })`. Remeasures on scroll, resize, ResizeObserver, and MutationObserver. | `CitationHighlights.tsx` |
| **Hover popover** | `ui.viewport.entityAt({ x, y })` returns the content control under the cursor; `metadata.get({ id })` fetches the payload to render. | `CitationPopover.tsx` |
| **Persistence** | Hidden inline content controls in the body carry the stable id in `w:tag`; payloads live in a namespaced custom XML data part. Survives DOCX export, reopen, and Word save (validated by the `word-roundtrip` fixtures in the monorepo). | `editor.doc.metadata.*` |

`ui.metadata.*` is the supported public surface for consumer-side geometry and navigation; consumers carry the metadata id and never see the SDT node id underneath.

## Architecture

```
SuperDocUIProvider one controller per app
└── EditorMount <SuperDocEditor> + onReady + disableContextMenu
├── Toolbar ui.commands + setDocumentMode
├── SelectionPopover ui.selection.getAnchorRect, bubble menu over the selection
├── ContextMenu ui.viewport.contextAt + ui.commands.getContextMenuItems(context) + item.invoke()
├── ContextMenuRegistrations ui.commands.register({ contextMenu: { when } })
├── CitationHighlights ui.metadata.getRect, painted overlay across cited spans
├── CitationPopover ui.viewport.entityAt + metadata.get, hover preview
└── ActivitySidebar ui.comments + ui.trackChanges + ui.selection (Activity tab)
├── CitationsPanel editor.doc.metadata.list/get/update/remove + ui.metadata.scrollIntoView (Sources tab)
└── CommentComposer ui.selection.capture / restore + ui.comments.createFromCapture
```

Components consume the controller via `useSuperDocUI()`. They never reach into `editor.state` or `editor.view`.

## Three surfaces, three subjects

The demo keeps a strict separation between the three editor UI surfaces. Each one answers a different "what's the subject of this action?" question:

| Surface | Subject | Items in the demo |
| --- | --- | --- |
| **Toolbar** | The **document** | Bold, Italic, Lists, Undo, Redo, Mode toggle, Insert clause, Export, Import. |
| **Floating bubble menu** | The **selection** | Bold, Italic, Comment on selection. |
| **Right-click context menu** | The **clicked target** | Accept / Reject on tracked change, Resolve on comment, Copy / Comment on selection (when the click is inside the selection rect), Insert clause here (when the click lands on plain caret-only text). |

`ui.viewport.contextAt({ x, y })` returns one bundle with the click point, the entities under it, the resolved caret position, the live selection, and `insideSelection` (whether the click landed in the painted selection rects). Each predicate filters on the same shape its handler receives, so "Copy" / "Comment on selection" gate themselves on `insideSelection === true` and "Insert clause here" gates on `position !== null && entities.length === 0 && insideSelection !== true`. A stale selection elsewhere on the page can't leak into a right-click somewhere else.

The `Insert clause here` handler reads `context.position.target` (a collapsed `SelectionTarget` at the click point) and passes it straight to `editor.doc.insert`. The same predicate the menu was filtered with becomes the target the action acts on. Without the bundle, the registration would have to insert against the user's prior selection somewhere else in the doc, making the label a lie.

Right-click on plain text where no item matches falls through to the browser's native menu. The handler deliberately doesn't `preventDefault` when `getContextMenuItems(context)` returns nothing, so the user gets Copy / Paste / Inspect from the browser instead of a dead right-click.

## The four custom-UI patterns

1. **Floating selection toolbar.** `ui.selection.getAnchorRect({ placement: 'start' })` returns viewport-relative coords for the painted selection. Re-position on `useSuperDocSelection()` change plus `scroll`/`resize`. Don't reach for `window.getSelection()`; SuperDoc's painted DOM is separate from the offscreen ProseMirror DOM and the browser API returns the wrong rect. See `SelectionPopover.tsx`.

2. **Right-click context menu.** Set `disableContextMenu` on `<SuperDocEditor>` to suppress the built-in. On `contextmenu`, call `ui.viewport.contextAt({ x, y })` to get the bundle, then `ui.commands.getContextMenuItems(context)` to get items contributed via `register({ contextMenu })`. Each item carries `invoke()`, which fires the registered `execute({ context })` with the bundle bound, so handlers act on the click target without the menu component threading payloads. Scope the listener with `ui.viewport.getHost()` instead of a CSS class. See `ContextMenu.tsx` and `ContextMenuRegistrations.tsx`.

3. **Custom command + keyboard shortcut.** Declare `shortcut: 'Mod-Shift-C'` on the registration. The controller installs a single bubble-phase keydown listener scoped to the painted host; matched shortcuts dispatch through the same path the toolbar button uses. No per-command keymap wiring. See `InsertClauseButton.tsx`.

4. **Composer capture + restore.** `ui.selection.capture()` on open holds the selection across focus moves. `ui.comments.createFromCapture(captured, { text })` posts the comment using the frozen target. `ui.selection.restore(captured)` puts the visible selection back so the user keeps their place. See `CommentComposer.tsx`.

## Adapting this to your stack

- **One provider, many components.** Toolbar, sidebar, and review panel all subscribe to the same controller via hooks. They don't pass props down a tree.
- **No design system.** Plain React, plain CSS. Drop the same patterns into Tailwind / shadcn / MUI / Mantine.
- **`modules: { comments: false }` and your own panel.** The demo turns off the built-in comments UI and renders its own. Imported comments still flow through export and import.
- **Capture, then restore.** Composers freeze the selection at open, post on submit, then `restore(capture)` on close. The user sees their range come back instead of typing into a vanished selection.
- **Activity feed merge.** `ActivitySidebar.tsx` interleaves `ui.comments` and `ui.trackChanges` into one panel with about thirty lines of merge logic. The two slices stay separate on the controller so apps that only render one don't pay for the other.

## What this demo deliberately doesn't do

- No design system. Patterns over CSS, copy them into yours.
- No backend. The clause library in `<InsertClauseButton>` is hardcoded. Real consumers fetch from their own API and call `reg.invalidate()` when permissions or availability change.
- No live AI provider. The citation flow uses pre-canned draft text + payloads in `mockDraft.ts` instead of calling an LLM. Real consumers replace this with their RAG output, but the shape that flows into `editor.doc.metadata.attach` (text + cited ranges + payloads) stays the same.
- Telemetry is off (`telemetry: { enabled: false }` in `EditorMount.tsx`) because there's no analytics endpoint to receive events. SuperDoc defaults to enabled.
It now sits under `demos/editor/` to mirror the docs nav (Editor > Custom UI). The workspace is a reference for composing many SuperDoc UI surfaces (toolbar, comments, tracked changes, citations, custom commands, context menus) in one app, including the source-grounded citation flow.
8 changes: 3 additions & 5 deletions demos/docx-from-html/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# SuperDoc: Init a DOCX from HTML Content
# Moved to demos/editor/superdoc/docx-from-html

An example of initializing SuperDoc with HTML content.
The "init a DOCX from HTML" demo moved to [`demos/editor/superdoc/docx-from-html`](../editor/superdoc/docx-from-html).

This will load a DOCX file (or a blank document), replacing the main contents with the provided HTML.

In the example we pass `document: sample-document.docx` to load a template with a header and footer. You can omit this key to start with a blank document.
It now sits under `demos/editor/superdoc/` because it teaches an editor-side initialization pattern (passing HTML to `<SuperDoc>`), not a headless Document API operation.
Loading
Loading