Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
bd43191
chore: add root pickled.yml for agent-legibility checks
caio-pizzol May 25, 2026
8e768fc
feat(sdk): add LLM tools preset registry (SD-3128)
andrii-harbour May 27, 2026
595564b
Merge remote-tracking branch 'origin/main' into andrii/llm-tools-bundles
andrii-harbour May 27, 2026
b4cb0d9
Merge branch 'main' into andrii/llm-tools-bundles
andrii-harbour May 29, 2026
3385cad
fix: make toc toolbar icon configurable
VladaHarbour May 28, 2026
2a893ba
feat: add layered style export
May 26, 2026
af05f1c
feat(demo): smart-tags palette that inserts custom-styled SDT fields
caio-pizzol May 29, 2026
05f6cbb
feat(demo): amber token palette, document->palette sync, README
caio-pizzol May 29, 2026
de7cac4
fix(demo): unify SDT field look + kill hover/select jitter; drop the …
caio-pizzol May 29, 2026
195c670
feat: namespace internal ui classes (#3544)
artem-harbour May 29, 2026
63c71be
Merge pull request #3563 from superdoc-dev/artem/layer-css-v2
harbournick May 29, 2026
37a0e04
Merge remote-tracking branch 'origin/stable' into sync/stable-to-main…
github-actions[bot] May 29, 2026
989f060
feat(demo): contract-templates as a clause + field library on locked …
caio-pizzol May 29, 2026
29a5d2f
Merge remote-tracking branch 'origin/main' into andrii/llm-tools-bundles
andrii-harbour May 29, 2026
bfd0525
fix(sdk): address PR review for preset registry (SD-3128)
andrii-harbour May 29, 2026
955b2a3
refactor(demo): single-use clause library + brand-blue fields
caio-pizzol May 29, 2026
6c9ae00
Merge pull request #3574 from superdoc-dev/caio/contract-templates-sm…
caio-pizzol May 29, 2026
09d85ec
Merge branch 'main' into andrii/llm-tools-bundles
andrii-harbour May 29, 2026
fa9d733
Merge pull request #3582 from superdoc-dev/sync/stable-to-main-202605…
caio-pizzol May 29, 2026
64d9236
Merge pull request #3547 from superdoc-dev/sd-2751_configurable-toc-icon
harbournick May 29, 2026
4c51e5c
Merge pull request #3541 from superdoc-dev/andrii/llm-tools-bundles
harbournick May 29, 2026
1371d49
fix: nested replacement tracked-change decisions (#3557)
harbournick May 30, 2026
ba1f849
feat(painter-dom): custom SDT styling variables under chrome:'none' (…
caio-pizzol May 30, 2026
d01af47
fix(painter-dom): custom hover wins on locked SDTs under chrome:'none…
caio-pizzol May 30, 2026
928475b
docs(theming): point chrome:'none' styling at the custom SDT variable…
caio-pizzol May 30, 2026
2acccf1
demo/docs: contract-templates use the custom SDT styling variables (S…
caio-pizzol May 30, 2026
402a67d
refactor: move pm adapter out of layout engine (#3530)
VladaHarbour May 30, 2026
7348c74
fix(presentation): keep virtualized pages in sync with host scroll (#…
tupizz May 30, 2026
b1f39e7
Merge pull request #3590 from superdoc-dev/caio/sd-3322-content-contr…
caio-pizzol May 31, 2026
cae408b
Merge pull request #3591 from superdoc-dev/caio/contract-templates-cu…
caio-pizzol May 31, 2026
fc1480e
fix(super-converter): normalize single-paragraph BIBLIOGRAPHY/INDEX/T…
tupizz May 31, 2026
83013e4
feat: add per-author tracked change colors (#3559)
harbournick Jun 1, 2026
de5d484
Merge pull request #3494 from superdoc-dev/caio-pizzol/add-pickled-co…
caio-pizzol Jun 1, 2026
ce7590b
chore: remove root pickled.yml (pickled config schema changed)
caio-pizzol Jun 1, 2026
bce055c
Merge pull request #3601 from superdoc-dev/caio-pizzol/remove-pickled…
caio-pizzol Jun 1, 2026
36c81cb
feat(footnote): rendering fidelity (SD-2656) (#3220)
tupizz Jun 1, 2026
4c74ec6
fix: track new lines in suggested mode (#3602)
harbournick Jun 1, 2026
a594ce2
Merge remote-tracking branch 'origin/main' into merge/main-into-stabl…
github-actions[bot] Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/scripts/risk-label.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { readFileSync } from 'node:fs';
const CRITICAL_PATHS = [
'packages/layout-engine/style-engine/',
'packages/layout-engine/layout-engine/',
'packages/layout-engine/pm-adapter/',
'packages/pm-adapter/',
'packages/layout-engine/layout-bridge/',
'packages/layout-engine/measuring/',
'packages/layout-engine/painters/',
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/risk-label.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe('classify', () => {

it('critical: pm-adapter', () => {
assert.equal(
classify(['packages/layout-engine/pm-adapter/src/foo.js']).level,
classify(['packages/pm-adapter/src/foo.js']).level,
'critical',
);
});
Expand Down
18 changes: 12 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,31 @@ SuperDoc uses its own rendering pipeline. ProseMirror stores document state; it
```
.docx
→ super-converter parses OOXML into the hidden PM doc
→ pm-adapter reads PM state and resolved styles
→ v1 layout-adapter (super-editor: src/editors/v1/core/layout-adapter)
reads PM state and resolved styles
→ FlowBlock[]
→ layout-engine paginates
→ ResolvedLayout
→ DomPainter paints DOM
```

- The v1 ProseMirror → `FlowBlock[]` adapter is owned by `@superdoc/super-editor`
(`src/editors/v1/core/layout-adapter`). It is v1 SuperEditor's projection from
hidden ProseMirror state into layout data. v2 owns its own projection adapter.
`layout-engine` runtime packages consume `FlowBlock[]` and layout contracts
only; they must never import either concrete adapter.
- `PresentationEditor` wraps a hidden ProseMirror `Editor`. Its contenteditable DOM is never shown. PresentationEditor bridges editor events into layout/paint state; do not resolve OOXML semantics there.
- **DomPainter** (`layout-engine/painters/dom/`) owns all visual rendering.
- Style-resolved properties flow `pm-adapter` → DomPainter. Do not style document content with PM decorations.
- Style-resolved properties flow `layout-adapter` → DomPainter. Do not style document content with PM decorations.

### Where To Put Your Change

| Concern | Where | Rule |
|---|---|---|
| DOCX import/export | `super-editor/src/editors/v1/core/super-converter/` | Parse and preserve OOXML, style refs, inline properties. Do not bake resolved formatting into direct attrs. |
| Style cascade | `layout-engine/style-engine/` | Single source of truth for defaults, styles, conditional formatting, inline overrides. |
| Static document visuals | `pm-adapter/` data + `layout-engine/painters/dom/` rendering | Feed typed data into DomPainter. Do not style static content with PM decorations. |
| Direction-aware properties | `layout-engine/painters/dom/` | DomPainter mirrors at paint time for `w:bidiVisual`. pm-adapter stores logical sides LTR-default. Pre-mirroring upstream is a double-swap. See `packages/layout-engine/pm-adapter/src/direction/README.md`. |
| Static document visuals | v1 `core/layout-adapter/` data + `layout-engine/painters/dom/` rendering | Feed typed data into DomPainter. Do not style static content with PM decorations. |
| Direction-aware properties | `layout-engine/painters/dom/` | DomPainter mirrors at paint time for `w:bidiVisual`. The v1 layout-adapter stores logical sides LTR-default. Pre-mirroring upstream is a double-swap. See `packages/super-editor/src/editors/v1/core/layout-adapter/direction/README.md`. |
| Editing behavior | `super-editor/src/editors/v1/extensions/` | Commands, keybindings, editor plugins. Do not duplicate cascade or render document visuals here. |
| Final DOM rendering | `layout-engine/painters/dom/` | Render `ResolvedLayout`. Paint-time transforms (e.g. RTL mirror) live here. |
| New doc-api operation | `packages/document-api/src/contract/operation-definitions.ts` | Contract-first; touches 4 files. See `packages/document-api/README.md`. |
Expand All @@ -39,8 +45,8 @@ For specialized boundaries (interaction mapping, geometry/pagination, ephemeral
Before adding a visual or direction-aware path, run:

```bash
# Painter must not import upstream packages.
rg "@superdoc/(pm-adapter|style-engine|layout-bridge|layout-resolved)" packages/layout-engine/painters/dom/src
# Painter must not import upstream packages or the concrete v1 adapter.
rg "@superdoc/(super-editor|style-engine|layout-bridge|layout-resolved)" packages/layout-engine/painters/dom/src
```

More checks in `packages/layout-engine/AGENTS.md`.
Expand Down
10 changes: 7 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,17 @@ SuperDoc uses its own rendering pipeline -- ProseMirror is NOT used for visual o
```
DOCX File
→ super-converter (parse OOXML into ProseMirror document)
pm-adapter (convert PM nodes into FlowBlocks)
v1 layout-adapter (super-editor: convert PM nodes into FlowBlocks)
→ layout-engine (paginate FlowBlocks into Layouts)
→ DomPainter (render Layouts to DOM)
```

A hidden ProseMirror `Editor` instance manages document state and editing commands, but its DOM is never shown to the user. All visual rendering goes through DomPainter.

The PM → FlowBlock adapter is owned by `super-editor`
(`src/editors/v1/core/layout-adapter`), not by `layout-engine`. The layout
engine packages consume `FlowBlock[]` and shared layout contracts only.

### Project Structure

```
Expand All @@ -64,9 +68,9 @@ packages/
src/editors/v1/
core/
super-converter/ DOCX import/export (OOXML ↔ ProseMirror)
layout-adapter/ ProseMirror → FlowBlock[] projection (v1-owned)
extensions/ Editing behaviors (bold, lists, tables, etc.)
layout-engine/ Layout & pagination pipeline
pm-adapter/ ProseMirror → Layout bridge
layout-engine/ Pagination algorithms
painters/dom/ DOM rendering (DomPainter)
style-engine/ OOXML style resolution & cascade
Expand All @@ -84,7 +88,7 @@ tests/visual/ Visual regression tests (Playwright)
|--------------------------|---------------|
| How something looks (visual rendering) | `layout-engine/painters/dom/` |
| Style resolution (fonts, colors, borders) | `layout-engine/style-engine/` |
| Data flowing from editor to renderer | `layout-engine/pm-adapter/` |
| Data flowing from editor to renderer | `super-editor/src/editors/v1/core/layout-adapter/` |
| Editing behavior (keyboard, commands) | `super-editor/src/editors/v1/extensions/` |
| DOCX import/export | `super-editor/src/editors/v1/core/super-converter/` |
| React integration | `packages/react/` |
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ const superdoc = new SuperDoc({
});
```

Optional layered CSS mode:

```css
@layer reset, superdoc, app;
@import 'superdoc/style.layered.css';
@import 'your-app.css' layer(app);
```

Or use the CDN:

```html
Expand Down
1 change: 0 additions & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
},
"devDependencies": {
"@superdoc/document-api": "workspace:*",
"@superdoc/pm-adapter": "workspace:*",
"@superdoc/super-editor": "workspace:*",
"@types/bun": "catalog:",
"@types/node": "catalog:",
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/__tests__/lib/cli-import-boundaries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const BANNED_IMPORT_PATTERNS: ReadonlyArray<{ pattern: RegExp; reason: string }>
reason: 'CLI modules must not import super-editor source internals directly.',
},
{
pattern: /(?:^|\/)layout-engine\/(?:pm-adapter|layout-engine|painters|style-engine)\//,
pattern: /(?:^|\/)layout-engine\/(?:layout-engine|painters|style-engine)\//,
reason: 'CLI modules must not import layout-engine internals directly.',
},
{
Expand Down
10 changes: 7 additions & 3 deletions apps/docs/document-api/features/content-controls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ for (const control of items) {
}
```

Composed runtime: [`demos/contract-templates`](https://github.com/superdoc-dev/superdoc/tree/main/demos/contract-templates).
Or keep clauses **single-use and governed**: a clause is either in the contract or available to add from a library, and it appears once. Track inclusion by querying `contentControls.list` for the `sectionId` instead of comparing versions, and lock each placed clause (`contentLocked`) so its prose is fixed. A clause can also carry nested smart fields - inline controls inside the block - that fill from one place.

The [`demos/contract-templates`](https://github.com/superdoc-dev/superdoc/tree/main/demos/contract-templates) runtime composes the single-use approach: a clause library that inserts locked block clauses (some with nested fields), each filled by tag from a form.

## Why `tag`, not `nodeId`

Expand Down Expand Up @@ -134,10 +136,12 @@ Set `lockMode` when you create a control to govern which changes are allowed.
|---|---|
| `unlocked` | Content and properties can be updated through the Document API. |
| `sdtLocked` | The wrapper is preserved through user edits. |
| `contentLocked` | The content can't be modified through the editor surface. |
| `contentLocked` | The user can't edit the content, **and** content writes through the Document API (`text.setValue`, `replaceContent`) are rejected too - they return a `LOCK_VIOLATION`. |
| `sdtContentLocked` | Both wrapper and content are preserved. |

For controls your app drives with `text.setValue`, `replaceContent`, or `patch`, use `lockMode: 'unlocked'`.
For controls your app drives freely with `text.setValue` or `replaceContent`, use `lockMode: 'unlocked'`.

For a **locked template** - controls the user can't touch, but your app still updates - keep them `contentLocked` and unlock around each write: `setLockMode({ lockMode: 'unlocked' })`, write, then `setLockMode({ lockMode: 'contentLocked' })`. Use `try`/`finally` so a failed write never leaves a control unlocked. `setLockMode` and `patch` are not blocked by `contentLocked`, so only the content write needs the unlock window. A smart field nested inside a locked block control needs the **parent** unlocked for the write too, since the parent's content lock vetoes writes to anything inside it.

## Data binding

Expand Down
40 changes: 40 additions & 0 deletions apps/docs/editor/built-in-ui/track-changes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,46 @@ const superdoc = new SuperDoc({
</Expandable>
</ParamField>

<ParamField path="modules.trackChanges.authorColors" type="Object">
Resolve one highlight color per tracked-change author. This replaces app-side CSS overrides like `[data-track-change-author]` selectors.

<Expandable title="Fields">
<ParamField path="modules.trackChanges.authorColors.enabled" type="boolean" default="true">
Set to `false` to keep the default insert, delete, and format colors.
</ParamField>
<ParamField path="modules.trackChanges.authorColors.overrides" type="Record<string, string>">
Exact color overrides keyed by author email or author name. Email matches first, then name.
</ParamField>
<ParamField path="modules.trackChanges.authorColors.resolve" type="(author) => string | undefined">
Callback for authors not covered by `overrides`. Receives `{ name, email, image }`. Return any CSS color string, or `undefined` to use SuperDoc's deterministic fallback color.
</ParamField>
</Expandable>
</ParamField>

```javascript
new SuperDoc({
selector: "#editor",
document: "contract.docx",
modules: {
trackChanges: {
visible: true,
authorColors: {
overrides: {
"alice@example.com": "#1f6feb",
"Bob Reviewer": "#d1242f",
},
resolve: (author) => {
if (author.email?.endsWith("@outside-counsel.com")) return "#8250df";
return undefined; // SuperDoc assigns a stable fallback color
},
},
},
},
});
```

Per-author colors apply to insertion, deletion, and format-change highlights. SuperDoc derives lighter background variants from the same author color and exposes the resolved colors through the custom UI snapshot.

## Viewing mode visibility

Tracked-change markup is hidden by default when `documentMode` is `'viewing'`. Flip `modules.trackChanges.visible` to show it in read-only mode.
Expand Down
40 changes: 39 additions & 1 deletion apps/docs/editor/custom-ui/content-controls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,33 @@ new SuperDoc({

The event tells you *what* is active; `getRect` tells you *where* to draw. `active` is an `SdtRef` with `id`, `tag`, `alias`, `controlType`, and `scope`.

## Style the controls in place

Turning off chrome erases the built-in look, including hover and selection. To paint your own field and clause look, set `--sd-content-controls-custom-*` variables on the painted wrapper. Target it by your own `data-sdt-*` attributes. No `!important`, and no need to touch SuperDoc's internal state classes: the painter applies your variables across rest, hover, and selected, so the box stays stable and you never write `.ProseMirror-selectednode` or hover rules yourself.

```css
/* A field your app tagged { kind: 'smartField', ... } */
.superdoc-cc-chrome-none .superdoc-structured-content-inline[data-sdt-tag*='smartField'] {
--sd-content-controls-custom-inline-border: 1px solid #1355ff;
--sd-content-controls-custom-inline-bg: color-mix(in srgb, #1355ff 12%, transparent);
--sd-content-controls-custom-inline-hover-bg: color-mix(in srgb, #1355ff 20%, transparent);
--sd-content-controls-custom-inline-radius: 4px;
--sd-content-controls-custom-inline-padding: 1px 6px;
}

/* A clause your app tagged { kind: 'reusableSection', ... } */
.superdoc-cc-chrome-none .superdoc-structured-content-block[data-sdt-tag*='reusableSection'] {
--sd-content-controls-custom-block-border: 1px solid #d6e0ff;
--sd-content-controls-custom-block-border-left: 4px solid #1355ff; /* accent rail */
--sd-content-controls-custom-block-bg: color-mix(in srgb, #1355ff 4%, transparent);
--sd-content-controls-custom-block-radius: 6px;
}
```

`border` is a full CSS shorthand; `border-left` is an optional accent rail for block clauses. The background variables cascade, so set only what changes: `-hover-bg` defaults to `-bg`, and `-selected-bg` defaults to `-hover-bg`.

This is the path for `chrome: 'none'`. To theme the **built-in** chrome instead (`chrome: 'default'`), use the `--sd-content-controls-*` variables (without `custom`).

## Pick the right surface

| Goal | API |
Expand All @@ -55,8 +82,19 @@ The event tells you *what* is active; `getRect` tells you *where* to draw. `acti

You build your UI *over* the control, not inside it. SuperDoc owns how the control's content is painted in the document; you turn off its built-in chrome and draw your own (chips, badges, panels) anchored with `getRect`, react with the events, and change content through `editor.doc.contentControls.*`. Custom field types are expressed as a `tag` - for example `{ kind: 'smartField', key: 'party_name' }`, interpreted by your own UI - the underlying control stays a standard Word SDT so it round-trips to `.docx`.

## Build a custom field system

Putting it together into a fillable template, the way the contract-templates demo does:

1. **Define a tag schema.** Give each control a JSON `tag` your app owns - e.g. `{ kind: 'smartField', key }` for inline variables and `{ kind: 'reusableSection', sectionId }` for clauses.
2. **Insert from a palette.** Drop a control at a point with `editor.doc.create.contentControl({ kind, at, content, tag, lockMode })`, resolving the drop point with `ui.viewport.positionAt({ x, y })`. A clause can wrap its `{ field }` slots as nested inline controls.
3. **Style it.** Set the `--sd-content-controls-custom-*` variables on a `data-sdt-tag` selector (see [Style the controls in place](#style-the-controls-in-place)). The sidebar chips can reuse the same tokens, so palette and document match.
4. **React.** Highlight the active control from `content-control:active-change` / `:click`, and anchor overlays with `getRect` + `ui.viewport.observe`.
5. **Fill by tag.** A form edits a value and fans it to every occurrence: `editor.doc.contentControls.selectByTag({ tag })`, then `text.setValue` per occurrence.
6. **Govern with locks.** Keep controls `contentLocked` so users can't edit them, and have the form unlock → write → relock (see [Lock modes](/document-api/features/content-controls#lock-modes)). For a field nested in a locked clause, unlock the parent for the write.

## See also

- [Contract templates demo](https://github.com/superdoc-dev/superdoc/tree/main/demos/contract-templates) - a working field chip built on these APIs.
- [Contract templates demo](https://github.com/superdoc-dev/superdoc/tree/main/demos/contract-templates) - a full custom contract-template UI: a field + clause library, custom SDT styling, locks, form-driven values, events, insert, and export.
- [Configuration](/editor/superdoc/configuration) - the `modules.contentControls.chrome` option.
- [Document API: content controls](/document-api/features/content-controls) - read and change controls.
Loading
Loading