diff --git a/.prettierignore b/.prettierignore index c534479776..6b9ae5eca4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,6 @@ examples +demos +devtools/visual-testing/results public dist node_modules @@ -29,4 +31,4 @@ tests/data tests/data/** # Markdown files -*.md \ No newline at end of file +*.md diff --git a/apps/docs/getting-started/frameworks/react.mdx b/apps/docs/getting-started/frameworks/react.mdx index 9453e32f52..0f415a2e46 100644 --- a/apps/docs/getting-started/frameworks/react.mdx +++ b/apps/docs/getting-started/frameworks/react.mdx @@ -31,6 +31,7 @@ function DocEditor({ document }) { }); return () => { + superdocRef.current?.destroy(); superdocRef.current = null; }; }, [document]); @@ -69,6 +70,7 @@ const DocEditor = forwardRef(({ document, user, onReady }, ref) => { }); return () => { + superdocRef.current?.destroy(); superdocRef.current = null; }; }, [document, user, onReady]); @@ -190,6 +192,7 @@ const DocEditor = forwardRef( superdocRef.current = new SuperDoc(config); return () => { + superdocRef.current?.destroy(); superdocRef.current = null; }; }, [document, userId, onReady]); @@ -247,6 +250,7 @@ function useSuperDoc(config) { }); return () => { + superdocRef.current?.destroy(); superdocRef.current = null; setReady(false); }; diff --git a/eslint.config.mjs b/eslint.config.mjs index eba622b284..d12c28d189 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,6 +12,7 @@ export default [ { ignores: [ '**/dist/**', + '**/dist-types/**', '**/node_modules/**', // Generated/vendor files that shouldn't be linted '**/pdfjs.js', @@ -134,6 +135,8 @@ export default [ ignore: [ '^@.*$', '^bun:.*$', // Bun built-in modules + '^superdoc$', + '^superdoc/style\\.css$', ], } ] diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index bb05658475..ed5d3fa797 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -14,14 +14,6 @@ export { createAIProvider } from './ai-actions/providers'; export * from './shared/types'; export * from './shared/utils'; export * from './shared/constants'; -export type { - AIToolActions, - SafeRecord, - SelectionRange, - SelectionSnapshot, - PlannerContextSnapshot, - BuilderPlanResult, -} from './shared/types'; export { createToolRegistry, getToolDescriptions, isValidTool } from './ai-actions/tools'; diff --git a/packages/esign/demo/server/server.js b/packages/esign/demo/server/server.js index f639d64cbf..d969bac4e9 100644 --- a/packages/esign/demo/server/server.js +++ b/packages/esign/demo/server/server.js @@ -1,14 +1,13 @@ import express from 'express'; import cors from 'cors'; -import dotenv from 'dotenv'; +import { config as dotenvConfig } from 'dotenv'; -dotenv.config(); +dotenvConfig(); const app = express(); const PORT = process.env.PORT || 3001; const SUPERDOC_SERVICES_API_KEY = process.env.SUPERDOC_SERVICES_API_KEY; -const SUPERDOC_SERVICES_BASE_URL = - process.env.SUPERDOC_SERVICES_BASE_URL || 'https://api.superdoc.dev'; +const SUPERDOC_SERVICES_BASE_URL = process.env.SUPERDOC_SERVICES_BASE_URL || 'https://api.superdoc.dev'; const CONSENT_FIELD_IDS = new Set(['consent_agreement', 'terms', 'email', '406948812']); const SIGNATURE_FIELD_ID = '789012'; const IP_ADDRESS = '127.0.0.1'; // Replace with real client IP once available @@ -85,8 +84,7 @@ const sendPdfBuffer = (res, base64, fileName, contentType = 'application/pdf') = app.post('/v1/download', async (req, res) => { try { - const { document, fields = {}, fileName = 'document.pdf', signatureMode = 'annotate' } = - req.body || {}; + const { document, fields = {}, fileName = 'document.pdf', signatureMode = 'annotate' } = req.body || {}; if (!SUPERDOC_SERVICES_API_KEY) { return res.status(500).json({ error: 'Missing SUPERDOC_SERVICES_API_KEY on the server' }); diff --git a/packages/esign/demo/src/App.css b/packages/esign/demo/src/App.css index 6f47d6c9f8..5ac150c885 100644 --- a/packages/esign/demo/src/App.css +++ b/packages/esign/demo/src/App.css @@ -1,443 +1,446 @@ * { - margin: 0; - padding: 0; - box-sizing: border-box; + margin: 0; + padding: 0; + box-sizing: border-box; } body { - font-family: system-ui, -apple-system, sans-serif; - background: #f5f5f7; + font-family: + system-ui, + -apple-system, + sans-serif; + background: #f5f5f7; } .demo { - min-height: 100vh; + min-height: 100vh; } header { - background: white; - padding: 1.5rem 2rem; - border-bottom: 1px solid #e5e5e7; + background: white; + padding: 1.5rem 2rem; + border-bottom: 1px solid #e5e5e7; } header h1 { - font-size: 1.5rem; - margin: 0; + font-size: 1.5rem; + margin: 0; } header p { - color: #86868b; - margin: 0.25rem 0 0 0; + color: #86868b; + margin: 0.25rem 0 0 0; } .layout { - display: grid; - grid-template-columns: 1fr 400px; - gap: 2rem; - max-width: 1400px; - margin: 2rem auto; - padding: 0 2rem; + display: grid; + grid-template-columns: 1fr 400px; + gap: 2rem; + max-width: 1400px; + margin: 2rem auto; + padding: 0 2rem; } /* Application Side */ .app-side { - background: white; - border-radius: 12px; - padding: 2rem; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + background: white; + border-radius: 12px; + padding: 2rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .app-header { - display: flex; - align-items: center; - gap: 1rem; - margin-bottom: 2rem; + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 2rem; } .app-header h2 { - font-size: 1.25rem; + font-size: 1.25rem; } .badge { - background: #f0f0f2; - padding: 0.25rem 0.75rem; - border-radius: 12px; - font-size: 0.75rem; - color: #86868b; + background: #f0f0f2; + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + color: #86868b; } .document-section label { - display: block; - font-weight: 600; - margin-bottom: 0.5rem; - color: #1d1d1f; + display: block; + font-weight: 600; + margin-bottom: 0.5rem; + color: #1d1d1f; } .document-container { - height: 400px; - border: 2px solid #007aff; - border-radius: 8px; - overflow: auto; - background: white; - margin-bottom: 2rem; + height: 400px; + border: 2px solid #007aff; + border-radius: 8px; + overflow: auto; + background: white; + margin-bottom: 2rem; } .form-section { - margin-bottom: 1.5rem; + margin-bottom: 1.5rem; } .form-section label { - display: block; - font-weight: 600; - margin-bottom: 0.5rem; - color: #1d1d1f; + display: block; + font-weight: 600; + margin-bottom: 0.5rem; + color: #1d1d1f; } .signature-input { - width: 100%; - padding: 0.75rem; - font-size: 1.5rem; - font-family: cursive; - border: none; - border-bottom: 2px solid #d2d2d7; - transition: border-color 0.2s; + width: 100%; + padding: 0.75rem; + font-size: 1.5rem; + font-family: cursive; + border: none; + border-bottom: 2px solid #d2d2d7; + transition: border-color 0.2s; } .signature-input:focus { - outline: none; - border-color: #007aff; + outline: none; + border-color: #007aff; } .checkbox-input { - padding: 1rem; - background: #f5f5f7; - border-radius: 8px; + padding: 1rem; + background: #f5f5f7; + border-radius: 8px; } .checkbox-input label { - display: flex; - align-items: center; - font-weight: normal; - cursor: pointer; + display: flex; + align-items: center; + font-weight: normal; + cursor: pointer; } -.checkbox-input input[type="checkbox"] { - width: 20px; - height: 20px; - margin-right: 0.75rem; +.checkbox-input input[type='checkbox'] { + width: 20px; + height: 20px; + margin-right: 0.75rem; } small { - display: block; - color: #86868b; - font-size: 0.875rem; - margin-top: 0.5rem; + display: block; + color: #86868b; + font-size: 0.875rem; + margin-top: 0.5rem; } .check { - color: #34c759; - margin-left: 0.5rem; + color: #34c759; + margin-left: 0.5rem; } .success-message { - padding: 1.5rem; - background: #34c759; - color: white; - border-radius: 8px; - text-align: center; + padding: 1.5rem; + background: #34c759; + color: white; + border-radius: 8px; + text-align: center; } .success-message button { - margin-top: 1rem; - padding: 0.75rem 1.5rem; - background: white; - color: #34c759; - border: none; - border-radius: 6px; - font-weight: 600; - cursor: pointer; + margin-top: 1rem; + padding: 0.75rem 1.5rem; + background: white; + color: #34c759; + border: none; + border-radius: 6px; + font-weight: 600; + cursor: pointer; } /* Control Side */ .control-side { - display: flex; - flex-direction: column; - gap: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; } .panel { - background: white; - border-radius: 12px; - padding: 1.5rem; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + background: white; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .panel h3 { - font-size: 1rem; - margin-bottom: 1rem; - color: #1d1d1f; + font-size: 1rem; + margin-bottom: 1rem; + color: #1d1d1f; } .panel label { - display: flex; - align-items: center; - padding: 0.5rem 0; - cursor: pointer; + display: flex; + align-items: center; + padding: 0.5rem 0; + cursor: pointer; } -.panel input[type="checkbox"] { - margin-right: 0.75rem; +.panel input[type='checkbox'] { + margin-right: 0.75rem; } -.status-list>div { - padding: 0.5rem; - margin-bottom: 0.25rem; - border-radius: 6px; - background: #f5f5f7; - color: #86868b; - transition: all 0.2s; +.status-list > div { + padding: 0.5rem; + margin-bottom: 0.25rem; + border-radius: 6px; + background: #f5f5f7; + color: #86868b; + transition: all 0.2s; } -.status-list>div.active { - background: #e8f5e9; - color: #2e7d32; +.status-list > div.active { + background: #e8f5e9; + color: #2e7d32; } .event-log { - max-height: 200px; - overflow-y: auto; + max-height: 200px; + overflow-y: auto; } .event-item { - padding: 0.5rem; - font-size: 0.875rem; - border-bottom: 1px solid #f0f0f2; - font-family: monospace; + padding: 0.5rem; + font-size: 0.875rem; + border-bottom: 1px solid #f0f0f2; + font-family: monospace; } .empty { - color: #86868b; - font-style: italic; - padding: 1rem; - text-align: center; + color: #86868b; + font-style: italic; + padding: 1rem; + text-align: center; } /* Responsive */ @media (max-width: 1024px) { - .layout { - grid-template-columns: 1fr; - } + .layout { + grid-template-columns: 1fr; + } - .control-side { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - } + .control-side { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + } } /* Updated demo/src/App.css - key changes */ .success-state { - text-align: center; - padding: 3rem 2rem; + text-align: center; + padding: 3rem 2rem; } .success-icon { - font-size: 4rem; - margin-bottom: 1rem; + font-size: 4rem; + margin-bottom: 1rem; } .success-state h3 { - font-size: 1.5rem; - margin-bottom: 0.5rem; - color: #1d1d1f; + font-size: 1.5rem; + margin-bottom: 0.5rem; + color: #1d1d1f; } .success-state p { - color: #86868b; - margin-bottom: 2rem; + color: #86868b; + margin-bottom: 2rem; } .audit-info { - background: #f5f5f7; - padding: 1.5rem; - border-radius: 8px; - margin-bottom: 2rem; - text-align: left; - max-width: 400px; - margin-left: auto; - margin-right: auto; + background: #f5f5f7; + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 2rem; + text-align: left; + max-width: 400px; + margin-left: auto; + margin-right: auto; } .audit-info p { - margin: 0.5rem 0; - font-size: 0.875rem; + margin: 0.5rem 0; + font-size: 0.875rem; } .primary-button { - padding: 1rem 2rem; - background: #007aff; - color: white; - border: none; - border-radius: 8px; - font-size: 1rem; - font-weight: 600; - cursor: pointer; + padding: 1rem 2rem; + background: #007aff; + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; } .hint { - background: #fff3cd; - color: #856404; - padding: 0.5rem; - text-align: center; - font-size: 0.875rem; - margin-top: -2px; - border-radius: 0 0 8px 8px; + background: #fff3cd; + color: #856404; + padding: 0.5rem; + text-align: center; + font-size: 0.875rem; + margin-top: -2px; + border-radius: 0 0 8px 8px; } .checkbox-list { - display: flex; - flex-direction: column; - gap: 0.75rem; + display: flex; + flex-direction: column; + gap: 0.75rem; } .checkbox-item { - display: flex; - align-items: center; - padding: 1rem; - background: #f5f5f7; - border-radius: 8px; - cursor: pointer; + display: flex; + align-items: center; + padding: 1rem; + background: #f5f5f7; + border-radius: 8px; + cursor: pointer; } .checkbox-item input { - width: 20px; - height: 20px; - margin-right: 0.75rem; + width: 20px; + height: 20px; + margin-right: 0.75rem; } .accept-button { - width: 100%; - padding: 1rem; - background: #34c759; - color: white; - border: none; - border-radius: 8px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; + width: 100%; + padding: 1rem; + background: #34c759; + color: white; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; } .accept-button:disabled { - background: #d2d2d7; - cursor: not-allowed; + background: #d2d2d7; + cursor: not-allowed; } .config-options label { - display: flex; - align-items: center; - padding: 0.5rem 0; + display: flex; + align-items: center; + padding: 0.5rem 0; } .config-options code { - background: #f5f5f7; - padding: 0.25rem 0.5rem; - border-radius: 4px; - font-size: 0.875rem; - margin-left: 0.5rem; + background: #f5f5f7; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.875rem; + margin-left: 0.5rem; } .fields-list { - display: flex; - flex-direction: column; - gap: 0.75rem; + display: flex; + flex-direction: column; + gap: 0.75rem; } .field-item { - display: flex; - flex-direction: column; - gap: 0.25rem; + display: flex; + flex-direction: column; + gap: 0.25rem; } .field-item label { - font-size: 0.75rem; - color: #86868b; + font-size: 0.75rem; + color: #86868b; } .field-item input { - padding: 0.5rem; - border: 1px solid #e5e5e7; - border-radius: 6px; - font-size: 0.875rem; - font-family: inherit; + padding: 0.5rem; + border: 1px solid #e5e5e7; + border-radius: 6px; + font-size: 0.875rem; + font-family: inherit; } .field-item input:focus { - outline: none; - border-color: #007aff; + outline: none; + border-color: #007aff; } .indicator { - display: inline-block; - width: 12px; - margin-right: 0.5rem; - color: #86868b; + display: inline-block; + width: 12px; + margin-right: 0.5rem; + color: #86868b; } .status-list .active .indicator { - color: #34c759; + color: #34c759; } header { - background: white; - padding: 1.5rem 2rem; - border-bottom: 1px solid #e5e5e7; + background: white; + padding: 1.5rem 2rem; + border-bottom: 1px solid #e5e5e7; } .header-content { - max-width: 1400px; - margin: 0 auto; - display: flex; - justify-content: space-between; - align-items: center; + max-width: 1400px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; } .header-left h1 { - font-size: 1.5rem; - margin: 0; + font-size: 1.5rem; + margin: 0; } .header-left p { - color: #86868b; - margin: 0.25rem 0 0 0; + color: #86868b; + margin: 0.25rem 0 0 0; } .header-nav { - display: flex; - gap: 2rem; + display: flex; + gap: 2rem; } .header-nav a { - color: #1d1d1f; - text-decoration: none; - font-weight: 500; - font-size: 0.95rem; - transition: color 0.2s; + color: #1d1d1f; + text-decoration: none; + font-weight: 500; + font-size: 0.95rem; + transition: color 0.2s; } .header-nav a:hover { - color: #007aff; + color: #007aff; } /* Mobile responsive */ @media (max-width: 640px) { - .header-content { - flex-direction: column; - gap: 1rem; - text-align: center; - } + .header-content { + flex-direction: column; + gap: 1rem; + text-align: center; + } - .header-nav { - gap: 1.5rem; - } + .header-nav { + gap: 1.5rem; + } } /* =========================================== @@ -462,82 +465,82 @@ header { /* Example: Card-like styling */ .superdoc-esign-container { - border: 1px solid #e5e7eb; - border-radius: 12px; - overflow: hidden; + border: 1px solid #e5e7eb; + border-radius: 12px; + overflow: hidden; } .superdoc-esign-document-viewer { - background: #f9fafb; + background: #f9fafb; } .superdoc-esign-controls { - margin-top: 0; /* Remove gap between viewer and controls */ - padding: 16px 20px; - background: #f9fafb; - border-top: 1px solid #e5e7eb; + margin-top: 0; /* Remove gap between viewer and controls */ + padding: 16px 20px; + background: #f9fafb; + border-top: 1px solid #e5e7eb; } .superdoc-esign-fields { - margin-bottom: 16px; + margin-bottom: 16px; } .superdoc-esign-actions { - gap: 12px; + gap: 12px; } /* Main layout container */ .main-layout-container { - display: flex; - gap: 24px; + display: flex; + gap: 24px; } /* Main content area */ .main-content-area { - flex: 1; - min-width: 0; + flex: 1; + min-width: 0; } /* Right sidebar */ .document-fields-sidebar { - width: 280px; - flex-shrink: 0; - padding: 16px; - background: #f9fafb; - border: 1px solid #e5e7eb; - border-radius: 8px; - align-self: flex-start; + width: 280px; + flex-shrink: 0; + padding: 16px; + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 8px; + align-self: flex-start; } .document-fields-sidebar h3 { - margin: 0 0 16px; - font-size: 14px; - font-weight: 600; - color: #374151; + margin: 0 0 16px; + font-size: 14px; + font-weight: 600; + color: #374151; } .document-fields-list { - display: flex; - flex-direction: column; - gap: 12px; + display: flex; + flex-direction: column; + gap: 12px; } .document-field { - /* Individual field styles handled inline for now */ + /* Individual field styles handled inline for now */ } /* Responsive layout - stack vertically on small screens */ @media (max-width: 768px) { - .main-layout-container { - flex-direction: column; - } - - .document-fields-sidebar { - width: 100%; - order: 2; /* Move sidebar below the main content */ - } - - .main-content-area { - order: 1; - } -} \ No newline at end of file + .main-layout-container { + flex-direction: column; + } + + .document-fields-sidebar { + width: 100%; + order: 2; /* Move sidebar below the main content */ + } + + .main-content-area { + order: 1; + } +} diff --git a/packages/esign/demo/src/CustomSignature.tsx b/packages/esign/demo/src/CustomSignature.tsx index 507b055986..568e0850de 100644 --- a/packages/esign/demo/src/CustomSignature.tsx +++ b/packages/esign/demo/src/CustomSignature.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, type FC } from 'react'; import SignaturePad from 'signature_pad'; import type { FieldComponentProps } from '@superdoc-dev/esign'; @@ -17,12 +17,7 @@ const cropSVG = (svgText: string): string => { if (bbox.width === 0 || bbox.height === 0) return svgText; const padding = 5; - const viewBox = [ - bbox.x - padding, - bbox.y - padding, - bbox.width + padding * 2, - bbox.height + padding * 2, - ].join(' '); + const viewBox = [bbox.x - padding, bbox.y - padding, bbox.width + padding * 2, bbox.height + padding * 2].join(' '); svgElement.setAttribute('viewBox', viewBox); svgElement.setAttribute('width', String(Math.ceil(bbox.width + padding * 2))); svgElement.setAttribute('height', String(Math.ceil(bbox.height + padding * 2))); @@ -54,7 +49,7 @@ const svgToPngDataUrl = (svgText: string): Promise => img.src = svgDataUrl; }); -const CustomSignature: React.FC = ({ value, onChange, isDisabled, label }) => { +const CustomSignature: FC = ({ value, onChange, isDisabled, label }) => { const [mode, setMode] = useState<'type' | 'draw'>('type'); const canvasRef = useRef(null); const signaturePadRef = useRef(null); @@ -164,12 +159,10 @@ const CustomSignature: React.FC = ({ value, onChange, isDis return (
- {label && ( - - )} + {label && }
{mode === 'type' ? ( onChange(e.target.value)} disabled={isDisabled} - placeholder="Type your full name" + placeholder='Type your full name' style={{ fontFamily: 'cursive', fontSize: '20px', @@ -234,7 +227,7 @@ const CustomSignature: React.FC = ({ value, onChange, isDis }} /> diff --git a/packages/esign/src/defaults/SignatureInput.tsx b/packages/esign/src/defaults/SignatureInput.tsx index f81c1d154b..6d28067005 100644 --- a/packages/esign/src/defaults/SignatureInput.tsx +++ b/packages/esign/src/defaults/SignatureInput.tsx @@ -1,24 +1,16 @@ -import React from 'react'; +import type { FC } from 'react'; import type { FieldComponentProps } from '../types'; -export const SignatureInput: React.FC = ({ - value, - onChange, - isDisabled, - label, -}) => { +export const SignatureInput: FC = ({ value, onChange, isDisabled, label }) => { return ( -
+
{label && } onChange(e.target.value)} disabled={isDisabled} - placeholder="Type your full name" + placeholder='Type your full name' style={{ fontFamily: 'cursive', fontSize: '18px', diff --git a/packages/esign/src/defaults/SubmitButton.tsx b/packages/esign/src/defaults/SubmitButton.tsx index b16b58348e..128a2e9c65 100644 --- a/packages/esign/src/defaults/SubmitButton.tsx +++ b/packages/esign/src/defaults/SubmitButton.tsx @@ -1,13 +1,8 @@ -import React from 'react'; +import type { FC } from 'react'; import type { SubmitButtonProps, SubmitConfig } from '../types'; export const createSubmitButton = (config?: SubmitConfig) => { - const Component: React.FC = ({ - onClick, - isValid, - isDisabled, - isSubmitting, - }) => { + const Component: FC = ({ onClick, isValid, isDisabled, isSubmitting }) => { const label = config?.label || 'Submit'; const disabled = !isValid || isDisabled || isSubmitting; @@ -32,7 +27,7 @@ export const createSubmitButton = (config?: SubmitConfig) => { transition: 'opacity 0.2s ease', }} > - {isSubmitting && } + {isSubmitting && } {isSubmitting ? 'Submitting...' : label} ); diff --git a/packages/esign/tsconfig.json b/packages/esign/tsconfig.json index 8a5d6056c0..19776f943e 100644 --- a/packages/esign/tsconfig.json +++ b/packages/esign/tsconfig.json @@ -1,29 +1,20 @@ { - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], - "types": ["vitest/globals"], - "jsx": "react-jsx", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "moduleResolution": "bundler", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "dist", - "rootDir": "src" - }, - "include": [ - "src" - ], - "exclude": [ - "node_modules", - "dist" - ] -} \ No newline at end of file + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vitest/globals"], + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "bundler", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/layout-engine/pm-adapter/src/cache.test.ts b/packages/layout-engine/pm-adapter/src/cache.test.ts index 95296dbaf8..934af3f996 100644 --- a/packages/layout-engine/pm-adapter/src/cache.test.ts +++ b/packages/layout-engine/pm-adapter/src/cache.test.ts @@ -8,10 +8,7 @@ describe('shiftBlockPositions', () => { const block: ParagraphBlock = { kind: 'paragraph', id: 'p1', - runs: [ - { text: 'hello', pmStart: 10, pmEnd: 15 } as Run, - { text: 'world', pmStart: 15, pmEnd: 20 } as Run, - ], + runs: [{ text: 'hello', pmStart: 10, pmEnd: 15 } as Run, { text: 'world', pmStart: 15, pmEnd: 20 } as Run], }; const shifted = shiftBlockPositions(block, 5) as ParagraphBlock; diff --git a/packages/super-editor/src/core/super-converter/v2/importer/footnoteReferenceImporter.js b/packages/super-editor/src/core/super-converter/v2/importer/footnoteReferenceImporter.js index 677b3340e0..babb7ffbb1 100644 --- a/packages/super-editor/src/core/super-converter/v2/importer/footnoteReferenceImporter.js +++ b/packages/super-editor/src/core/super-converter/v2/importer/footnoteReferenceImporter.js @@ -5,4 +5,3 @@ import { translator } from '../../v3/handlers/w/footnoteReference/footnoteRefere * @type {import("./docxImporter").NodeHandlerEntry} */ export const footnoteReferenceHandlerEntity = generateV2HandlerEntity('footnoteReferenceHandler', translator); - diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/footnoteReference/attributes/w-id.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/footnoteReference/attributes/w-id.js index 7a569dd4aa..75660f9dde 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/footnoteReference/attributes/w-id.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/footnoteReference/attributes/w-id.js @@ -27,4 +27,3 @@ export const attrConfig = Object.freeze({ encode, decode, }); - diff --git a/packages/super-editor/src/extensions/footnote/index.js b/packages/super-editor/src/extensions/footnote/index.js index 9756813998..8d069ac148 100644 --- a/packages/super-editor/src/extensions/footnote/index.js +++ b/packages/super-editor/src/extensions/footnote/index.js @@ -1,2 +1 @@ export * from './footnote.js'; - diff --git a/packages/superdoc/src/core/SuperDoc.js b/packages/superdoc/src/core/SuperDoc.js index d0f8618285..0c029cba65 100644 --- a/packages/superdoc/src/core/SuperDoc.js +++ b/packages/superdoc/src/core/SuperDoc.js @@ -42,6 +42,9 @@ export class SuperDoc extends EventEmitter { /** @type {boolean} */ #destroyed = false; + /** @type {HTMLDivElement | null} */ + #mountWrapper = null; + /** @type {string} */ version; @@ -134,10 +137,21 @@ export class SuperDoc extends EventEmitter { */ constructor(config) { super(); - this.#init(config); + + if (!config.selector) { + throw new Error('SuperDoc: selector is required'); + } + + const container = typeof config.selector === 'string' ? document.querySelector(config.selector) : config.selector; + + if (!(container instanceof HTMLElement)) { + throw new Error('SuperDoc: selector must be a valid CSS selector string or DOM element'); + } + + this.#init(config, container); } - async #init(config) { + async #init(config, container) { this.config = { ...this.config, ...config, @@ -243,11 +257,13 @@ export class SuperDoc extends EventEmitter { this.activeEditor = null; this.comments = []; - if (!this.config.selector) { - throw new Error('SuperDoc: selector is required'); - } - - this.app.mount(this.config.selector); + // Mount Vue into a child wrapper element instead of directly on the user's + // container. This prevents conflicts with host frameworks (React, Angular) + // that manage the container's DOM. See SD-1832. + this.#mountWrapper = document.createElement('div'); + this.#mountWrapper.style.display = 'contents'; + container.appendChild(this.#mountWrapper); + this.app.mount(this.#mountWrapper); // Required editors this.readyEditors = 0; @@ -1125,6 +1141,12 @@ export class SuperDoc extends EventEmitter { this.removeAllListeners(); delete this.app.config.globalProperties.$config; delete this.app.config.globalProperties.$superdoc; + + // Remove the internal wrapper element from the user's container + if (this.#mountWrapper) { + this.#mountWrapper.remove(); + this.#mountWrapper = null; + } } /** diff --git a/packages/superdoc/src/core/SuperDoc.test.js b/packages/superdoc/src/core/SuperDoc.test.js index 8024a3f6e1..51a9a84d77 100644 --- a/packages/superdoc/src/core/SuperDoc.test.js +++ b/packages/superdoc/src/core/SuperDoc.test.js @@ -210,7 +210,10 @@ describe('SuperDoc core', () => { await flushMicrotasks(); expect(createVueAppMock).toHaveBeenCalled(); - expect(app.mount).toHaveBeenCalledWith('#host'); + // Vue mounts on a child wrapper element inside the user's container (SD-1832) + const mountArg = app.mount.mock.calls[0][0]; + expect(mountArg).toBeInstanceOf(HTMLDivElement); + expect(mountArg.parentElement).toBe(document.querySelector('#host')); expect(superdocStore.init).toHaveBeenCalledWith(instance.config); expect(instance.config.documents).toHaveLength(1); expect(instance.config.documents[0]).toMatchObject({ type: DOCX, url: 'https://example.com/doc.docx' }); @@ -518,6 +521,121 @@ describe('SuperDoc core', () => { expect(instance.listenerCount('ready')).toBe(0); }); + it('mounts Vue on a wrapper element inside the user container', async () => { + const { app } = createAppHarness(); + const instance = new SuperDoc({ + selector: '#host', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {} }, + colors: ['red'], + user: { name: 'Jane', email: 'jane@example.com' }, + }); + await flushMicrotasks(); + + const host = document.querySelector('#host'); + const mountArg = app.mount.mock.calls[0][0]; + + // Vue should mount on a child wrapper, not the user's container + expect(mountArg).toBeInstanceOf(HTMLDivElement); + expect(mountArg.parentElement).toBe(host); + }); + + it('removes wrapper element on destroy', async () => { + const { app } = createAppHarness(); + const instance = new SuperDoc({ + selector: '#host', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {} }, + colors: ['red'], + user: { name: 'Jane', email: 'jane@example.com' }, + }); + await flushMicrotasks(); + + const host = document.querySelector('#host'); + expect(host.children.length).toBe(1); + + instance.destroy(); + + expect(host.children.length).toBe(0); + expect(app.unmount).toHaveBeenCalled(); + }); + + it('allows re-mounting after destroy (React StrictMode pattern)', async () => { + const { app } = createAppHarness(); + + // First mount + const instance1 = new SuperDoc({ + selector: '#host', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {} }, + colors: ['red'], + user: { name: 'Jane', email: 'jane@example.com' }, + }); + await flushMicrotasks(); + + const host = document.querySelector('#host'); + expect(host.children.length).toBe(1); + + // Destroy (simulates React cleanup) + instance1.destroy(); + expect(host.children.length).toBe(0); + + // Re-mount (simulates React re-render) + const { app: app2 } = createAppHarness(); + const instance2 = new SuperDoc({ + selector: '#host', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {} }, + colors: ['red'], + user: { name: 'Jane', email: 'jane@example.com' }, + }); + await flushMicrotasks(); + + // Second mount should work without errors + expect(app2.mount).toHaveBeenCalled(); + const mountArg = app2.mount.mock.calls[0][0]; + expect(mountArg.parentElement).toBe(host); + expect(host.children.length).toBe(1); + }); + + it('mounts Vue on wrapper when selector is a DOM element', async () => { + const { app } = createAppHarness(); + const host = document.querySelector('#host'); + + const instance = new SuperDoc({ + selector: host, + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {} }, + colors: ['red'], + user: { name: 'Jane', email: 'jane@example.com' }, + }); + await flushMicrotasks(); + + const mountArg = app.mount.mock.calls[0][0]; + expect(mountArg).toBeInstanceOf(HTMLDivElement); + expect(mountArg.parentElement).toBe(host); + }); + + it('throws when selector does not match any DOM element', () => { + createAppHarness(); + expect( + () => + new SuperDoc({ + selector: '#nonexistent', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {} }, + colors: ['red'], + user: { name: 'Jane', email: 'jane@example.com' }, + }), + ).toThrow('SuperDoc: selector must be a valid CSS selector string or DOM element'); + }); + it('prevents app mounting if destroy is called during async init', async () => { const { app } = createAppHarness();