diff --git a/.github/workflows/ci-react.yml b/.github/workflows/ci-react.yml new file mode 100644 index 0000000000..d4f6f23fb0 --- /dev/null +++ b/.github/workflows/ci-react.yml @@ -0,0 +1,45 @@ +name: CI React + +permissions: + contents: read + +on: + pull_request: + paths: + - 'packages/react/**' + workflow_dispatch: + +concurrency: + group: ci-react-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: pnpm + + - name: Install dependencies + run: pnpm install + + - name: Build superdoc (dependency) + run: pnpm run build:superdoc + + - name: Lint + run: pnpm --filter @superdoc-dev/react lint + + - name: Type check + run: pnpm --filter @superdoc-dev/react type-check + + - name: Build + run: pnpm --filter @superdoc-dev/react build + + - name: Test + run: pnpm --filter @superdoc-dev/react test diff --git a/.github/workflows/release-react.yml b/.github/workflows/release-react.yml new file mode 100644 index 0000000000..c122ec6163 --- /dev/null +++ b/.github/workflows/release-react.yml @@ -0,0 +1,66 @@ +# Auto-releases on push to main (@next channel) +# For stable (@latest): cherry-pick commits to stable branch, then manually dispatch this workflow +name: 📦 Release react + +on: + push: + branches: + - main + paths: + - 'packages/react/**' + - 'packages/layout-engine/**' + - 'packages/super-editor/**' + - 'packages/ai/**' + - 'packages/word-layout/**' + - 'packages/preset-geometry/**' + - '!**/*.md' + workflow_dispatch: + +permissions: + contents: write + packages: write + +concurrency: + group: release-react-${{ github.ref }} + cancel-in-progress: true + +jobs: + release: + runs-on: ubuntu-24.04 + steps: + - name: Generate token + id: generate_token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + token: ${{ steps.generate_token.outputs.token }} + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: pnpm + registry-url: 'https://registry.npmjs.org' + + - uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: pnpm install + + - name: Build packages + run: pnpm run build + + - name: Release + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + LINEAR_TOKEN: ${{ secrets.LINEAR_TOKEN }} + working-directory: packages/react + run: pnpx semantic-release diff --git a/CLAUDE.md b/CLAUDE.md index a1a50d7c2a..cdb6ba29a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,6 +25,7 @@ State flows from super-editor → Layout Engine via: ``` packages/ superdoc/ Main entry point (npm: superdoc) + react/ React wrapper (@superdoc-dev/react) super-editor/ ProseMirror editor (@superdoc/super-editor) layout-engine/ Layout & pagination pipeline contracts/ - Shared type definitions @@ -43,11 +44,12 @@ e2e-tests/ Playwright tests | Task | Location | |------|----------| +| React integration | `packages/react/src/SuperDocEditor.tsx` | | Editing features | `super-editor/src/extensions/` | | Presentation mode visuals | `layout-engine/painters/dom/src/renderer.ts` | | DOCX import/export | `super-editor/src/core/super-converter/` | | Style resolution | `layout-engine/style-engine/` | -| Main entry point | `superdoc/src/SuperDoc.vue` | +| Main entry point (Vue) | `superdoc/src/SuperDoc.vue` | ## When to Modify Which System diff --git a/README.md b/README.md index d72d449c58..74c23b0943 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,30 @@ Or install with CDN ``` -### Basic usage +### React + +```bash +npm install @superdoc-dev/react +``` + +```tsx +import { SuperDocEditor } from '@superdoc-dev/react'; +import '@superdoc-dev/react/style.css'; + +function App() { + return ( + console.log('Ready!')} + /> + ); +} +``` + +See the [@superdoc-dev/react README](packages/react/README.md) for full documentation. + +### Vanilla JavaScript ```javascript import 'superdoc/style.css'; diff --git a/apps/docs/getting-started/frameworks/nextjs.mdx b/apps/docs/getting-started/frameworks/nextjs.mdx index 0a244afdc0..eef78f0e0e 100644 --- a/apps/docs/getting-started/frameworks/nextjs.mdx +++ b/apps/docs/getting-started/frameworks/nextjs.mdx @@ -3,124 +3,96 @@ title: Next.js keywords: "nextjs docx editor, next word editor, superdoc nextjs, ssr document editor, dynamic import docx" --- -SuperDoc works with Next.js using dynamic imports to avoid SSR issues. +SuperDoc works seamlessly with Next.js. The recommended approach is using `@superdoc-dev/react`, which handles SSR automatically. -## Installation +## Recommended: Using @superdoc-dev/react -```bash -npm install @harbour-enterprises/superdoc -``` - -## Basic component - -```jsx -// components/DocumentEditor.jsx -import { useEffect, useRef } from 'react'; -import dynamic from 'next/dynamic'; - -// Prevent SSR issues -const DocumentEditor = dynamic( - () => Promise.resolve(DocumentEditorComponent), - { ssr: false } -); - -function DocumentEditorComponent({ document }) { - const containerRef = useRef(null); - const superdocRef = useRef(null); - - useEffect(() => { - const initEditor = async () => { - const { SuperDoc } = await import('@harbour-enterprises/superdoc'); - - if (containerRef.current) { - superdocRef.current = new SuperDoc({ - selector: containerRef.current, - document - }); - } - }; - - initEditor(); - - return () => { - superdocRef.current = null; - }; - }, [document]); - - return
; -} +The React wrapper is the simplest way to integrate SuperDoc with Next.js: -export default DocumentEditor; +```bash +npm install @superdoc-dev/react ``` -## App Router (Next.js 13+) +### App Router (Next.js 13+) ```jsx // app/editor/page.jsx 'use client'; -import dynamic from 'next/dynamic'; - -const DocumentEditor = dynamic( - () => import('@/components/DocumentEditor'), - { - ssr: false, - loading: () =>
Loading editor...
- } -); +import { SuperDocEditor } from '@superdoc-dev/react'; +import '@superdoc-dev/react/style.css'; export default function EditorPage() { return ( -
- -
+ console.log('Editor ready!')} + style={{ height: '100vh' }} + /> ); } ``` -## API route for document handling - -```javascript -// pages/api/documents/[id].js (Pages Router) -// app/api/documents/[id]/route.js (App Router) - -export async function GET(request, { params }) { - const docId = params.id; - - // Fetch document from storage - const document = await fetchDocumentFromStorage(docId); - - return new Response(document, { - headers: { - 'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' - } - }); +### Pages Router + +```jsx +// pages/editor.jsx +import { SuperDocEditor } from '@superdoc-dev/react'; +import '@superdoc-dev/react/style.css'; + +export default function EditorPage() { + return ( + + ); } ``` -## With authentication + +The React wrapper is client-only (returns null on server, renders after hydration). For custom loading UI during SSR, use Next.js dynamic imports with a `loading` component. + -```jsx -// components/SecureEditor.jsx -import { useSession } from 'next-auth/react'; -import dynamic from 'next/dynamic'; +--- -const DocumentEditor = dynamic(() => import('./DocumentEditor'), { ssr: false }); +## CSS Import -export default function SecureEditor() { - const { data: session, status } = useSession(); +Import styles in your layout or page: - if (status === 'loading') return
Loading...
; - if (!session) return
Please sign in
; +```jsx +// app/layout.jsx +import '@superdoc-dev/react/style.css'; +export default function RootLayout({ children }) { return ( - + + {children} + ); } -``` \ No newline at end of file +``` + +--- + +## Next Steps + + + + Full React wrapper documentation + + + Configuration options + + + Real-time collaboration + + + React + TypeScript example + + + Next.js SSR integration + + diff --git a/apps/docs/getting-started/frameworks/react.mdx b/apps/docs/getting-started/frameworks/react.mdx index a23c1e324f..0dadd0bc5d 100644 --- a/apps/docs/getting-started/frameworks/react.mdx +++ b/apps/docs/getting-started/frameworks/react.mdx @@ -1,268 +1,584 @@ --- title: React Integration sidebarTitle: React -keywords: "react docx editor, react word component, superdoc react, microsoft word react, document editor react hooks" +keywords: "react docx editor, react word component, superdoc react, microsoft word react, document editor react hooks, @superdoc-dev/react" --- -SuperDoc works with React 16.8+ (hooks) and React 18+ (concurrent features). -## Install +SuperDoc provides `@superdoc-dev/react` - a first-party wrapper with proper lifecycle management, SSR safety, and React Strict Mode compatibility. + +## Installation ```bash -npm install @harbour-enterprises/superdoc +npm install @superdoc-dev/react ``` -## Basic Setup + +`superdoc` is included as a dependency - you don't need to install it separately. + -```jsx -import { useEffect, useRef } from 'react'; -import { SuperDoc } from '@harbour-enterprises/superdoc'; -import '@harbour-enterprises/superdoc/style.css'; - -function DocEditor({ document }) { - const containerRef = useRef(null); - const superdocRef = useRef(null); - - useEffect(() => { - if (!containerRef.current) return; - - superdocRef.current = new SuperDoc({ - selector: containerRef.current, - document - }); +## Quick Start - return () => { - superdocRef.current = null; - }; - }, [document]); +```jsx +import { SuperDocEditor } from '@superdoc-dev/react'; +import '@superdoc-dev/react/style.css'; - return
; +function App() { + return ( + console.log('Editor ready!')} + /> + ); } ``` -## Full Component +That's it! You now have a fully functional DOCX editor in your React app. + +--- + +## Core Concepts + +### The Component + +`` handles everything for you: -Build a reusable editor with controls: +- **Mounting** - Creates a SuperDoc instance when the component mounts +- **Updates** - Rebuilds automatically when the `document` prop changes +- **Cleanup** - Properly destroys the instance on unmount +- **Client-Only** - Returns null on server, renders only after hydration + +### Document Modes + +SuperDoc supports three editing modes: + +| Mode | Description | Use Case | +|------|-------------|----------| +| `editing` | Full editing capabilities | Default editing experience | +| `viewing` | Read-only presentation | Document preview | +| `suggesting` | Track changes mode | Collaborative review | ```jsx -import { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'; -import { SuperDoc } from '@harbour-enterprises/superdoc'; -import '@harbour-enterprises/superdoc/style.css'; - -const DocEditor = forwardRef(({ document, user, onReady }, ref) => { - const containerRef = useRef(null); - const superdocRef = useRef(null); - - useImperativeHandle(ref, () => ({ - export: (options) => superdocRef.current?.export(options), - setMode: (mode) => superdocRef.current?.setDocumentMode(mode), - getHTML: () => superdocRef.current?.getHTML() - })); - - useEffect(() => { - if (!containerRef.current) return; - - superdocRef.current = new SuperDoc({ - selector: containerRef.current, - document, - user, - onReady: () => onReady?.(superdocRef.current) - }); + +``` + + +Changing `documentMode` via props is efficient - the component calls `setDocumentMode()` internally without rebuilding. You can also use `getInstance()?.setDocumentMode()` directly if preferred. + - return () => { - superdocRef.current = null; - }; - }, [document, user, onReady]); +### User Roles - return
; -}); +Roles control what actions a user can perform: + +| Role | Can Edit | Can Suggest | Can View | +|------|----------|-------------|----------| +| `editor` | Yes | Yes | Yes | +| `suggester` | No | Yes | Yes | +| `viewer` | No | No | Yes | + +```jsx + +``` + +--- + +## Working with Refs + +For programmatic control, use a ref to access the SuperDoc instance: + +```jsx +import { useRef } from 'react'; +import { SuperDocEditor } from '@superdoc-dev/react'; +import '@superdoc-dev/react/style.css'; -// Usage function App() { - const editorRef = useRef(); - + const editorRef = useRef(null); + const handleExport = async () => { - await editorRef.current.export({ isFinalDoc: true }); + // Export as DOCX and trigger download + await editorRef.current?.getInstance()?.export({ triggerDownload: true }); }; - + + const handleModeSwitch = () => { + // Switch mode without rebuilding + editorRef.current?.getInstance()?.setDocumentMode('suggesting'); + }; + return ( <> - - - Download DOCX + + console.log('Ready', editor)} + onReady={({ superdoc }) => console.log('Ready', superdoc)} /> ); } ``` -## File Upload +### Ref API + +The ref exposes a single method: + +| Method | Returns | Description | +|--------|---------|-------------| +| `getInstance()` | `SuperDoc \| null` | Access the underlying SuperDoc instance | + + +`getInstance()` returns `null` before the editor is ready. Use optional chaining (`?.`) for safe access. + + +### Available Instance Methods + +Once you have the SuperDoc instance via `getInstance()`, you can call any SuperDoc method: + +| Method | Returns | Description | +|--------|---------|-------------| +| `setDocumentMode(mode)` | `void` | Change mode without rebuild | +| `export(options?)` | `Promise` | Export document as DOCX | +| `getHTML(options?)` | `string[]` | Get document as HTML | +| `focus()` | `void` | Focus the editor | +| `search(text)` | `SearchResult[]` | Search document content | +| `goToSearchResult(match)` | `void` | Navigate to a search result | +| `setLocked(locked)` | `void` | Lock/unlock editing | +| `toggleRuler()` | `void` | Toggle ruler visibility | +| `save()` | `Promise` | Save (in collaboration mode) | + +--- + +## Common Patterns + +### File Upload ```jsx +import { useState, useRef } from 'react'; +import { SuperDocEditor } from '@superdoc-dev/react'; +import '@superdoc-dev/react/style.css'; + function FileEditor() { const [file, setFile] = useState(null); - const editorRef = useRef(); + const editorRef = useRef(null); - const handleFile = (e) => { - setFile(e.target.files[0]); + const handleFileChange = (e) => { + const selected = e.target.files?.[0]; + if (selected) setFile(selected); }; const handleExport = async () => { - const blob = await editorRef.current?.export({ - isFinalDoc: true - }); - // Download blob... + const blob = await editorRef.current?.getInstance()?.export({ triggerDownload: false }); + // Use blob for custom handling... }; return ( - <> - +
+ {file && ( <> - )} - +
+ ); +} +``` + +### Loading State + +Show a custom loading indicator while SuperDoc initializes: + +```jsx + ( +
+ Loading document... +
+ )} + onReady={() => console.log('Ready!')} +/> +``` + +### Document Switching + +The editor automatically rebuilds when the `document` prop changes: + +```jsx +function MultiDocEditor() { + const [currentDoc, setCurrentDoc] = useState(doc1); + + return ( +
+ + + +
); } ``` -## TypeScript +### View-Only Mode + +```jsx + +``` + +### With User Information + +```jsx + +``` + +--- + +## TypeScript Support + +The wrapper includes full TypeScript support: ```tsx -import { useEffect, useRef, forwardRef } from 'react'; -import { SuperDoc } from '@harbour-enterprises/superdoc'; -import type { SuperDocConfig } from '@harbour-enterprises/superdoc'; +import { useRef } from 'react'; +import { SuperDocEditor } from '@superdoc-dev/react'; +import type { SuperDocRef } from '@superdoc-dev/react'; +import '@superdoc-dev/react/style.css'; interface EditorProps { document: string | File | Blob; userId: string; - onReady?: (editor: SuperDoc) => void; } -interface EditorRef { - export: (options?: ExportOptions) => Promise; - setMode: (mode: 'editing' | 'viewing' | 'suggesting') => void; - getHTML: () => string[]; +function DocEditor({ document, userId }: EditorProps) { + const editorRef = useRef(null); + + const handleReady = ({ superdoc }: { superdoc: any }) => { + console.log('SuperDoc ready'); + }; + + const handleExport = async () => { + const blob = await editorRef.current?.getInstance()?.export({ + triggerDownload: true + }); + return blob; + }; + + return ( + + ); } +``` -const DocEditor = forwardRef( - ({ document, userId, onReady }, ref) => { - const containerRef = useRef(null); - const superdocRef = useRef(null); - - useImperativeHandle(ref, () => ({ - export: async (options) => { - if (!superdocRef.current) throw new Error('Editor not ready'); - return await superdocRef.current.export(options); - }, - setMode: (mode) => { - superdocRef.current?.setDocumentMode(mode); - }, - getHTML: () => { - return superdocRef.current?.getHTML() || []; - } - })); - - useEffect(() => { - if (!containerRef.current) return; - - const config: SuperDocConfig = { - selector: containerRef.current, - document, - user: { - name: userId, - email: `${userId}@company.com` - }, - onReady: () => onReady?.(superdocRef.current!) - }; - - superdocRef.current = new SuperDoc(config); - - return () => { - superdocRef.current = null; - }; - }, [document, userId, onReady]); - - return
; - } -); +### Exported Types + +```tsx +import type { + SuperDocEditorProps, + SuperDocRef, + DocumentMode, + UserRole, + SuperDocUser, + SuperDocModules, + SuperDocConfig, + SuperDocInstance, +} from '@superdoc-dev/react'; ``` -## SSR Support +--- + +## Framework Integration + +### Next.js (App Router) -For Next.js or other SSR frameworks: +The wrapper handles SSR automatically. For additional control: ```jsx +'use client'; + import dynamic from 'next/dynamic'; -const DocEditor = dynamic( - () => import('./DocEditor'), - { +const SuperDocEditor = dynamic( + () => import('@superdoc-dev/react').then(mod => mod.SuperDocEditor), + { ssr: false, loading: () =>
Loading editor...
} ); -// Or manually check for client-side -function SafeEditor(props) { - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - if (!mounted) return
Loading...
; - - return ; +export default function EditorPage() { + return ; } ``` -## Custom Hook +### Next.js (Pages Router) ```jsx -function useSuperDoc(config) { - const [ready, setReady] = useState(false); - const superdocRef = useRef(null); - - useEffect(() => { - if (!config.selector) return; - - superdocRef.current = new SuperDoc({ - ...config, - onReady: () => { - setReady(true); - config.onReady?.(); - } - }); - - return () => { - superdocRef.current = null; - setReady(false); - }; - }, [config.selector, config.document]); - - return { - editor: superdocRef.current, - ready, - export: (options) => superdocRef.current?.export(options), - setMode: (mode) => superdocRef.current?.setDocumentMode(mode) - }; +import dynamic from 'next/dynamic'; + +const SuperDocEditor = dynamic( + () => import('@superdoc-dev/react').then(mod => mod.SuperDocEditor), + { ssr: false } +); + +export default function EditorPage() { + return ; } ``` +### Vite / Create React App + +Works out of the box - just import and use: + +```jsx +import { SuperDocEditor } from '@superdoc-dev/react'; +import '@superdoc-dev/react/style.css'; + +function App() { + return ; +} +``` + +--- + +## Advanced Features + +### Real-time Collaboration + +```jsx +import * as Y from 'yjs'; +import { WebsocketProvider } from 'y-websocket'; + +function CollaborativeEditor() { + const ydoc = useMemo(() => new Y.Doc(), []); + const provider = useMemo( + () => new WebsocketProvider('wss://your-server.com', 'doc-id', ydoc), + [ydoc] + ); + + return ( + + ); +} +``` + + + + Learn more about setting up real-time collaboration + + + +### AI Features + +```jsx + +``` + +### Search and Navigate + +```jsx +const editorRef = useRef(null); + +const handleSearch = (query) => { + const instance = editorRef.current?.getInstance(); + const results = instance?.search(query); + if (results?.length) { + instance?.goToSearchResult(results[0]); + } +}; +``` + +### Export to HTML + +```jsx +const editorRef = useRef(null); + +const getHtmlContent = () => { + const htmlArray = editorRef.current?.getInstance()?.getHTML(); + console.log(htmlArray); // Array of HTML strings per section +}; +``` + +--- + +## Props Reference + +### Document Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `document` | `File \| Blob \| string \| object` | required | Document to load | +| `documentMode` | `'editing' \| 'viewing' \| 'suggesting'` | `'editing'` | Initial editing mode | +| `role` | `'editor' \| 'viewer' \| 'suggester'` | `'editor'` | User's permission level | + +### User Props + +| Prop | Type | Description | +|------|------|-------------| +| `user` | `{ name, email?, image? }` | Current user info | +| `users` | `Array<{ name, email, image? }>` | All users (for @-mentions) | + +### UI Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `id` | `string` | auto-generated | Custom container ID | +| `hideToolbar` | `boolean` | `false` | Hide the toolbar | +| `rulers` | `boolean` | - | Show/hide rulers (SuperDoc default) | +| `className` | `string` | - | CSS class for wrapper | +| `style` | `CSSProperties` | - | Inline styles | +| `renderLoading` | `() => ReactNode` | - | Custom loading UI | + +### Event Callbacks + +| Prop | Type | Description | +|------|------|-------------| +| `onReady` | `({ superdoc }) => void` | Editor initialized | +| `onEditorCreate` | `({ editor }) => void` | ProseMirror editor created | +| `onEditorDestroy` | `() => void` | Editor destroyed | +| `onEditorUpdate` | `({ editor }) => void` | Content changed | +| `onContentError` | `(event) => void` | Document parsing error | +| `onException` | `({ error }) => void` | Runtime error | + +### Advanced Props + +| Prop | Type | Description | +|------|------|-------------| +| `modules` | `object` | Configure collaboration, AI, comments | + + +All SuperDoc config options are available as props. The component extends `SuperDocConfig`, so any option from the core package can be passed directly. + + +### Props That Trigger Rebuild + +These props trigger a full instance rebuild when changed: + +| Prop | Reason | +|------|--------| +| `document` | New document to load | +| `user` | User identity changed | +| `users` | Users list changed | +| `modules` | Module configuration changed | +| `role` | Permission level changed | +| `hideToolbar` | Toolbar visibility changed | + +Other props like `documentMode` and callbacks are handled efficiently without rebuild. + +--- + +## Troubleshooting + +### "document is not defined" (SSR) + +The component handles SSR internally, but if you still see errors: + +```jsx +// Use dynamic import in Next.js +const SuperDocEditor = dynamic( + () => import('@superdoc-dev/react').then(mod => mod.SuperDocEditor), + { ssr: false } +); +``` + +### React Strict Mode Double-Mount + +The component handles React 18 Strict Mode correctly. The internal cleanup flag prevents issues from double-invocation during development. + +### Document Not Loading + +1. Verify the file is a valid `.docx` document +2. Check that `document` prop is a `File`, `Blob`, URL string, or config object +3. Listen for `onContentError` events for parsing errors + +### Changing Document Mode + +The component handles `documentMode` prop changes efficiently without rebuilding: + +```jsx +const [mode, setMode] = useState('editing'); + +// Just update state - no rebuild, no flicker + + +``` + +You can also use the imperative API if preferred: + +```jsx +editorRef.current?.getInstance()?.setDocumentMode('viewing'); +``` + +--- + +## Requirements + +| Requirement | Version | +|-------------|---------| +| React | 16.8.0+ | +| Node.js | 16+ | + ## Next Steps -- [Vue Integration](/getting-started/frameworks/vue) - Vue setup -- [API Reference](/core/superdoc/configuration) - Configuration options -- [Examples](https://github.com/Harbour-Enterprises/SuperDoc/tree/main/examples/react-example) - Working examples + + + Full configuration options + + + All available methods + + + Real-time collaboration setup + + + React + TypeScript example + + + Next.js SSR integration + + diff --git a/examples/getting-started/nextjs/.gitignore b/examples/getting-started/nextjs/.gitignore new file mode 100644 index 0000000000..5ef6a52078 --- /dev/null +++ b/examples/getting-started/nextjs/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/getting-started/nextjs/README.md b/examples/getting-started/nextjs/README.md new file mode 100644 index 0000000000..e215bc4ccf --- /dev/null +++ b/examples/getting-started/nextjs/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples/getting-started/nextjs/eslint.config.mjs b/examples/getting-started/nextjs/eslint.config.mjs new file mode 100644 index 0000000000..05e726d1b4 --- /dev/null +++ b/examples/getting-started/nextjs/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/examples/getting-started/nextjs/next.config.ts b/examples/getting-started/nextjs/next.config.ts new file mode 100644 index 0000000000..e9ffa3083a --- /dev/null +++ b/examples/getting-started/nextjs/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/examples/getting-started/nextjs/package.json b/examples/getting-started/nextjs/package.json new file mode 100644 index 0000000000..8eef8c18f8 --- /dev/null +++ b/examples/getting-started/nextjs/package.json @@ -0,0 +1,27 @@ +{ + "name": "nextjs-superdoc", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "@superdoc-dev/react": "workspace:*", + "next": "16.1.6", + "react": "19.2.3", + "react-dom": "19.2.3" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.1.6", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/examples/getting-started/nextjs/postcss.config.mjs b/examples/getting-started/nextjs/postcss.config.mjs new file mode 100644 index 0000000000..61e36849cf --- /dev/null +++ b/examples/getting-started/nextjs/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/examples/getting-started/nextjs/public/file.svg b/examples/getting-started/nextjs/public/file.svg new file mode 100644 index 0000000000..004145cddf --- /dev/null +++ b/examples/getting-started/nextjs/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/getting-started/nextjs/public/globe.svg b/examples/getting-started/nextjs/public/globe.svg new file mode 100644 index 0000000000..567f17b0d7 --- /dev/null +++ b/examples/getting-started/nextjs/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/getting-started/nextjs/public/next.svg b/examples/getting-started/nextjs/public/next.svg new file mode 100644 index 0000000000..5174b28c56 --- /dev/null +++ b/examples/getting-started/nextjs/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/getting-started/nextjs/public/vercel.svg b/examples/getting-started/nextjs/public/vercel.svg new file mode 100644 index 0000000000..7705396033 --- /dev/null +++ b/examples/getting-started/nextjs/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/getting-started/nextjs/public/window.svg b/examples/getting-started/nextjs/public/window.svg new file mode 100644 index 0000000000..b2b2a44f6e --- /dev/null +++ b/examples/getting-started/nextjs/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/getting-started/nextjs/src/app/favicon.ico b/examples/getting-started/nextjs/src/app/favicon.ico new file mode 100644 index 0000000000..718d6fea48 Binary files /dev/null and b/examples/getting-started/nextjs/src/app/favicon.ico differ diff --git a/examples/getting-started/nextjs/src/app/globals.css b/examples/getting-started/nextjs/src/app/globals.css new file mode 100644 index 0000000000..a2dc41ecee --- /dev/null +++ b/examples/getting-started/nextjs/src/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/examples/getting-started/nextjs/src/app/layout.tsx b/examples/getting-started/nextjs/src/app/layout.tsx new file mode 100644 index 0000000000..eab146199b --- /dev/null +++ b/examples/getting-started/nextjs/src/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "SuperDoc + Next.js", + description: "Document editor powered by SuperDoc", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/examples/getting-started/nextjs/src/app/page.tsx b/examples/getting-started/nextjs/src/app/page.tsx new file mode 100644 index 0000000000..f0a972148b --- /dev/null +++ b/examples/getting-started/nextjs/src/app/page.tsx @@ -0,0 +1,127 @@ +'use client'; + +import { useState, useRef } from 'react'; +import { SuperDocEditor, SuperDocRef, DocumentMode } from '@superdoc-dev/react'; +import '@superdoc-dev/react/style.css'; + +export default function Home() { + const [file, setFile] = useState(null); + const [mode, setMode] = useState('editing'); + const editorRef = useRef(null); + + const handleFileChange = (e: React.ChangeEvent) => { + const selectedFile = e.target.files?.[0]; + if (selectedFile) { + setFile(selectedFile); + } + }; + + const handleExport = async () => { + await editorRef.current?.getInstance()?.export({ triggerDownload: true }); + }; + + return ( +
+ {/* Header */} +
+

+ SuperDoc + Next.js +

+ +
+ {/* Mode Toggle */} + {file && ( + <> +
+ + +
+ + + + )} +
+
+ + {/* Main Content */} +
+ {!file ? ( + /* File Upload UI */ +
+ +
+ ) : ( + /* SuperDoc Editor */ +
+ console.log('SuperDoc is ready!')} + onEditorUpdate={() => console.log('Document updated')} + renderLoading={() => ( +
+
Loading document...
+
+ )} + style={{ height: 'calc(100vh - 73px)' }} + /> +
+ )} +
+
+ ); +} diff --git a/examples/getting-started/nextjs/tsconfig.json b/examples/getting-started/nextjs/tsconfig.json new file mode 100644 index 0000000000..cf9c65d3e0 --- /dev/null +++ b/examples/getting-started/nextjs/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/examples/getting-started/typescript/.gitignore b/examples/getting-started/react/.gitignore similarity index 100% rename from examples/getting-started/typescript/.gitignore rename to examples/getting-started/react/.gitignore diff --git a/examples/getting-started/react/README.md b/examples/getting-started/react/README.md new file mode 100644 index 0000000000..5f0de5b164 --- /dev/null +++ b/examples/getting-started/react/README.md @@ -0,0 +1,47 @@ +# SuperDoc React + TypeScript Example + +A TypeScript example demonstrating `@superdoc-dev/react` integration with full type safety. + +## Features Demonstrated + +- **File Upload** - Load `.docx` files with type-safe event handlers +- **Mode Switching** - Toggle between editing, suggesting, and viewing modes +- **Ref API** - Access SuperDoc instance methods with proper typing +- **Export** - Download documents as DOCX +- **User Info** - Pass typed user information to the editor +- **Loading States** - Custom loading UI with `renderLoading` +- **Event Callbacks** - Typed callbacks for editor events + +## Run + +```bash +# From repo root +pnpm install +pnpm -C examples/getting-started/react dev +``` + +## Key Types Used + +```typescript +import type { SuperDocRef, DocumentMode } from '@superdoc-dev/react'; + +// Ref for accessing instance methods +const editorRef = useRef(null); + +// Typed document mode state +const [mode, setMode] = useState('editing'); + +// Access instance with proper types +const instance = editorRef.current?.getInstance(); +await instance?.export({ triggerDownload: true }); +``` + +## Project Structure + +``` +src/ +├── App.tsx # Main component with SuperDoc integration +├── App.css # Styles +├── main.tsx # Entry point +└── index.css # Global styles +``` diff --git a/examples/getting-started/react/demo-config.json b/examples/getting-started/react/demo-config.json deleted file mode 100644 index 787ff342c8..0000000000 --- a/examples/getting-started/react/demo-config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dirname": "react-example", - "tags": [ - "editing", - "viewing", - "react" - ], - "title": "React" -} \ No newline at end of file diff --git a/examples/getting-started/react/demo-thumbnail.png b/examples/getting-started/react/demo-thumbnail.png deleted file mode 100644 index 8425d2ff17..0000000000 Binary files a/examples/getting-started/react/demo-thumbnail.png and /dev/null differ diff --git a/examples/getting-started/react/demo-video.mp4 b/examples/getting-started/react/demo-video.mp4 deleted file mode 100644 index 77bbd8c622..0000000000 Binary files a/examples/getting-started/react/demo-video.mp4 and /dev/null differ diff --git a/examples/getting-started/react/index.html b/examples/getting-started/react/index.html index 5a4e3e6da2..fa31e5e69d 100644 --- a/examples/getting-started/react/index.html +++ b/examples/getting-started/react/index.html @@ -1,12 +1,12 @@ - + - SuperDoc React Example + SuperDoc React + TypeScript Example
- + - \ No newline at end of file + diff --git a/examples/getting-started/react/package.json b/examples/getting-started/react/package.json index 698373a5db..51b816db19 100644 --- a/examples/getting-started/react/package.json +++ b/examples/getting-started/react/package.json @@ -1,19 +1,23 @@ { - "name": "react-superdoc-example", + "name": "react-with-typescript-example", "private": true, "version": "0.0.1", "type": "module", "scripts": { - "dev": "vite" + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" }, "dependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0", - "styled-jsx": "^5.1.7", - "superdoc": "0.20.0-next.13" + "@superdoc-dev/react": "workspace:*", + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@vitejs/plugin-react": "^4.0.4", - "vite": "^6.2.0" + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "~5.5.0", + "vite": "^5.4.0" } } diff --git a/examples/getting-started/react/src/App.css b/examples/getting-started/react/src/App.css new file mode 100644 index 0000000000..c9fb3dbd23 --- /dev/null +++ b/examples/getting-started/react/src/App.css @@ -0,0 +1,218 @@ +/* Layout */ +.app { + height: 100vh; + display: flex; + flex-direction: column; + background: #f5f5f5; +} + +/* Header */ +.header { + padding: 1rem 1.5rem; + background: #1a1a2e; + color: white; + display: flex; + align-items: center; + gap: 1.5rem; + flex-wrap: wrap; +} + +.header h1 { + font-size: 1.25rem; + font-weight: 600; + margin-right: auto; +} + +/* Controls */ +.controls { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +/* Buttons */ +.btn { + padding: 0.5rem 1rem; + font-size: 0.875rem; + font-weight: 500; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 6px; + background: transparent; + color: white; + cursor: pointer; + transition: all 0.15s; +} + +.btn:hover { + background: rgba(255, 255, 255, 0.1); +} + +.btn.primary { + background: #3b82f6; + border-color: #3b82f6; +} + +.btn.primary:hover { + background: #2563eb; + border-color: #2563eb; +} + +.btn.large { + padding: 0.75rem 1.5rem; + font-size: 1rem; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Mode Switcher */ +.mode-switcher { + display: flex; + background: rgba(255, 255, 255, 0.1); + border-radius: 6px; + padding: 2px; +} + +.mode-btn { + padding: 0.4rem 0.75rem; + font-size: 0.8rem; + font-weight: 500; + border: none; + border-radius: 4px; + background: transparent; + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + transition: all 0.15s; +} + +.mode-btn:hover:not(:disabled) { + color: white; +} + +.mode-btn.active { + background: white; + color: #1a1a2e; +} + +.mode-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Actions */ +.actions { + display: flex; + gap: 0.5rem; +} + +/* Status */ +.status { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.7); +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #fbbf24; +} + +.status-dot.ready { + background: #22c55e; +} + +.status-dot.loading { + animation: pulse 1s infinite; +} + +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +/* Editor Area */ +.editor-area { + flex: 1; + min-height: 0; + background: white; +} + +/* Empty State */ +.empty-state { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: #fafafa; +} + +.empty-content { + text-align: center; + color: #666; +} + +.empty-content h2 { + font-size: 1.5rem; + font-weight: 600; + color: #333; + margin-bottom: 0.5rem; +} + +.empty-content p { + margin-bottom: 1.5rem; +} + +/* Loading State */ +.loading-state { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + color: #666; +} + +.spinner { + width: 32px; + height: 32px; + border: 3px solid #e5e7eb; + border-top-color: #3b82f6; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Responsive */ +@media (max-width: 768px) { + .header { + padding: 1rem; + } + + .header h1 { + width: 100%; + margin-bottom: 0.5rem; + } + + .controls { + width: 100%; + justify-content: flex-start; + } +} diff --git a/examples/getting-started/react/src/App.jsx b/examples/getting-started/react/src/App.jsx deleted file mode 100644 index 95bb007893..0000000000 --- a/examples/getting-started/react/src/App.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useRef, useState } from 'react'; -import DocumentEditor from './components/DocumentEditor'; - -function App() { - const [documentFile, setDocumentFile] = useState(null); - const fileInputRef = useRef(null); - - const handleFileChange = (event) => { - const file = event.target.files?.[0]; - if (file) { - setDocumentFile(file); - } - }; - - const handleEditorReady = (editor) => { - console.log('SuperDoc editor is ready', editor); - }; - - return ( -
-
-

SuperDoc Example

- - -
- -
- -
- - -
- ); -} - -export default App; diff --git a/examples/getting-started/react/src/App.tsx b/examples/getting-started/react/src/App.tsx new file mode 100644 index 0000000000..2f1ce76b59 --- /dev/null +++ b/examples/getting-started/react/src/App.tsx @@ -0,0 +1,181 @@ +import { useRef, useState } from 'react'; +import { SuperDocEditor } from '@superdoc-dev/react'; +import type { SuperDocRef, DocumentMode } from '@superdoc-dev/react'; +import '@superdoc-dev/react/style.css'; +import './App.css'; + +/** + * SuperDoc React + TypeScript Example + * + * Demonstrates: + * - File upload with type safety + * - Document mode switching (editing/viewing/suggesting) + * - Export functionality via ref API + * - User information + * - Loading states + * - Event callbacks + */ +function App() { + // Document state + const [document, setDocument] = useState(null); + const [mode, setMode] = useState('editing'); + const [isReady, setIsReady] = useState(false); + + // Ref for accessing SuperDoc instance methods + const editorRef = useRef(null); + const fileInputRef = useRef(null); + + // Current user (typed) + const currentUser = { + name: 'John Doe', + email: 'john@example.com', + }; + + // Handle file selection + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file && file.name.endsWith('.docx')) { + setDocument(file); + setIsReady(false); + } + }; + + // Export document as DOCX + const handleExport = async () => { + const instance = editorRef.current?.getInstance(); + if (instance) { + await instance.export({ triggerDownload: true }); + } + }; + + // Get document as HTML + const handleGetHTML = () => { + const instance = editorRef.current?.getInstance(); + if (instance) { + const html = instance.getHTML(); + console.log('Document HTML:', html); + alert(`Document has ${html.length} section(s). Check console for HTML.`); + } + }; + + // Mode button component for cleaner JSX + const ModeButton = ({ + targetMode, + label, + }: { + targetMode: DocumentMode; + label: string; + }) => ( + + ); + + return ( +
+ {/* Header with controls */} +
+

SuperDoc React + TypeScript

+ +
+ {/* File upload */} + + + + {/* Mode switcher */} + {document && ( +
+ + + +
+ )} + + {/* Actions */} + {document && isReady && ( +
+ + +
+ )} +
+ + {/* Status indicator */} + {document && ( +
+ + {isReady ? `Ready - ${mode} mode` : 'Loading...'} +
+ )} +
+ + {/* Editor area */} +
+ {document ? ( + { + console.log('SuperDoc ready:', superdoc); + setIsReady(true); + }} + onEditorCreate={({ editor }) => { + console.log('ProseMirror editor created:', editor); + }} + onEditorUpdate={() => { + console.log('Document updated'); + }} + onContentError={(event) => { + console.error('Content error:', event); + }} + renderLoading={() => ( +
+
+

Loading document...

+
+ )} + style={{ height: '100%' }} + /> + ) : ( +
+
+

No Document Loaded

+

Click "Open Document" to load a .docx file

+ +
+
+ )} +
+
+ ); +} + +export default App; diff --git a/examples/getting-started/react/src/components/DocumentEditor.jsx b/examples/getting-started/react/src/components/DocumentEditor.jsx deleted file mode 100644 index 8b34e45318..0000000000 --- a/examples/getting-started/react/src/components/DocumentEditor.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import { SuperDoc } from 'superdoc'; -import 'superdoc/style.css'; -import { useEffect, useRef } from 'react'; - -const DocumentEditor = ({ - initialData = null, - readOnly = false, - onEditorReady -}) => { - const editorRef = useRef(null); - - useEffect(() => { - const config = { - selector: '#superdoc', - toolbar: '#superdoc-toolbar', - documentMode: readOnly ? 'viewing' : 'editing', - pagination: true, - rulers: true, - onReady: () => { - if (onEditorReady) { - onEditorReady(editor); - } - }, - onEditorCreate: (event) => { - console.log('Editor is created', event); - }, - onEditorDestroy: () => { - console.log('Editor is destroyed'); - } - } - - if (initialData) config.document = initialData; - // config.document = './sample.docx'; // or use path to file - - const editor = new SuperDoc(config); - - editorRef.current = editor; - - // Cleanup on unmount - return () => { - if (editorRef.current) { - editorRef.current.destroy(); - editorRef.current = null; - } - }; - }, [initialData, readOnly, onEditorReady]); - - return ( -
-
-
- -
- ); -}; - -export default DocumentEditor; diff --git a/examples/getting-started/react/src/index.css b/examples/getting-started/react/src/index.css new file mode 100644 index 0000000000..558e2a6f6f --- /dev/null +++ b/examples/getting-started/react/src/index.css @@ -0,0 +1,20 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body, +#root { + height: 100%; +} + +body { + font-family: + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + sans-serif; +} diff --git a/examples/getting-started/react/src/main.jsx b/examples/getting-started/react/src/main.jsx deleted file mode 100644 index d76b758740..0000000000 --- a/examples/getting-started/react/src/main.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; - -ReactDOM.createRoot(document.getElementById('root')).render( - - - -); diff --git a/examples/getting-started/react/src/main.tsx b/examples/getting-started/react/src/main.tsx new file mode 100644 index 0000000000..a46835a4fc --- /dev/null +++ b/examples/getting-started/react/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; +import './index.css'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/examples/getting-started/typescript/src/vite-env.d.ts b/examples/getting-started/react/src/vite-env.d.ts similarity index 100% rename from examples/getting-started/typescript/src/vite-env.d.ts rename to examples/getting-started/react/src/vite-env.d.ts diff --git a/examples/getting-started/typescript/tsconfig.app.json b/examples/getting-started/react/tsconfig.json similarity index 70% rename from examples/getting-started/typescript/tsconfig.app.json rename to examples/getting-started/react/tsconfig.json index 358ca9ba93..109f0ac280 100644 --- a/examples/getting-started/typescript/tsconfig.app.json +++ b/examples/getting-started/react/tsconfig.json @@ -1,26 +1,20 @@ { "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, - - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", - - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noFallthroughCasesInSwitch": true }, "include": ["src"] } diff --git a/examples/getting-started/react/vite.config.js b/examples/getting-started/react/vite.config.js deleted file mode 100644 index 3e238c019c..0000000000 --- a/examples/getting-started/react/vite.config.js +++ /dev/null @@ -1,17 +0,0 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; - -export default defineConfig({ - plugins: [ - react({ - babel: { - plugins: [ -"styled-jsx/babel" - ], - }, - }), - ], - optimizeDeps: { - include: ['superdoc'] - } -}); diff --git a/examples/getting-started/typescript/vite.config.ts b/examples/getting-started/react/vite.config.ts similarity index 55% rename from examples/getting-started/typescript/vite.config.ts rename to examples/getting-started/react/vite.config.ts index 7fcf49f040..8b0f57b91a 100644 --- a/examples/getting-started/typescript/vite.config.ts +++ b/examples/getting-started/react/vite.config.ts @@ -3,13 +3,5 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ - plugins: [ - react({ - babel: { - plugins: [ -"styled-jsx/babel" - ], - }, - }), - ], + plugins: [react()], }) diff --git a/examples/getting-started/typescript/demo-config.json b/examples/getting-started/typescript/demo-config.json deleted file mode 100644 index e02cc7fc21..0000000000 --- a/examples/getting-started/typescript/demo-config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "dirname": "typescript-example", - "tags": [ - "editing", - "viewing", - "typescript", - "vanilla-js", - "react" - ], - "title": "Typescript" -} \ No newline at end of file diff --git a/examples/getting-started/typescript/demo-thumbnail.png b/examples/getting-started/typescript/demo-thumbnail.png deleted file mode 100644 index 94f386a039..0000000000 Binary files a/examples/getting-started/typescript/demo-thumbnail.png and /dev/null differ diff --git a/examples/getting-started/typescript/demo-video.mp4 b/examples/getting-started/typescript/demo-video.mp4 deleted file mode 100644 index c4780447b4..0000000000 Binary files a/examples/getting-started/typescript/demo-video.mp4 and /dev/null differ diff --git a/examples/getting-started/typescript/eslint.config.js b/examples/getting-started/typescript/eslint.config.js deleted file mode 100644 index 092408a9f0..0000000000 --- a/examples/getting-started/typescript/eslint.config.js +++ /dev/null @@ -1,28 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' - -export default tseslint.config( - { ignores: ['dist'] }, - { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - }, - rules: { - ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, - }, -) diff --git a/examples/getting-started/typescript/index.html b/examples/getting-started/typescript/index.html deleted file mode 100644 index e4b78eae12..0000000000 --- a/examples/getting-started/typescript/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/getting-started/typescript/package.json b/examples/getting-started/typescript/package.json deleted file mode 100644 index c446024c6b..0000000000 --- a/examples/getting-started/typescript/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "typescript-example", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0", - "styled-jsx": "^5.1.7", - "superdoc": "0.20.0-next.13" - }, - "devDependencies": { - "@eslint/js": "^9.19.0", - "@types/react": "^19.0.8", - "@types/react-dom": "^19.0.3", - "@vitejs/plugin-react": "^4.3.4", - "eslint": "^9.19.0", - "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.18", - "globals": "^15.14.0", - "typescript": "~5.7.2", - "typescript-eslint": "^8.22.0", - "vite": "^6.2.0" - } -} diff --git a/examples/getting-started/typescript/src/App.tsx b/examples/getting-started/typescript/src/App.tsx deleted file mode 100644 index da4adaf0fe..0000000000 --- a/examples/getting-started/typescript/src/App.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { useRef, useState, ChangeEvent } from 'react'; -import DocumentEditor from './components/DocumentEditor'; - -function App() { - const [documentFile, setDocumentFile] = useState(null); - const fileInputRef = useRef(null); - - const handleFileChange = (event: ChangeEvent) => { - const file = event.target.files?.[0]; - if (file) { - setDocumentFile(file); - } - }; - - return ( -
-
-

SuperDoc Example

- - -
- -
- -
- - -
- ); -} - -export default App; \ No newline at end of file diff --git a/examples/getting-started/typescript/src/components/DocumentEditor.tsx b/examples/getting-started/typescript/src/components/DocumentEditor.tsx deleted file mode 100644 index bd5e792ac8..0000000000 --- a/examples/getting-started/typescript/src/components/DocumentEditor.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { SuperDoc, Config } from 'superdoc'; - -import 'superdoc/style.css'; -import { useEffect, useRef } from 'react'; - -interface Props { - initialData: File | null, - readOnly?: boolean, -} - -const DocumentEditor = ({ - initialData = null, - readOnly = false, -}: Props) => { - const editorRef = useRef(null); - useEffect(() => { - const config: Config = { - selector: '#superdoc', - toolbar: '#superdoc-toolbar', - document: initialData, // URL, File or document config - documentMode: readOnly ? 'viewing' : 'editing', - pagination: true, - rulers: true, - onReady: (event) => { - console.log('SuperDoc is ready', event); - }, - onEditorCreate: (event) => { - console.log('Editor is created', event); - }, - }; - - const editor = new SuperDoc(config); - editorRef.current = editor; - - // Cleanup on unmount - return () => { - if (editorRef.current) { - editorRef.current.destroy(); - editorRef.current = null; - } - }; - }, [initialData, readOnly]); - - return ( -
-
-
- -
- ); -}; - -export default DocumentEditor; diff --git a/examples/getting-started/typescript/src/main.tsx b/examples/getting-started/typescript/src/main.tsx deleted file mode 100644 index a996c1e074..0000000000 --- a/examples/getting-started/typescript/src/main.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { createRoot } from 'react-dom/client' -import App from './App.tsx' - -createRoot(document.getElementById('root')!).render( - -) diff --git a/examples/getting-started/typescript/tsconfig.json b/examples/getting-started/typescript/tsconfig.json deleted file mode 100644 index 1e45f34b23..0000000000 --- a/examples/getting-started/typescript/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ], - "compilerOptions": { - "checkJs": true, - "allowJs": true - } -} diff --git a/examples/getting-started/typescript/tsconfig.node.json b/examples/getting-started/typescript/tsconfig.node.json deleted file mode 100644 index db0becc8b0..0000000000 --- a/examples/getting-started/typescript/tsconfig.node.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2022", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/examples/pnpm-workspace.yaml b/examples/pnpm-workspace.yaml index 1736a0e370..c20f156108 100644 --- a/examples/pnpm-workspace.yaml +++ b/examples/pnpm-workspace.yaml @@ -15,3 +15,4 @@ onlyBuiltDependencies: overrides: superdoc: file:../packages/superdoc/superdoc.tgz + '@superdoc-dev/react': file:../packages/react/react.tgz diff --git a/examples/tests/test-config.js b/examples/tests/test-config.js index ea7986aa6f..385893ac4a 100644 --- a/examples/tests/test-config.js +++ b/examples/tests/test-config.js @@ -2,7 +2,6 @@ export default { packages: [ "getting-started/cdn", "getting-started/react", - "getting-started/typescript", "getting-started/vanilla", "getting-started/vue", // Customization diff --git a/packages/react/.releaserc.cjs b/packages/react/.releaserc.cjs new file mode 100644 index 0000000000..ce3571a882 --- /dev/null +++ b/packages/react/.releaserc.cjs @@ -0,0 +1,42 @@ +/* eslint-env node */ +const branch = process.env.GITHUB_REF_NAME || process.env.CI_COMMIT_BRANCH; + +const config = { + branches: [ + { name: 'stable', channel: 'latest' }, + { name: 'main', prerelease: 'next', channel: 'next' }, + ], + tagFormat: 'react-v${version}', + plugins: [ + '@semantic-release/commit-analyzer', + '@semantic-release/release-notes-generator', + ['@semantic-release/npm', { npmPublish: true }], + ], +}; + +const isPrerelease = config.branches.some( + (b) => typeof b === 'object' && b.name === branch && b.prerelease +); + +if (!isPrerelease) { + config.plugins.push([ + '@semantic-release/git', + { + assets: ['package.json'], + message: + 'chore(react): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', + }, + ]); +} + +// Linear integration - labels issues with version on release +config.plugins.push(['semantic-release-linear-app', { teamKeys: ['SD'], addComment: true, packageName: 'react' }]); + +config.plugins.push([ + '@semantic-release/github', + { + successComment: ':tada: This ${issue.pull_request ? "PR" : "issue"} is included in **@superdoc-dev/react** v${nextRelease.version}\n\nThe release is available on [GitHub release]()', + } +]); + +module.exports = config; diff --git a/packages/react/CLAUDE.md b/packages/react/CLAUDE.md new file mode 100644 index 0000000000..1a53c37ca6 --- /dev/null +++ b/packages/react/CLAUDE.md @@ -0,0 +1,85 @@ +# @superdoc-dev/react + +React wrapper for SuperDoc. + +## Files + +| File | Purpose | +|------|---------| +| `src/SuperDocEditor.tsx` | Main component | +| `src/types.ts` | TypeScript types (extracted from superdoc) | +| `src/utils.ts` | ID generation | +| `src/index.ts` | Public exports | + +## Type System + +Types are extracted from `superdoc` constructor to avoid duplication: + +```typescript +type SuperDocConstructorConfig = ConstructorParameters[0]; + +export type DocumentMode = NonNullable; +export type UserRole = NonNullable; +export type SuperDocUser = NonNullable; +export type SuperDocModules = NonNullable; +export type SuperDocConfig = SuperDocConstructorConfig; +export type SuperDocInstance = InstanceType; + +// Props = SuperDocConfig (minus internal) + React-specific +type InternalProps = 'selector'; // managed by component +type OptionalInReact = 'documentMode'; // defaults to 'editing' + +export interface SuperDocEditorProps + extends Omit, + Partial>, + ReactProps {} +``` + +## React-Specific Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `id` | `string` | auto-generated | Custom container ID | +| `renderLoading` | `() => ReactNode` | - | Loading UI during init | +| `hideToolbar` | `boolean` | `false` | Hide the toolbar | +| `className` | `string` | - | Wrapper CSS class | +| `style` | `CSSProperties` | - | Wrapper inline styles | + +## SSR Behavior + +- Returns `renderLoading()` on server if provided, otherwise `null` +- Initializes SuperDoc only after client-side hydration +- Container div must exist before SuperDoc mounts + +## Ref API + +```typescript +const editorRef = useRef(null); + +// Access SuperDoc instance +const instance = editorRef.current?.getInstance(); + +// Call methods +instance?.setDocumentMode('viewing'); +instance?.export({ triggerDownload: true }); +instance?.getHTML(); +``` + +## Props That Trigger Rebuild + +These props cause the SuperDoc instance to be destroyed and recreated: +- `document` - new document to load +- `user` - user identity changed +- `users` - users list changed +- `modules` - module config changed +- `role` - permission level changed +- `hideToolbar` - toolbar visibility changed + +Other props like `documentMode` and callbacks are handled without rebuild. + +## Commands + +```bash +pnpm --filter @superdoc-dev/react build +pnpm --filter @superdoc-dev/react test +``` diff --git a/packages/react/README.md b/packages/react/README.md new file mode 100644 index 0000000000..4ef78e63d1 --- /dev/null +++ b/packages/react/README.md @@ -0,0 +1,184 @@ +# @superdoc-dev/react + +Official React wrapper for [SuperDoc](https://www.superdoc.dev). + +## Installation + +```bash +npm install @superdoc-dev/react +``` + +> `superdoc` is included as a dependency - no need to install it separately. + +## Quick Start + +```tsx +import { SuperDocEditor } from '@superdoc-dev/react'; +import '@superdoc-dev/react/style.css'; + +function App() { + return ; +} +``` + +## Changing Mode + +Just update the `documentMode` prop - the component handles it efficiently (no rebuild): + +```tsx +function App() { + const [mode, setMode] = useState('editing'); + + return ( + <> + + + + + ); +} +``` + +## Using the Ref + +Access SuperDoc methods via `getInstance()`: + +```tsx +import { useRef } from 'react'; +import { SuperDocEditor, SuperDocRef } from '@superdoc-dev/react'; + +function App() { + const ref = useRef(null); + + const handleExport = async () => { + await ref.current?.getInstance()?.export({ triggerDownload: true }); + }; + + return ( + <> + + + + ); +} +``` + +## Props + +All [SuperDoc config options](https://docs.superdoc.dev) are available as props, plus: + +| Prop | Type | Description | +|------|------|-------------| +| `id` | `string` | Custom container ID (auto-generated if not provided) | +| `renderLoading` | `() => ReactNode` | Loading UI | +| `hideToolbar` | `boolean` | Hide toolbar (default: false) | +| `className` | `string` | Wrapper CSS class | +| `style` | `CSSProperties` | Wrapper inline styles | + +### Props That Trigger Rebuilds + +These props cause the SuperDoc instance to be destroyed and recreated when changed: + +- `document` - The document to load +- `user` - Current user identity +- `users` - List of users +- `modules` - Module configuration (collaboration, comments, etc.) +- `role` - User permission level +- `hideToolbar` - Toolbar visibility + +### Props Handled Efficiently + +These props are applied without rebuilding: + +- `documentMode` - Calls `setDocumentMode()` internally + +### Initial-Only Props + +Other SuperDoc options (`rulers`, `pagination`, etc.) are applied only on initialization. To change them at runtime, use `getInstance()`: + +```tsx +ref.current?.getInstance()?.toggleRuler(); +``` + +### Common Props + +```tsx + console.log('Ready!')} + onEditorCreate={({ editor }) => console.log('Editor created')} +/> +``` + +## Examples + +### View-Only Mode + +```tsx + +``` + +### File Upload + +```tsx +function Editor() { + const [file, setFile] = useState(null); + + return ( + <> + setFile(e.target.files?.[0] || null)} /> + {file && } + + ); +} +``` + +### With Collaboration + +```tsx + +``` + +## Next.js + +```tsx +'use client'; + +import dynamic from 'next/dynamic'; + +const SuperDocEditor = dynamic( + () => import('@superdoc-dev/react').then((m) => m.SuperDocEditor), + { ssr: false } +); +``` + +## TypeScript + +```tsx +import type { + SuperDocEditorProps, + SuperDocRef, + DocumentMode, + UserRole, + SuperDocUser, +} from '@superdoc-dev/react'; +``` + +Types are extracted from the `superdoc` package, ensuring they stay in sync. + +## License + +AGPL-3.0 + diff --git a/packages/react/package.json b/packages/react/package.json new file mode 100644 index 0000000000..a58c418f5a --- /dev/null +++ b/packages/react/package.json @@ -0,0 +1,78 @@ +{ + "name": "@superdoc-dev/react", + "version": "0.1.0", + "description": "Official React wrapper for SuperDoc document editor", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.cjs", + "import": "./dist/index.js" + }, + "./style.css": { + "import": "./style.css", + "require": "./style.css" + } + }, + "scripts": { + "build": "vite build", + "dev": "vite build --watch", + "test": "vitest run", + "type-check": "tsc --noEmit", + "lint": "eslint src --ext .ts,.tsx", + "prepublishOnly": "pnpm run build" + }, + "keywords": [ + "superdoc", + "react", + "document", + "editor", + "docx", + "word" + ], + "license": "AGPL-3.0", + "dependencies": { + "superdoc": ">=1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, + "devDependencies": { + "@testing-library/react": "catalog:", + "@types/node": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@typescript-eslint/eslint-plugin": "catalog:", + "@typescript-eslint/parser": "catalog:", + "eslint": "catalog:", + "happy-dom": "catalog:", + "react": "catalog:", + "react-dom": "catalog:", + "typescript": "catalog:", + "vite": "catalog:", + "vite-plugin-dts": "catalog:", + "@vitejs/plugin-react": "catalog:", + "vitest": "catalog:" + }, + "files": [ + "dist", + "style.css", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/superdoc-dev/superdoc.git", + "directory": "packages/react" + }, + "bugs": { + "url": "https://github.com/superdoc-dev/superdoc/issues" + }, + "homepage": "https://github.com/superdoc-dev/superdoc/tree/main/packages/react#readme" +} diff --git a/packages/react/src/SuperDocEditor.test.tsx b/packages/react/src/SuperDocEditor.test.tsx new file mode 100644 index 0000000000..f7aae07dc3 --- /dev/null +++ b/packages/react/src/SuperDocEditor.test.tsx @@ -0,0 +1,183 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, cleanup, waitFor } from '@testing-library/react'; +import { createRef, StrictMode } from 'react'; +import { SuperDocEditor } from './SuperDocEditor'; +import type { SuperDocRef } from './types'; + +describe('SuperDocEditor', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + cleanup(); + }); + + describe('mounting and unmounting', () => { + it('should render container elements', () => { + const { container } = render(); + + expect(container.querySelector('.superdoc-wrapper')).toBeTruthy(); + expect(container.querySelector('.superdoc-editor-container')).toBeTruthy(); + expect(container.querySelector('.superdoc-toolbar-container')).toBeTruthy(); + }); + + it('should hide toolbar when hideToolbar={true}', () => { + const { container } = render(); + + expect(container.querySelector('.superdoc-toolbar-container')).toBeFalsy(); + }); + + it('should apply className and style props', () => { + const { container } = render(); + + const wrapper = container.querySelector('.superdoc-wrapper'); + expect(wrapper?.classList.contains('custom-class')).toBe(true); + expect((wrapper as HTMLElement)?.style.backgroundColor).toBe('red'); + }); + + it('should handle unmount without throwing', async () => { + const onReady = vi.fn(); + const { unmount } = render(); + + // Wait for initialization to complete + await waitFor( + () => { + expect(onReady).toHaveBeenCalled(); + }, + { timeout: 5000 }, + ); + + // Unmount should not throw + expect(() => unmount()).not.toThrow(); + }); + }); + + describe('ref methods', () => { + it('should expose getInstance method only', () => { + const ref = createRef(); + render(); + + // Ref should be available immediately with getInstance + expect(ref.current).not.toBeNull(); + expect(typeof ref.current?.getInstance).toBe('function'); + }); + + it('should return null from getInstance before ready', () => { + const ref = createRef(); + render(); + + // Before async init completes, getInstance returns null + const instance = ref.current?.getInstance(); + expect(instance).toBeNull(); + }); + + it('should safely handle calls through getInstance before ready', () => { + const ref = createRef(); + render(); + + // Using optional chaining through getInstance is safe + expect(() => ref.current?.getInstance()?.focus()).not.toThrow(); + expect(() => ref.current?.getInstance()?.setDocumentMode('viewing')).not.toThrow(); + expect(() => ref.current?.getInstance()?.toggleRuler()).not.toThrow(); + }); + }); + + describe('loading state', () => { + it('should show loading content initially', () => { + const { container } = render( +
Loading...
} />, + ); + + expect(container.querySelector('[data-testid="loading"]')).toBeTruthy(); + }); + }); + + describe('callbacks', () => { + it('should call onReady when SuperDoc is ready', async () => { + const onReady = vi.fn(); + render(); + + await waitFor( + () => { + expect(onReady).toHaveBeenCalled(); + }, + { timeout: 5000 }, + ); + }); + + it('should call onEditorCreate when editor is created', async () => { + const onEditorCreate = vi.fn(); + render(); + + await waitFor( + () => { + expect(onEditorCreate).toHaveBeenCalled(); + }, + { timeout: 5000 }, + ); + }); + }); + + describe('Strict Mode compatibility', () => { + it('should not throw in Strict Mode', () => { + expect(() => { + render( + + + , + ); + }).not.toThrow(); + }); + }); + + describe('unique IDs', () => { + it('should generate unique container IDs for multiple instances', () => { + const { container: container1 } = render(); + const { container: container2 } = render(); + + const id1 = container1.querySelector('.superdoc-editor-container')?.id; + const id2 = container2.querySelector('.superdoc-editor-container')?.id; + + expect(id1).toBeTruthy(); + expect(id2).toBeTruthy(); + expect(id1).not.toBe(id2); + }); + }); + + describe('with real superdoc', () => { + it('should initialize superdoc instance', async () => { + const ref = createRef(); + const onReady = vi.fn(); + + render(); + + await waitFor( + () => { + expect(onReady).toHaveBeenCalled(); + expect(ref.current?.getInstance()).not.toBeNull(); + }, + { timeout: 5000 }, + ); + }); + + it('should provide access to superdoc methods after ready', async () => { + const ref = createRef(); + const onReady = vi.fn(); + + render(); + + await waitFor( + () => { + expect(onReady).toHaveBeenCalled(); + }, + { timeout: 5000 }, + ); + + const instance = ref.current?.getInstance(); + expect(instance).toBeTruthy(); + expect(typeof instance?.destroy).toBe('function'); + expect(typeof instance?.setDocumentMode).toBe('function'); + }); + }); +}); diff --git a/packages/react/src/SuperDocEditor.tsx b/packages/react/src/SuperDocEditor.tsx new file mode 100644 index 0000000000..3a062e8d49 --- /dev/null +++ b/packages/react/src/SuperDocEditor.tsx @@ -0,0 +1,277 @@ +import { forwardRef, useEffect, useImperativeHandle, useRef, useState, type ForwardedRef } from 'react'; +import { generateId } from './utils'; +import type { + DocumentMode, + SuperDocEditorProps, + SuperDocInstance, + SuperDocRef, + SuperDocReadyEvent, + SuperDocEditorCreateEvent, + SuperDocEditorUpdateEvent, + SuperDocContentErrorEvent, + SuperDocExceptionEvent, +} from './types'; + +/** Callback props type for the ref */ +type CallbacksType = { + onReady?: (event: SuperDocReadyEvent) => void; + onEditorCreate?: (event: SuperDocEditorCreateEvent) => void; + onEditorDestroy?: () => void; + onEditorUpdate?: (event: SuperDocEditorUpdateEvent) => void; + onContentError?: (event: SuperDocContentErrorEvent) => void; + onException?: (event: SuperDocExceptionEvent) => void; +}; + +/** + * SuperDocEditor - React wrapper component for SuperDoc + * + * Provides a component-based API with proper lifecycle management + * and React Strict Mode compatibility. + * + * NOTE: This is a client-only component. During SSR, it renders the + * `renderLoading` placeholder if provided, otherwise returns null. + * For Next.js, use dynamic import with { ssr: false }. + */ +function SuperDocEditorInner(props: SuperDocEditorProps, ref: ForwardedRef) { + const [isClient, setIsClient] = useState(false); + const [hasError, setHasError] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + // Destructure React-specific props and key rebuild triggers + const { + // React-specific + id, + renderLoading, + hideToolbar = false, + className, + style, + // Callbacks (stored in ref to avoid triggering rebuilds) + onReady, + onEditorCreate, + onEditorDestroy, + onEditorUpdate, + onContentError, + onException, + // Key props that trigger rebuild when changed + document: documentProp, + user, + users, + modules, + // All other props passed through + ...restProps + } = props; + + // Apply defaults + const documentMode = props.documentMode ?? 'editing'; + const role = props.role ?? 'editor'; + + const instanceRef = useRef(null); + const toolbarContainerRef = useRef(null); + + // Generate stable IDs once per component instance (use provided id if available) + const idsRef = useRef<{ containerId: string; toolbarId: string } | null>(null); + if (idsRef.current === null) { + const baseId = id ?? generateId(); + idsRef.current = { containerId: baseId, toolbarId: `${baseId}-toolbar` }; + } + const { containerId, toolbarId } = idsRef.current; + + const [isLoading, setIsLoading] = useState(true); + + // Store callbacks in refs to avoid triggering effect on callback changes + const callbacksRef = useRef({ + onReady, + onEditorCreate, + onEditorDestroy, + onEditorUpdate, + onContentError, + onException, + }); + + // Update callback refs when props change + useEffect(() => { + callbacksRef.current = { + onReady, + onEditorCreate, + onEditorDestroy, + onEditorUpdate, + onContentError, + onException, + }; + }, [onReady, onEditorCreate, onEditorDestroy, onEditorUpdate, onContentError, onException]); + + // Queue mode changes that happen during init + const pendingModeRef = useRef(null); + const isInitializingRef = useRef(false); + + // Capture the initial documentMode for the effect + const initialDocumentModeRef = useRef(documentMode); + + // Track documentMode changes and apply imperatively + const prevDocumentModeRef = useRef(documentMode); + useEffect(() => { + if (prevDocumentModeRef.current !== documentMode) { + if (instanceRef.current) { + // Instance exists, apply immediately + instanceRef.current.setDocumentMode(documentMode); + } else if (isInitializingRef.current) { + // Instance is initializing, queue the mode change + pendingModeRef.current = documentMode; + } + } + prevDocumentModeRef.current = documentMode; + }, [documentMode]); + + // Expose ref methods - simplified API with just getInstance() + useImperativeHandle( + ref, + () => ({ + getInstance: () => instanceRef.current, + }), + [], + ); + + // Main effect: create and destroy SuperDoc instance + useEffect(() => { + // Wait for client-side render so the container div exists in DOM + if (!isClient) return; + + // Reset states when document changes + setIsLoading(true); + setHasError(false); + isInitializingRef.current = true; + + // Capture the current documentMode for this effect run + const effectDocumentMode = initialDocumentModeRef.current; + initialDocumentModeRef.current = documentMode; + + let destroyed = false; + let instance: SuperDocInstance | null = null; + + const initSuperDoc = async () => { + try { + // Dynamic import for SSR safety + const modulePath = 'superdoc'; + const superdocModule = await import(/* @vite-ignore */ modulePath); + const SuperDoc = superdocModule.SuperDoc as new (config: Record) => SuperDocInstance; + + // Check if we were destroyed while loading + if (destroyed) return; + + // Build configuration - pass through all props + const superdocConfig = { + ...restProps, + selector: `#${CSS.escape(containerId)}`, + // Use internal toolbar container unless hideToolbar is true + ...(!hideToolbar && toolbarContainerRef.current ? { toolbar: `#${CSS.escape(toolbarId)}` } : {}), + documentMode: effectDocumentMode, + role, + ...(documentProp != null ? { document: documentProp } : {}), + ...(user ? { user } : {}), + ...(users ? { users } : {}), + ...(modules ? { modules } : {}), + // Wire up callbacks with lifecycle guards + onReady: (event: SuperDocReadyEvent) => { + if (!destroyed) { + setIsLoading(false); + isInitializingRef.current = false; + + // Apply any pending mode changes + if (pendingModeRef.current && pendingModeRef.current !== effectDocumentMode) { + event.superdoc.setDocumentMode(pendingModeRef.current); + pendingModeRef.current = null; + } + + callbacksRef.current.onReady?.(event); + } + }, + onEditorCreate: (event: SuperDocEditorCreateEvent) => { + if (!destroyed) { + callbacksRef.current.onEditorCreate?.(event); + } + }, + onEditorDestroy: () => { + if (!destroyed) { + callbacksRef.current.onEditorDestroy?.(); + } + }, + onEditorUpdate: (event: SuperDocEditorUpdateEvent) => { + if (!destroyed) { + callbacksRef.current.onEditorUpdate?.(event); + } + }, + onContentError: (event: SuperDocContentErrorEvent) => { + if (!destroyed) { + callbacksRef.current.onContentError?.(event); + } + }, + onException: (event: SuperDocExceptionEvent) => { + if (!destroyed) { + callbacksRef.current.onException?.(event); + } + }, + }; + + instance = new SuperDoc(superdocConfig) as SuperDocInstance; + instanceRef.current = instance; + } catch (error) { + if (!destroyed) { + isInitializingRef.current = false; + setIsLoading(false); + setHasError(true); + console.error('[SuperDocEditor] Failed to initialize SuperDoc:', error); + callbacksRef.current.onException?.({ error: error as Error }); + } + } + }; + + initSuperDoc(); + + // Cleanup function + return () => { + destroyed = true; + isInitializingRef.current = false; + pendingModeRef.current = null; + if (instance) { + instance.destroy(); + instanceRef.current = null; + } + }; + // Only these props trigger a full rebuild. Other props (rulers, etc.) are + // initial values - use getInstance() methods to change them at runtime. + // Note: restProps is intentionally excluded to avoid rebuilds on every render. + // documentMode is handled separately via setDocumentMode() for efficiency. + }, [isClient, documentProp, user, users, modules, role, hideToolbar, containerId, toolbarId]); + + const wrapperClassName = ['superdoc-wrapper', className].filter(Boolean).join(' '); + + // Client-only: show renderLoading placeholder on server if provided, otherwise null + if (!isClient) { + return renderLoading ? ( +
+ {renderLoading()} +
+ ) : null; + } + + return ( +
+ {!hideToolbar &&
} +
+ {isLoading && !hasError && renderLoading &&
{renderLoading()}
} + {hasError &&
Failed to load editor. Check console for details.
} +
+ ); +} + +/** + * SuperDocEditor component with forwardRef - Initializes SuperDoc instance and handles cleanup. + */ +export const SuperDocEditor = forwardRef(SuperDocEditorInner); + +SuperDocEditor.displayName = 'SuperDocEditor'; + +export default SuperDocEditor; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts new file mode 100644 index 0000000000..f9d2a7d99b --- /dev/null +++ b/packages/react/src/index.ts @@ -0,0 +1,31 @@ +/** + * @superdoc-dev/react - Official React wrapper for SuperDoc + * @packageDocumentation + * @version 1.0.0 + */ + +// Main component +export { SuperDocEditor, default } from './SuperDocEditor'; + +// Types - extracted from superdoc package for convenience +export type { + // Component props and ref + SuperDocEditorProps, + SuperDocRef, + + // Core types (extracted from superdoc constructor) + DocumentMode, + UserRole, + SuperDocUser, + SuperDocModules, + SuperDocConfig, + SuperDocInstance, + + // Callback event types + Editor, + SuperDocReadyEvent, + SuperDocEditorCreateEvent, + SuperDocEditorUpdateEvent, + SuperDocContentErrorEvent, + SuperDocExceptionEvent, +} from './types'; diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts new file mode 100644 index 0000000000..5eca6211de --- /dev/null +++ b/packages/react/src/types.ts @@ -0,0 +1,163 @@ +import type { CSSProperties, ReactNode } from 'react'; +import type { SuperDoc, Editor } from 'superdoc'; + +/** + * Types for @superdoc-dev/react + * + * Core types are extracted from the SuperDoc constructor parameter type, + * ensuring they stay in sync with the superdoc package. + */ + +// ============================================================================= +// Extract types from SuperDoc constructor (single source of truth) +// ============================================================================= + +/** SuperDoc constructor config - extracted from superdoc package */ +type SuperDocConstructorConfig = ConstructorParameters[0]; + +/** SuperDoc instance type - from superdoc package */ +export type SuperDocInstance = InstanceType; + +/** Document mode - extracted from Config.documentMode */ +export type DocumentMode = NonNullable; + +/** User role - extracted from Config.role */ +export type UserRole = NonNullable; + +/** User object - extracted from Config.user */ +export type SuperDocUser = NonNullable; + +/** Modules configuration - extracted from Config.modules */ +export type SuperDocModules = NonNullable; + +/** Full SuperDoc config - extracted from constructor */ +export type SuperDocConfig = SuperDocConstructorConfig; + +// ============================================================================= +// Callback Event Types +// ============================================================================= + +// Re-export Editor type from superdoc +export type { Editor } from 'superdoc'; + +/** Event passed to onReady callback */ +export interface SuperDocReadyEvent { + superdoc: SuperDocInstance; +} + +/** Event passed to onEditorCreate callback */ +export interface SuperDocEditorCreateEvent { + editor: Editor; +} + +/** Event passed to onEditorUpdate callback */ +export interface SuperDocEditorUpdateEvent { + editor: Editor; +} + +/** Event passed to onContentError callback */ +export interface SuperDocContentErrorEvent { + error: Error; + editor: Editor; + documentId: string; + file: File; +} + +/** Event passed to onException callback */ +export interface SuperDocExceptionEvent { + error: Error; +} + +// ============================================================================= +// React Component Types +// ============================================================================= + +/** + * Props managed internally by the React component (not exposed to users). + * - selector: managed by component (creates internal container) + */ +type InternalProps = 'selector'; + +/** + * Props that are required in core but should be optional in React. + * - documentMode: defaults to 'editing' if not provided + */ +type OptionalInReact = 'documentMode'; + +/** + * Callback props that are explicitly typed in CallbackProps. + * These are excluded from SuperDocConfig to avoid type conflicts. + */ +type ExplicitCallbackProps = + | 'onReady' + | 'onEditorCreate' + | 'onEditorDestroy' + | 'onEditorUpdate' + | 'onContentError' + | 'onException'; + +/** + * Explicitly typed callback props to ensure proper TypeScript inference. + * These override any loosely-typed callbacks from SuperDocConfig. + */ +interface CallbackProps { + /** Callback when SuperDoc is ready */ + onReady?: (event: SuperDocReadyEvent) => void; + + /** Callback after an editor is created */ + onEditorCreate?: (event: SuperDocEditorCreateEvent) => void; + + /** Callback when editor is destroyed */ + onEditorDestroy?: () => void; + + /** Callback when document content is updated */ + onEditorUpdate?: (event: SuperDocEditorUpdateEvent) => void; + + /** Callback when there is a content parsing error */ + onContentError?: (event: SuperDocContentErrorEvent) => void; + + /** Callback when an exception is thrown */ + onException?: (event: SuperDocExceptionEvent) => void; +} + +/** + * React-specific props added on top of SuperDocConfig. + */ +interface ReactProps { + /** Optional ID for the editor container. Auto-generated if not provided. */ + id?: string; + + /** Render function for loading state */ + renderLoading?: () => ReactNode; + + /** Hide the toolbar container. When true, no toolbar is rendered. @default false */ + hideToolbar?: boolean; + + /** Additional CSS class name for the wrapper element */ + className?: string; + + /** Additional inline styles for the wrapper element */ + style?: CSSProperties; +} + +/** + * Props for SuperDocEditor component. + * + * Extends SuperDocConfig (minus internal props) with React-specific additions. + * When new props are added to SuperDoc core, they're automatically available here. + * + * Callback props are explicitly typed to ensure proper TypeScript inference. + */ +export interface SuperDocEditorProps + extends Omit, + Partial>, + CallbackProps, + ReactProps {} + +/** + * Ref interface for SuperDocEditor component + */ +export interface SuperDocRef { + /** Get the underlying SuperDoc instance. Returns null if not yet initialized. */ + getInstance(): SuperDocInstance | null; +} diff --git a/packages/react/src/utils.ts b/packages/react/src/utils.ts new file mode 100644 index 0000000000..97277bba59 --- /dev/null +++ b/packages/react/src/utils.ts @@ -0,0 +1,16 @@ +/** @module utils */ + +/** + * Generate a unique ID for SuperDoc container elements. + * + * Uses a combination of timestamp and random string to ensure uniqueness + * across multiple instances without relying on a global counter. + * + * Note: This function only runs on the client after hydration since + * IDs are generated in a ref initializer (not during SSR render). + * + * @returns A unique identifier string + */ +export function generateId(): string { + return `superdoc-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; +} diff --git a/packages/react/style.css b/packages/react/style.css new file mode 100644 index 0000000000..7242c28bd0 --- /dev/null +++ b/packages/react/style.css @@ -0,0 +1,2 @@ +/* @superdoc-dev/react styles - re-exports superdoc styles */ +@import 'superdoc/style.css'; diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json new file mode 100644 index 0000000000..309d389c06 --- /dev/null +++ b/packages/react/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "outDir": "./dist", + "rootDir": "./src", + "jsx": "react-jsx", + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts new file mode 100644 index 0000000000..bd48e2cb0e --- /dev/null +++ b/packages/react/vite.config.ts @@ -0,0 +1,38 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import dts from 'vite-plugin-dts'; + +export default defineConfig({ + plugins: [ + react(), + dts({ + include: ['src/**/*'], + exclude: ['src/**/*.test.ts', 'src/**/*.test.tsx'], + outDir: 'dist', + rollupTypes: true, + }), + ], + build: { + target: 'es2020', + lib: { + entry: 'src/index.ts', + name: 'SuperDocReact', + formats: ['es', 'cjs'], + fileName: (format) => (format === 'es' ? 'index.js' : 'index.cjs'), + }, + minify: true, + sourcemap: false, + rollupOptions: { + external: ['react', 'react-dom', 'react/jsx-runtime', 'superdoc'], + output: { + exports: 'named', + globals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react/jsx-runtime': 'jsxRuntime', + superdoc: 'SuperDoc', + }, + }, + }, + }, +}); diff --git a/packages/react/vitest.config.ts b/packages/react/vitest.config.ts new file mode 100644 index 0000000000..e223796487 --- /dev/null +++ b/packages/react/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'happy-dom', + include: ['src/**/*.test.{ts,tsx}'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: ['src/**/*.{ts,tsx}'], + exclude: ['src/**/*.d.ts', 'src/**/*.test.{ts,tsx}'], + }, + }, + esbuild: { + jsx: 'automatic', + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b225afdbd4..f132426be9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -217,11 +217,11 @@ catalogs: specifier: ^1.33.8 version: 1.41.4 react: - specifier: 19.2.0 - version: 19.2.0 + specifier: 19.2.4 + version: 19.2.4 react-dom: - specifier: 19.2.0 - version: 19.2.0 + specifier: 19.2.4 + version: 19.2.4 rehype-parse: specifier: ^9.0.1 version: 9.0.1 @@ -510,6 +510,19 @@ importers: specifier: 'catalog:' version: 7.2.7(@types/node@25.2.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + examples/tests: + dependencies: + patch-package: + specifier: ^8.0.1 + version: 8.0.1 + devDependencies: + '@playwright/test': + specifier: ^1.55.0 + version: 1.57.0 + playwright: + specifier: ^1.55.0 + version: 1.57.0 + packages/ai: devDependencies: '@types/node': @@ -605,7 +618,7 @@ importers: version: 6.9.1 '@testing-library/react': specifier: 'catalog:' - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@testing-library/user-event': specifier: 'catalog:' version: 14.6.1(@testing-library/dom@10.4.1) @@ -629,10 +642,10 @@ importers: version: 27.3.0(canvas@3.2.0)(postcss@8.5.6) react: specifier: 'catalog:' - version: 19.2.0 + version: 19.2.4 react-dom: specifier: 'catalog:' - version: 19.2.0(react@19.2.0) + version: 19.2.4(react@19.2.4) superdoc: specifier: workspace:* version: link:../superdoc @@ -656,10 +669,10 @@ importers: version: link:.. react: specifier: 'catalog:' - version: 19.2.0 + version: 19.2.4 react-dom: specifier: 'catalog:' - version: 19.2.0(react@19.2.0) + version: 19.2.4(react@19.2.4) signature_pad: specifier: ^5.1.1 version: 5.1.3 @@ -877,6 +890,58 @@ importers: packages/preset-geometry: {} + packages/react: + dependencies: + superdoc: + specifier: '>=1.0.0' + version: 1.10.0(@hocuspocus/provider@2.15.3(y-protocols@1.0.6(yjs@13.6.19))(yjs@13.6.19))(canvas@3.2.0)(pdfjs-dist@4.3.136)(postcss@8.5.6)(typescript@5.9.3)(y-prosemirror@1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4)(y-protocols@1.0.6(yjs@13.6.19))(yjs@13.6.19))(yjs@13.6.19) + devDependencies: + '@testing-library/react': + specifier: 'catalog:' + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@types/node': + specifier: 'catalog:' + version: 22.19.2 + '@types/react': + specifier: 'catalog:' + version: 19.2.9 + '@types/react-dom': + specifier: 'catalog:' + version: 19.2.3(@types/react@19.2.9) + '@typescript-eslint/eslint-plugin': + specifier: 'catalog:' + version: 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: 'catalog:' + version: 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@vitejs/plugin-react': + specifier: 'catalog:' + version: 5.1.2(vite@7.2.7(@types/node@22.19.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + eslint: + specifier: 'catalog:' + version: 9.39.1(jiti@2.6.1) + happy-dom: + specifier: 'catalog:' + version: 20.3.4 + react: + specifier: 'catalog:' + version: 19.2.4 + react-dom: + specifier: 'catalog:' + version: 19.2.4(react@19.2.4) + typescript: + specifier: 'catalog:' + version: 5.9.3 + vite: + specifier: 'catalog:' + version: 7.2.7(@types/node@22.19.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite-plugin-dts: + specifier: 'catalog:' + version: 4.5.4(@types/node@22.19.2)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.7(@types/node@22.19.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + vitest: + specifier: 'catalog:' + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(happy-dom@20.3.4)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.0)(postcss@8.5.6))(tsx@4.21.0)(yaml@2.8.2) + packages/super-editor: dependencies: buffer-crc32: @@ -1150,7 +1215,7 @@ importers: version: 6.9.1 '@testing-library/react': specifier: 'catalog:' - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@testing-library/user-event': specifier: 'catalog:' version: 14.6.1(@testing-library/dom@10.4.1) @@ -1174,10 +1239,10 @@ importers: version: 27.3.0(canvas@3.2.0)(postcss@8.5.6) react: specifier: 'catalog:' - version: 19.2.0 + version: 19.2.4 react-dom: specifier: 'catalog:' - version: 19.2.0(react@19.2.0) + version: 19.2.4(react@19.2.4) superdoc: specifier: workspace:* version: link:../superdoc @@ -1201,10 +1266,10 @@ importers: version: link:.. react: specifier: 'catalog:' - version: 19.2.0 + version: 19.2.4 react-dom: specifier: 'catalog:' - version: 19.2.0(react@19.2.0) + version: 19.2.4(react@19.2.4) superdoc: specifier: workspace:* version: link:../../superdoc @@ -8712,11 +8777,6 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - react-dom@19.2.0: - resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} - peerDependencies: - react: ^19.2.0 - react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -8768,14 +8828,14 @@ packages: '@types/react': optional: true - react@19.2.0: - resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} - engines: {node: '>=0.10.0'} - react@19.2.3: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -9525,6 +9585,14 @@ packages: resolution: {integrity: sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==} engines: {node: '>=18'} + superdoc@1.10.0: + resolution: {integrity: sha512-3I3c5B2ja5HXEpgSesXMiRadJXoWezeYlwpwAvGr8Uh47gCT773LXbL7GrH49xTZCR0lzPvTNyrZRj0Uz+SCUw==} + peerDependencies: + '@hocuspocus/provider': ^2.13.6 + pdfjs-dist: '>=4.3.136 <=4.6.82' + y-prosemirror: ^1.3.7 + yjs: 13.6.19 + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -11906,7 +11974,6 @@ snapshots: '@rushstack/node-core-library': 5.19.1(@types/node@22.19.2) transitivePeerDependencies: - '@types/node' - optional: true '@microsoft/api-extractor-model@7.32.2(@types/node@25.2.0)': dependencies: @@ -11934,7 +12001,6 @@ snapshots: typescript: 5.8.2 transitivePeerDependencies: - '@types/node' - optional: true '@microsoft/api-extractor@7.55.2(@types/node@25.2.0)': dependencies: @@ -12800,7 +12866,6 @@ snapshots: semver: 7.5.4 optionalDependencies: '@types/node': 22.19.2 - optional: true '@rushstack/node-core-library@5.19.1(@types/node@25.2.0)': dependencies: @@ -12818,7 +12883,6 @@ snapshots: '@rushstack/problem-matcher@0.1.1(@types/node@22.19.2)': optionalDependencies: '@types/node': 22.19.2 - optional: true '@rushstack/problem-matcher@0.1.1(@types/node@25.2.0)': optionalDependencies: @@ -12836,7 +12900,6 @@ snapshots: supports-color: 8.1.1 optionalDependencies: '@types/node': 22.19.2 - optional: true '@rushstack/terminal@0.19.5(@types/node@25.2.0)': dependencies: @@ -12854,7 +12917,6 @@ snapshots: string-argv: 0.3.2 transitivePeerDependencies: - '@types/node' - optional: true '@rushstack/ts-command-line@5.1.5(@types/node@25.2.0)': dependencies: @@ -13209,12 +13271,12 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@babel/runtime': 7.28.6 '@testing-library/dom': 10.4.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.9 '@types/react-dom': 19.2.3(@types/react@19.2.9) @@ -13721,6 +13783,18 @@ snapshots: lodash: 4.17.21 minimatch: 7.4.6 + '@vitejs/plugin-react@5.1.2(vite@7.2.7(@types/node@22.19.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@babel/core': 7.28.6 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.6) + '@rolldown/pluginutils': 1.0.0-beta.53 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.2.7(@types/node@22.19.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + '@vitejs/plugin-react@5.1.2(vite@7.2.7(@types/node@25.2.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.6 @@ -16621,12 +16695,12 @@ snapshots: happy-dom@20.3.4: dependencies: - '@types/node': 22.19.2 + '@types/node': 25.2.0 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 4.5.0 whatwg-mimetype: 3.0.0 - ws: 8.18.3 + ws: 8.19.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -19987,14 +20061,14 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-dom@19.2.0(react@19.2.0): + react-dom@19.2.4(react@19.2.3): dependencies: - react: 19.2.0 + react: 19.2.3 scheduler: 0.27.0 - react-dom@19.2.4(react@19.2.3): + react-dom@19.2.4(react@19.2.4): dependencies: - react: 19.2.3 + react: 19.2.4 scheduler: 0.27.0 react-is@16.13.1: {} @@ -20035,10 +20109,10 @@ snapshots: optionalDependencies: '@types/react': 19.2.9 - react@19.2.0: {} - react@19.2.3: {} + react@19.2.4: {} + read-cache@1.0.0: dependencies: pify: 2.3.0 @@ -21161,6 +21235,30 @@ snapshots: make-asynchronous: 1.0.1 time-span: 5.1.0 + superdoc@1.10.0(@hocuspocus/provider@2.15.3(y-protocols@1.0.6(yjs@13.6.19))(yjs@13.6.19))(canvas@3.2.0)(pdfjs-dist@4.3.136)(postcss@8.5.6)(typescript@5.9.3)(y-prosemirror@1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4)(y-protocols@1.0.6(yjs@13.6.19))(yjs@13.6.19))(yjs@13.6.19): + dependencies: + '@hocuspocus/provider': 2.15.3(y-protocols@1.0.6(yjs@13.6.19))(yjs@13.6.19) + buffer-crc32: 1.0.0 + eventemitter3: 5.0.1 + jsdom: 27.3.0(canvas@3.2.0)(postcss@8.5.6) + naive-ui: 2.43.2(vue@3.5.25(typescript@5.9.3)) + pdfjs-dist: 4.3.136 + pinia: 2.3.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)) + rollup-plugin-copy: 3.5.0 + uuid: 9.0.1 + vue: 3.5.25(typescript@5.9.3) + y-prosemirror: 1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4)(y-protocols@1.0.6(yjs@13.6.19))(yjs@13.6.19) + y-websocket: 3.0.0(yjs@13.6.19) + yjs: 13.6.19 + transitivePeerDependencies: + - '@vue/composition-api' + - bufferutil + - canvas + - postcss + - supports-color + - typescript + - utf-8-validate + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -22007,6 +22105,25 @@ snapshots: - tsx - yaml + vite-plugin-dts@4.5.4(@types/node@22.19.2)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.7(@types/node@22.19.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@microsoft/api-extractor': 7.55.2(@types/node@22.19.2) + '@rollup/pluginutils': 5.3.0(rollup@4.53.3) + '@volar/typescript': 2.4.27 + '@vue/language-core': 2.2.0(typescript@5.9.3) + compare-versions: 6.1.1 + debug: 4.4.3(supports-color@5.5.0) + kolorist: 1.8.0 + local-pkg: 1.1.2 + magic-string: 0.30.21 + typescript: 5.9.3 + optionalDependencies: + vite: 7.2.7(@types/node@22.19.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + vite-plugin-dts@4.5.4(@types/node@25.2.0)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.7(@types/node@25.2.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@microsoft/api-extractor': 7.55.2(@types/node@25.2.0) @@ -22417,8 +22534,7 @@ snapshots: ws@8.18.3: {} - ws@8.19.0: - optional: true + ws@8.19.0: {} wsl-utils@0.1.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5f0f616612..ab6e07864d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,6 +4,7 @@ packages: - apps/* - packages/**/* - shared/* + - examples/* catalog: '@commitlint/cli': ^19.8.1 @@ -76,8 +77,8 @@ catalog: prosemirror-test-builder: ^1.1.1 prosemirror-transform: ^1.9.0 prosemirror-view: ^1.33.8 - react: 19.2.0 - react-dom: 19.2.0 + react: 19.2.4 + react-dom: 19.2.4 rehype-parse: ^9.0.1 rehype-remark: ^10.0.1 remark-gfm: ^4.0.1