Skip to content

Commit c446999

Browse files
authored
docs: add content controls page, examples, and demo (IT-1046) (#3265)
* docs: add content controls page, examples, and demo (IT-1046) Customer asked whether SuperDoc supports composable reusable sections and "smart fields" — placeholders that update everywhere when edited centrally. Both map cleanly to Word content controls (OOXML SDT), and the surface is fully wired in the Document API. Ships two narrow examples plus a composed runtime demo, plus a conceptual docs page so the Document API surface gets the same hand-written narrative the Editor extensions already have. - examples/document-api/content-controls/smart-fields/ and reusable-section/ each teach one pattern in isolation, copy-pasteable. - demos/contract-templates/ is a composed workflow that detects stale section versions and applies updates in place. Not yet gallery-ready (homepage: false) pending the lock-mutation fix. - apps/docs/document-api/features/content-controls.mdx is a new home for hand-written feature narratives, slotted under a new Features sidebar group inside Document API. Frames Template Builder and the raw Document API as two valid paths. - Document API path is fully editor.doc.*. The same operation set runs headless via the Node SDK and CLI. Follow-ups: SD-3123 (locked-SDT mutations silently no-op via the legacy command path), SD-3124 (filter-rejected transactions report false success), SD-3125 (imported-controls example with a Word-authored fixture). * docs: replace content-control examples with one canonical tagged-inline-text (IT-1046) The first pass shipped two examples (smart-fields, reusable-section) that read as demo decompositions: same primitives as demos/contract-templates, ~200 lines each, with markdown seeds and panel chrome. They didn't earn their place against the demo. Replaced with one tight example, modelled on examples/editor/custom-ui/selection-capture: smallest readable form of the canonical create-find-update loop, with a README that distinguishes setup mutations from the teaching surface (selectByTag + text.setValue). Sets the template that future content-control examples copy. - examples/document-api/content-controls/tagged-inline-text/ is the new single example. - Old smart-fields/, reusable-section/, and the parent README.md removed. - features/content-controls.mdx Next steps cards updated to point at the new example. - Manifest and README rows replaced. * docs: add examples/demos contributor guidance and align styles (IT-1046) Captures the conventions IT-1046 produced so future contributors don't rediscover them. Examples and demos each get a scoped AGENTS.md (with a CLAUDE.md symlink for compat) defining what belongs in each surface, the cross-surface Document API placement rule, and the "don't ship known-bug or unverified paths" rule that drove the lockMode: 'unlocked' choice in the tagged-inline-text example. - examples/README.md and demos/README.md rewritten with clear definitions, contributor checklists, and homepage-readiness criteria. - examples/AGENTS.md (+ CLAUDE.md symlink) covers Document API placement, setup-vs-teaching separation, and verification rules. - demos/AGENTS.md (+ CLAUDE.md symlink) covers composed-workflow criteria and source-vs-live demo distinction. - demos/README.md adds contract-templates to the curated demos table. - demos/contract-templates/README.md calls out its demo status and links back to the tagged-inline-text example. - tagged-inline-text/README.md notes the lockMode: 'unlocked' choice and points at SD-3123. - Both style.css files now read from --sd-* tokens with fallbacks, so examples and demos pick up the host SuperDoc theme. * docs: rewrite content-controls limits sections in customer voice (IT-1046) Replaces internal-status framing with API guidance. Removes references to engine internals, legacy paths, known bugs, and 'not yet' language. - Lock modes: behavior table, then concrete guidance for programmatic use. - Data binding: describe what the API supports and what to drive instead. - Replacing content: scope replaceContent to plain text and point at doc.insert + contentControls.wrap for richer fragments. * docs: correct rich-fragment wrap guidance (IT-1046) contentControls.wrap takes an existing SDT target, not a selection range. Use create.contentControl with an at: SelectionTarget to wrap an inserted range.
1 parent 16bad0d commit c446999

26 files changed

Lines changed: 1365 additions & 27 deletions

File tree

apps/docs/docs.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,12 @@
165165
"group": "Document API",
166166
"pages": [
167167
"document-api/overview",
168-
"document-api/reference/index",
169168
"document-api/common-workflows",
169+
{
170+
"group": "Features",
171+
"pages": ["document-api/features/content-controls"]
172+
},
173+
"document-api/reference/index",
170174
"document-api/available-operations",
171175
"document-api/migration"
172176
]
@@ -292,6 +296,10 @@
292296
"dismissible": true
293297
},
294298
"redirects": [
299+
{
300+
"source": "/document-api/content-controls",
301+
"destination": "/document-api/features/content-controls"
302+
},
295303
{
296304
"source": "/editor/proofing/overview",
297305
"destination": "/editor/spell-check/overview"
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
---
2+
title: Content controls
3+
sidebarTitle: Content controls
4+
description: Attach stable, Word-compatible identity to regions of a document and update them programmatically.
5+
keywords: "content controls, SDT, structured document tags, smart fields, reusable sections, template fields, document automation"
6+
---
7+
8+
Content controls are Word's native primitive for **stable, identity-bearing regions** inside a document. They survive Word round-trips, carry app-defined metadata in a `tag` string, and can be discovered, updated, locked, or replaced from any surface that drives SuperDoc: the browser editor, the Node SDK, the CLI, an MCP tool, an AI agent.
9+
10+
In OOXML they are `w:sdt` elements (structured document tags). SuperDoc exposes the full surface under `editor.doc.contentControls.*` and `editor.doc.create.contentControl`.
11+
12+
## Two patterns to start
13+
14+
### Smart fields: one value, every occurrence
15+
16+
Wrap every occurrence of a template variable in an inline text content control sharing the same `tag`. Select by tag, then push the same value to each matching control.
17+
18+
```ts
19+
// Wrap once, at template-authoring time.
20+
editor.doc.create.contentControl({
21+
kind: 'inline',
22+
controlType: 'text',
23+
at: range,
24+
tag: 'customer',
25+
alias: 'Customer',
26+
lockMode: 'unlocked',
27+
});
28+
29+
// Push a new value. Every occurrence with tag === 'customer' updates.
30+
const { items } = editor.doc.contentControls.selectByTag({ tag: 'customer' });
31+
for (const { target } of items) {
32+
editor.doc.contentControls.text.setValue({ target, value: 'Acme Therapeutics' });
33+
}
34+
```
35+
36+
Smallest copy-pasteable form: [`examples/document-api/content-controls/tagged-inline-text`](https://github.com/superdoc-dev/superdoc/tree/main/examples/document-api/content-controls/tagged-inline-text).
37+
38+
### Reusable sections: tagged blocks that know their version
39+
40+
Encode `{ sectionId, version }` in the `tag` of a block content control. The app reads the live version from `contentControls.list` and offers an in-place update when the document falls behind the section library.
41+
42+
```ts
43+
// Wrap a section paragraph as a block content control with a structured tag.
44+
editor.doc.create.contentControl({
45+
kind: 'block',
46+
controlType: 'text',
47+
at: range,
48+
tag: JSON.stringify({ kind: 'reusableSection', sectionId: 'limitation-liability', version: 'v1' }),
49+
alias: 'Limitation of liability (v1)',
50+
lockMode: 'unlocked',
51+
});
52+
53+
// On reopen: list sections, parse their tags, compare versions.
54+
const { items } = editor.doc.contentControls.list({});
55+
for (const control of items) {
56+
const meta = parseTag(control.properties?.tag); // your helper
57+
if (meta?.kind === 'reusableSection' && meta.version !== latestVersionFromLibrary(meta.sectionId)) {
58+
// Swap content, bump version in tag.
59+
editor.doc.contentControls.replaceContent({ target: control.target, content: newBody, format: 'text' });
60+
editor.doc.contentControls.patch({
61+
target: control.target,
62+
tag: JSON.stringify({ ...meta, version: 'v2' }),
63+
alias: 'Limitation of liability (v2)',
64+
});
65+
}
66+
}
67+
```
68+
69+
Composed runtime: [`demos/contract-templates`](https://github.com/superdoc-dev/superdoc/tree/main/demos/contract-templates).
70+
71+
## Why `tag`, not `nodeId`
72+
73+
Two channels of identity exist on a content control:
74+
75+
| Channel | Source | Stable across loads | Stable through Word edits |
76+
|---|---|---|---|
77+
| `nodeId` | SuperDoc-assigned at parse time | Best-effort | No |
78+
| `tag` | App-defined, written to OOXML `<w:tag w:val="...">` | Yes | Yes (Word preserves the SDT and its tag) |
79+
80+
Use `nodeId` for in-session targeting. Use `tag` for durable identity that survives DOCX round-trips, including documents edited in Word and reopened. JSON-encode the `tag` when you need to carry structured metadata (kind, version, owner, group).
81+
82+
## Cross-surface: same operations everywhere
83+
84+
Document API content controls are not editor-specific. The same operation IDs are available on every surface that drives SuperDoc.
85+
86+
| Surface | Binding |
87+
|---|---|
88+
| Browser editor | `editor.doc.contentControls.*` |
89+
| Node SDK | bound document handle methods |
90+
| CLI | `superdoc` commands |
91+
| MCP / AI tools | tool wrappers around the same operation IDs |
92+
93+
A field updated by your backend job, a clause swapped by an agent, and a value typed by a user in the editor all hit the same engine.
94+
95+
## When to use Template Builder vs Document API
96+
97+
Two valid paths. Both build on Word content controls.
98+
99+
| Use [Template Builder](/solutions/template-builder/introduction) when | Use Document API content controls when |
100+
|---|---|
101+
| You're building in React and want a packaged authoring component | You're on vanilla JS, Vue, Angular, or any non-React stack |
102+
| You want the `{{` trigger menu, field sidebar, linked field groups, and DOCX export wired up out of the box | You need a custom UX (your own field menu, your own sidebar) |
103+
| Owner/signer field types and inline custom field creation match your workflow | You're operating headless: server-side jobs, AI agents, CLI scripts |
104+
| You want a shorter path to a working template authoring UI | You need runtime updates against existing tagged regions (smart fields, version-aware section swaps) |
105+
106+
The two paths are not mutually exclusive. A common pattern is Template Builder for authoring, Document API for runtime updates on the authored document.
107+
108+
## Operation reference at a glance
109+
110+
| Concept | Operation |
111+
|---|---|
112+
| Create a control around a range | `editor.doc.create.contentControl` |
113+
| Wrap an existing range | `editor.doc.contentControls.wrap` |
114+
| Find by tag | `editor.doc.contentControls.selectByTag` |
115+
| Find by alias | `editor.doc.contentControls.selectByTitle` |
116+
| List all controls | `editor.doc.contentControls.list` |
117+
| Inspect one | `editor.doc.contentControls.get` |
118+
| Update text value | `editor.doc.contentControls.text.setValue` |
119+
| Replace whole content | `editor.doc.contentControls.replaceContent` |
120+
| Patch metadata (tag, alias, appearance) | `editor.doc.contentControls.patch` |
121+
| Set lock mode | `editor.doc.contentControls.setLockMode` |
122+
| Delete (with content) | `editor.doc.contentControls.delete` |
123+
| Unwrap (keep content) | `editor.doc.contentControls.unwrap` |
124+
| Read `sdtPr` directly | `editor.doc.contentControls.getRawProperties` |
125+
| Edit `sdtPr` directly | `editor.doc.contentControls.patchRawProperties` |
126+
127+
Typed sub-APIs exist for `text.*`, `date.*`, `checkbox.*`, `choiceList.*` (combo/dropdown), `repeatingSection.*`, and `group.*`. See the [reference index](/document-api/reference/content-controls/index) for the full catalog.
128+
129+
## Lock modes
130+
131+
Set `lockMode` when you create a control to govern which changes are allowed.
132+
133+
| Mode | Behavior |
134+
|---|---|
135+
| `unlocked` | Content and properties can be updated through the Document API. |
136+
| `sdtLocked` | The wrapper is preserved through user edits. |
137+
| `contentLocked` | The content can't be modified through the editor surface. |
138+
| `sdtContentLocked` | Both wrapper and content are preserved. |
139+
140+
For controls your app drives with `text.setValue`, `replaceContent`, or `patch`, use `lockMode: 'unlocked'`.
141+
142+
## Data binding
143+
144+
Content controls can carry an OOXML `<w:dataBinding>` link to a custom XML data part. Read and write the binding metadata with `contentControls.getBinding`, `setBinding`, and `clearBinding`. The binding survives DOCX round-trips.
145+
146+
For runtime synchronization with backing data, drive the control directly with `text.setValue` or `replaceContent`.
147+
148+
## Replacing content
149+
150+
`contentControls.replaceContent` accepts plain text. For richer fragments (paragraphs with formatting, tables, lists), use `doc.insert` to place the content, then `create.contentControl({ at: range, ... })` to wrap the inserted range with a tag.
151+
152+
## Next steps
153+
154+
<CardGroup cols={2}>
155+
<Card title="Tagged inline text example" icon="text-cursor-input" href="https://github.com/superdoc-dev/superdoc/tree/main/examples/document-api/content-controls/tagged-inline-text">
156+
The smallest content-control workflow. `create.contentControl` + `selectByTag` + `text.setValue`.
157+
</Card>
158+
<Card title="Contract templates demo" icon="file-text" href="https://github.com/superdoc-dev/superdoc/tree/main/demos/contract-templates">
159+
Smart fields and versioned sections composed into one runtime app.
160+
</Card>
161+
<Card title="Template Builder" icon="layout-template" href="/solutions/template-builder/introduction">
162+
Ready-made React authoring component for content-control templates.
163+
</Card>
164+
<Card title="Reference: all operations" icon="code" href="/document-api/reference/content-controls/index">
165+
Every `contentControls.*` operation with input, output, and failure codes.
166+
</Card>
167+
</CardGroup>

demos/AGENTS.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Demos
2+
3+
Demos are composed product workflows and showcase surfaces. They answer what someone can build with SuperDoc, not just how one API call works.
4+
5+
If the work only teaches one primitive or one integration pattern, put it in `examples/` instead.
6+
7+
## What belongs here
8+
9+
- Multiple SuperDoc features working together in a realistic workflow.
10+
- Product-shaped UI, fake backend state, library data, or scenario copy when it helps the workflow make sense.
11+
- A README that explains the scenario, the features being composed, how to run it, and related examples or docs.
12+
- An entry in `demos/manifest.json`.
13+
14+
Set `homepage: true` only when the demo is gallery-ready: verified locally, clear enough for users, and backed by the metadata or assets the homepage expects. Use `homepage: false` for source demos that are useful but not ready for the gallery.
15+
16+
## UI baseline
17+
18+
- Import `superdoc/style.css` before local CSS when the demo renders SuperDoc.
19+
- Use the SuperDoc token contract (`--sd-*`) or local aliases that resolve to those tokens. Avoid Vite starter purple and one-off palettes unless the demo is intentionally showing theming.
20+
- Product demos should feel precise and functional: flat surfaces, clear hierarchy, restrained whitespace, 1px borders, 4-8px radii, SuperDoc blue for primary actions.
21+
- Do not use marketing gradients in product UI. `brand.md` reserves gradients for marketing heroes and landing pages.
22+
- Show the working product surface first. Avoid landing-page heroes unless the demo is specifically a marketing page.
23+
24+
## Source demos vs live demos
25+
26+
This monorepo owns source demos. Some live demos at `demos.superdoc.dev` live in the separate `superdoc-dev/demos` repository. Do not assume every source demo has a `liveUrl`, thumbnail, or deployed counterpart.
27+
28+
## Verification
29+
30+
- Run the package build for each touched demo workspace.
31+
- Browser-smoke workflows when the change affects UI, document state, import/export, or a customer-recordable flow.
32+
- Do not commit generated `dist/` output or `node_modules/`.
33+
- Treat stale README, AGENTS, and CLAUDE instructions as bugs; see `../comment-policy.md`.

demos/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

demos/README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
# SuperDoc Demos
22

3-
Source-only demos used by the [SuperDoc demo gallery](https://superdoc.dev). A demo composes multiple SuperDoc features into a workflow. If you want the smallest copy-pasteable path for one feature, use [`examples/`](../examples/) instead.
3+
Source-only demos used by the [SuperDoc demo gallery](https://superdoc.dev).
4+
5+
Demos answer: "What can I build with SuperDoc?"
6+
7+
A demo composes multiple SuperDoc features into a workflow, often with realistic UI, fake backend data, product copy, gallery metadata, or a video-ready scenario. If you want the smallest copy-pasteable path for one primitive, use [`examples/`](../examples/) instead.
48

59
The machine-readable index lives in [`manifest.json`](./manifest.json).
610

11+
## Demos vs examples
12+
13+
Use `demos/` when the point is the workflow: contract templates, grading papers, Slack redlining, browser extensions, Word add-ins, and similar product-shaped experiences.
14+
15+
Use `examples/` when the point is the API call or integration pattern. Examples can overlap with demos, but the example should remove the product story and show the smallest useful code path.
16+
17+
Before marking a demo as homepage-ready, make sure it has been verified locally, has a clear README, and has the gallery metadata or assets the homepage expects. Leave `homepage: false` while a demo is useful for source review but not ready for the gallery.
18+
719
## Source demos vs live demos
820

921
This monorepo's `demos/` folder is the source showcase surface. Demos here run locally from workspace builds and are smoke-tested against the current repository state.
@@ -14,6 +26,7 @@ Live demos that run at `demos.superdoc.dev` live in the separate `superdoc-dev/d
1426

1527
| Demo | Category | Notes |
1628
|------|----------|-------|
29+
| [contract-templates](./contract-templates) | Editor | Content-controls workflow with smart fields, versioned clauses, and update detection |
1730
| [custom-ui](./custom-ui) | Editor | Full Custom UI reference workspace |
1831
| [grading-papers](./grading-papers) | Editor | Product workflow for paper review |
1932
| [slack-redlining](./slack-redlining) | AI | Slack and AI redlining workflow |

demos/contract-templates/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Contract templates
2+
3+
A runtime workflow that uses Word content controls to manage smart fields and versioned clauses inside a document. Single-page, no backend, no framework.
4+
5+
This is a demo: it shows a composed contract-template workflow. For the smallest copy-pasteable content-control primitive, see the [tagged inline text example](../../examples/document-api/content-controls/tagged-inline-text).
6+
7+
## What this shows
8+
9+
Two flows of the same primitive, composed into one app:
10+
11+
1. **Smart fields.** Inline content controls share a `tag` value (`{ kind: 'smartField', key: 'customerName' }`) across every occurrence. Select by tag, then push the same value to each matching control with `contentControls.text.setValue`.
12+
2. **Versioned reusable sections.** A block content control carries `{ kind: 'reusableSection', sectionId, version }` in its `tag`. The app reads the live version from `contentControls.list` after every change. When the section in the document falls behind the section library, an "update available" CTA appears. Updating is `replaceContent` + `patch`.
13+
14+
Every mutation goes through `editor.doc.*`. The same operation set runs headless via the Node SDK and CLI.
15+
16+
## Run
17+
18+
```bash
19+
pnpm install
20+
pnpm dev
21+
```
22+
23+
Edit a smart-field value on the right, click **Apply fields**, watch every occurrence update. The seed section ships as v1; the library has v2. The **Apply update** CTA appears because they diverge. Click it, the section swaps, the CTA disappears.
24+
25+
## Related work
26+
27+
If you need a **ready-made React component for authoring templates** with content controls (trigger `{{` to insert fields, linked field groups, owner/signer field types, export to .docx), see [`@superdoc-dev/template-builder`](https://docs.superdoc.dev/solutions/template-builder/introduction). This demo focuses on the *runtime* side: an app filling and updating already-tagged regions. Template Builder focuses on the *authoring* side.
28+
29+
## Honest limits
30+
31+
- The demo uses `lockMode: 'unlocked'` for every content control. The OOXML spec says `sdtLocked` leaves content editable, but the current adapter routes content and attr updates through `editor.commands.updateStructuredContentById`, which rewrites the whole SDT wrapper. The lock plugin reads that as wrapper damage and silently filters the transaction for `sdtLocked` and `sdtContentLocked` SDTs. Result: programmatic `text.setValue`, `replaceContent`, `patch`, and `setLockMode` return success but do not persist on locked SDTs. Tracked as a follow-up engine bug; the demo sidesteps it.
32+
- `contentControls.replaceContent` is plain-text in the current adapter. Rich-content swap (formatting, tables) is not first-class today. Sections in this demo are kept plain.
33+
- `contentControls.setBinding` writes `<w:dataBinding>` and round-trips through DOCX, but SuperDoc does not yet evaluate XPath against `customXml/` parts. The metadata channel works; the live binding engine does not.
34+
35+
## See also
36+
37+
- [Tagged inline text example](../../examples/document-api/content-controls/tagged-inline-text)
38+
- [Document API > Content controls](https://docs.superdoc.dev/document-api/features/content-controls)
39+
- [Document API > Reference > Content controls](https://docs.superdoc.dev/document-api/reference/content-controls/index)
40+
- [Solutions > Template Builder](https://docs.superdoc.dev/solutions/template-builder/introduction)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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: content controls</title>
7+
</head>
8+
<body>
9+
<div class="app">
10+
<section class="editor-area">
11+
<div id="editor"></div>
12+
</section>
13+
<aside class="sidebar">
14+
<section class="panel">
15+
<h2>Smart fields</h2>
16+
<p class="hint">Tagged inline content controls. <code>selectByTag</code> + <code>text.setValue</code> fan one value to every occurrence.</p>
17+
<label>
18+
<span>Customer</span>
19+
<input id="field-customerName" />
20+
</label>
21+
<label>
22+
<span>Jurisdiction</span>
23+
<input id="field-jurisdiction" />
24+
</label>
25+
<label>
26+
<span>Effective date</span>
27+
<input id="field-effectiveDate" />
28+
</label>
29+
<button class="btn primary" id="apply-fields" type="button">Apply fields</button>
30+
</section>
31+
32+
<section class="panel">
33+
<h2>Reusable section</h2>
34+
<p class="hint">Block content control with <code>{sectionId, version}</code> in its tag. <code>replaceContent</code> + <code>patch</code> swap the content and the version.</p>
35+
<div class="meta">
36+
<span>In document</span>
37+
<strong id="section-version">v1</strong>
38+
</div>
39+
<div class="meta">
40+
<span>Library</span>
41+
<strong>v2</strong>
42+
</div>
43+
<div class="banner" id="update-banner" hidden>
44+
<span class="banner-text">Update available.</span>
45+
<button class="btn primary" id="apply-update" type="button">Apply v2</button>
46+
</div>
47+
</section>
48+
49+
<p class="status" id="status">Loading</p>
50+
</aside>
51+
</div>
52+
<script type="module" src="/src/main.ts"></script>
53+
</body>
54+
</html>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "@superdoc-demos/contract-templates",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"predev": "pnpm --filter superdoc build",
7+
"dev": "vite",
8+
"build": "tsc --noEmit && vite build",
9+
"preview": "vite preview"
10+
},
11+
"dependencies": {
12+
"superdoc": "workspace:*"
13+
},
14+
"devDependencies": {
15+
"typescript": "catalog:",
16+
"vite": "catalog:"
17+
}
18+
}

0 commit comments

Comments
 (0)