Skip to content

Commit 58e292d

Browse files
feat: headless toolbar (#2657)
* feat: headless toolbar * feat: headless toolbar (caio) (#2669) * feat: headless toolbar * fix(headless-toolbar): correctness fixes and DX improvements Correctness: - Use resolveStateEditor for undo/redo history depth (fixes header/footer) - Remove early return gating on color/highlight annotation sync - Subscribe to zoomChange event for immediate zoom state updates - Refresh snapshot after execute() for superdoc-level commands - Fix redundant documentMode self-comparison in isCommandDisabled DX: - Make execute() required on HeadlessToolbarController type - Normalize font-size values with unit (e.g. '12pt' not '12') - Preserve full font-family CSS value (e.g. 'Arial, sans-serif' not 'Arial') - Normalize color values to lowercase - Add execute('image') handler (file picker + insertion) - Fix demo to use execute() consistently for all commands - Fix demo font selects to use option.value not option.label - Remove unused RegistryMode/mode abstraction - Rewrite README with toolbar: null setup and command reference table * feat(headless-toolbar): add multi-framework examples and DX improvements Replace single Vue demo with 5 framework examples showcasing different toolbar patterns: - react-shadcn: classic top ribbon (Radix + Tailwind + Lucide) - react-mui: floating bubble bar (MUI + Material Icons) - vue-vuetify: sidebar panel (Vuetify 3 + MDI) - svelte-shadcn: compact bottom bar (Svelte 5 + Tailwind + Lucide) - vanilla: minimal top bar (plain HTML/CSS/JS + Lucide) API improvements: - execute() now auto-restores editor focus after commands - Add DEFAULT_TEXT_COLOR_OPTIONS and DEFAULT_HIGHLIGHT_COLOR_OPTIONS constants * fix(headless-toolbar): address review findings - Color execute: run annotation sync unconditionally but return the mark command result (not always true) - Image execute: add .catch() with console.error instead of silently swallowing errors - MUI example: remove unused variables, guard exec against null controller on first render * feat(headless-toolbar): add React hook and Vue composable Ship useHeadlessToolbar() for React and Vue: import { useHeadlessToolbar } from 'superdoc/headless-toolbar/react'; const { snapshot, execute } = useHeadlessToolbar(superdoc, commands); Handles subscribe/unsubscribe, state updates, and cleanup automatically. Eliminates the useState + useEffect + useRef boilerplate that every React consumer would write. Vue composable follows the same API with shallowRef reactivity and onBeforeUnmount cleanup. Update react-shadcn example to use the hook as proof. * feat(headless-toolbar): add typed payloads and snapshot values Add ToolbarPayloadMap and ToolbarValueMap type maps that give compile-time safety to execute() and snapshot.commands[id].value: toolbar.execute('font-size', '14pt') // ✓ toolbar.execute('font-size', 14) // ✗ type error toolbar.execute('bold', 'wrong') // ✗ type error snapshot.commands['zoom']?.value // type: number | undefined snapshot.commands['font-size']?.value // type: string | undefined No runtime changes — types only. * fix(superdoc): make documentMode optional in Config type documentMode defaults to 'editing' at runtime but the JSDoc typedef marked it as required, causing TypeScript errors when constructing SuperDoc without explicitly passing it. * chore(examples): remove toolbar: null from all examples * docs(headless-toolbar): add headless toolbar documentation Restructure toolbar docs into a group with four pages: - overview: decision page (built-in vs headless) - built-in: existing toolbar docs (moved from toolbar.mdx) - headless: full API reference with command table and typed examples - examples: 5 framework showcases (React shadcn, React MUI, Vue Vuetify, Svelte, vanilla JS) Add doctest support for headless toolbar code examples. Update SuperDoc configuration docs for toolbar parameter. Add redirect from /modules/toolbar to /modules/toolbar/overview. Add superdoc/headless-toolbar to import allowlist. * fix(headless-toolbar): include fallback fonts in Aptos constant Aptos constant now includes fallback fonts ('Aptos, Arial, sans-serif') matching what documents actually store. Without fallbacks, the snapshot value wouldn't match the constant, breaking select components. * fix(examples): register Vuetify components and directives * refactor(examples): simplify Vue Vuetify sidebar toolbar layout * fix(examples): add Tailwind v4 @reference directive for Svelte styles * fix(examples): only set select value when it matches an option * fix(headless-toolbar): address author review feedback - Remove editorDestroy subscription — the event fires during teardown when the editor may be in an inconsistent state, causing the refresh cycle to read from a dying editor. editorCreate is sufficient. - Remove auto-focus from execute() — the built-in toolbar has nuanced focus logic (pending marks, header/footer exclusion, dropdown detection) that a simple view.focus() doesn't replicate. Better handled as a follow-up with proper parity. - Restore onMouseDown preventDefault in react-shadcn example since focus is no longer handled by execute(). - Update docs and README to remove focus handling claims. --------- Co-authored-by: Artem Nistuley <artem@harbourshare.com> * fix(headless-toolbar): add missing type exports, null guard, and cleanup - Export ToolbarCommandStates, ToolbarExecuteFn, ToolbarPayloadMap, ToolbarValueMap from super-editor index (fixes consumer TS errors) - Accept null/undefined in Vue composable to match React hook behavior - Replace duplicated React/Vue hooks in superdoc with re-exports - Add vite aliases for headless-toolbar sub-path resolution - Remove unused ALIGN_ICONS, dead CSS in vanilla example * ci(examples): add headless toolbar examples to CI smoke tests Add advanced-headless-toolbar job to ci-examples workflow, running all 5 framework examples (react-shadcn, react-mui, vue-vuetify, svelte-shadcn, vanilla) as a matrix strategy. * fix(headless-toolbar): add @types/react and fix type-check error - Add @types/react as devDependency for headless-toolbar React hook - Fix TS2345 in react.ts execute callback (unknown → any cast for typed payload passthrough) --------- Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com> Co-authored-by: Caio Pizzol <caio@harbourshare.com>
1 parent d2c45ec commit 58e292d

76 files changed

Lines changed: 7967 additions & 184 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci-examples.yml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,26 @@ jobs:
134134
working-directory: examples/__tests__
135135
run: EXAMPLE=features/${{ matrix.example }} npx playwright test
136136

137+
advanced-headless-toolbar:
138+
needs: build
139+
runs-on: ubuntu-latest
140+
strategy:
141+
fail-fast: false
142+
matrix:
143+
example: [react-shadcn, react-mui, vue-vuetify, svelte-shadcn, vanilla]
144+
steps:
145+
- name: Restore workspace
146+
uses: actions/cache/restore@v4
147+
with:
148+
path: |
149+
.
150+
~/.cache/ms-playwright
151+
key: examples-workspace-${{ github.sha }}
152+
153+
- name: Run smoke test
154+
working-directory: examples/__tests__
155+
run: EXAMPLE=advanced/headless-toolbar/${{ matrix.example }} npx playwright test
156+
137157
headless:
138158
needs: build
139159
runs-on: ubuntu-latest
@@ -152,7 +172,7 @@ jobs:
152172

153173
validate:
154174
if: always()
155-
needs: [getting-started, collaboration, features, headless]
175+
needs: [getting-started, collaboration, features, advanced-headless-toolbar, headless]
156176
runs-on: ubuntu-latest
157177
steps:
158178
- name: Check results

apps/docs/modules/toolbar/headless.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ snapshot.commands['font-size']?.value // '12pt' — the current value
4646

4747
### Execute
4848

49-
Run a command by ID. The editor gets focus back automatically — no need for `onMouseDown={e => e.preventDefault()}` or manual focus management.
49+
Run a command by ID. Returns `true` if the command executed, `false` otherwise.
5050

5151
```ts
5252
toolbar.execute('bold');
@@ -136,7 +136,7 @@ unsub();
136136

137137
#### `execute(id, payload?)`
138138

139-
Runs the command identified by `id`. Returns `true` if the command executed, `false` otherwise. Automatically restores focus to the editor.
139+
Runs the command identified by `id`. Returns `true` if the command executed, `false` otherwise.
140140

141141
<CodeGroup>
142142

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>SuperDoc - Headless Toolbar (React + MUI)</title>
7+
<link rel="preconnect" href="https://fonts.googleapis.com" />
8+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
10+
</head>
11+
<body>
12+
<div id="root"></div>
13+
<script type="module" src="/src/main.tsx"></script>
14+
</body>
15+
</html>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "headless-toolbar-react-mui",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite",
7+
"build": "tsc -b && vite build",
8+
"preview": "vite preview"
9+
},
10+
"dependencies": {
11+
"@emotion/react": "^11.14.0",
12+
"@emotion/styled": "^11.14.0",
13+
"@mui/icons-material": "^7.3.0",
14+
"@mui/material": "^7.3.0",
15+
"react": "^19.2.0",
16+
"react-dom": "^19.2.0",
17+
"superdoc": "../../../../packages/superdoc"
18+
},
19+
"devDependencies": {
20+
"@types/react": "^19.2.0",
21+
"@types/react-dom": "^19.2.0",
22+
"@vitejs/plugin-react": "^4.3.0",
23+
"typescript": "^5.7.0",
24+
"vite": "^6.2.0"
25+
}
26+
}
Binary file not shown.

0 commit comments

Comments
 (0)