Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions apps/docs/editor/superdoc/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,34 @@ See [Surfaces](/editor/superdoc/surfaces) for the full API and examples.
<Warning>**Deprecated**: Use `modules.contextMenu` instead. See the [Context Menu module](/editor/built-in-ui/context-menu) for configuration options.</Warning>
</ParamField>

### Content controls module

<ParamField path="modules.contentControls" type="Object">
Content-control rendering configuration.

<Expandable title="properties" defaultOpen>
<ParamField path="modules.contentControls.chrome" type="'default' | 'none'" default="'default'">
Built-in content-control chrome mode.
- `'default'`: Word-like built-in chrome (labels, borders, hover, selected outline)
- `'none'`: disables built-in chrome while keeping SDT data/geometry and `data-sdt-*` wrappers
</ParamField>
</Expandable>
</ParamField>

Use this when you want to keep content controls in the document but disable SuperDoc’s built-in field visuals.

```javascript
new SuperDoc({
selector: '#editor',
document: 'contract.docx',
modules: {
contentControls: {
chrome: 'none',
},
},
});
```

## Spell check

Provider-based spell check for the layout-engine editor surface. The configuration key is `proofing` because the provider contract is designed to support spelling, grammar, and style. The current UI renders spelling only.
Expand Down
10 changes: 5 additions & 5 deletions demos/contract-templates/src/field-chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
* the Clauses tab. Linked-occurrence highlights, field-details popovers,
* and clause badges are deliberate follow-ups (SD-3155 umbrella).
*
* The chip renders ALONGSIDE SuperDoc's built-in SDT chrome (blue
* label, border, hover background) by design: this demo demonstrates
* the API's ability to add contextual UI on top of the document, not
* to replace the editor's default visuals. Suppressing the built-in
* chrome is filed as SD-3159 (`modules.contentControls.chrome: 'default' | 'none'`).
* This demo runs with SuperDoc's built-in SDT chrome turned off
* (`modules.contentControls.chrome: 'none'`, SD-3159), so the chip is the
* smart field's active-state UI rather than an addition on top of the
* built-in blue label/border. The wrappers and data-sdt-* datasets are
* still emitted, which is what `observe`/`getRect`/`get` rely on.
*/
import type { SuperDocUI } from 'superdoc/ui';

Expand Down
6 changes: 5 additions & 1 deletion demos/contract-templates/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,11 @@ const superdoc = new SuperDoc({
selector: '#editor',
documentMode: 'editing',
document: '/nda-template.docx',
modules: { comments: false },
// Disable SuperDoc's built-in SDT chrome (border, label, hover/selection
// highlight). The wrappers and data-sdt-* datasets are preserved, so the
// contextual field chip (field-chip.ts) and the document API still work;
// this demo paints its own SDT visuals in style.css instead.
modules: { comments: false, contentControls: { chrome: 'none' } },
telemetry: { enabled: false },
onReady: ({ superdoc: sd }) => void initialize(sd as DemoSuperDoc),
});
Expand Down
43 changes: 19 additions & 24 deletions demos/contract-templates/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -238,42 +238,37 @@ input:focus {
}

/* -----------------------------------------------------------------------
In-editor SDT chrome (visible DomPainter classes only)
The painter renders block SDTs with `position: absolute` and explicit
coordinates, so the rules below color the existing 1px border slot and
add background only. They never change padding, margin, or box size.
Host-owned SDT styling.
This demo turns off SuperDoc's built-in content-control chrome
(`modules.contentControls.chrome: 'none'` in main.ts) and paints its
own. The painter adds `.superdoc-cc-chrome-none` to the mount and resets
border/padding/radius/background on the SDT wrappers; scoping under that
class keeps these rules above the reset in specificity and cascade, and
restores the box properties the reset strips. No painter label element
exists under chrome-none, so there is nothing to style for it.
----------------------------------------------------------------------- */

/* Inline smart fields: blue pill */
.superdoc-structured-content-inline[data-sdt-tag*='smartField'] {
border-color: var(--demo-accent);
.superdoc-cc-chrome-none .superdoc-structured-content-inline[data-sdt-tag*='smartField'] {
padding: 1px 4px;
border: 1px solid var(--demo-accent);
border-radius: 4px;
background-color: var(--demo-accent-soft);
}

/* Block clauses: visible bordered card with soft background */
.superdoc-structured-content-block[data-sdt-tag*='reusableSection'] {
border-color: var(--demo-border);
background-color: var(--demo-bg);
}
.superdoc-structured-content-block[data-sdt-tag*='reusableSection'][data-lock-mode='unlocked'] {
/* Block clauses: bordered card with soft background */
.superdoc-cc-chrome-none .superdoc-structured-content-block[data-sdt-tag*='reusableSection'] {
border: 1px solid var(--demo-border);
border-radius: 4px;
background-color: var(--demo-bg);
}

/* Existing painter-provided label gets a typographic tune */
.superdoc-structured-content-block__label {
font-size: var(--sd-font-size-200, 11px);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--demo-text-muted);
font-weight: 600;
}

/*
* Contextual smart-field chip (SD-3157). Floats over the active smart-
* field SDT showing field label + live value. Wired in field-chip.ts
* against `ui.contentControls.observe` + `getRect`. Renders alongside
* SuperDoc's built-in SDT chrome by design — the chip demonstrates
* additive contextual UI, not a chrome replacement (that's SD-3159).
* against `ui.contentControls.observe` + `getRect`. With built-in chrome
* off (SD-3159), the chip is the smart field's active-state affordance:
* custom UI anchored to the SDT via the public geometry API.
*/
.sd-field-chip {
display: inline-flex;
Expand Down
182 changes: 182 additions & 0 deletions packages/layout-engine/painters/dom/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2546,6 +2546,62 @@ describe('DomPainter', () => {
expect(fragment.dataset.sdtSectionLocked).toBe('true');
});

it('keeps documentSection tooltip when contentControlsChrome is none', () => {
const sectionBlock: FlowBlock = {
kind: 'paragraph',
id: 'section-para-tooltip',
runs: [{ text: 'Confidential terms', fontFamily: 'Arial', fontSize: 16, pmStart: 0, pmEnd: 18 }],
attrs: {
sdt: {
type: 'documentSection',
id: 'section-2',
title: 'Locked Section',
description: 'Confidential clause',
sectionType: 'locked',
isLocked: true,
},
},
};

const sectionMeasure: Measure = {
kind: 'paragraph',
lines: [{ fromRun: 0, fromChar: 0, toRun: 0, toChar: 18, width: 120, ascent: 12, descent: 4, lineHeight: 20 }],
totalHeight: 20,
};

const sectionLayout: Layout = {
pageSize: { w: 400, h: 500 },
pages: [
{
number: 1,
fragments: [
{
kind: 'para',
blockId: 'section-para-tooltip',
fromLine: 0,
toLine: 1,
x: 20,
y: 30,
width: 320,
pmStart: 0,
pmEnd: 18,
},
],
},
],
};

const painter = createTestPainter({
blocks: [sectionBlock],
measures: [sectionMeasure],
contentControlsChrome: 'none',
});
painter.paint(sectionLayout, mount);

expect(mount.classList.contains('superdoc-cc-chrome-none')).toBe(true);
expect(mount.querySelector('.superdoc-document-section__tooltip')).toBeTruthy();
});

it('annotates fragments with both primary SDT and container SDT metadata', () => {
// Test case: TOC paragraph inside a documentSection
// Should have docPart metadata as primary (data-sdt-*) and section as container (data-sdt-container-*)
Expand Down Expand Up @@ -2858,6 +2914,74 @@ describe('DomPainter', () => {
expect(wrapper?.dataset.pmEnd).toBe('22');
});

it('omits inline content-control label when contentControlsChrome is none', () => {
const inlineScBlock: FlowBlock = {
kind: 'paragraph',
id: 'inline-sc-no-chrome',
runs: [
{ text: 'Before ', fontFamily: 'Arial', fontSize: 16, pmStart: 0, pmEnd: 7 },
{
text: 'controlled',
fontFamily: 'Arial',
fontSize: 16,
pmStart: 7,
pmEnd: 17,
sdt: {
type: 'structuredContent',
scope: 'inline',
id: 'sc-inline-none',
tag: 'dropdown',
alias: 'Test Dropdown',
},
},
{ text: ' after', fontFamily: 'Arial', fontSize: 16, pmStart: 17, pmEnd: 23 },
],
attrs: {},
};

const inlineScMeasure: Measure = {
kind: 'paragraph',
lines: [{ fromRun: 0, fromChar: 0, toRun: 2, toChar: 6, width: 200, ascent: 12, descent: 4, lineHeight: 20 }],
totalHeight: 20,
};

const inlineScLayout: Layout = {
pageSize: { w: 612, h: 792 },
pages: [
{
number: 1,
fragments: [
{
kind: 'para',
blockId: 'inline-sc-no-chrome',
fromLine: 0,
toLine: 1,
x: 30,
y: 40,
width: 552,
pmStart: 0,
pmEnd: 23,
},
],
},
],
};

const painter = createTestPainter({
blocks: [inlineScBlock],
measures: [inlineScMeasure],
contentControlsChrome: 'none',
});
painter.paint(inlineScLayout, mount);

const wrapper = mount.querySelector(
'.superdoc-structured-content-inline[data-sdt-id="sc-inline-none"]',
) as HTMLElement | null;
expect(wrapper).toBeTruthy();
if (!wrapper) return;
expect(wrapper.querySelector('.superdoc-structured-content-inline__label')).toBeNull();
});

it('omits chrome and alias label when inline SDT appearance is hidden (SD-3110)', () => {
// ECMA-376 `<w15:appearance w15:val="hidden"/>` should render the
// SDT transparently: no padding/border/label, and the alias text
Expand Down Expand Up @@ -13918,6 +14042,64 @@ describe('applyRunDataAttributes', () => {
expect(fragment.style.getPropertyValue('--sd-sdt-chrome-width')).toBe('100px');
});

it('omits block structured-content label when contentControlsChrome is none', () => {
const blockSdtBlock: FlowBlock = {
kind: 'paragraph',
id: 'block-sdt-none',
runs: [{ text: 'Content in block SDT', fontFamily: 'Arial', fontSize: 16, pmStart: 0, pmEnd: 20 }],
attrs: {
sdt: {
type: 'structuredContent',
scope: 'block',
id: 'scb-block-none',
tag: '{"fieldType":"signer"}',
alias: 'Block Content Control',
},
},
};

const blockSdtMeasure: Measure = {
kind: 'paragraph',
lines: [
{ fromRun: 0, fromChar: 0, toRun: 0, toChar: 20, width: 180, ascent: 12, descent: 4, lineHeight: 20 },
],
totalHeight: 20,
};

const blockSdtLayout: Layout = {
pageSize: { w: 400, h: 500 },
pages: [
{
number: 1,
fragments: [
{
kind: 'para',
blockId: 'block-sdt-none',
fromLine: 0,
toLine: 1,
x: 20,
y: 30,
width: 320,
pmStart: 0,
pmEnd: 20,
},
],
},
],
};

const painter = createTestPainter({
blocks: [blockSdtBlock],
measures: [blockSdtMeasure],
contentControlsChrome: 'none',
});
painter.paint(blockSdtLayout, mount);

const fragment = mount.querySelector('.superdoc-fragment') as HTMLElement;
expect(fragment.classList.contains('superdoc-structured-content-block')).toBe(true);
expect(fragment.querySelector('.superdoc-structured-content__label')).toBeFalsy();
});

it('updates block SDT boundaries when appending a new fragment during patch rendering', () => {
const sdtMetadata = {
type: 'structuredContent' as const,
Expand Down
2 changes: 2 additions & 0 deletions packages/layout-engine/painters/dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ export type DomPainterOptions = {
onPaintSnapshot?: (snapshot: PaintSnapshot) => void;
/** Render nonprinting formatting marks such as spaces, tabs, and paragraph marks. */
showFormattingMarks?: boolean;
/** Built-in SDT chrome rendering mode. */
contentControlsChrome?: 'default' | 'none';
};

export type DomPainterHandle = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export type RenderParagraphContentParams = {
convertFinalParagraphMark?: boolean;
lineTopOffset?: number;
sourceAnchor?: SourceAnchor;
contentControlsChrome?: 'default' | 'none';
};

export type RenderParagraphContentResult = {
Expand Down Expand Up @@ -139,6 +140,7 @@ export const renderParagraphContent = (params: RenderParagraphContentParams): Re
onSdtContainerChrome,
applySdtDataset,
applyContainerSdtDataset,
contentControlsChrome,
renderDropCap,
lineTopOffset = 0,
} = params;
Expand All @@ -163,7 +165,17 @@ export const renderParagraphContent = (params: RenderParagraphContentParams): Re
ancestorContainerSdts,
});
if (applySdtChrome) {
if (applySdtContainerChrome(doc, frameEl, block.attrs?.sdt, block.attrs?.containerSdt, sdtBoundary)) {
if (
applySdtContainerChrome(
doc,
frameEl,
block.attrs?.sdt,
block.attrs?.containerSdt,
sdtBoundary,
undefined,
contentControlsChrome,
)
) {
onSdtContainerChrome?.();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type RenderParagraphFragmentParams = {
options?: { sourceAnchor?: ResolvedFragmentItem['sourceAnchor']; wrapperEl?: HTMLElement },
) => void;
createErrorPlaceholder: (blockId: string, error: unknown) => HTMLElement;
contentControlsChrome?: 'default' | 'none';
};

const isMinimalWordLayout = (value: unknown): value is MinimalWordLayout => isMinimalWordLayoutShared(value);
Expand All @@ -51,6 +52,7 @@ export const renderParagraphFragment = (params: RenderParagraphFragmentParams):
renderLine,
captureLineSnapshot,
createErrorPlaceholder,
contentControlsChrome,
} = params;

try {
Expand Down Expand Up @@ -132,6 +134,7 @@ export const renderParagraphFragment = (params: RenderParagraphFragmentParams):
});
},
sourceAnchor: resolvedItem?.sourceAnchor,
contentControlsChrome,
});

return fragmentEl;
Expand Down
Loading
Loading