diff --git a/examples/advanced/grading-papers-comments-annotations/.gitignore b/examples/advanced/grading-papers-comments-annotations/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/advanced/grading-papers-comments-annotations/.vscode/extensions.json b/examples/advanced/grading-papers-comments-annotations/.vscode/extensions.json new file mode 100644 index 0000000000..a7cea0b067 --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/examples/advanced/grading-papers-comments-annotations/index.html b/examples/advanced/grading-papers-comments-annotations/index.html new file mode 100644 index 0000000000..2387edcb29 --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/index.html @@ -0,0 +1,13 @@ + + + + + + + Student Portal + + +
+ + + diff --git a/examples/advanced/grading-papers-comments-annotations/package.json b/examples/advanced/grading-papers-comments-annotations/package.json new file mode 100644 index 0000000000..e11d3c67b2 --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/package.json @@ -0,0 +1,20 @@ +{ + "name": "grading-papers-comments-annotations", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "pdfjs-dist": "4.3.136", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.0.4", + "vite": "^7.0.4" + } +} diff --git a/examples/advanced/grading-papers-comments-annotations/public/favicon.svg b/examples/advanced/grading-papers-comments-annotations/public/favicon.svg new file mode 100644 index 0000000000..ad9cf9c032 --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/advanced/grading-papers-comments-annotations/public/nick.pdf b/examples/advanced/grading-papers-comments-annotations/public/nick.pdf new file mode 100755 index 0000000000..3c210fa729 Binary files /dev/null and b/examples/advanced/grading-papers-comments-annotations/public/nick.pdf differ diff --git a/examples/advanced/grading-papers-comments-annotations/public/stickers/check-mark.svg b/examples/advanced/grading-papers-comments-annotations/public/stickers/check-mark.svg new file mode 100644 index 0000000000..30e795cb6c --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/public/stickers/check-mark.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/advanced/grading-papers-comments-annotations/public/stickers/needs-improvement.svg b/examples/advanced/grading-papers-comments-annotations/public/stickers/needs-improvement.svg new file mode 100644 index 0000000000..d707221cb4 --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/public/stickers/needs-improvement.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/advanced/grading-papers-comments-annotations/public/stickers/nice.svg b/examples/advanced/grading-papers-comments-annotations/public/stickers/nice.svg new file mode 100644 index 0000000000..b3b17f9c6c --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/public/stickers/nice.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/examples/advanced/grading-papers-comments-annotations/src/App.jsx b/examples/advanced/grading-papers-comments-annotations/src/App.jsx new file mode 100644 index 0000000000..4a75320afc --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/src/App.jsx @@ -0,0 +1,245 @@ +import '@harbour-enterprises/superdoc/style.css'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import Header from './components/Header.jsx'; +import AssignmentHeader from './components/AssignmentHeader.jsx'; +import Drawer from './components/Drawer.jsx'; +import { SuperDoc } from '@harbour-enterprises/superdoc'; +import NickPDF from '/nick.pdf?url'; +import * as pdfjsLib from 'pdfjs-dist/build/pdf.mjs'; +import * as pdfjsViewer from 'pdfjs-dist/web/pdf_viewer.mjs'; + +const defaultWhiteboardOpacity = 1; +const disabledWhiteboardOpacity = 0.5; + +const App = () => { + const superdocRef = useRef(null); + const docFileRef = useRef(null); + + // UI state only (do not store SuperDoc instance in state). + const [whiteboardReady, setWhiteboardReady] = useState(false); + const [whiteboardEnabled, setWhiteboardEnabled] = useState(true); + const [activeTool, setActiveTool] = useState('select'); + + const toolButtons = useMemo(() => [ + { id: 'select', label: 'Select' }, + { id: 'text', label: 'Text' }, + { id: 'draw', label: 'Draw' }, + { id: 'erase', label: 'Erase' }, + ], []); + + const registerStickers = useCallback(() => { + const superdoc = superdocRef.current; + if (!superdoc?.whiteboard) return; + superdoc.whiteboard.register('stickers', [ + { id: 'check-mark', label: 'Check Mark', src: '/stickers/check-mark.svg', width: 40, height: 40 }, + { id: 'nice', label: 'Nice!', src: '/stickers/nice.svg', width: 40, height: 40 }, + { id: 'needs-improvement', label: 'Needs improvement', src: '/stickers/needs-improvement.svg', width: 40, height: 40 }, + ]); + }, []); + + const registerComments = useCallback(() => { + const superdoc = superdocRef.current; + if (!superdoc?.whiteboard) return; + superdoc.whiteboard.register('comments', [ + { id: 'great-job', text: 'Great job!' }, + { id: 'expand-this', text: 'Expand this' }, + { id: 'your-references', text: 'Where are your references?' }, + ]); + }, []); + + const attachEventListeners = useCallback(() => { + const superdoc = superdocRef.current; + if (!superdoc) return; + superdoc.on('whiteboard:change', (data) => { + console.log('whiteboard:change', { data }); + }); + superdoc.on('whiteboard:tool', (tool) => { + setActiveTool(tool); + }); + }, []); + + const onWhiteboardReady = useCallback((whiteboard) => { + setWhiteboardReady(true); + setActiveTool(whiteboard?.getTool?.() ?? 'select'); + registerStickers(); + registerComments(); + attachEventListeners(); + }, [attachEventListeners, registerComments, registerStickers]); + + // (Re)initialize SuperDoc with current file. + const initSuperDoc = useCallback(() => { + if (superdocRef.current?.destroy) { + superdocRef.current.destroy(); + superdocRef.current = null; + } + + const superdocInstance = new SuperDoc({ + selector: '#superdoc', + document: { data: docFileRef.current }, + toolbar: 'superdoc-toolbar', + licenseKey: 'community-and-eval-agplv3', + modules: { + comments: {}, + toolbar: { + selector: '#superdoc-toolbar', + responsiveToContainer: true, + excludeItems: [ + 'acceptTrackedChangeBySelection', + 'rejectTrackedChangeOnSelection', + 'zoom', + 'documentMode', + ], + }, + pdf: { + pdfLib: pdfjsLib, + pdfViewer: pdfjsViewer, + setWorker: true, + textLayerMode: 0, + }, + whiteboard: { + enabled: whiteboardEnabled, + }, + }, + user: { + name: 'Sarah Smith', + email: 'sarah.smith@example.com', + }, + onCommentsUpdate: (data) => { + console.log(`onCommentsUpdate:`, { data }); + }, + }); + + superdocRef.current = superdocInstance; + window.superdoc = superdocInstance; + + superdocInstance.on('whiteboard:ready', ({ whiteboard }) => { + onWhiteboardReady(whiteboard); + }); + }, [onWhiteboardReady]); + + // Load selected file and boot SuperDoc. + const handleNewFile = useCallback(async (fileName) => { + let url; + let fileType; + let fileNameStr; + + switch (fileName) { + case 'nick': + url = NickPDF; + fileType = 'application/pdf'; + fileNameStr = 'nick.pdf'; + break; + default: + return; + } + + try { + const response = await fetch(url); + const blob = await response.blob(); + const file = new File([blob], fileNameStr, { type: fileType }); + docFileRef.current = file; + initSuperDoc(); + } catch (err) { + console.error('Error fetching file:', err); + } + }, [initSuperDoc]); + + const handleToolSelect = useCallback((tool) => { + setActiveTool(tool); + superdocRef.current?.whiteboard?.setTool(tool); + }, []); + + const toggleWhiteboard = useCallback(() => { + setWhiteboardEnabled((prev) => { + const enabled = !prev; + const opacity = enabled ? defaultWhiteboardOpacity : disabledWhiteboardOpacity; + superdocRef.current?.whiteboard?.setEnabled(enabled); + superdocRef.current?.whiteboard?.setOpacity(opacity); + return enabled; + }); + }, []); + + const exportWhiteboard = useCallback(() => { + const data = superdocRef.current?.whiteboard?.getWhiteboardData(); + if (!data) return; + console.log('[Whiteboard] export', { data }); + console.log('[Whiteboard] export json:', JSON.stringify(data, null, 2)); + }, []); + + const importWhiteboard = useCallback(() => { + const json = window.prompt('Paste whiteboard JSON'); + if (!json) return; + try { + const data = JSON.parse(json); + superdocRef.current?.whiteboard?.setWhiteboardData(data); + } catch (err) { + console.error('Invalid JSON', err); + } + }, []); + + // Initial load + cleanup on unmount. + useEffect(() => { + handleNewFile('nick'); + return () => { + if (superdocRef.current?.destroy) { + superdocRef.current.destroy(); + superdocRef.current = null; + } + }; + }, [handleNewFile]); + + return ( +
+
+ +
+
+
+ + +
+
+
+

Document Viewer

+ {whiteboardReady && ( +
+
+ {toolButtons.map((tool) => ( + + ))} +
+ +
+ + + +
+
+ )} +
+
+
+
+
+
+
+ + +
+
+ ); +}; + +export default App; diff --git a/examples/advanced/grading-papers-comments-annotations/src/components/AssignmentHeader.jsx b/examples/advanced/grading-papers-comments-annotations/src/components/AssignmentHeader.jsx new file mode 100644 index 0000000000..a02583d931 --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/src/components/AssignmentHeader.jsx @@ -0,0 +1,50 @@ +const AssignmentHeader = () => { + return ( +
+

Midterm Assignment

+
+
+ + + + + + + + Due: August 16, 2026 at 11:59 PM + +
+
+ + + + + + + + Format: PDF +
+
+ + + + + Instructor: Dr. Sarah Smith +
+
+

+ Submit your midterm paper on "The Impact of Technology on Modern Education". The paper should be 2000-3000 words + and include proper citations. +

+
+ + + + + Submission Pending +
+
+ ); +}; + +export default AssignmentHeader; diff --git a/examples/advanced/grading-papers-comments-annotations/src/components/Drawer.jsx b/examples/advanced/grading-papers-comments-annotations/src/components/Drawer.jsx new file mode 100644 index 0000000000..d0dfd53c62 --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/src/components/Drawer.jsx @@ -0,0 +1,105 @@ +const Drawer = ({ onSelectFile }) => { + const onCommentDragStart = (comment, event) => { + event.dataTransfer.setData('text/plain', comment); + event.dataTransfer.setData('application/comment', comment); + event.dataTransfer.effectAllowed = 'copy'; + }; + + const onStickerDragStart = (stickerType, event) => { + event.dataTransfer.setData('text/plain', stickerType); + event.dataTransfer.setData('application/sticker', stickerType); + event.dataTransfer.effectAllowed = 'copy'; + }; + + return ( +
+
+

Document Versions

+
+ +
+

Comments

+
+
+
onCommentDragStart('great-job', event)}> + Great job! +
+
onCommentDragStart('expand-this', event)}> + Expand this +
+
onCommentDragStart('your-references', event)}> + Where are your references? +
+
+
+ +
+ + Add new comment +
+
+ +
+

Stickers

+
+
+
onStickerDragStart('check-mark', event)}> + + + + + Check Mark +
+ +
onStickerDragStart('nice', event)}> + + + + + + + + Nice! +
+ +
onStickerDragStart('needs-improvement', event)}> + + + + + + Needs Improvement +
+
+
+
+ +
+

Submissions

+
+
onSelectFile?.('nick')}> +
+
+ Nick_Bernal_version4.pdf + Latest +
+
8/6/2025 at 12:20 PM
+
Submitted by: Nick Bernal
+
+
+
onSelectFile?.('nick')}> +
+
+ Nick_Bernal_version3.pdf + Latest +
+
8/6/2025 at 12:20 PM
+
Submitted by: Nick Bernal
+
+
+
+
+
+ ); +}; + +export default Drawer; diff --git a/examples/advanced/grading-papers-comments-annotations/src/components/Header.jsx b/examples/advanced/grading-papers-comments-annotations/src/components/Header.jsx new file mode 100644 index 0000000000..2a1aa430c7 --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/src/components/Header.jsx @@ -0,0 +1,42 @@ +const Header = () => { + return ( + + ); +}; + +export default Header; diff --git a/examples/advanced/grading-papers-comments-annotations/src/main.js b/examples/advanced/grading-papers-comments-annotations/src/main.js new file mode 100644 index 0000000000..2425c0f745 --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/src/main.js @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/examples/advanced/grading-papers-comments-annotations/src/main.jsx b/examples/advanced/grading-papers-comments-annotations/src/main.jsx new file mode 100644 index 0000000000..47613f4118 --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/src/main.jsx @@ -0,0 +1,11 @@ +import { createRoot } from 'react-dom/client'; +import React from 'react'; +import './style.css'; +import App from './App.jsx'; + +const root = document.getElementById('app'); +createRoot(root).render( + + + , +); diff --git a/examples/advanced/grading-papers-comments-annotations/src/style.css b/examples/advanced/grading-papers-comments-annotations/src/style.css new file mode 100644 index 0000000000..f9962255b1 --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/src/style.css @@ -0,0 +1,711 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background-color: #f5f7fa; + color: #333; +} + +.header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 1rem 2rem; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + height: 90px; +} + +.header-content { + display: flex; + align-items: center; + justify-content: space-between; + max-width: 1200px; + margin: 0 auto; +} + +.logo { + display: flex; + align-items: center; + gap: 12px; +} + +.logo-icon { + width: 40px; + height: 40px; + background: white; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; +} + +.user-info { + display: flex; + align-items: center; + gap: 12px; +} + +.user-avatar { + width: 36px; + height: 36px; + background: rgba(255,255,255,0.2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; +} + +.user-switcher { + position: relative; + cursor: pointer; +} + +.user-dropdown { + position: absolute; + top: 100%; + right: 0; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + min-width: 200px; + display: none; + z-index: 1000; + margin-top: 8px; +} + +.user-dropdown.show { + display: block; +} + +.user-option { + padding: 12px 16px; + border-bottom: 1px solid #e2e8f0; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.user-option:last-child { + border-bottom: none; +} + +.user-option:hover { + background-color: #f7fafc; +} + +.user-option.active { + background-color: #ebf8ff; +} + +.user-option-name { + font-weight: 500; + color: #2d3748; +} + +.user-option-email { + font-size: 0.75rem; + color: #666; + margin-top: 2px; +} + +.user-option-role { + font-size: 0.75rem; + color: #667eea; + margin-top: 2px; +} + +/* Avatar styling for different users */ +.user-avatar.professor { + background: rgb(255 131 249 / 78%); +} + + +.container { + max-width: 1300px; + margin: 0 auto; + padding: 2rem; +} + +.assignment-header { + background: white; + border-radius: 12px; + padding: 2rem; + margin-bottom: 2rem; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); +} + +.assignment-title { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 0.5rem; + color: #2d3748; +} + +.assignment-meta { + display: flex; + gap: 2rem; + margin-bottom: 1rem; + flex-wrap: wrap; +} + +.meta-item { + display: flex; + align-items: center; + gap: 0.5rem; + color: #666; +} + +.due-date { + background: #fef5e7; + color: #d69e2e; + padding: 0.25rem 0.75rem; + border-radius: 20px; + font-size: 0.875rem; + font-weight: 500; +} + + +.due-date.submitted { + background: #c6f6d5; + color: #38a169; +} + +.due-date.upcoming { + background: #c6f6d5; + color: #38a169; +} + +.main-content { + display: block; + position: relative; + width: 100%; +} + +.document-viewer { + background: white; + border-radius: 12px; + overflow: visible; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); + width: 100%; + position: relative; +} + +.viewer-header { + position: sticky; + top: 0; + z-index: 5; + background: #fff; + padding: 1rem 1.5rem; + border-bottom: 1px solid #e2e8f0; + display: flex; + justify-content: space-between; + align-items: center; +} + +.sidebar { + height: calc(100vh - 90px); + background: #f5f7fa; + display: flex; + flex-direction: column; + gap: 1.5rem; + padding: 2rem 1.5rem; + box-shadow: -4px 0 20px rgba(0,0,0,0.1); + transition: right 0.3s ease; + z-index: 1000; + overflow-y: auto; +} + +.sidebar.open { + right: 0; +} + +.sidebar-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid #e2e8f0; +} + +.sidebar-title { + font-size: 1.25rem; + font-weight: 600; + color: #2d3748; +} + +.close-drawer-btn { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: #666; + padding: 0.5rem; + border-radius: 4px; + transition: all 0.2s ease; +} + +.close-drawer-btn:hover { + background: #e2e8f0; + color: #2d3748; +} + +.open-drawer-btn { + position: fixed; + top: 50%; + right: 20px; + transform: translateY(-50%); + background: #667eea; + color: white; + border: none; + border-radius: 50%; + width: 50px; + height: 50px; + cursor: pointer; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); + transition: all 0.2s ease; + z-index: 999; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; +} + +.open-drawer-btn:hover { + background: #5a67d8; + transform: translateY(-50%) scale(1.05); +} + +.open-drawer-btn.hidden { + opacity: 0; + pointer-events: none; +} + +.download-btn { + background: #48bb78; + color: white; + border: none; + border-radius: 6px; + padding: 0.5rem 1rem; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + display: none; + align-items: center; + gap: 0.5rem; + transition: all 0.2s ease; +} + +.download-btn:hover { + background: #38a169; +} + +.download-btn.visible { + display: flex; +} + +.card { + background: white; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); +} + +.card-title { + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 1rem; + color: #2d3748; +} + +.upload-area { + border: 2px dashed #cbd5e0; + border-radius: 8px; + padding: 2rem; + text-align: center; + transition: all 0.3s ease; + cursor: pointer; +} + +.upload-area:hover { + border-color: #667eea; + background-color: #f7fafc; +} + +.upload-area.dragover { + border-color: #667eea; + background-color: #ebf8ff; +} + +.upload-icon { + width: 48px; + height: 48px; + margin: 0 auto 1rem; + background: #e2e8f0; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.file-input { + display: none; +} + +.btn { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 8px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 0.5rem; + justify-content: center; +} + +.btn-primary { + background: #667eea; + color: white; +} + +.btn-primary:hover { + background: #5a67d8; +} + +.btn-secondary { + background: #e2e8f0; + color: #4a5568; +} + +.btn-secondary:hover { + background: #cbd5e0; +} + + +.btn-success { + background: #48bb78; + color: white; +} + +.btn-success:hover { + background: #38a169; +} + +.version-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.version-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem; + background: #f7fafc; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.version-item:hover { + background: #edf2f7; +} + +.version-item.active { + background: #ebf8ff; + border: 1px solid #667eea; +} + +.version-info { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.version-name { + font-weight: 500; + font-size: 0.875rem; +} + +.version-date { + font-size: 0.75rem; + color: #666; +} + +.version-author { + font-size: 0.75rem; + color: #667eea; + font-style: italic; +} + +.version-latest-label { + background: #48bb78; + color: white; + font-size: 0.6rem; + font-weight: 600; + padding: 2px 6px; + border-radius: 10px; + letter-spacing: 0.5px; + margin-left: 0.5rem; +} + +.version-download-btn { + background: none; + border: none; + color: #667eea; + cursor: pointer; + padding: 0.25rem; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + opacity: 0.7; +} + +.version-download-btn:hover { + background: #f0f4ff; + color: #5a67d8; + opacity: 1; +} + +.version-badge { + padding: 0.25rem 0.5rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 500; +} + + +.empty-state { + text-align: center; + padding: 3rem; + color: #666; +} + +.empty-icon { + width: 64px; + height: 64px; + margin: 0 auto 1rem; + background: #e2e8f0; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +#superdoc-toolbar { + border-bottom: 1px solid #e2e8f0; +} + +#superdoc { + display: flex; + justify-content: center; + background-color: white; + overflow: visible; + min-height: 600px; +} + +.submission-status { + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 1rem; + padding: 1rem; + border-radius: 8px; + font-weight: 500; +} + +.status-pending { + background: #fef5e7; + color: #d69e2e; +} + +.status-submitted { + background: #c6f6d5; + color: #38a169; +} + +@media (max-width: 768px) { + .assignment-meta { + flex-direction: column; + gap: 1rem; + } + + .container { + padding: 1rem; + } + + .sidebar { + width: 100vw; + right: -100vw; + } + + .open-drawer-btn { + right: 15px; + width: 45px; + height: 45px; + } +} + +.superdoc__right-sidebar { + background-color: transparent !important; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.comment-entry { + display: flex; + padding: 4px 8px; + border-radius: 8px; + margin: 5px 0; + border: 1px solid #DBDBDB; + user-select: none; +} +.comment-entry:hover { + background-color: #f7fafc; + border: 1px solid #ABABAB; + cursor: pointer; +} + +.add-new-comment { + padding: 1rem; + border-top: 1px solid #DBDBDB; + text-align: right; + color: #003a78; + font-size: 14px; +} + +.stickers-container { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.sticker-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem; + border-radius: 6px; + cursor: grab; + transition: background-color 0.2s; +} + +.sticker-item:hover { + background-color: #f8f9fa; +} + +.sticker-item:active { + cursor: grabbing; +} + +.sticker-svg { + flex-shrink: 0; + filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1)); +} + +.sticker-label { + font-size: 14px; + color: #374151; + font-weight: 500; +} + +.sticker-item:hover .sticker-svg { + transform: scale(1.05); + transition: transform 0.2s; +} + +#superdoc { + margin: 10px 0; + padding: 0 0 10px 0; +} + +.app { + display: grid; + width: 100%; + height: 100%; +} + +.app-container { + display: grid; + grid-template-columns: 1fr 350px; +} + +.app-container-view { + height: calc(100vh - 90px); + overflow-y: auto; +} + +.whiteboard-toolbar { + display: flex; + align-items: center; + gap: 24px; +} + +.whiteboard-tools { + display: flex; + gap: 8px; +} + +.whiteboard-tool-btn { + padding: 6px 10px; + border: 1px solid #d1d5db; + background: #fff; + border-radius: 6px; + cursor: pointer; + font-size: 12px; +} + +.whiteboard-tool-btn.is-active { + border-color: #2563eb; + background: #dbeafe; + color: #1e3a8a; +} + +.whiteboard-controls { + display: flex; + gap: 8px; + margin-left: auto; +} + +.whiteboard-toggle, +.whiteboard-action { + padding: 6px 10px; + border: 1px solid #d1d5db; + background: #fff; + border-radius: 6px; + cursor: pointer; + font-size: 12px; +} + +.superdoc-pdf-viewer-container { + border: 1px solid #DBDBDB; + border-radius: 8px; +} + +.hiddenCanvasElement{ + position:absolute; + top:0; + left:0; + width:0; + height:0; + display:none; +} diff --git a/examples/advanced/grading-papers-comments-annotations/vite.config.js b/examples/advanced/grading-papers-comments-annotations/vite.config.js new file mode 100644 index 0000000000..8b0f57b91a --- /dev/null +++ b/examples/advanced/grading-papers-comments-annotations/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/package.json b/package.json index 79a731cf3f..2deccfd6b2 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "eslint-plugin-jsdoc": "catalog:", "happy-dom": "catalog:", "jsdom": "catalog:", + "konva": "catalog:", "lefthook": "catalog:", "prettier": "catalog:", "semantic-release": "catalog:", diff --git a/packages/super-editor/src/core/commands/list-helpers/is-list.js b/packages/super-editor/src/core/commands/list-helpers/is-list.js index 947c594473..878aa37c5d 100644 --- a/packages/super-editor/src/core/commands/list-helpers/is-list.js +++ b/packages/super-editor/src/core/commands/list-helpers/is-list.js @@ -7,5 +7,5 @@ import { getResolvedParagraphProperties } from '@extensions/paragraph/resolvedPr export const isList = (node) => !!node && node.type?.name === 'paragraph' && - getResolvedParagraphProperties(node).numberingProperties && + getResolvedParagraphProperties(node)?.numberingProperties && node.attrs?.listRendering; diff --git a/packages/superdoc/package.json b/packages/superdoc/package.json index 168127dd51..1b899e1716 100644 --- a/packages/superdoc/package.json +++ b/packages/superdoc/package.json @@ -77,6 +77,7 @@ "buffer-crc32": "catalog:", "eventemitter3": "catalog:", "jsdom": "catalog:", + "konva": "catalog:", "naive-ui": "catalog:", "pinia": "catalog:", "rollup-plugin-copy": "catalog:", diff --git a/packages/superdoc/src/SuperDoc.test.js b/packages/superdoc/src/SuperDoc.test.js index e2f99ab414..bdb5fc36cd 100644 --- a/packages/superdoc/src/SuperDoc.test.js +++ b/packages/superdoc/src/SuperDoc.test.js @@ -77,7 +77,7 @@ const stubComponent = (name) => defineComponent({ name, props: ['comment', 'autoFocus', 'parent', 'documentData', 'config', 'documentId', 'fileSource', 'state', 'options'], - emits: ['pageMarginsChange', 'ready', 'selection-change', 'page-loaded', 'bypass-selection'], + emits: ['pageMarginsChange', 'ready', 'selection-change', 'page-loaded', 'page-ready', 'bypass-selection'], setup(props, { slots }) { return () => h('div', { class: `${name}-stub` }, slots.default ? slots.default() : undefined); }, diff --git a/packages/superdoc/src/SuperDoc.vue b/packages/superdoc/src/SuperDoc.vue index 3c377a045a..8cdfab3a8b 100644 --- a/packages/superdoc/src/SuperDoc.vue +++ b/packages/superdoc/src/SuperDoc.vue @@ -22,6 +22,8 @@ import CommentsLayer from './components/CommentsLayer/CommentsLayer.vue'; import CommentDialog from '@superdoc/components/CommentsLayer/CommentDialog.vue'; import FloatingComments from '@superdoc/components/CommentsLayer/FloatingComments.vue'; import HrbrFieldsLayer from '@superdoc/components/HrbrFieldsLayer/HrbrFieldsLayer.vue'; +import WhiteboardLayer from './components/Whiteboard/WhiteboardLayer.vue'; +import { useWhiteboard } from './components/Whiteboard/use-whiteboard'; import useSelection from '@superdoc/helpers/use-selection'; import { useSuperdocStore } from '@superdoc/stores/superdoc-store'; @@ -728,7 +730,7 @@ const resetSelection = () => { toolsMenuPosition.top = null; }; -const updateSelection = ({ startX, startY, x, y, source }) => { +const updateSelection = ({ startX, startY, x, y, source, page }) => { const hasStartCoords = typeof startX === 'number' || typeof startY === 'number'; const hasEndCoords = typeof x === 'number' || typeof y === 'number'; @@ -748,6 +750,7 @@ const updateSelection = ({ startX, startY, x, y, source }) => { startX, startY, source, + page: page ?? null, }; } @@ -774,23 +777,44 @@ const updateSelection = ({ startX, startY, x, y, source }) => { } }; +const getPdfPageNumberFromEvent = (event) => { + const x = event?.clientX; + const y = event?.clientY; + if (typeof x !== 'number' || typeof y !== 'number') return null; + const elements = document.elementsFromPoint(x, y); + const pageEl = elements.find((el) => el?.classList?.contains?.('pdf-page')); + if (pageEl) { + const pageNumber = Number(pageEl.dataset?.pageNumber); + return Number.isFinite(pageNumber) ? pageNumber : null; + } + return null; +}; + const handleSelectionStart = (e) => { resetSelection(); selectionLayer.value.style.pointerEvents = 'auto'; nextTick(() => { isDragging.value = true; - const y = e.offsetY / (activeZoom.value / 100); - const x = e.offsetX / (activeZoom.value / 100); - updateSelection({ startX: x, startY: y }); + selectionLayer.value.style.pointerEvents = 'none'; + const pageNumber = getPdfPageNumberFromEvent(e); + selectionLayer.value.style.pointerEvents = 'auto'; + if (!pageNumber) return; + const layerBounds = selectionLayer.value.getBoundingClientRect(); + const zoom = activeZoom.value / 100; + const x = (e.clientX - layerBounds.left) / zoom; + const y = (e.clientY - layerBounds.top) / zoom; + updateSelection({ startX: x, startY: y, page: pageNumber }); selectionLayer.value.addEventListener('mousemove', handleDragMove); }); }; const handleDragMove = (e) => { if (!isDragging.value) return; - const y = e.offsetY / (activeZoom.value / 100); - const x = e.offsetX / (activeZoom.value / 100); + const layerBounds = selectionLayer.value.getBoundingClientRect(); + const zoom = activeZoom.value / 100; + const x = (e.clientX - layerBounds.left) / zoom; + const y = (e.clientY - layerBounds.top) / zoom; updateSelection({ x, y }); }; @@ -799,6 +823,7 @@ const handleDragEnd = (e) => { selectionLayer.value.removeEventListener('mousemove', handleDragMove); if (!selectionPosition.value) return; + const pageNumber = selectionPosition.value.page ?? getPdfPageNumberFromEvent(e); const selection = useSelection({ selectionBounds: { top: selectionPosition.value.top, @@ -806,6 +831,7 @@ const handleDragEnd = (e) => { right: selectionPosition.value.right, bottom: selectionPosition.value.bottom, }, + page: pageNumber ?? 1, documentId: documents.value[0].id, }); @@ -836,6 +862,10 @@ watch( if (proxy.$superdoc.config.useLayoutEngine !== false) { PresentationEditor.setGlobalZoom((zoom ?? 100) / 100); } + nextTick(() => { + updateWhiteboardPageSizes(); + updateWhiteboardPageOffsets(); + }); }, ); @@ -845,6 +875,24 @@ watch(getFloatingComments, () => { hasInitializedLocations.value = true; }); }); + +const { + whiteboardModuleConfig, + whiteboard, + whiteboardPages, + whiteboardPageSizes, + whiteboardPageOffsets, + whiteboardEnabled, + whiteboardOpacity, + handleWhiteboardPageReady, + updateWhiteboardPageSizes, + updateWhiteboardPageOffsets, +} = useWhiteboard({ + proxy, + layers, + documents, + modules, +});