Skip to content

Commit 91feebf

Browse files
authored
feat(superdoc): support fixed-height container embedding with contained mode (SD-2242) (#2423)
* feat(superdoc): support fixed-height container embedding with contained mode SD-2242: Add `contained` option that enables SuperDoc to scroll within a fixed-height parent container. When `contained: true`, height propagates through the DOM chain and `.super-editor-container` becomes the scroll container with `overflow: auto`. Key changes: - New `contained` boolean in Config type and React props - CSS height chain: .superdoc--contained → layers → document → sub-document - .super-editor-container.contained becomes scroll container (overflow: auto) - .super-editor overflow changed from hidden to visible in contained mode - CommentsLayer switched to position: absolute with pointer-events: none in contained mode to avoid blocking scroll and taking flow space - React wrapper conditionally sets height: 100% on editor container * docs: add contained mode configuration to SuperDoc and React docs Document the new `contained` option in both the vanilla SuperDoc configuration page and the React component configuration page, with usage examples showing fixed-height container embedding. * fix(react): use flex layout for contained mode to account for toolbar When contained mode is active with a visible toolbar, height: 100% on the editor container would overflow. Switch to flex layout so toolbar and editor share the fixed-height wrapper properly. * feat(superdoc): add contained mode scroll support for PDF and HTML viewers The sub-document container gets overflow: auto in contained mode so PDF and HTML documents scroll within fixed-height containers, not just DOCX. SuperEditor already manages its own scroll container. * fix(react): resolve type-check error for contained prop access The contained property is defined via JSDoc in superdoc's types and may not appear in generated .d.ts files. Use a type assertion instead of accessing restProps.contained directly. * chore: add pre-commit type-check hook for React package * fix(superdoc): restore pointer-events on comment anchors in contained mode The comments layer has pointer-events: none in contained mode to let scroll events pass through, but this also disables comment highlight clicks. Re-enable pointer-events on .sd-comment-anchor so comments remain interactive. * docs: update AGENTS.md and docs snippet to use contained mode Add contained mode section to AGENTS.md for AI agent consumers. Simplify the docs embed component by replacing manual CSS height/ overflow hacks with contained: true. * docs: add contained mode section to CLAUDE.md * docs(create): add contained option to AGENTS.md config table * docs(react): add contained mode section to AGENTS.md and CLAUDE.md * refactor(react): make contained an explicit React prop Instead of accessing contained through restProps with an unsafe cast, add it to ReactProps interface and destructure it like hideToolbar. This gives TypeScript users autocomplete and removes the cast. * fix(react): resolve type-check error from empty @types/minimatch The root node_modules/@types/minimatch is an empty shell with no .d.ts files. TypeScript auto-discovers all @types/* packages and fails on it. Set explicit types in tsconfig to only include react and react-dom. * fix(react): address review findings for contained prop - Pass contained unconditionally (always boolean, no need for spread) - Add contained to useEffect dep array so toggling triggers rebuild
1 parent d34ec3f commit 91feebf

14 files changed

Lines changed: 167 additions & 16 deletions

File tree

apps/create/src/templates.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ Key options for the editor:
172172
| \`documentMode\` | \`'editing' \\| 'viewing' \\| 'suggesting'\` | Editor mode |
173173
| \`user\` | \`{ name, email }\` | Current user (for comments/tracked changes) |
174174
| \`toolbar\` | \`string \\| HTMLElement\` | Toolbar mount selector or element |
175+
| \`contained\` | \`boolean\` | Scroll inside a fixed-height parent instead of expanding |
175176
| \`modules.comments\` | \`object\` | Comments panel configuration |
176177
| \`modules.collaboration\` | \`object\` | Real-time collaboration (Yjs) |
177178

apps/docs/core/react/configuration.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,23 @@ All props are passed directly to the `<SuperDocEditor>` component. Only `documen
7878
Hide the toolbar.
7979
</ParamField>
8080

81+
<ParamField path="contained" type="boolean" default="false">
82+
Enable contained mode for fixed-height container embedding. When `true`, SuperDoc fits within its parent's height and scrolls internally.
83+
84+
Your wrapper must have a definite height (e.g., `height: 400px`, `flex: 1`, or a CSS class with a fixed height). Pass `style={{ height: '100%' }}` to propagate the height.
85+
86+
```jsx
87+
<div style={{ height: 500 }}>
88+
<SuperDocEditor
89+
document={file}
90+
documentMode="viewing"
91+
contained
92+
style={{ height: '100%' }}
93+
/>
94+
</div>
95+
```
96+
</ParamField>
97+
8198
<ParamField path="rulers" type="boolean">
8299
Show or hide rulers. Uses SuperDoc default if not set.
83100
</ParamField>

apps/docs/core/superdoc/configuration.mdx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,42 @@ new SuperDoc({
395395
```
396396
</ParamField>
397397

398+
<ParamField path="contained" type="boolean" default="false">
399+
Enable contained mode for fixed-height container embedding. When `true`, SuperDoc fits within its parent's height and scrolls internally instead of expanding to the document's natural height. Works with DOCX, PDF, and HTML documents.
400+
401+
Use this when embedding SuperDoc inside a panel, sidebar, or any container with a fixed height (e.g., `height: 400px` or `flex: 1`).
402+
403+
<CodeGroup>
404+
405+
```javascript Usage
406+
const superdoc = new SuperDoc({
407+
selector: '#editor',
408+
document: file,
409+
contained: true,
410+
});
411+
```
412+
413+
```html Full Example
414+
<!-- Parent must have a definite height -->
415+
<div style="height: 500px;">
416+
<div id="editor"></div>
417+
</div>
418+
419+
<script type="module">
420+
import { SuperDoc } from 'superdoc';
421+
import 'superdoc/style.css';
422+
423+
new SuperDoc({
424+
selector: '#editor',
425+
document: yourFile,
426+
contained: true,
427+
});
428+
</script>
429+
```
430+
431+
</CodeGroup>
432+
</ParamField>
433+
398434
<ParamField path="layoutMode" type="string" deprecated>
399435
<Warning>**Removed in v1.0** — Use `viewOptions.layout` instead. `'paginated'``'print'`, `'responsive'``'web'`.</Warning>
400436
</ParamField>

apps/docs/snippets/components/superdoc-editor.jsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export const SuperDocEditor = ({
7979
selector: `#${containerIdRef.current}`,
8080
html,
8181
rulers: true,
82+
contained: true,
8283
onReady: () => {
8384
setReady(true);
8485
if (onReady) onReady(editorRef.current);
@@ -151,26 +152,17 @@ export const SuperDocEditor = ({
151152
)}
152153
</div>
153154
)}
154-
<div
155-
id={containerIdRef.current}
156-
style={{ minHeight: height, maxHeight, paddingLeft: '5px', overflow: 'scroll' }}
157-
/>
155+
<div id={containerIdRef.current} style={{ height, maxHeight, paddingLeft: '5px' }} />
158156
<style jsx>{`
159157
#${containerIdRef.current} .superdoc__layers {
160158
max-width: 660px !important;
161159
}
162-
#${containerIdRef.current} .super-editor-container {
163-
min-width: unset !important;
164-
min-height: unset !important;
165-
width: 100% !important;
166-
}
167160
#${containerIdRef.current} .super-editor {
168161
max-width: 100% !important;
169162
width: 100% !important;
170163
color: #000;
171164
}
172165
#${containerIdRef.current} .editor-element {
173-
min-height: ${height} !important;
174166
width: 100% !important;
175167
min-width: unset !important;
176168
transform: none !important;

lefthook.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ pre-commit:
2424
root: "apps/docs/"
2525
glob: "apps/docs/**/*.mdx"
2626
run: pnpm run test:examples
27+
react-type-check:
28+
root: "packages/react/"
29+
glob: "packages/react/**/*.{ts,tsx}"
30+
run: pnpm run type-check
2731
generate-all:
2832
glob: "packages/document-api/src/contract/**/*.ts"
2933
run: pnpm run generate:all && git add apps/docs/document-api/reference apps/docs/document-api/overview.mdx apps/docs/document-engine/sdks.mdx

packages/react/AGENTS.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ export interface SuperDocEditorProps
4545
| `className` | `string` | - | Wrapper CSS class |
4646
| `style` | `CSSProperties` | - | Wrapper inline styles |
4747

48+
## Fixed-height container embedding
49+
50+
Pass `contained` to scroll inside a fixed-height parent. The wrapper must have a definite height.
51+
52+
```tsx
53+
<div style={{ height: 500 }}>
54+
<SuperDocEditor
55+
document={file}
56+
documentMode="viewing"
57+
contained
58+
style={{ height: '100%' }}
59+
/>
60+
</div>
61+
```
62+
4863
## SSR Behavior
4964

5065
- Container divs are always rendered (hidden with `display: none` until initialized)

packages/react/src/SuperDocEditor.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ function SuperDocEditorInner(props: SuperDocEditorProps, ref: ForwardedRef<Super
3838
id,
3939
renderLoading,
4040
hideToolbar = false,
41+
contained = false,
4142
className,
4243
style,
4344
// Callbacks (stored in ref to avoid triggering rebuilds)
@@ -149,6 +150,7 @@ function SuperDocEditorInner(props: SuperDocEditorProps, ref: ForwardedRef<Super
149150
...(!hideToolbar && toolbarContainerRef.current ? { toolbar: `#${CSS.escape(toolbarId)}` } : {}),
150151
documentMode,
151152
role,
153+
contained,
152154
...(documentProp != null ? { document: documentProp } : {}),
153155
...(user ? { user } : {}),
154156
...(users ? { users } : {}),
@@ -224,17 +226,26 @@ function SuperDocEditorInner(props: SuperDocEditorProps, ref: ForwardedRef<Super
224226
// initial values - use getInstance() methods to change them at runtime.
225227
// Note: restProps is intentionally excluded to avoid rebuilds on every render.
226228
// documentMode is handled separately via setDocumentMode() for efficiency.
227-
}, [documentProp, user, users, modules, role, hideToolbar, containerId, toolbarId]);
229+
}, [documentProp, user, users, modules, role, hideToolbar, contained, containerId, toolbarId]);
228230

229231
const wrapperClassName = ['superdoc-wrapper', className].filter(Boolean).join(' ');
230232
const hideWhenLoading: CSSProperties | undefined = isLoading ? { display: 'none' } : undefined;
231233

234+
const wrapperStyle: CSSProperties = {
235+
...style,
236+
...(contained && { display: 'flex', flexDirection: 'column' as const }),
237+
};
238+
232239
return (
233-
<div className={wrapperClassName} style={style}>
240+
<div className={wrapperClassName} style={wrapperStyle}>
234241
{!hideToolbar && (
235242
<div ref={toolbarContainerRef} id={toolbarId} className='superdoc-toolbar-container' style={hideWhenLoading} />
236243
)}
237-
<div id={containerId} className='superdoc-editor-container' style={hideWhenLoading} />
244+
<div
245+
id={containerId}
246+
className='superdoc-editor-container'
247+
style={{ ...hideWhenLoading, ...(contained && { flex: 1, minHeight: 0 }) }}
248+
/>
238249
{isLoading && !hasError && renderLoading && <div className='superdoc-loading-container'>{renderLoading()}</div>}
239250
{hasError && <div className='superdoc-error-container'>Failed to load editor. Check console for details.</div>}
240251
</div>

packages/react/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ interface ReactProps {
136136
/** Hide the toolbar container. When true, no toolbar is rendered. @default false */
137137
hideToolbar?: boolean;
138138

139+
/** Enable contained mode for fixed-height container embedding. When true, SuperDoc
140+
* fits within its parent's height and scrolls internally. @default false */
141+
contained?: boolean;
142+
139143
/** Additional CSS class name for the wrapper element */
140144
className?: string;
141145

packages/react/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"rootDir": "./src",
77
"jsx": "react-jsx",
88
"forceConsistentCasingInFileNames": true,
9-
"allowSyntheticDefaultImports": true
9+
"allowSyntheticDefaultImports": true,
10+
"types": ["react", "react-dom"]
1011
},
1112
"include": ["src/**/*"],
1213
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]

packages/super-editor/src/editors/v1/components/SuperEditor.vue

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ const isWebLayout = computed(() => {
7272
return props.options.viewOptions?.layout === 'web';
7373
});
7474
75+
const isContained = computed(() => {
76+
return Boolean(props.options.contained);
77+
});
78+
7579
/**
7680
* Reactive ruler visibility state.
7781
* Uses a ref with a deep watcher to ensure proper reactivity when options.rulers changes.
@@ -1264,7 +1268,11 @@ onBeforeUnmount(() => {
12641268
</script>
12651269

12661270
<template>
1267-
<div class="super-editor-container" :class="{ 'web-layout': isWebLayout }" :style="containerStyle">
1271+
<div
1272+
class="super-editor-container"
1273+
:class="{ 'web-layout': isWebLayout, contained: isContained }"
1274+
:style="containerStyle"
1275+
>
12681276
<!-- Ruler: teleport to external container if specified, otherwise render inline (hidden in web layout) -->
12691277
<Teleport
12701278
v-if="options.rulerContainer && rulersVisible && !isWebLayout && !!activeEditor"
@@ -1403,4 +1411,20 @@ onBeforeUnmount(() => {
14031411
overflow: hidden;
14041412
position: relative;
14051413
}
1414+
1415+
/* Contained mode: fixed-height container embedding with internal scrolling.
1416+
* The super-editor-container becomes the scroll container (overflow: auto).
1417+
* The .super-editor overflow is changed from hidden to visible so content
1418+
* flows through to the scroll container. The visibleHost (.presentation-editor)
1419+
* stays overflow: visible per PresentationEditor's design — it is NOT the scroller.
1420+
*/
1421+
.super-editor-container.contained {
1422+
height: 100%;
1423+
min-height: 0;
1424+
overflow: auto;
1425+
}
1426+
1427+
.super-editor-container.contained .super-editor {
1428+
overflow: visible;
1429+
}
14061430
</style>

0 commit comments

Comments
 (0)