Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/create/src/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ Key options for the editor:
| \`documentMode\` | \`'editing' \\| 'viewing' \\| 'suggesting'\` | Editor mode |
| \`user\` | \`{ name, email }\` | Current user (for comments/tracked changes) |
| \`toolbar\` | \`string \\| HTMLElement\` | Toolbar mount selector or element |
| \`contained\` | \`boolean\` | Scroll inside a fixed-height parent instead of expanding |
| \`modules.comments\` | \`object\` | Comments panel configuration |
| \`modules.collaboration\` | \`object\` | Real-time collaboration (Yjs) |

Expand Down
17 changes: 17 additions & 0 deletions apps/docs/core/react/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,23 @@ All props are passed directly to the `<SuperDocEditor>` component. Only `documen
Hide the toolbar.
</ParamField>

<ParamField path="contained" type="boolean" default="false">
Enable contained mode for fixed-height container embedding. When `true`, SuperDoc fits within its parent's height and scrolls internally.

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.

```jsx
<div style={{ height: 500 }}>
<SuperDocEditor
document={file}
documentMode="viewing"
contained
style={{ height: '100%' }}
/>
</div>
```
</ParamField>

<ParamField path="rulers" type="boolean">
Show or hide rulers. Uses SuperDoc default if not set.
</ParamField>
Expand Down
36 changes: 36 additions & 0 deletions apps/docs/core/superdoc/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,42 @@ new SuperDoc({
```
</ParamField>

<ParamField path="contained" type="boolean" default="false">
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.

Use this when embedding SuperDoc inside a panel, sidebar, or any container with a fixed height (e.g., `height: 400px` or `flex: 1`).

<CodeGroup>

```javascript Usage
const superdoc = new SuperDoc({
selector: '#editor',
document: file,
contained: true,
});
```

```html Full Example
<!-- Parent must have a definite height -->
<div style="height: 500px;">
<div id="editor"></div>
</div>

<script type="module">
import { SuperDoc } from 'superdoc';
import 'superdoc/style.css';

new SuperDoc({
selector: '#editor',
document: yourFile,
contained: true,
});
</script>
```

</CodeGroup>
</ParamField>

<ParamField path="layoutMode" type="string" deprecated>
<Warning>**Removed in v1.0** — Use `viewOptions.layout` instead. `'paginated'` → `'print'`, `'responsive'` → `'web'`.</Warning>
</ParamField>
Expand Down
12 changes: 2 additions & 10 deletions apps/docs/snippets/components/superdoc-editor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export const SuperDocEditor = ({
selector: `#${containerIdRef.current}`,
html,
rulers: true,
contained: true,
onReady: () => {
setReady(true);
if (onReady) onReady(editorRef.current);
Expand Down Expand Up @@ -151,26 +152,17 @@ export const SuperDocEditor = ({
)}
</div>
)}
<div
id={containerIdRef.current}
style={{ minHeight: height, maxHeight, paddingLeft: '5px', overflow: 'scroll' }}
/>
<div id={containerIdRef.current} style={{ height, maxHeight, paddingLeft: '5px' }} />
<style jsx>{`
#${containerIdRef.current} .superdoc__layers {
max-width: 660px !important;
}
#${containerIdRef.current} .super-editor-container {
min-width: unset !important;
min-height: unset !important;
width: 100% !important;
}
#${containerIdRef.current} .super-editor {
max-width: 100% !important;
width: 100% !important;
color: #000;
}
#${containerIdRef.current} .editor-element {
min-height: ${height} !important;
width: 100% !important;
min-width: unset !important;
transform: none !important;
Expand Down
4 changes: 4 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ pre-commit:
root: "apps/docs/"
glob: "apps/docs/**/*.mdx"
run: pnpm run test:examples
react-type-check:
root: "packages/react/"
glob: "packages/react/**/*.{ts,tsx}"
run: pnpm run type-check
generate-all:
glob: "packages/document-api/src/contract/**/*.ts"
run: pnpm run generate:all && git add apps/docs/document-api/reference apps/docs/document-api/overview.mdx apps/docs/document-engine/sdks.mdx
Expand Down
15 changes: 15 additions & 0 deletions packages/react/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ export interface SuperDocEditorProps
| `className` | `string` | - | Wrapper CSS class |
| `style` | `CSSProperties` | - | Wrapper inline styles |

## Fixed-height container embedding

Pass `contained` to scroll inside a fixed-height parent. The wrapper must have a definite height.

```tsx
<div style={{ height: 500 }}>
<SuperDocEditor
document={file}
documentMode="viewing"
contained
style={{ height: '100%' }}
/>
</div>
```

## SSR Behavior

- Container divs are always rendered (hidden with `display: none` until initialized)
Expand Down
15 changes: 13 additions & 2 deletions packages/react/src/SuperDocEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function SuperDocEditorInner(props: SuperDocEditorProps, ref: ForwardedRef<Super
id,
renderLoading,
hideToolbar = false,
contained = false,
className,
style,
// Callbacks (stored in ref to avoid triggering rebuilds)
Expand Down Expand Up @@ -149,6 +150,7 @@ function SuperDocEditorInner(props: SuperDocEditorProps, ref: ForwardedRef<Super
...(!hideToolbar && toolbarContainerRef.current ? { toolbar: `#${CSS.escape(toolbarId)}` } : {}),
documentMode,
role,
...(contained ? { contained } : {}),
...(documentProp != null ? { document: documentProp } : {}),
...(user ? { user } : {}),
...(users ? { users } : {}),
Expand Down Expand Up @@ -229,12 +231,21 @@ function SuperDocEditorInner(props: SuperDocEditorProps, ref: ForwardedRef<Super
const wrapperClassName = ['superdoc-wrapper', className].filter(Boolean).join(' ');
const hideWhenLoading: CSSProperties | undefined = isLoading ? { display: 'none' } : undefined;

const wrapperStyle: CSSProperties = {
...style,
...(contained && { display: 'flex', flexDirection: 'column' as const }),
};

return (
<div className={wrapperClassName} style={style}>
<div className={wrapperClassName} style={wrapperStyle}>
{!hideToolbar && (
<div ref={toolbarContainerRef} id={toolbarId} className='superdoc-toolbar-container' style={hideWhenLoading} />
)}
<div id={containerId} className='superdoc-editor-container' style={hideWhenLoading} />
<div
id={containerId}
className='superdoc-editor-container'
style={{ ...hideWhenLoading, ...(contained && { flex: 1, minHeight: 0 }) }}
/>
{isLoading && !hasError && renderLoading && <div className='superdoc-loading-container'>{renderLoading()}</div>}
{hasError && <div className='superdoc-error-container'>Failed to load editor. Check console for details.</div>}
</div>
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ interface ReactProps {
/** Hide the toolbar container. When true, no toolbar is rendered. @default false */
hideToolbar?: boolean;

/** Enable contained mode for fixed-height container embedding. When true, SuperDoc
* fits within its parent's height and scrolls internally. @default false */
contained?: boolean;

/** Additional CSS class name for the wrapper element */
className?: string;

Expand Down
3 changes: 2 additions & 1 deletion packages/react/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"rootDir": "./src",
"jsx": "react-jsx",
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"types": ["react", "react-dom"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]
Expand Down
26 changes: 25 additions & 1 deletion packages/super-editor/src/components/SuperEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ const isWebLayout = computed(() => {
return props.options.viewOptions?.layout === 'web';
});

const isContained = computed(() => {
return Boolean(props.options.contained);
});

/**
* Reactive ruler visibility state.
* Uses a ref with a deep watcher to ensure proper reactivity when options.rulers changes.
Expand Down Expand Up @@ -1222,7 +1226,11 @@ onBeforeUnmount(() => {
</script>

<template>
<div class="super-editor-container" :class="{ 'web-layout': isWebLayout }" :style="containerStyle">
<div
class="super-editor-container"
:class="{ 'web-layout': isWebLayout, contained: isContained }"
:style="containerStyle"
>
<!-- Ruler: teleport to external container if specified, otherwise render inline (hidden in web layout) -->
<Teleport
v-if="options.rulerContainer && rulersVisible && !isWebLayout && !!activeEditor"
Expand Down Expand Up @@ -1361,4 +1369,20 @@ onBeforeUnmount(() => {
overflow: hidden;
position: relative;
}

/* Contained mode: fixed-height container embedding with internal scrolling.
* The super-editor-container becomes the scroll container (overflow: auto).
* The .super-editor overflow is changed from hidden to visible so content
* flows through to the scroll container. The visibleHost (.presentation-editor)
* stays overflow: visible per PresentationEditor's design — it is NOT the scroller.
*/
.super-editor-container.contained {
height: 100%;
min-height: 0;
overflow: auto;
}

.super-editor-container.contained .super-editor {
overflow: visible;
}
</style>
12 changes: 11 additions & 1 deletion packages/superdoc/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ Uses `layout-engine` for virtualized rendering with pagination.
`PresentationEditor.ts` bridges state between modes.
See `super-editor/src/core/presentation-editor/` for implementation.

### Fixed-height container embedding
By default SuperDoc expands to the document's full height. To embed it inside a fixed-height container (panel, sidebar, modal, dashboard widget), pass `contained: true`. The parent element must have a definite height.

```javascript
new SuperDoc({
selector: '#editor',
document: file,
contained: true,
});
```

## Theming

SuperDoc UI is themed via `--sd-*` CSS variables. Use `createTheme()` for JS-based theming or set variables directly in CSS.
Expand All @@ -76,7 +87,6 @@ document.documentElement.classList.add(theme);
- Preset themes — `src/assets/styles/helpers/themes.css`
- Backward-compat aliases — `src/assets/styles/helpers/compat.css`
- Consumer-facing agent guide — `AGENTS.md` (ships with npm package)

## Testing

- Unit tests: `src/SuperDoc.test.js`
Expand Down
17 changes: 17 additions & 0 deletions packages/superdoc/src/SuperDoc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ const editorOptions = (doc) => {
disableContextMenu: proxy.$superdoc.config.disableContextMenu,
jsonOverride: proxy.$superdoc.config.jsonOverride,
viewOptions: proxy.$superdoc.config.viewOptions,
contained: proxy.$superdoc.config.contained,
linkPopoverResolver: proxy.$superdoc.config.modules?.links?.popoverResolver,
layoutEngineOptions: useLayoutEngine
? {
Expand Down Expand Up @@ -1441,6 +1442,7 @@ const getPDFViewer = () => {
:class="{
'superdoc--with-sidebar': showCommentsSidebar,
'superdoc--web-layout': proxy.$superdoc.config.viewOptions?.layout === 'web',
'superdoc--contained': proxy.$superdoc.config.contained,
'high-contrast': isHighContrastMode,
}"
:style="superdocStyleVars"
Expand Down Expand Up @@ -1640,6 +1642,21 @@ const getPDFViewer = () => {
position: relative;
}

/* In contained mode, overlay layers must not take flow space.
* With height:100% resolved on .superdoc__document, this element's
* position:relative + height:100% takes the full container height,
* pushing .superdoc__sub-document out of view. */
.superdoc--contained .superdoc__comments-layer {
position: absolute;
width: 100%;
pointer-events: none;
}

/* Re-enable pointer events on comment anchors so highlights remain clickable */
.superdoc--contained .sd-comment-anchor {
pointer-events: auto;
}

.superdoc__right-sidebar {
width: 320px;
min-width: 320px;
Expand Down
15 changes: 15 additions & 0 deletions packages/superdoc/src/assets/styles/elements/superdoc.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,18 @@
.superdoc .ProseMirror-active-search-match {
background: var(--sd-ui-search-match-active-bg);
}

/* Contained Mode - fixed-height container embedding with internal scrolling */
.superdoc--contained,
.superdoc--contained .superdoc__layers,
.superdoc--contained .superdoc__document,
.superdoc--contained .superdoc__sub-document {
height: 100%;
}

/* Sub-document scroll fallback for non-SuperEditor content (PDF, HTML).
* SuperEditor manages its own scroll container via .super-editor-container.contained,
* but PDF/HTML viewers don't — they need the sub-document to scroll. */
.superdoc--contained .superdoc__sub-document {
overflow: auto;
}
4 changes: 4 additions & 0 deletions packages/superdoc/src/core/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,10 @@
* @property {boolean} [warnOnUnsupportedContent] When true and no onUnsupportedContent callback is provided, emits a console.warn with unsupported items
* @property {boolean} [isDebug=false] Whether to enable debug mode
* @property {ViewOptions} [viewOptions] Document view options (OOXML ST_View compatible)
* @property {boolean} [contained] Enable contained mode for fixed-height container embedding.
* When true, SuperDoc propagates height through its DOM tree and adds internal scrolling,
* so multi-page documents scroll within the consumer's fixed-height container.
* Default behavior (false) lets the document expand to its natural height.
* @property {string} [cspNonce] Content Security Policy nonce for dynamically injected styles
* @property {string} [licenseKey] License key for organization identification
* @property {{ enabled: boolean, endpoint?: string, metadata?: Record<string, unknown>, licenseKey?: string }} [telemetry] Telemetry configuration
Expand Down
Loading