Skip to content

Commit efec5f4

Browse files
authored
docs(custom-ui): add Controller setup page (SD-2929) (#3136)
* docs(custom-ui): add Controller setup page (SD-2929) The Custom UI docs section was React-only. Customers building with Vue, Svelte, Angular, or vanilla TypeScript had no entry point that described the framework-agnostic controller surface, even though that surface is the actual product (the React hooks are sugar over it). Adds editor/custom-ui/controller-setup.mdx covering: - When to read this page vs React setup - createSuperDocUI({ superdoc }) - ui.<domain>.observe(snapshot => ...) shape (and the wrapped subscribe alias) - ui.createScope() for lifecycle, with auto-cascade on ui.destroy() - scope.add / scope.register / scope.on - BUILT_IN_COMMAND_IDS / ui.commands.has(id) / ui.commands.require(id) for config-driven id validation - Tiny vanilla skeleton in one file - Common pitfalls: forgetting ui.destroy(), reading live selection at submit time, built-in UI overlap Wires the page into the Custom UI nav between React setup and Toolbar and commands. Adds a Tip callout on react-setup.mdx pointing non-React readers here. This is the docs gap the audit surfaced: examples were drifting toward 'framework matrix' coverage because there was no docs page that owned the framework-agnostic story. With this page in place, focused examples (selection-capture, configurable-toolbar) can land linked from a real docs home. * docs(custom-ui): group nav into Setup / Commands / Review / Selection (SD-2929) The Custom UI section grew to 11 pages with this PR's controller-setup addition. That's nearly twice the next-biggest Editor subsection (Built-in UI / SuperDoc, both 6 pages). A flat list at that size is hard to scan, and the existing Toolbar and commands subgroup with a single child of the same name was already awkward. Restructure into four sub-groups that match how customers actually read the section: - Setup (react-setup, controller-setup): the React vs framework- agnostic choice is the first decision a reader makes. - Commands and controls (toolbar-and-commands, custom-commands, document-control): wiring the chrome that drives the editor. - Review workflows (comments, track-changes): the two surfaces a reviewer-style sidebar consumes. - Selection and navigation (selection-and-viewport, navigation): positioning utilities that the surfaces above lean on. Overview stays at the top as the layer model; api-reference stays at the bottom as reference, not a learning path. The previous Toolbar and commands subgroup (which had a child of the same name) goes away; document-control moves out of the trailing-utility cluster into Commands and controls where it belongs. Also tightens the controller-setup page description: 'The framework- agnostic path under React' kept React as the implicit center, which inverts the actual relationship. Reframed as 'Use Custom UI without React' so the controller is the subject, not the alternative. * docs(custom-ui): flatten nav and reorder for teach-flow (SD-2929) Reversed the sub-grouped nav from the previous commit. Custom UI is 11 pages, which is on the line but not over it — every other Editor subsection (SuperDoc, React, Built-in UI, Spell check, Theming, PDF) reads as a flat list. Sub-groups would break that convention without materially helping discoverability for either teach-flow readers or return-lookup readers. The teach-flow ordering carries the structure instead: overview → react-setup → controller-setup → toolbar-and-commands → custom-commands → comments → selection-and-viewport → track-changes → document-control → navigation → api-reference Selection-and-viewport sits immediately after comments because the capture / focus-loss problem is the concrete case that motivates the selection mechanics page. The previous Toolbar and commands subgroup (awkwardly named the same as one of its children) is gone; the two pages now sit at the top level next to each other. Reverts only the nav restructure from 8125da9. Keeps the controller- setup page itself, the Tip callout on react-setup, and the controller-setup description fix from earlier in this PR.
1 parent 1a3b7dd commit efec5f4

3 files changed

Lines changed: 190 additions & 6 deletions

File tree

apps/docs/docs.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,14 @@
9898
"pages": [
9999
"editor/custom-ui/overview",
100100
"editor/custom-ui/react-setup",
101-
{
102-
"group": "Toolbar and commands",
103-
"pages": ["editor/custom-ui/toolbar-and-commands", "editor/custom-ui/custom-commands"]
104-
},
101+
"editor/custom-ui/controller-setup",
102+
"editor/custom-ui/toolbar-and-commands",
103+
"editor/custom-ui/custom-commands",
105104
"editor/custom-ui/comments",
106-
"editor/custom-ui/track-changes",
107105
"editor/custom-ui/selection-and-viewport",
108-
"editor/custom-ui/navigation",
106+
"editor/custom-ui/track-changes",
109107
"editor/custom-ui/document-control",
108+
"editor/custom-ui/navigation",
110109
"editor/custom-ui/api-reference"
111110
]
112111
},
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
---
2+
title: 'Controller setup'
3+
description: 'Use Custom UI without React. createSuperDocUI, scope, observe, destroy.'
4+
---
5+
6+
`superdoc/ui/react` is sugar over a controller. If you are not using React, talk to the controller directly. The hooks just call its methods on your behalf.
7+
8+
## When to use this page
9+
10+
| You are... | Read |
11+
|---|---|
12+
| Building with React | [React setup](/editor/custom-ui/react-setup). Skip this page. |
13+
| Building with Vue, Svelte, Angular, or vanilla | This page. |
14+
| Building a framework adapter | This page is the contract. |
15+
16+
The controller exposes the same surface the React hooks consume. Domain handles, scope-based lifecycle, value-shaped observers. No framework primitives.
17+
18+
## Install
19+
20+
```bash
21+
pnpm add superdoc
22+
```
23+
24+
## Create the controller
25+
26+
`createSuperDocUI({ superdoc })` runs once per editor mount and returns a controller. Hand it the SuperDoc instance.
27+
28+
```ts
29+
import { SuperDoc } from 'superdoc';
30+
import { createSuperDocUI } from 'superdoc/ui';
31+
import 'superdoc/style.css';
32+
33+
const superdoc = new SuperDoc({
34+
selector: '#editor',
35+
document: '/contract.docx',
36+
});
37+
38+
const ui = createSuperDocUI({ superdoc });
39+
```
40+
41+
`ui` is `null`-safe to keep around until your app tears down. Call `ui.destroy()` on unmount.
42+
43+
## Bind state with observe
44+
45+
Domain handles emit through `observe(snapshot => ...)`. The listener fires once synchronously with the current snapshot, then again on every change. Returns an unsubscribe.
46+
47+
```ts
48+
const off = ui.comments.observe((snapshot) => {
49+
renderSidebar(snapshot.items);
50+
});
51+
52+
// Later, on tear-down:
53+
off();
54+
```
55+
56+
The same shape works for every domain: `ui.toolbar.observe`, `ui.selection.observe`, `ui.trackChanges.observe`, `ui.document.observe`. Per-command state binds the same way: `ui.commands.bold.observe(state => ...)`.
57+
58+
A wrapped `subscribe(({ snapshot }) => ...)` form is also exported. Either works; pick one and stay consistent.
59+
60+
## Group teardown with createScope
61+
62+
Without `useEffect` cleanup, you have to track every unsubscribe yourself. `ui.createScope()` does that for you.
63+
64+
```ts
65+
const scope = ui.createScope();
66+
67+
scope.add(ui.commands.bold.observe((state) => render(state)));
68+
scope.add(ui.comments.observe((snapshot) => renderSidebar(snapshot.items)));
69+
scope.on(window, 'beforeunload', save);
70+
71+
// One call drops everything:
72+
scope.destroy();
73+
```
74+
75+
`ui.destroy()` cascades into every live scope, so a typical app needs only one teardown call:
76+
77+
```ts
78+
const teardown = () => {
79+
ui.destroy();
80+
superdoc.destroy();
81+
};
82+
window.addEventListener('beforeunload', teardown);
83+
```
84+
85+
## Register custom commands
86+
87+
`scope.register(...)` is the same as `ui.commands.register(...)` but the scope auto-unregisters on tear-down. The registration is reachable through the same `ui.commands.<id>` and `ui.commands.get(id)` paths as built-ins.
88+
89+
```ts
90+
scope.register({
91+
id: 'company.aiRewrite',
92+
getState: ({ state }) => ({ disabled: state.selection.empty }),
93+
execute: async ({ editor }) => {
94+
const target = ui.selection.getSnapshot().selectionTarget;
95+
if (!target || !editor?.doc?.insert) return false;
96+
const next = await rewrite(ui.selection.getSnapshot().quotedText);
97+
return editor.doc.insert({ target, value: next, type: 'text' }).success;
98+
},
99+
});
100+
```
101+
102+
## Validate config-driven command ids
103+
104+
If your toolbar reads ids from a config file or feature flag, validate at startup. `ui.commands.has(id)` is the cheap check; `ui.commands.require(id)` throws on unknown ids at trusted dispatch sites.
105+
106+
```ts
107+
import { BUILT_IN_COMMAND_IDS } from 'superdoc/ui';
108+
109+
for (const id of toolbarConfig) {
110+
if (!ui.commands.has(id)) {
111+
console.warn(`[toolbar] unknown command: ${id}`);
112+
continue;
113+
}
114+
const handle = ui.commands.require(id);
115+
scope.add(handle.observe(state => updateButton(id, state)));
116+
}
117+
```
118+
119+
`BUILT_IN_COMMAND_IDS` is the readonly list of every valid built-in id. `PublicToolbarItemId` is the matching type.
120+
121+
## Tiny skeleton
122+
123+
The whole picture in one file:
124+
125+
```ts
126+
import { SuperDoc } from 'superdoc';
127+
import { createSuperDocUI } from 'superdoc/ui';
128+
import 'superdoc/style.css';
129+
130+
const superdoc = new SuperDoc({
131+
selector: '#editor',
132+
document: '/contract.docx',
133+
});
134+
135+
const ui = createSuperDocUI({ superdoc });
136+
const scope = ui.createScope();
137+
138+
scope.add(
139+
ui.commands.bold.observe((state) => {
140+
document.querySelector('#bold')!.classList.toggle('active', state.active);
141+
}),
142+
);
143+
144+
document.querySelector('#bold')!.addEventListener('click', () => {
145+
ui.commands.bold.execute();
146+
});
147+
148+
const teardown = () => {
149+
ui.destroy();
150+
superdoc.destroy();
151+
};
152+
window.addEventListener('beforeunload', teardown);
153+
```
154+
155+
## What ships
156+
157+
| Surface | Purpose |
158+
|---|---|
159+
| `createSuperDocUI({ superdoc })` | One controller per editor mount |
160+
| `ui.createScope()` | Lifecycle bag for subscriptions, registrations, DOM listeners |
161+
| `ui.<domain>.observe(snapshot => ...)` | Read state. Domains: `toolbar`, `commands.<id>`, `comments`, `trackChanges`, `selection`, `document` |
162+
| `ui.<domain>.<action>(...)` | Mutate. Examples: `ui.comments.resolve(id)`, `ui.trackChanges.accept(id)`, `ui.document.setMode('suggesting')` |
163+
| `ui.commands.has(id)` / `require(id)` | Validate config-driven ids |
164+
| `BUILT_IN_COMMAND_IDS` | Readonly list of every built-in command id |
165+
| `ui.destroy()` | Teardown. Cascades into every live scope. |
166+
167+
See the [API reference](/editor/custom-ui/api-reference) for full signatures.
168+
169+
## Common pitfalls
170+
171+
<AccordionGroup>
172+
<Accordion title="Forgetting to call ui.destroy()">
173+
Without `ui.destroy()`, internal listeners and any live scope keep running after your app unmounts. Hot-reload sessions accumulate dead controllers. Always call it on unload and on every framework-specific destroy hook (`onScopeDispose` in Vue, `onDestroy` in Svelte, `DestroyRef` in Angular).
174+
</Accordion>
175+
<Accordion title="Reading the live selection at submit time">
176+
A composer that reads `ui.selection.getSnapshot()` at submit time will see `null` if the user typed in a textarea between opening the composer and pressing Send. Capture the selection at composer-open with `ui.selection.capture()` and pass the snapshot into `ui.comments.createFromCapture(capture, { text })`.
177+
</Accordion>
178+
<Accordion title="Built-in UI overlapping with yours">
179+
Pass `modules: { comments: false }` to `new SuperDoc(...)` to disable the built-in comment bubble. Same shape for tracked changes. Document-level features (DOCX import/export, comments round-trip) keep working.
180+
</Accordion>
181+
</AccordionGroup>

apps/docs/editor/custom-ui/react-setup.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ description: 'Provider, onReady, hooks. The scaffolding every page in this secti
55

66
The provider holds one controller per editor mount. Components read it through `useSuperDocUI()` and the per-domain hooks. Drop your toolbar, sidebar, and review components anywhere inside the provider.
77

8+
<Tip>
9+
Not using React? See [Controller setup](/editor/custom-ui/controller-setup) for the framework-agnostic path. The hooks on this page are sugar over that controller.
10+
</Tip>
11+
812
## Install
913

1014
```bash

0 commit comments

Comments
 (0)