diff --git a/.github/scripts/risk-label.mjs b/.github/scripts/risk-label.mjs index 236407cd02..badd044763 100644 --- a/.github/scripts/risk-label.mjs +++ b/.github/scripts/risk-label.mjs @@ -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/', diff --git a/.github/scripts/risk-label.test.mjs b/.github/scripts/risk-label.test.mjs index 608420b4de..cb13fce77a 100644 --- a/.github/scripts/risk-label.test.mjs +++ b/.github/scripts/risk-label.test.mjs @@ -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', ); }); diff --git a/AGENTS.md b/AGENTS.md index bf219229e6..3a29083be5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,16 +9,22 @@ 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 @@ -26,8 +32,8 @@ SuperDoc uses its own rendering pipeline. ProseMirror stores document state; it |---|---|---| | 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`. | @@ -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`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f450127e7..4f24f28780 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 ``` @@ -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 @@ -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/` | diff --git a/README.md b/README.md index c576761708..836b43f13a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/apps/cli/package.json b/apps/cli/package.json index 5f4736c3d3..32655354b3 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -46,7 +46,6 @@ }, "devDependencies": { "@superdoc/document-api": "workspace:*", - "@superdoc/pm-adapter": "workspace:*", "@superdoc/super-editor": "workspace:*", "@types/bun": "catalog:", "@types/node": "catalog:", diff --git a/apps/cli/src/__tests__/lib/cli-import-boundaries.test.ts b/apps/cli/src/__tests__/lib/cli-import-boundaries.test.ts index 6cf42dfd6c..27145fe693 100644 --- a/apps/cli/src/__tests__/lib/cli-import-boundaries.test.ts +++ b/apps/cli/src/__tests__/lib/cli-import-boundaries.test.ts @@ -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.', }, { diff --git a/apps/docs/document-api/features/content-controls.mdx b/apps/docs/document-api/features/content-controls.mdx index 2f48d35d3c..ee7a34850d 100644 --- a/apps/docs/document-api/features/content-controls.mdx +++ b/apps/docs/document-api/features/content-controls.mdx @@ -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` @@ -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 diff --git a/apps/docs/editor/built-in-ui/track-changes.mdx b/apps/docs/editor/built-in-ui/track-changes.mdx index b9fe67687d..9b50954b28 100644 --- a/apps/docs/editor/built-in-ui/track-changes.mdx +++ b/apps/docs/editor/built-in-ui/track-changes.mdx @@ -75,6 +75,46 @@ const superdoc = new SuperDoc({ + + Resolve one highlight color per tracked-change author. This replaces app-side CSS overrides like `[data-track-change-author]` selectors. + + + + Set to `false` to keep the default insert, delete, and format colors. + + + Exact color overrides keyed by author email or author name. Email matches first, then name. + + + 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. + + + + +```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. diff --git a/apps/docs/editor/custom-ui/content-controls.mdx b/apps/docs/editor/custom-ui/content-controls.mdx index c0a94bcce8..2ff5e09fce 100644 --- a/apps/docs/editor/custom-ui/content-controls.mdx +++ b/apps/docs/editor/custom-ui/content-controls.mdx @@ -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 | @@ -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. diff --git a/apps/docs/editor/custom-ui/track-changes.mdx b/apps/docs/editor/custom-ui/track-changes.mdx index e614f4f44e..04695b4543 100644 --- a/apps/docs/editor/custom-ui/track-changes.mdx +++ b/apps/docs/editor/custom-ui/track-changes.mdx @@ -12,16 +12,25 @@ description: 'Build your own track-changes review panel. Accept, reject, navigat import { useSuperDocTrackChanges, useSuperDocUI } from 'superdoc/ui/react'; export function ReviewPanel() { - const { items, total } = useSuperDocTrackChanges(); + const { items, total, authors } = useSuperDocTrackChanges(); const ui = useSuperDocUI(); return (