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 ( <> - Export Final - editorRef.current.setMode('suggesting')}> - Review Mode - - Download DOCX + Review Mode + 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 && ( <> Export - > )} - > + + ); +} +``` + +### 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 ( + + setCurrentDoc(doc1)}>Document 1 + setCurrentDoc(doc2)}>Document 2 + + ); } ``` -## 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 + setMode('viewing')}>View + +``` + +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 && ( + <> + + setMode('editing')} + className={`px-3 py-1.5 text-sm font-medium transition-colors ${ + mode === 'editing' + ? 'bg-zinc-900 text-white' + : 'text-zinc-600 hover:text-zinc-900' + } rounded-l-md`} + > + Edit + + setMode('viewing')} + className={`px-3 py-1.5 text-sm font-medium transition-colors ${ + mode === 'viewing' + ? 'bg-zinc-900 text-white' + : 'text-zinc-600 hover:text-zinc-900' + } rounded-r-md`} + > + View + + + + + Export DOCX + + > + )} + + + + {/* Main Content */} + + {!file ? ( + /* File Upload UI */ + + + + + + + Upload a DOCX file + + + Click to browse or drag and drop + + + + + ) : ( + /* 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 +