diff --git a/CLAUDE.md b/CLAUDE.md
index 7972865b45..f6d065eb18 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -82,6 +82,14 @@ tests/visual/ Visual regression tests (Playwright + R2 baselines)
- **Editing commands/behavior**: Modify `super-editor/src/extensions/`
- **State bridging**: Modify `PresentationEditor.ts`
+## JSDoc types
+
+Many packages use `.js` files with JSDoc `@typedef` for type definitions (e.g., `packages/superdoc/src/core/types/index.js`). These typedefs ARE the published type declarations — `vite-plugin-dts` generates `.d.ts` files from them.
+
+- **Keep JSDoc typedefs in sync with code.** If a function destructures `{ a, b, c }`, the `@typedef` must include all three properties. Missing properties become type errors for consumers.
+- **Verify types after adding parameters.** When adding a parameter to a function, update its `@typedef` or `@param` JSDoc. Build with `pnpm run --filter superdoc build:es` and check the generated `.d.ts` in `dist/`.
+- **Workspace packages don't publish types.** `@superdoc/common`, `@superdoc/contracts`, etc. are private. If a public API references their types, those types must be inlined or resolved through path aliases — consumers can't resolve workspace packages.
+
## Commands
- `pnpm build` - Build all packages
diff --git a/apps/docs/getting-started/frameworks/angular.mdx b/apps/docs/getting-started/frameworks/angular.mdx
index 63b594d195..9facafb7ee 100644
--- a/apps/docs/getting-started/frameworks/angular.mdx
+++ b/apps/docs/getting-started/frameworks/angular.mdx
@@ -1,99 +1,132 @@
---
-title: Angular
+title: Angular Integration
+sidebarTitle: Angular
keywords: "angular docx editor, angular word component, superdoc angular, angular document editor, angular integration"
---
-SuperDoc works with Angular through direct DOM manipulation.
+SuperDoc works with Angular through direct DOM manipulation. No wrapper needed.
-## Installation
+## Install
```bash
npm install superdoc
```
-## Basic component
+## Basic setup
```typescript
import { Component, ElementRef, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { SuperDoc } from 'superdoc';
+import 'superdoc/style.css';
@Component({
- selector: 'app-document-editor',
- template: `
-
- `,
- styles: [`
- .editor-container {
- height: 700px;
- }
- `]
+ selector: 'app-editor',
+ template: `
`,
})
-export class DocumentEditorComponent implements OnInit, OnDestroy {
- @ViewChild('editor', { static: true }) editorElement!: ElementRef;
+export class EditorComponent implements OnInit, OnDestroy {
+ @ViewChild('editor', { static: true }) editorRef!: ElementRef;
private superdoc: SuperDoc | null = null;
ngOnInit() {
this.superdoc = new SuperDoc({
- selector: this.editorElement.nativeElement,
- document: 'contract.docx'
+ selector: this.editorRef.nativeElement,
+ document: 'contract.docx',
+ documentMode: 'editing',
});
}
ngOnDestroy() {
- this.superdoc = null;
- }
-
- async exportDocument() {
- if (this.superdoc) {
- await this.superdoc.export();
- }
+ this.superdoc?.destroy();
}
}
```
-## With file input
+## Full component
+
+A reusable editor component with mode switching and export:
```typescript
+import { Component, ElementRef, ViewChild, Input, OnInit, OnDestroy } from '@angular/core';
+import { SuperDoc } from 'superdoc';
+import 'superdoc/style.css';
+
@Component({
- selector: 'app-editor',
+ selector: 'app-doc-editor',
template: `
-
-
- `
+
+ Edit
+ Review
+ View
+ Export
+
+
+ `,
})
-export class EditorComponent {
- @ViewChild('editor', { static: false }) editorElement!: ElementRef;
+export class DocEditorComponent implements OnInit, OnDestroy {
+ @ViewChild('editor', { static: true }) editorRef!: ElementRef;
+ @Input() document!: File | string;
+ @Input() user?: { name: string; email: string };
+
private superdoc: SuperDoc | null = null;
- onFileSelected(event: Event) {
- const file = (event.target as HTMLInputElement).files?.[0];
- if (file && this.editorElement) {
- this.superdoc = new SuperDoc({
- selector: this.editorElement.nativeElement,
- document: file
- });
- }
+ ngOnInit() {
+ this.superdoc = new SuperDoc({
+ selector: this.editorRef.nativeElement,
+ document: this.document,
+ documentMode: 'editing',
+ user: this.user,
+ onReady: () => console.log('Editor ready'),
+ });
+ }
+
+ ngOnDestroy() {
+ this.superdoc?.destroy();
+ }
+
+ setMode(mode: 'editing' | 'viewing' | 'suggesting') {
+ this.superdoc?.setDocumentMode(mode);
+ }
+
+ async exportDoc() {
+ await this.superdoc?.export({ isFinalDoc: true });
}
}
```
-## Service pattern
+## Handle file uploads
```typescript
-@Injectable({ providedIn: 'root' })
-export class DocumentService {
+@Component({
+ selector: 'app-upload-editor',
+ template: `
+
+
+ `,
+})
+export class UploadEditorComponent implements OnDestroy {
+ @ViewChild('editor', { static: true }) editorRef!: ElementRef;
private superdoc: SuperDoc | null = null;
- initialize(element: HTMLElement, document: File | string) {
+ onFileChange(event: Event) {
+ const file = (event.target as HTMLInputElement).files?.[0];
+ if (!file) return;
+
+ this.superdoc?.destroy();
this.superdoc = new SuperDoc({
- selector: element,
- document
+ selector: this.editorRef.nativeElement,
+ document: file,
+ documentMode: 'editing',
});
- return this.superdoc;
}
- destroy() {
- this.superdoc = null;
+ ngOnDestroy() {
+ this.superdoc?.destroy();
}
}
-```
\ No newline at end of file
+```
+
+## Next steps
+
+- [Configuration](/core/superdoc/configuration) - Full configuration options
+- [Methods](/core/superdoc/methods) - All available methods
+- [Angular Example](https://github.com/superdoc-dev/superdoc/tree/main/examples/getting-started/angular) - Working example on GitHub
diff --git a/examples/__tests__/playwright.config.ts b/examples/__tests__/playwright.config.ts
index 8cbdfbae64..26282f888f 100644
--- a/examples/__tests__/playwright.config.ts
+++ b/examples/__tests__/playwright.config.ts
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
// EXAMPLE can be:
-// "react", "vue", "vanilla", "cdn" (getting-started)
+// "react", "vue", "vanilla", "cdn", "angular" (getting-started)
// "collaboration/superdoc-yjs", "collaboration/hocuspocus", etc.
const example = process.env.EXAMPLE || 'react';
diff --git a/examples/getting-started/angular/.gitignore b/examples/getting-started/angular/.gitignore
new file mode 100644
index 0000000000..5e355bc6d4
--- /dev/null
+++ b/examples/getting-started/angular/.gitignore
@@ -0,0 +1,2 @@
+dist
+.angular
diff --git a/examples/getting-started/angular/README.md b/examples/getting-started/angular/README.md
new file mode 100644
index 0000000000..316c742cc4
--- /dev/null
+++ b/examples/getting-started/angular/README.md
@@ -0,0 +1,15 @@
+# SuperDoc — Angular
+
+Minimal Angular example.
+
+## Run
+
+```bash
+npm install
+npm run dev
+```
+
+## Learn more
+
+- [Angular Guide](https://docs.superdoc.dev/getting-started/frameworks/angular)
+- [Configuration Reference](https://docs.superdoc.dev/core/superdoc/configuration)
diff --git a/examples/getting-started/angular/angular.json b/examples/getting-started/angular/angular.json
new file mode 100644
index 0000000000..4238285402
--- /dev/null
+++ b/examples/getting-started/angular/angular.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "cli": { "analytics": false },
+ "projects": {
+ "superdoc-angular-example": {
+ "projectType": "application",
+ "root": "",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:application",
+ "options": {
+ "outputPath": "dist",
+ "index": "src/index.html",
+ "browser": "src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "tsconfig.json",
+ "styles": ["src/styles.css"]
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ { "type": "initial", "maximumWarning": "5MB", "maximumError": "10MB" }
+ ]
+ },
+ "development": {
+ "optimization": false,
+ "sourceMap": true
+ }
+ }
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "buildTarget": "superdoc-angular-example:build:development"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/examples/getting-started/angular/package.json b/examples/getting-started/angular/package.json
new file mode 100644
index 0000000000..c29ff8b8b1
--- /dev/null
+++ b/examples/getting-started/angular/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "superdoc-angular-example",
+ "private": true,
+ "scripts": {
+ "dev": "ng serve",
+ "build": "ng build"
+ },
+ "dependencies": {
+ "@angular/common": "^21.1.4",
+ "@angular/compiler": "^21.1.4",
+ "@angular/core": "^21.1.4",
+ "@angular/platform-browser": "^21.1.4",
+ "@angular/platform-browser-dynamic": "^21.1.4",
+ "rxjs": "~7.8.2",
+ "superdoc": "latest",
+ "tslib": "^2.8.1",
+ "zone.js": "~0.16.0"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "^21.1.4",
+ "@angular/cli": "^21.1.4",
+ "@angular/compiler-cli": "^21.1.4",
+ "typescript": "~5.9.3"
+ }
+}
diff --git a/examples/getting-started/angular/public/.gitkeep b/examples/getting-started/angular/public/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/examples/getting-started/angular/src/app/app.component.ts b/examples/getting-started/angular/src/app/app.component.ts
new file mode 100644
index 0000000000..105196b5b0
--- /dev/null
+++ b/examples/getting-started/angular/src/app/app.component.ts
@@ -0,0 +1,33 @@
+import { Component, ElementRef, ViewChild, OnDestroy } from '@angular/core';
+import { SuperDoc } from 'superdoc';
+
+@Component({
+ selector: 'app-root',
+ template: `
+
+
+
+
+ `,
+})
+export class AppComponent implements OnDestroy {
+ @ViewChild('editor', { static: true }) editorRef!: ElementRef;
+
+ private superdoc: SuperDoc | null = null;
+
+ onFileChange(event: Event) {
+ const file = (event.target as HTMLInputElement).files?.[0];
+ if (!file) return;
+
+ this.superdoc?.destroy();
+ this.superdoc = new SuperDoc({
+ selector: this.editorRef.nativeElement,
+ documentMode: 'editing',
+ document: file,
+ });
+ }
+
+ ngOnDestroy() {
+ this.superdoc?.destroy();
+ }
+}
diff --git a/examples/getting-started/angular/src/index.html b/examples/getting-started/angular/src/index.html
new file mode 100644
index 0000000000..079b16223e
--- /dev/null
+++ b/examples/getting-started/angular/src/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ SuperDoc — Angular
+
+
+
+
+
diff --git a/examples/getting-started/angular/src/main.ts b/examples/getting-started/angular/src/main.ts
new file mode 100644
index 0000000000..9cd15da95d
--- /dev/null
+++ b/examples/getting-started/angular/src/main.ts
@@ -0,0 +1,4 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent);
diff --git a/examples/getting-started/angular/src/styles.css b/examples/getting-started/angular/src/styles.css
new file mode 100644
index 0000000000..2939823895
--- /dev/null
+++ b/examples/getting-started/angular/src/styles.css
@@ -0,0 +1 @@
+@import 'superdoc/style.css';
diff --git a/examples/getting-started/angular/tsconfig.json b/examples/getting-started/angular/tsconfig.json
new file mode 100644
index 0000000000..b927fa4405
--- /dev/null
+++ b/examples/getting-started/angular/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "moduleResolution": "bundler",
+ "target": "ES2022",
+ "module": "ES2022"
+ },
+ "angularCompilerOptions": {
+ "strictTemplates": true
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/superdoc/package.json b/packages/superdoc/package.json
index 8137e47e98..8645524919 100644
--- a/packages/superdoc/package.json
+++ b/packages/superdoc/package.json
@@ -24,7 +24,7 @@
"require": "./dist/superdoc.cjs"
},
"./types": {
- "types": "./dist/types.d.ts",
+ "types": "./dist/super-editor/src/types.d.ts",
"source": "./src/types.ts",
"import": "./dist/types.es.js",
"require": "./dist/types.cjs"
@@ -52,7 +52,7 @@
"./dist/super-editor/src/index.d.ts"
],
"types": [
- "./dist/types.d.ts"
+ "./dist/super-editor/src/types.d.ts"
]
}
},
@@ -62,10 +62,10 @@
"dev": "vite",
"dev:collab": "concurrently -k -n VITE,COLLAB -c cyan,green \"vite\" \"node src/dev/collab-server.js\"",
"collab-server": "node src/dev/collab-server.js",
- "build": "pnpm --prefix ../super-editor run types:build && vite build && pnpm run build:umd",
+ "build": "vite build && pnpm run build:umd",
"build:dev": "SUPERDOC_SKIP_DTS=1 vite build",
"postbuild": "node ./scripts/ensure-types.cjs",
- "build:es": "pnpm --prefix ../super-editor run types:build && vite build && node ./scripts/ensure-types.cjs",
+ "build:es": "vite build && node ./scripts/ensure-types.cjs",
"watch:es": "vite build --watch",
"build:umd": "vite build --config vite.config.umd.js",
"clean": "rm -rf dist",
diff --git a/packages/superdoc/scripts/ensure-types.cjs b/packages/superdoc/scripts/ensure-types.cjs
index 9583c225ca..aa6a6e253e 100644
--- a/packages/superdoc/scripts/ensure-types.cjs
+++ b/packages/superdoc/scripts/ensure-types.cjs
@@ -3,71 +3,59 @@
const fs = require('node:fs');
const path = require('node:path');
-// TypeScript emits to dist/superdoc/src/index.d.ts when common is included
-const basePath = 'dist/superdoc/src/index.d.ts';
-const distIndexPath = path.resolve(__dirname, '..', basePath);
-
-if (!fs.existsSync(distIndexPath)) {
- console.error(`[ensure-types] Missing ${basePath}`);
- process.exit(1);
-}
-
-const content = fs.readFileSync(distIndexPath, 'utf8');
-const hasSuperDocExport = /export\s+\{[^}]*\bSuperDoc\b[^}]*\}/m.test(content);
-
-if (!hasSuperDocExport) {
- console.error(`[ensure-types] SuperDoc export missing in ${basePath}`);
- process.exit(1);
-}
-
+// Verify that vite-plugin-dts generated the expected type entry points.
+// Path aliases are resolved by vite-plugin-dts via tsconfig.json paths.
const distRoot = path.resolve(__dirname, '..', 'dist');
-const typeTargets = ['types.d.ts', 'types.es.d.ts', 'types.cjs.d.ts'];
-const superEditorDist = path.resolve(__dirname, '..', '..', 'super-editor', 'dist');
-const superDocSuperEditorDist = path.resolve(distRoot, 'super-editor');
-const superEditorTypesPath = path.join(superEditorDist, 'types.d.ts');
-
-const copyDir = (src, dest) => {
- fs.mkdirSync(dest, { recursive: true });
- const entries = fs.readdirSync(src, { withFileTypes: true });
- entries.forEach((entry) => {
- const srcPath = path.join(src, entry.name);
- const destPath = path.join(dest, entry.name);
- if (entry.isDirectory()) {
- copyDir(srcPath, destPath);
- } else if (entry.isFile()) {
- fs.copyFileSync(srcPath, destPath);
- }
- });
-};
-try {
- fs.mkdirSync(distRoot, { recursive: true });
+const requiredEntryPoints = [
+ 'superdoc/src/index.d.ts',
+ 'super-editor/src/index.d.ts',
+ 'super-editor/src/types.d.ts',
+];
- if (!fs.existsSync(superEditorTypesPath)) {
- console.error(`[ensure-types] Missing super-editor types at ${superEditorTypesPath}`);
+for (const entry of requiredEntryPoints) {
+ const fullPath = path.join(distRoot, entry);
+ if (!fs.existsSync(fullPath)) {
+ console.error(`[ensure-types] Missing ${entry}`);
process.exit(1);
}
+}
- let superEditorTypes = fs.readFileSync(superEditorTypesPath, 'utf8');
-
- // Rewrite relative paths to point into the super-editor subdirectory
- // e.g. './core/types/...' -> './super-editor/core/types/...'
- // './extensions/types/...' -> './super-editor/extensions/types/...'
- superEditorTypes = superEditorTypes
- .replace(/from\s+['"]\.\/(core|extensions)\//g, "from './super-editor/$1/")
- .replace(/import\s+['"]\.\/(core|extensions)\//g, "import './super-editor/$1/");
-
- typeTargets.forEach((target) => {
- const targetPath = path.join(distRoot, target);
- fs.writeFileSync(targetPath, superEditorTypes, 'utf8');
- });
+const indexPath = path.join(distRoot, 'superdoc/src/index.d.ts');
+let content = fs.readFileSync(indexPath, 'utf8');
- copyDir(superEditorDist, superDocSuperEditorDist);
-} catch (error) {
- console.error(`[ensure-types] Failed to prepare types entrypoints: ${error?.message || error}`);
+const hasSuperDocExport = /export\s+\{[^}]*\bSuperDoc\b[^}]*\}/m.test(content);
+if (!hasSuperDocExport) {
+ console.error(`[ensure-types] SuperDoc export missing in superdoc/src/index.d.ts`);
process.exit(1);
}
-console.log(`[ensure-types] ✓ Verified SuperDoc export in ${basePath}`);
-console.log(`[ensure-types] ✓ Copied super-editor dist into dist/super-editor`);
-console.log(`[ensure-types] ✓ Copied super-editor types into dist/ (${typeTargets.join(', ')})`);
+// Fix workspace package imports that aren't resolvable by consumers.
+// @superdoc/common is a private workspace package — inline its types.
+const hadWorkspaceImport = content.includes('@superdoc/common');
+if (hadWorkspaceImport) {
+ // Replace the @superdoc/common import with inline declarations
+ content = content.replace(
+ /import\s*\{[^}]*\}\s*from\s*['"]@superdoc\/common['"];?\s*\n?/g,
+ '',
+ );
+
+ // BlankDOCX comes from a Vite ?url import (resolves to a string at runtime)
+ // Declare it since vite-plugin-dts can't generate types for ?url imports
+ const inlineDeclarations = [
+ '/** Document MIME type constants */',
+ "declare const DOCX: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';",
+ "declare const PDF: 'application/pdf';",
+ "declare const HTML: 'text/html';",
+ 'declare function getFileObject(fileUrl: string, name: string, type: string): Promise;',
+ 'declare function compareVersions(version1: string, version2: string): -1 | 0 | 1;',
+ '/** URL to the blank DOCX template */',
+ 'declare const BlankDOCX: string;',
+ ].join('\n');
+
+ content = inlineDeclarations + '\n' + content;
+ fs.writeFileSync(indexPath, content);
+ console.log('[ensure-types] ✓ Inlined @superdoc/common types');
+}
+
+console.log('[ensure-types] ✓ Verified type entry points');
diff --git a/packages/superdoc/src/core/SuperDoc.js b/packages/superdoc/src/core/SuperDoc.js
index 770a7f59cf..21ef32cbf9 100644
--- a/packages/superdoc/src/core/SuperDoc.js
+++ b/packages/superdoc/src/core/SuperDoc.js
@@ -1035,7 +1035,7 @@ export class SuperDoc extends EventEmitter {
/**
* Export editors to DOCX format.
- * @param {{ commentsType?: string, isFinalDoc?: boolean }} [options]
+ * @param {{ commentsType?: string, isFinalDoc?: boolean, fieldsHighlightColor?: string }} [options]
* @returns {Promise>}
*/
async exportEditorsToDOCX({ commentsType, isFinalDoc, fieldsHighlightColor } = {}) {
diff --git a/packages/superdoc/src/core/types/index.js b/packages/superdoc/src/core/types/index.js
index a76d21314e..163b21e969 100644
--- a/packages/superdoc/src/core/types/index.js
+++ b/packages/superdoc/src/core/types/index.js
@@ -119,6 +119,9 @@
* @property {ExportType[]} [exportType=['docx']] - File formats to export
* @property {CommentsType} [commentsType='external'] - How to handle comments
* @property {string} [exportedName] - Custom filename (without extension)
+ * @property {Blob[]} [additionalFiles] - Extra files to include in the export zip
+ * @property {string[]} [additionalFileNames] - Filenames for the additional files
+ * @property {boolean} [isFinalDoc=false] - Whether this is a final document export
* @property {boolean} [triggerDownload=true] - Auto-download or return blob
* @property {string} [fieldsHighlightColor] - Color for field highlights
*/
diff --git a/packages/superdoc/src/helpers/schema-introspection.js b/packages/superdoc/src/helpers/schema-introspection.js
index 7c03bb1671..6e812ef64b 100644
--- a/packages/superdoc/src/helpers/schema-introspection.js
+++ b/packages/superdoc/src/helpers/schema-introspection.js
@@ -15,7 +15,7 @@ import { Editor, getRichTextExtensions, getStarterExtensions } from '@superdoc/s
* Useful for AI agents, documentation generation, or schema validation.
*
* @param {SchemaIntrospectionOptions} [options] - Configuration options.
- * @returns {Promise} Schema summary with nodes and marks.
+ * @returns {Promise} Schema summary with nodes and marks.
* @throws {Error} If the editor schema is not initialized.
*
* @example
diff --git a/packages/superdoc/tsconfig.json b/packages/superdoc/tsconfig.json
index 8b0039035b..c6ab997ae8 100644
--- a/packages/superdoc/tsconfig.json
+++ b/packages/superdoc/tsconfig.json
@@ -7,7 +7,20 @@
"emitDeclarationOnly": true,
"outDir": "dist",
"declarationMap": true,
- "skipLibCheck": true
+ "skipLibCheck": true,
+ "baseUrl": "../..",
+ "paths": {
+ "@core/*": ["packages/super-editor/src/core/*"],
+ "@extensions/*": ["packages/super-editor/src/extensions/*"],
+ "@features/*": ["packages/super-editor/src/features/*"],
+ "@components/*": ["packages/super-editor/src/components/*"],
+ "@helpers/*": ["packages/super-editor/src/core/helpers/*"],
+ "@converter/*": ["packages/super-editor/src/core/super-converter/*"],
+ "@tests/*": ["packages/super-editor/src/tests/*"],
+ "@translator": ["packages/super-editor/src/core/super-converter/v3/node-translator/"],
+ "@utils/*": ["packages/super-editor/src/utils/*"],
+ "@shared/*": ["shared/*"]
+ }
},
- "include": ["src"]
+ "include": ["src", "../super-editor/src"]
}