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: ` - -
- ` +
+ + + + +
+
+ `, }) -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"] }