SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
1 h agolast push
JavaScript 56.7% TypeScript 42.6%

feat: Excel/Word edit, PDF annotations, FileCard redesign & AI fixes

UI improvements:
- ExcelPreview: white Excel-like design (green tabs, gray headers), inline cell editing, XLSX save
- WordPreview: Tiptap rich text editor for edit mode, html-docx-js save as .docx
- PdfPreview: persistent SVG annotation overlay (highlight/note/pen) saved to DB
- FileCard: tag color dots, favorite star, colored bg per file type, relative dates

Layout fixes:
- AppShell: h-screen overflow-hidden for sticky sidebar, h-full on columns
- PreviewEngine: lg:left-64 offset in fullscreen to clear sidebar
- WordTiptapEditor: absolute inset-0 layout so toolbar stays visible in fullscreen
- Navigation arrows hidden while editing

AI fixes:
- /api/ai/files: fix session reading (NextResponse.next → new NextResponse), add try/catch
- /api/ai/chat: buildFileBlocks now async; adds Word (mammoth) and Excel (xlsx) extraction
- AiPanel: MarkdownContent component with remark-gfm + react-syntax-highlighter (Prism)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
simon-pierre boucher committed 7 mo ago (Feb 21, 2026) parent 9992f40

16 changed files +2,677 −196

modified app/api/ai/chat/route.ts +39 −5
@@ -29,12 +29,12 @@ async function findMentionedFile(message: string, userId?: string) {
29 29 }
30 30
31 31 // Build content blocks for a file to send to Claude
32 −function buildFileBlocks(file: {
32 +async function buildFileBlocks(file: {
33 33 name: string;
34 34 mimeType: string;
35 35 storagePath: string;
36 36 ocrText: string | null;
37 −}): Anthropic.ContentBlockParam[] {
37 +}): Promise<Anthropic.ContentBlockParam[]> {
38 38 const filePath = getFilePath(file.storagePath);
39 39 if (!existsSync(filePath)) return [];
40 40
@@ -50,7 +50,7 @@ function buildFileBlocks(file: {
50 50 ];
51 51 }
52 52
53 − // Images — send as image block
53 + // Images — send as image block (base64)
54 54 if (file.mimeType.startsWith("image/")) {
55 55 const supported = ["image/jpeg", "image/png", "image/gif", "image/webp"];
56 56 if (supported.includes(file.mimeType)) {
@@ -68,6 +68,38 @@ function buildFileBlocks(file: {
68 68 }
69 69 }
70 70
71 + // Word documents — extract text via mammoth
72 + if (
73 + file.mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
74 + file.mimeType === "application/msword"
75 + ) {
76 + const mammoth = await import("mammoth");
77 + const result = await mammoth.extractRawText({ path: filePath });
78 + const text = result.value.substring(0, 50000);
79 + if (text.trim()) {
80 + return [{ type: "text", text: `Contenu du document Word "${file.name}":\n${text}` }];
81 + }
82 + }
83 +
84 + // Excel spreadsheets — extract as CSV via SheetJS
85 + if (
86 + file.mimeType === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
87 + file.mimeType === "application/vnd.ms-excel"
88 + ) {
89 + const XLSX = await import("xlsx");
90 + const workbook = XLSX.readFile(filePath);
91 + const parts: string[] = [];
92 + for (const sheetName of workbook.SheetNames) {
93 + const sheet = workbook.Sheets[sheetName];
94 + const csv = XLSX.utils.sheet_to_csv(sheet);
95 + parts.push(`[Feuille: ${sheetName}]\n${csv.substring(0, 15000)}`);
96 + }
97 + const content = parts.join("\n\n").substring(0, 50000);
98 + if (content.trim()) {
99 + return [{ type: "text", text: `Contenu du tableur Excel "${file.name}":\n${content}` }];
100 + }
101 + }
102 +
71 103 // Text / code / markdown / JSON — read directly
72 104 if (
73 105 file.mimeType.startsWith("text/") ||
@@ -87,7 +119,9 @@ function buildFileBlocks(file: {
87 119 { type: "text", text: `Texte extrait du fichier "${file.name}" (OCR):\n${file.ocrText}` },
88 120 ];
89 121 }
90 − } catch {}
122 + } catch (err) {
123 + console.error("[chat] buildFileBlocks error:", err);
124 + }
91 125 return [];
92 126 }
93 127
@@ -112,7 +146,7 @@ ${fileContext}`;
112 146 // Build user message — attach file content if a file is mentioned
113 147 let userContent: Anthropic.ContentBlockParam[];
114 148 if (mentionedFile) {
115 − const fileBlocks = buildFileBlocks(mentionedFile);
149 + const fileBlocks = await buildFileBlocks(mentionedFile);
116 150 if (fileBlocks.length > 0) {
117 151 userContent = [...fileBlocks, { type: "text", text: message }];
118 152 } else {
modified app/api/ai/files/route.ts +27 −23
@@ -4,7 +4,7 @@ import { SessionData, sessionOptions } from "@/lib/session";
4 4 import { prisma } from "@/lib/prisma";
5 5
6 6 export async function GET(request: NextRequest) {
7 − const response = NextResponse.next();
7 + const response = new NextResponse();
8 8 const session = await getIronSession<SessionData>(request, response, sessionOptions);
9 9 if (!session.isLoggedIn) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
10 10
@@ -16,26 +16,30 @@ export async function GET(request: NextRequest) {
16 16 ? { OR: [{ userId: session.userId as string }, { userId: null as null }] }
17 17 : {};
18 18
19 − const files = await prisma.file.findMany({
20 − where: {
21 − AND: [
22 − ownerFilter,
23 − ...(q ? [{ name: { contains: q } }] : []),
24 − ],
25 − },
26 − select: {
27 − id: true,
28 − name: true,
29 − mimeType: true,
30 − size: true,
31 − aiTags: true,
32 − aiDescription: true,
33 − ocrText: true,
34 − folder: { select: { name: true } },
35 − },
36 − take: 30,
37 − orderBy: { createdAt: "desc" },
38 − });
39 −
40 − return NextResponse.json(files);
19 + try {
20 + const files = await prisma.file.findMany({
21 + where: {
22 + AND: [
23 + ownerFilter,
24 + ...(q ? [{ name: { contains: q } }] : []),
25 + ],
26 + },
27 + select: {
28 + id: true,
29 + name: true,
30 + mimeType: true,
31 + size: true,
32 + aiTags: true,
33 + aiDescription: true,
34 + ocrText: true,
35 + folder: { select: { name: true } },
36 + },
37 + take: 30,
38 + orderBy: { createdAt: "desc" },
39 + });
40 + return NextResponse.json(files);
41 + } catch (err) {
42 + console.error("[ai/files] Prisma error:", err);
43 + return NextResponse.json({ error: "Failed to fetch files" }, { status: 500 });
44 + }
41 45 }
modified components/ai/AiPanel.tsx +57 −9
@@ -23,6 +23,9 @@ import {
23 23 AlertCircle,
24 24 } from "lucide-react";
25 25 import ReactMarkdown from "react-markdown";
26 +import remarkGfm from "remark-gfm";
27 +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
28 +import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
26 29
27 30 interface AiResult {
28 31 fileId: string;
@@ -72,6 +75,57 @@ const mimeIcon = (mime: string) => {
72 75 const inputClasses =
73 76 "w-full bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg px-4 py-3 text-gray-900 dark:text-white text-sm focus:outline-none focus:border-purple-400 dark:focus:border-purple-500 placeholder-gray-400 dark:placeholder-gray-500";
74 77
78 +// Markdown renderer with code syntax highlighting
79 +function MarkdownContent({ content }: { content: string }) {
80 + return (
81 + <div className="prose prose-sm dark:prose-invert max-w-none prose-p:my-1 prose-ul:my-1.5 prose-ol:my-1.5 prose-li:my-0.5 prose-headings:my-2 prose-blockquote:my-1 prose-pre:p-0 prose-pre:bg-transparent prose-pre:my-2 prose-code:before:content-none prose-code:after:content-none prose-table:text-xs prose-th:py-1 prose-td:py-1">
82 + <ReactMarkdown
83 + remarkPlugins={[remarkGfm]}
84 + components={{
85 + code({ className, children, ...props }) {
86 + const match = /language-(\w+)/.exec(className || "");
87 + const codeStr = String(children).replace(/\n$/, "");
88 + const isBlock = match || codeStr.includes("\n");
89 + if (isBlock) {
90 + return (
91 + <SyntaxHighlighter
92 + style={oneDark as Record<string, React.CSSProperties>}
93 + language={match?.[1] || "text"}
94 + PreTag="div"
95 + className="!rounded-lg !text-xs !my-0 !bg-gray-900"
96 + customStyle={{ margin: 0, borderRadius: "0.5rem", fontSize: "0.75rem" }}
97 + >
98 + {codeStr}
99 + </SyntaxHighlighter>
100 + );
101 + }
102 + return (
103 + <code
104 + className="bg-gray-200 dark:bg-white/15 px-1.5 py-0.5 rounded text-[11px] font-mono text-purple-700 dark:text-purple-300"
105 + {...props}
106 + >
107 + {children}
108 + </code>
109 + );
110 + },
111 + pre({ children }) {
112 + return <>{children}</>;
113 + },
114 + a({ href, children }) {
115 + return (
116 + <a href={href} target="_blank" rel="noopener noreferrer" className="text-purple-500 underline underline-offset-2 hover:text-purple-400">
117 + {children}
118 + </a>
119 + );
120 + },
121 + }}
122 + >
123 + {content}
124 + </ReactMarkdown>
125 + </div>
126 + );
127 +}
128 +
75 129 export default function AiPanel() {
76 130 const [open, setOpen] = useState(false);
77 131 const [tab, setTab] = useState<Tab>("chat");
@@ -384,9 +438,7 @@ export default function AiPanel() {
384 438 }`}
385 439 >
386 440 {msg.role === "assistant" ? (
387 − <div className="prose prose-sm dark:prose-invert max-w-none prose-p:my-1 prose-ul:my-1 prose-li:my-0.5 prose-headings:my-1">
388 − <ReactMarkdown>{msg.content}</ReactMarkdown>
389 − </div>
441 + <MarkdownContent content={msg.content} />
390 442 ) : (
391 443 msg.content
392 444 )}
@@ -401,9 +453,7 @@ export default function AiPanel() {
401 453 </div>
402 454 <div className="max-w-[82%] px-3 py-2.5 rounded-2xl rounded-tl-sm bg-gray-100 dark:bg-white/10 text-sm text-gray-900 dark:text-gray-100 leading-relaxed">
403 455 {streamingContent ? (
404 − <div className="prose prose-sm dark:prose-invert max-w-none prose-p:my-1">
405 − <ReactMarkdown>{streamingContent}</ReactMarkdown>
406 − </div>
456 + <MarkdownContent content={streamingContent} />
407 457 ) : (
408 458 <span className="flex items-center gap-1">
409 459 {[0, 150, 300].map((d) => (
@@ -729,9 +779,7 @@ export default function AiPanel() {
729 779 {analyserResult && (
730 780 <div className="bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg p-3.5">
731 781 <p className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-2">{analyserResult.type}</p>
732 − <div className="prose prose-sm dark:prose-invert max-w-none prose-p:my-1">
733 − <ReactMarkdown>{analyserResult.content}</ReactMarkdown>
734 − </div>
782 + <MarkdownContent content={analyserResult.content} />
735 783 </div>
736 784 )}
737 785 </div>
modified components/files/FileCard.tsx +120 −53
@@ -11,19 +11,12 @@ import {
11 11 File,
12 12 Archive,
13 13 MoreVertical,
14 + Star,
14 15 } from "lucide-react";
15 −import { formatFileSize, formatDate, getFileIcon } from "@/lib/utils";
16 +import { formatFileSize, getFileIcon } from "@/lib/utils";
16 17 import { useState, useRef, useEffect } from "react";
17 18 import FileActions from "./FileActions";
18 −
19 −interface FileData {
20 − id: string;
21 − name: string;
22 − size: number;
23 − mimeType: string;
24 − createdAt: string;
25 − folderId?: string | null;
26 −}
19 +import type { FileData } from "@/types/files";
27 20
28 21 interface FileCardProps {
29 22 file: FileData;
@@ -46,25 +39,59 @@ const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
46 39 };
47 40
48 41 const iconColorMap: Record<string, string> = {
49 − image: "text-emerald-400 dark:text-emerald-500",
50 − video: "text-purple-400 dark:text-purple-500",
51 − audio: "text-pink-400 dark:text-pink-500",
52 − pdf: "text-red-400 dark:text-red-500",
53 − code: "text-amber-400 dark:text-amber-500",
54 − spreadsheet: "text-green-500 dark:text-green-400",
55 − archive: "text-orange-400 dark:text-orange-500",
56 − document: "text-blue-500 dark:text-blue-400",
57 − presentation: "text-orange-500 dark:text-orange-400",
58 − file: "text-gray-400 dark:text-gray-500",
42 + image: "text-emerald-500",
43 + video: "text-purple-500",
44 + audio: "text-pink-500",
45 + pdf: "text-red-500",
46 + code: "text-amber-500",
47 + spreadsheet: "text-green-600",
48 + archive: "text-orange-500",
49 + document: "text-blue-500",
50 + presentation: "text-orange-500",
51 + file: "text-gray-400",
52 +};
53 +
54 +// Soft tinted bg per file type (light + dark)
55 +const bgTintMap: Record<string, string> = {
56 + image: "bg-emerald-50 dark:bg-emerald-950/30",
57 + video: "bg-purple-50 dark:bg-purple-950/30",
58 + audio: "bg-pink-50 dark:bg-pink-950/30",
59 + pdf: "bg-red-50 dark:bg-red-950/30",
60 + code: "bg-amber-50 dark:bg-amber-950/30",
61 + spreadsheet: "bg-green-50 dark:bg-green-950/30",
62 + archive: "bg-orange-50 dark:bg-orange-950/30",
63 + document: "bg-blue-50 dark:bg-blue-950/30",
64 + presentation: "bg-orange-50 dark:bg-orange-950/30",
65 + file: "bg-gray-50 dark:bg-white/[0.03]",
59 66 };
60 67
68 +function formatRelativeDate(dateStr: string): string {
69 + const now = Date.now();
70 + const then = new Date(dateStr).getTime();
71 + const diff = now - then;
72 + const minutes = Math.floor(diff / 60000);
73 + const hours = Math.floor(diff / 3600000);
74 + const days = Math.floor(diff / 86400000);
75 + const months = Math.floor(days / 30);
76 + if (minutes < 1) return "à l'instant";
77 + if (minutes < 60) return `il y a ${minutes}m`;
78 + if (hours < 24) return `il y a ${hours}h`;
79 + if (days < 30) return `il y a ${days}j`;
80 + if (months < 12) return `il y a ${months} mois`;
81 + return new Date(dateStr).toLocaleDateString("fr-CA", { year: "numeric", month: "short" });
82 +}
83 +
61 84 export default function FileCard({ file, onPreview, onRefresh, viewMode }: FileCardProps) {
62 85 const [showActions, setShowActions] = useState(false);
63 86 const menuRef = useRef<HTMLDivElement>(null);
64 87
65 88 const iconType = getFileIcon(file.mimeType);
66 89 const Icon = iconMap[iconType] || File;
67 − const iconColor = iconColorMap[iconType] || "text-gray-400 dark:text-gray-500";
90 + const iconColor = iconColorMap[iconType] || "text-gray-400";
91 + const bgTint = bgTintMap[iconType] || "bg-gray-50 dark:bg-white/[0.03]";
92 + const isImage = file.mimeType.startsWith("image/");
93 + const isFavorite = file.metadata?.isFavorite;
94 + const tags = file.tags ?? [];
68 95
69 96 useEffect(() => {
70 97 function handleClick(e: MouseEvent) {
@@ -76,42 +103,79 @@ export default function FileCard({ file, onPreview, onRefresh, viewMode }: FileC
76 103 return () => document.removeEventListener("mousedown", handleClick);
77 104 }, []);
78 105
79 − const isImage = file.mimeType.startsWith("image/");
106 + // Tags as color dots — max 4 visible, then +N
107 + function TagDots() {
108 + if (tags.length === 0) return null;
109 + const visible = tags.slice(0, 4);
110 + const overflow = tags.length - 4;
111 + return (
112 + <div className="flex items-center gap-1 mt-1.5 flex-wrap">
113 + {visible.map(({ tag }) => (
114 + <span
115 + key={tag.id}
116 + className="w-2.5 h-2.5 rounded-full flex-shrink-0 border border-white/20"
117 + style={{ backgroundColor: tag.color }}
118 + title={tag.name}
119 + />
120 + ))}
121 + {overflow > 0 && (
122 + <span className="text-[10px] text-gray-400 leading-none">+{overflow}</span>
123 + )}
124 + </div>
125 + );
126 + }
80 127
81 128 if (viewMode === "list") {
82 129 return (
83 130 <motion.div
84 131 initial={{ opacity: 0 }}
85 132 animate={{ opacity: 1 }}
86 − className="flex items-center gap-3 px-3 py-3.5 hover:bg-gray-100 dark:hover:bg-white/5 rounded-lg cursor-pointer group transition-colors"
133 + className="flex items-center gap-3 px-3 py-3 hover:bg-gray-100 dark:hover:bg-white/5 rounded-lg cursor-pointer group transition-colors"
87 134 onClick={() => onPreview(file)}
88 135 >
89 − <Icon className={`w-5 h-5 flex-shrink-0 ${iconColor}`} />
90 − <span className="text-sm text-gray-700 dark:text-gray-200 flex-1 truncate">{file.name}</span>
91 − <span className="text-sm text-gray-500 hidden sm:block">{formatFileSize(file.size)}</span>
92 − <span className="text-xs text-gray-500 hidden md:block">{formatDate(file.createdAt)}</span>
93 − <div className="relative" ref={menuRef}>
136 + {/* Icon */}
137 + <div className={`w-8 h-8 rounded-md flex items-center justify-center flex-shrink-0 ${bgTint}`}>
138 + <Icon className={`w-4 h-4 ${iconColor}`} />
139 + </div>
140 +
141 + {/* Name */}
142 + <span className="text-sm text-gray-700 dark:text-gray-200 flex-1 truncate min-w-0">{file.name}</span>
143 +
144 + {/* Tags dots (list) */}
145 + {tags.length > 0 && (
146 + <div className="hidden sm:flex items-center gap-1 flex-shrink-0">
147 + {tags.slice(0, 3).map(({ tag }) => (
148 + <span
149 + key={tag.id}
150 + className="w-2 h-2 rounded-full"
151 + style={{ backgroundColor: tag.color }}
152 + title={tag.name}
153 + />
154 + ))}
155 + </div>
156 + )}
157 +
158 + {isFavorite && <Star className="w-3.5 h-3.5 text-amber-400 fill-amber-400 flex-shrink-0 hidden sm:block" />}
159 + <span className="text-sm text-gray-500 hidden sm:block flex-shrink-0">{formatFileSize(file.size)}</span>
160 + <span className="text-xs text-gray-400 hidden md:block flex-shrink-0 min-w-[80px] text-right">{formatRelativeDate(file.createdAt)}</span>
161 +
162 + {/* Actions menu */}
163 + <div className="relative flex-shrink-0" ref={menuRef}>
94 164 <button
95 − onClick={(e) => {
96 − e.stopPropagation();
97 − setShowActions(!showActions);
98 − }}
165 + onClick={(e) => { e.stopPropagation(); setShowActions(!showActions); }}
99 166 className="min-w-[44px] min-h-[44px] flex items-center justify-center text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white sm:opacity-0 sm:group-hover:opacity-100 transition-opacity"
100 167 >
101 168 <MoreVertical className="w-4 h-4" />
102 169 </button>
103 170 {showActions && (
104 − <FileActions
105 − file={file}
106 − onClose={() => setShowActions(false)}
107 − onRefresh={onRefresh}
108 − />
171 + <FileActions file={file} onClose={() => setShowActions(false)} onRefresh={onRefresh} />
109 172 )}
110 173 </div>
111 174 </motion.div>
112 175 );
113 176 }
114 177
178 + // Grid view
115 179 return (
116 180 <motion.div
117 181 initial={{ opacity: 0, scale: 0.95 }}
@@ -119,8 +183,8 @@ export default function FileCard({ file, onPreview, onRefresh, viewMode }: FileC
119 183 className={`group relative bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl hover:border-gray-300 dark:hover:border-white/20 transition-colors cursor-pointer shadow-sm dark:shadow-none ${showActions ? "z-20" : ""}`}
120 184 onClick={() => onPreview(file)}
121 185 >
122 − {/* Preview thumbnail — overflow-hidden stays here for image crop only */}
123 − <div className="aspect-square flex items-center justify-center bg-gray-50 dark:bg-white/[0.02] rounded-t-xl overflow-hidden">
186 + {/* Thumbnail */}
187 + <div className={`aspect-square flex items-center justify-center rounded-t-xl overflow-hidden ${isImage ? "" : bgTint}`}>
124 188 {isImage ? (
125 189 <img
126 190 src={`/api/files/${file.id}/preview`}
@@ -129,36 +193,39 @@ export default function FileCard({ file, onPreview, onRefresh, viewMode }: FileC
129 193 loading="lazy"
130 194 />
131 195 ) : (
132 − <Icon className={`w-12 h-12 ${iconColor}`} />
196 + <Icon className={`w-14 h-14 ${iconColor}`} />
133 197 )}
134 198 </div>
135 199
136 − {/* Actions button — outside overflow-hidden so dropdown is never clipped */}
200 + {/* Favorite star — top left */}
201 + {isFavorite && (
202 + <div className="absolute top-2 left-2 z-10">
203 + <Star className="w-4 h-4 text-amber-400 fill-amber-400 drop-shadow" />
204 + </div>
205 + )}
206 +
207 + {/* Actions button — top right */}
137 208 <div className="absolute top-2 right-2 z-20" ref={menuRef}>
138 209 <button
139 − onClick={(e) => {
140 − e.stopPropagation();
141 − setShowActions(!showActions);
142 − }}
210 + onClick={(e) => { e.stopPropagation(); setShowActions(!showActions); }}
143 211 className="p-2 bg-white/80 dark:bg-black/50 rounded-lg text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white sm:opacity-0 sm:group-hover:opacity-100 transition-opacity backdrop-blur-sm min-w-[40px] min-h-[40px] flex items-center justify-center"
144 212 >
145 213 <MoreVertical className="w-4 h-4" />
146 214 </button>
147 215 {showActions && (
148 − <FileActions
149 − file={file}
150 − onClose={() => setShowActions(false)}
151 − onRefresh={onRefresh}
152 − />
216 + <FileActions file={file} onClose={() => setShowActions(false)} onRefresh={onRefresh} />
153 217 )}
154 218 </div>
155 219
156 220 {/* Info */}
157 221 <div className="p-3">
158 − <p className="text-sm text-gray-700 dark:text-gray-200 truncate">{file.name}</p>
159 − <p className="text-sm text-gray-500 mt-1">
160 − {formatFileSize(file.size)}
222 + <p className="text-sm text-gray-700 dark:text-gray-200 truncate font-medium">{file.name}</p>
223 + <p className="text-xs text-gray-500 mt-0.5 flex items-center gap-1.5">
224 + <span>{formatFileSize(file.size)}</span>
225 + <span className="text-gray-300 dark:text-gray-700">·</span>
226 + <span>{formatRelativeDate(file.createdAt)}</span>
161 227 </p>
228 + <TagDots />
162 229 </div>
163 230 </motion.div>
164 231 );
modified components/files/FileExplorer.tsx +1 −9
@@ -12,15 +12,7 @@ import SearchBar from "../ui/SearchBar";
12 12 import Button from "../ui/Button";
13 13 import BulkActions from "./BulkActions";
14 14 import { motion, AnimatePresence } from "framer-motion";
15 −
16 −interface FileData {
17 − id: string;
18 − name: string;
19 − size: number;
20 − mimeType: string;
21 − createdAt: string;
22 − folderId?: string | null;
23 −}
15 +import type { FileData } from "@/types/files";
24 16
25 17 interface FolderData {
26 18 id: string;
modified components/layout/AppShell.tsx +9 −9
@@ -15,19 +15,19 @@ export default function AppShell({ children }: AppShellProps) {
15 15 const [sidebarOpen, setSidebarOpen] = useState(false);
16 16
17 17 return (
18 − <div className="min-h-screen bg-gray-50 dark:bg-[#0a0a0a] flex transition-colors">
19 − {/* Desktop Sidebar */}
20 − <div className="hidden lg:block w-64 flex-shrink-0">
18 + <div className="h-screen overflow-hidden bg-gray-50 dark:bg-[#0a0a0a] flex transition-colors">
19 + {/* Desktop Sidebar — fixed height, scrolls independently */}
20 + <div className="hidden lg:flex flex-col w-64 flex-shrink-0 h-full overflow-y-auto">
21 21 <Sidebar />
22 22 </div>
23 23
24 24 {/* Mobile Sidebar Overlay */}
25 25 <MobileNav isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
26 26
27 − {/* Main Content */}
28 − <div className="flex-1 flex flex-col min-h-screen min-w-0">
29 − {/* Mobile Header - safe area + larger touch targets */}
30 − <div className="lg:hidden flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-white/10 bg-white dark:bg-[#0a0a0a] transition-colors safe-top">
27 + {/* Main Content — fills remaining space, scrolls independently */}
28 + <div className="flex-1 flex flex-col min-w-0 h-full overflow-hidden">
29 + {/* Mobile Header */}
30 + <div className="lg:hidden flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-white/10 bg-white dark:bg-[#0a0a0a] transition-colors safe-top shrink-0">
31 31 <button
32 32 onClick={() => setSidebarOpen(true)}
33 33 className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white p-3 -ml-1 min-w-[44px] min-h-[44px] flex items-center justify-center active:bg-gray-100 dark:active:bg-white/10 rounded-xl transition-colors"
@@ -43,8 +43,8 @@ export default function AppShell({ children }: AppShellProps) {
43 43 </div>
44 44 </div>
45 45
46 − {/* Content - with safe area bottom for notched devices */}
47 − <main className="flex-1 overflow-auto pb-safe">
46 + {/* Content */}
47 + <main className="flex-1 overflow-auto pb-safe min-h-0">
48 48 {children}
49 49 </main>
50 50 </div>
modified components/layout/Sidebar.tsx +1 −1
@@ -107,7 +107,7 @@ export default function Sidebar({ onNavigate }: SidebarProps) {
107 107 const navItems = isAdmin ? [...userNavItems, ...adminNavItems] : userNavItems;
108 108
109 109 return (
110 − <div className="h-screen flex flex-col bg-white dark:bg-[#0a0a0a] border-r border-gray-200 dark:border-white/10 transition-colors">
110 + <div className="h-full flex flex-col bg-white dark:bg-[#0a0a0a] border-r border-gray-200 dark:border-white/10 transition-colors">
111 111 {/* Logo */}
112 112 <div className="px-4 py-4 flex items-center gap-2">
113 113 <div className="w-9 h-9 rounded-xl bg-gray-100 dark:bg-white/10 flex items-center justify-center flex-shrink-0">
modified components/preview/PreviewEngine.tsx +2 −2
@@ -82,7 +82,7 @@ export default function PreviewEngine({
82 82 animate={{ opacity: 1 }}
83 83 exit={{ opacity: 0 }}
84 84 className={`fixed inset-0 z-50 flex flex-col bg-white dark:bg-[#0a0a0a] transition-colors ${
85 − fullscreen ? "" : "lg:relative lg:inset-auto lg:h-full"
85 + fullscreen ? "lg:left-64" : "lg:relative lg:inset-auto lg:h-full"
86 86 }`}
87 87 >
88 88 <PreviewToolbar
@@ -107,7 +107,7 @@ export default function PreviewEngine({
107 107 fullscreen={fullscreen}
108 108 />
109 109
110 − {files.length > 1 && onNavigate && (
110 + {files.length > 1 && onNavigate && !isEditing && (
111 111 <PreviewNavigation
112 112 files={files}
113 113 currentFile={file}
added components/preview/plugins/ExcelPreview.tsx +394 −0
@@ -0,0 +1,394 @@
1 +"use client";
2 +
3 +import { useState, useEffect, useMemo, useCallback } from "react";
4 +import { Loader2, Download, Table2, ArrowUpDown, Save, Edit3 } from "lucide-react";
5 +import type { PreviewPluginProps } from "./types";
6 +
7 +// Column index → Excel-style letter (0 → A, 25 → Z, 26 → AA …)
8 +function colLetter(n: number): string {
9 + let result = "";
10 + let idx = n;
11 + do {
12 + result = String.fromCharCode(65 + (idx % 26)) + result;
13 + idx = Math.floor(idx / 26) - 1;
14 + } while (idx >= 0);
15 + return result;
16 +}
17 +
18 +interface SheetData {
19 + name: string;
20 + rows: (string | number | boolean | null)[][];
21 + colCount: number;
22 +}
23 +
24 +interface SortConfig {
25 + col: number;
26 + dir: "asc" | "desc";
27 +}
28 +
29 +const PAGE_SIZE = 100;
30 +
31 +export default function ExcelPreview({ file, isEditing, onSave }: PreviewPluginProps) {
32 + const [sheets, setSheets] = useState<SheetData[]>([]);
33 + const [activeSheet, setActiveSheet] = useState(0);
34 + const [loading, setLoading] = useState(true);
35 + const [error, setError] = useState(false);
36 + const [saving, setSaving] = useState(false);
37 + // editData mirrors sheets data for in-memory editing
38 + const [editData, setEditData] = useState<(string | number | null)[][][]>([]);
39 +
40 + useEffect(() => {
41 + let cancelled = false;
42 +
43 + const parse = async () => {
44 + setLoading(true);
45 + setError(false);
46 + try {
47 + const res = await fetch(`/api/files/${file.id}/download`);
48 + if (!res.ok) throw new Error("Download failed");
49 + const arrayBuffer = await res.arrayBuffer();
50 +
51 + const XLSX = await import("xlsx");
52 + const workbook = XLSX.read(new Uint8Array(arrayBuffer), { type: "array", cellDates: true });
53 +
54 + const parsed: SheetData[] = workbook.SheetNames.map((name) => {
55 + const ws = workbook.Sheets[name];
56 + const raw = XLSX.utils.sheet_to_json<(string | number | boolean | null)[]>(ws, {
57 + header: 1,
58 + defval: null,
59 + blankrows: true,
60 + });
61 + const colCount = raw.reduce((max, row) => Math.max(max, row.length), 0);
62 + return { name, rows: raw as (string | number | boolean | null)[][], colCount };
63 + });
64 +
65 + if (!cancelled) {
66 + setSheets(parsed);
67 + setActiveSheet(0);
68 + // Init edit data as string copies
69 + setEditData(parsed.map((s) => s.rows.map((row) => row.map((c) => (c === null ? null : String(c))))));
70 + }
71 + } catch {
72 + if (!cancelled) setError(true);
73 + } finally {
74 + if (!cancelled) setLoading(false);
75 + }
76 + };
77 +
78 + parse();
79 + return () => { cancelled = true; };
80 + }, [file.id]);
81 +
82 + const handleCellChange = useCallback((sheetIdx: number, rowIdx: number, colIdx: number, value: string) => {
83 + setEditData((prev) => {
84 + const next = prev.map((s) => s.map((r) => [...r]));
85 + // Ensure nested arrays exist
86 + while (next[sheetIdx].length <= rowIdx) next[sheetIdx].push([]);
87 + while (next[sheetIdx][rowIdx].length <= colIdx) next[sheetIdx][rowIdx].push(null);
88 + next[sheetIdx][rowIdx][colIdx] = value;
89 + return next;
90 + });
91 + }, []);
92 +
93 + const handleSave = useCallback(async () => {
94 + if (!onSave) return;
95 + setSaving(true);
96 + try {
97 + const XLSX = await import("xlsx");
98 + const wb = XLSX.utils.book_new();
99 + editData.forEach((sheetRows, si) => {
100 + const ws = XLSX.utils.aoa_to_sheet(sheetRows.map((row) =>
101 + row.map((c) => {
102 + if (c === null || c === "") return null;
103 + const num = Number(c);
104 + return isNaN(num) ? c : num;
105 + })
106 + ));
107 + XLSX.utils.book_append_sheet(wb, ws, sheets[si]?.name || `Sheet${si + 1}`);
108 + });
109 + const array = XLSX.write(wb, { bookType: "xlsx", type: "array" });
110 + const blob = new Blob([array], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
111 + await onSave(blob, true);
112 + } finally {
113 + setSaving(false);
114 + }
115 + }, [editData, sheets, onSave]);
116 +
117 + if (loading) {
118 + return (
119 + <div className="flex flex-col items-center justify-center h-full gap-3 text-gray-400 bg-white">
120 + <Loader2 className="w-8 h-8 animate-spin text-[#217346]" />
121 + <p className="text-sm text-gray-500">Chargement du classeur Excel…</p>
122 + </div>
123 + );
124 + }
125 +
126 + if (error || sheets.length === 0) {
127 + return (
128 + <div className="flex flex-col items-center justify-center h-full gap-4 text-gray-400 p-8 bg-white">
129 + <Table2 className="w-16 h-16 text-[#217346] opacity-50" />
130 + <p className="text-sm text-center text-gray-500">Impossible d&apos;afficher ce fichier Excel.</p>
131 + <button
132 + onClick={() => window.open(`/api/files/${file.id}/download`, "_blank")}
133 + className="flex items-center gap-2 px-4 py-2 bg-[#217346] hover:bg-[#1a5c38] rounded text-sm text-white transition-colors"
134 + >
135 + <Download className="w-4 h-4" />
136 + Télécharger
137 + </button>
138 + </div>
139 + );
140 + }
141 +
142 + const currentSheet = sheets[activeSheet];
143 + const currentEditRows = editData[activeSheet] ?? currentSheet.rows.map((r) => r.map((c) => (c === null ? null : String(c))));
144 +
145 + return (
146 + <div className="flex flex-col h-full bg-white text-gray-900">
147 + {/* Toolbar */}
148 + <div className="flex items-center gap-2 px-3 py-1.5 bg-[#f2f2f2] border-b border-[#d0d7de] shrink-0">
149 + <span className="text-xs text-gray-500 font-medium">{file.name}</span>
150 + <div className="flex-1" />
151 + {isEditing && (
152 + <button
153 + onClick={handleSave}
154 + disabled={saving}
155 + className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-[#217346] hover:bg-[#1a5c38] text-white rounded transition-colors disabled:opacity-60"
156 + >
157 + {saving ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
158 + Sauvegarder
159 + </button>
160 + )}
161 + {!isEditing && (
162 + <span className="text-xs text-gray-400 flex items-center gap-1">
163 + <Edit3 className="w-3.5 h-3.5" />
164 + Mode lecture
165 + </span>
166 + )}
167 + <button
168 + onClick={() => window.open(`/api/files/${file.id}/download`, "_blank")}
169 + className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 hover:text-[#217346] transition-colors"
170 + >
171 + <Download className="w-3.5 h-3.5" />
172 + </button>
173 + </div>
174 +
175 + {/* Sheet tabs — Excel style */}
176 + <div className="flex items-end gap-px px-2 pt-1 bg-[#f2f2f2] border-b border-[#d0d7de] overflow-x-auto shrink-0">
177 + {sheets.map((sheet, i) => (
178 + <button
179 + key={sheet.name}
180 + onClick={() => setActiveSheet(i)}
181 + className={`px-4 py-1.5 text-xs rounded-t transition-colors whitespace-nowrap border border-b-0 ${
182 + i === activeSheet
183 + ? "bg-[#217346] text-white border-[#217346]"
184 + : "bg-[#e0e0e0] text-gray-600 border-[#c0c0c0] hover:bg-[#d0d0d0]"
185 + }`}
186 + >
187 + {sheet.name}
188 + </button>
189 + ))}
190 + <div className="ml-auto flex items-center gap-2 pb-1">
191 + <span className="text-[10px] text-gray-500 whitespace-nowrap">
192 + {currentSheet.rows.length} lignes · {currentSheet.colCount} col.
193 + </span>
194 + </div>
195 + </div>
196 +
197 + {/* Sheet viewer */}
198 + <SheetViewer
199 + sheet={currentSheet}
200 + editRows={currentEditRows}
201 + sheetIdx={activeSheet}
202 + isEditing={isEditing}
203 + onCellChange={handleCellChange}
204 + key={activeSheet}
205 + />
206 + </div>
207 + );
208 +}
209 +
210 +function SheetViewer({
211 + sheet,
212 + editRows,
213 + sheetIdx,
214 + isEditing,
215 + onCellChange,
216 +}: {
217 + sheet: SheetData;
218 + editRows: (string | number | null)[][];
219 + sheetIdx: number;
220 + isEditing: boolean;
221 + onCellChange: (si: number, ri: number, ci: number, v: string) => void;
222 +}) {
223 + const [sortConfig, setSortConfig] = useState<SortConfig | null>(null);
224 + const [page, setPage] = useState(0);
225 +
226 + const headerRow = editRows[0] ?? [];
227 + const dataRows = editRows.slice(1);
228 + const colCount = sheet.colCount;
229 +
230 + const sortedRows = useMemo(() => {
231 + if (!sortConfig || isEditing) return dataRows;
232 + return [...dataRows].sort((a, b) => {
233 + const aVal = a[sortConfig.col] ?? "";
234 + const bVal = b[sortConfig.col] ?? "";
235 + const numA = parseFloat(String(aVal));
236 + const numB = parseFloat(String(bVal));
237 + if (!isNaN(numA) && !isNaN(numB)) {
238 + return sortConfig.dir === "asc" ? numA - numB : numB - numA;
239 + }
240 + return sortConfig.dir === "asc"
241 + ? String(aVal).localeCompare(String(bVal))
242 + : String(bVal).localeCompare(String(aVal));
243 + });
244 + }, [dataRows, sortConfig, isEditing]);
245 +
246 + const pageRows = sortedRows.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
247 + const totalPages = Math.ceil(sortedRows.length / PAGE_SIZE);
248 +
249 + const toggleSort = useCallback((col: number) => {
250 + if (isEditing) return;
251 + setSortConfig((prev) => {
252 + if (prev?.col === col) return prev.dir === "asc" ? { col, dir: "desc" } : null;
253 + return { col, dir: "asc" };
254 + });
255 + setPage(0);
256 + }, [isEditing]);
257 +
258 + const formatCell = (val: string | number | null | undefined): string => {
259 + if (val === null || val === undefined) return "";
260 + return String(val);
261 + };
262 +
263 + const isNumeric = (val: string | number | null | undefined): boolean =>
264 + typeof val === "number" || (!isNaN(parseFloat(String(val))) && val !== null && val !== "");
265 +
266 + return (
267 + <div className="flex flex-col flex-1 overflow-hidden bg-white">
268 + <div className="flex-1 overflow-auto">
269 + <table className="text-xs border-collapse min-w-max">
270 + {/* Column letter row */}
271 + <thead className="sticky top-0 z-10">
272 + <tr>
273 + {/* Row number gutter header */}
274 + <th className="w-10 min-w-[2.5rem] border border-[#d0d7de] px-2 py-1 bg-[#f2f2f2] text-gray-500 font-normal select-none" />
275 + {Array.from({ length: colCount }, (_, i) => (
276 + <th
277 + key={i}
278 + className="border border-[#d0d7de] px-1 py-1 bg-[#f2f2f2] text-gray-500 font-normal text-center min-w-[80px]"
279 + >
280 + {colLetter(i)}
281 + </th>
282 + ))}
283 + </tr>
284 +
285 + {/* Header data row */}
286 + <tr>
287 + <td className="border border-[#d0d7de] px-2 py-1 bg-[#f2f2f2] text-gray-400 text-center font-normal select-none">
288 + 1
289 + </td>
290 + {Array.from({ length: colCount }, (_, i) => {
291 + const val = headerRow[i] ?? null;
292 + const sorted = sortConfig?.col === i;
293 + return (
294 + <th
295 + key={i}
296 + className="border border-[#d0d7de] bg-[#f2f2f2] text-gray-700 font-semibold text-left p-0"
297 + >
298 + {isEditing ? (
299 + <input
300 + type="text"
301 + value={formatCell(val)}
302 + onChange={(e) => onCellChange(sheetIdx, 0, i, e.target.value)}
303 + className="w-full px-2 py-1 bg-white border-0 outline-none focus:ring-2 focus:ring-[#217346] focus:ring-inset text-gray-900 font-semibold"
304 + />
305 + ) : (
306 + <button
307 + onClick={() => toggleSort(i)}
308 + className="flex items-center gap-1 w-full px-2 py-1 hover:bg-[#e8f0e8] transition-colors"
309 + >
310 + <span className="truncate max-w-[120px]">{formatCell(val)}</span>
311 + <ArrowUpDown className={`w-3 h-3 shrink-0 ${sorted ? "text-[#217346]" : "text-gray-400"}`} />
312 + </button>
313 + )}
314 + </th>
315 + );
316 + })}
317 + </tr>
318 + </thead>
319 +
320 + <tbody>
321 + {pageRows.map((row, rowIdx) => {
322 + const actualRowIdx = page * PAGE_SIZE + rowIdx + 1; // 0-based in editRows (1 = first data row)
323 + return (
324 + <tr
325 + key={rowIdx}
326 + className={rowIdx % 2 === 0 ? "bg-white" : "bg-[#f9f9f9]"}
327 + >
328 + {/* Row number */}
329 + <td className="border border-[#e0e0e0] px-2 py-0.5 text-gray-400 text-center bg-[#f2f2f2] select-none">
330 + {actualRowIdx + 1}
331 + </td>
332 + {Array.from({ length: colCount }, (_, colIdx) => {
333 + const val = row[colIdx] ?? null;
334 + const numeric = isNumeric(val);
335 + return (
336 + <td
337 + key={colIdx}
338 + className={`border border-[#e0e0e0] p-0 ${!isEditing && numeric ? "text-right" : ""}`}
339 + >
340 + {isEditing ? (
341 + <input
342 + type="text"
343 + value={formatCell(val)}
344 + onChange={(e) => onCellChange(sheetIdx, actualRowIdx, colIdx, e.target.value)}
345 + className="w-full min-w-[80px] px-2 py-0.5 bg-transparent border-0 outline-none focus:ring-2 focus:ring-[#217346] focus:ring-inset text-gray-900"
346 + />
347 + ) : (
348 + <span className={`block px-2 py-0.5 ${numeric ? "text-[#1155cc] text-right" : "text-gray-800"}`}>
349 + {formatCell(val)}
350 + </span>
351 + )}
352 + </td>
353 + );
354 + })}
355 + </tr>
356 + );
357 + })}
358 +
359 + {pageRows.length === 0 && (
360 + <tr>
361 + <td colSpan={colCount + 1} className="border border-[#e0e0e0] px-4 py-8 text-center text-gray-400">
362 + Feuille vide
363 + </td>
364 + </tr>
365 + )}
366 + </tbody>
367 + </table>
368 + </div>
369 +
370 + {/* Pagination */}
371 + {totalPages > 1 && (
372 + <div className="flex items-center justify-between px-3 py-2 border-t border-[#d0d7de] bg-[#f2f2f2] shrink-0">
373 + <button
374 + onClick={() => setPage((p) => Math.max(0, p - 1))}
375 + disabled={page === 0}
376 + className="px-2 py-1 text-xs text-gray-500 hover:text-[#217346] disabled:opacity-30 transition-colors"
377 + >
378 + ← Précédent
379 + </button>
380 + <span className="text-xs text-gray-500">
381 + Lignes {page * PAGE_SIZE + 2}–{Math.min((page + 1) * PAGE_SIZE + 1, sortedRows.length + 1)} sur {sortedRows.length + 1}
382 + </span>
383 + <button
384 + onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
385 + disabled={page >= totalPages - 1}
386 + className="px-2 py-1 text-xs text-gray-500 hover:text-[#217346] disabled:opacity-30 transition-colors"
387 + >
388 + Suivant →
389 + </button>
390 + </div>
391 + )}
392 + </div>
393 + );
394 +}
modified components/preview/plugins/PdfPreview.tsx +368 −60
@@ -1,108 +1,213 @@
1 1 "use client";
2 2
3 3 import { useEffect, useRef, useState, useCallback } from "react";
4 −import { ChevronLeft, ChevronRight, ZoomIn, ZoomOut } from "lucide-react";
4 +import {
5 + ChevronLeft, ChevronRight, ZoomIn, ZoomOut,
6 + Highlighter, StickyNote, Pen, Trash2, MousePointer,
7 +} from "lucide-react";
5 8 import type { PreviewPluginProps } from "./types";
6 9
7 −export default function PdfPreview({ file }: PreviewPluginProps) {
10 +// ── Annotation types ──────────────────────────────────────────────────────────
11 +
12 +type AnnotationTool = "pointer" | "highlight" | "note" | "pen";
13 +
14 +interface HighlightAnnotation {
15 + id: string;
16 + type: "highlight";
17 + x: number; y: number; w: number; h: number;
18 + color: string;
19 +}
20 +
21 +interface NoteAnnotation {
22 + id: string;
23 + type: "note";
24 + x: number; y: number;
25 + text: string;
26 +}
27 +
28 +interface PenAnnotation {
29 + id: string;
30 + type: "pen";
31 + path: string; // SVG path data
32 + color: string;
33 +}
34 +
35 +type Annotation = HighlightAnnotation | NoteAnnotation | PenAnnotation;
36 +
37 +interface PageAnnotations {
38 + [page: number]: Annotation[];
39 +}
40 +
41 +// ── Tiny uid ──────────────────────────────────────────────────────────────────
42 +function uid() {
43 + return Math.random().toString(36).slice(2, 10);
44 +}
45 +
46 +// ── Main component ────────────────────────────────────────────────────────────
47 +export default function PdfPreview({ file, isEditing }: PreviewPluginProps) {
8 48 const canvasRef = useRef<HTMLCanvasElement>(null);
49 + const overlayRef = useRef<SVGSVGElement>(null);
9 50 const containerRef = useRef<HTMLDivElement>(null);
10 51 // eslint-disable-next-line @typescript-eslint/no-explicit-any
11 52 const pdfDocRef = useRef<any>(null);
12 53 const renderTaskRef = useRef<{ cancel: () => void } | null>(null);
54 +
13 55 const [pageNum, setPageNum] = useState(1);
14 56 const [totalPages, setTotalPages] = useState(0);
15 57 const [scale, setScale] = useState(1.2);
16 58 const [loading, setLoading] = useState(true);
17 59 const [error, setError] = useState(false);
18 60
61 + // Annotation state
62 + const [tool, setTool] = useState<AnnotationTool>("pointer");
63 + const [pageAnnotations, setPageAnnotations] = useState<PageAnnotations>({});
64 + const [editingNote, setEditingNote] = useState<{ id: string; page: number } | null>(null);
65 +
66 + // Drawing state for highlight
67 + const drawingRef = useRef<{ startX: number; startY: number } | null>(null);
68 + const [drawRect, setDrawRect] = useState<{ x: number; y: number; w: number; h: number } | null>(null);
69 +
70 + // Drawing state for pen
71 + const penRef = useRef<string | null>(null);
72 + const [penPath, setPenPath] = useState<string | null>(null);
73 +
74 + // Canvas dimensions (for overlay sizing)
75 + const [canvasDims, setCanvasDims] = useState({ w: 0, h: 0 });
76 +
77 + // ── PDF render ─────────────────────────────────────────────────────────────
19 78 const renderPage = useCallback(async (num: number, currentScale: number) => {
20 79 const pdf = pdfDocRef.current;
21 80 if (!pdf || !canvasRef.current) return;
22 −
23 81 try {
24 − // Cancel any in-progress render
25 82 if (renderTaskRef.current) {
26 83 renderTaskRef.current.cancel();
27 84 renderTaskRef.current = null;
28 85 }
29 −
30 86 const page = await pdf.getPage(num);
31 87 const viewport = page.getViewport({ scale: currentScale });
32 88 const canvas = canvasRef.current;
33 89 const context = canvas.getContext("2d");
34 90 if (!context) return;
35 −
36 91 canvas.height = viewport.height;
37 92 canvas.width = viewport.width;
38 −
93 + setCanvasDims({ w: viewport.width, h: viewport.height });
39 94 const renderTask = page.render({ canvasContext: context, viewport });
40 95 renderTaskRef.current = renderTask;
41 96 await renderTask.promise;
42 97 renderTaskRef.current = null;
43 98 } catch (e: unknown) {
44 − // Ignore cancelled render errors
45 99 if (e && typeof e === "object" && "name" in e && (e as { name: string }).name === "RenderingCancelledException") return;
46 100 }
47 101 }, []);
48 102
49 103 useEffect(() => {
50 104 let cancelled = false;
51 −
52 105 const loadPdf = async () => {
53 106 try {
54 107 const pdfjsLib = await import("pdfjs-dist");
55 −
56 − // Use local worker file copied to /public
57 108 pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs";
58 −
59 109 const loadingTask = pdfjsLib.getDocument({
60 110 url: `/api/files/${file.id}/preview`,
61 111 cMapUrl: "https://cdn.jsdelivr.net/npm/pdfjs-dist@5.4.624/cmaps/",
62 112 cMapPacked: true,
63 113 });
64 −
65 114 const pdf = await loadingTask.promise;
66 115 if (cancelled) return;
67 −
68 116 pdfDocRef.current = pdf;
69 117 setTotalPages(pdf.numPages);
70 118 setLoading(false);
71 −
72 − // Render first page after state is set
73 119 await renderPage(1, 1.2);
74 120 } catch (err) {
75 121 console.error("PDF load error:", err);
76 − if (!cancelled) {
77 − setError(true);
78 − setLoading(false);
79 − }
122 + if (!cancelled) { setError(true); setLoading(false); }
80 123 }
81 124 };
82 −
83 125 loadPdf();
84 126 return () => {
85 127 cancelled = true;
86 − if (renderTaskRef.current) {
87 − renderTaskRef.current.cancel();
88 − }
128 + if (renderTaskRef.current) renderTaskRef.current.cancel();
89 129 };
90 130 }, [file.id, renderPage]);
91 131
92 − // Re-render when page or scale changes (but not on initial load)
93 132 useEffect(() => {
94 − if (!loading && pdfDocRef.current) {
95 − renderPage(pageNum, scale);
96 − }
133 + if (!loading && pdfDocRef.current) renderPage(pageNum, scale);
97 134 }, [pageNum, scale, renderPage, loading]);
98 135
136 + // ── Load annotations ───────────────────────────────────────────────────────
137 + useEffect(() => {
138 + const load = async () => {
139 + try {
140 + const res = await fetch(`/api/files/${file.id}/annotations`);
141 + if (!res.ok) return;
142 + const data = await res.json();
143 + if (data.annotations) {
144 + try {
145 + const parsed = JSON.parse(data.annotations);
146 + if (parsed?.pageAnnotations) setPageAnnotations(parsed.pageAnnotations);
147 + } catch {
148 + // ignore
149 + }
150 + }
151 + } catch {
152 + // ignore
153 + }
154 + };
155 + load();
156 + }, [file.id]);
157 +
158 + // ── Save annotations ───────────────────────────────────────────────────────
159 + const saveAnnotations = useCallback(async (pa: PageAnnotations) => {
160 + try {
161 + await fetch(`/api/files/${file.id}/annotations`, {
162 + method: "PUT",
163 + headers: { "Content-Type": "application/json" },
164 + body: JSON.stringify({ annotations: JSON.stringify({ pageAnnotations: pa }) }),
165 + });
166 + } catch {
167 + // ignore
168 + }
169 + }, [file.id]);
170 +
171 + const addAnnotation = useCallback((ann: Annotation) => {
172 + setPageAnnotations((prev) => {
173 + const next = { ...prev, [pageNum]: [...(prev[pageNum] ?? []), ann] };
174 + saveAnnotations(next);
175 + return next;
176 + });
177 + }, [pageNum, saveAnnotations]);
178 +
179 + const deleteAnnotation = useCallback((id: string) => {
180 + setPageAnnotations((prev) => {
181 + const next = {
182 + ...prev,
183 + [pageNum]: (prev[pageNum] ?? []).filter((a) => a.id !== id),
184 + };
185 + saveAnnotations(next);
186 + return next;
187 + });
188 + }, [pageNum, saveAnnotations]);
189 +
190 + const updateNoteText = useCallback((id: string, text: string) => {
191 + setPageAnnotations((prev) => {
192 + const next = {
193 + ...prev,
194 + [pageNum]: (prev[pageNum] ?? []).map((a) =>
195 + a.id === id ? { ...a, text } : a
196 + ),
197 + };
198 + saveAnnotations(next);
199 + return next;
200 + });
201 + }, [pageNum, saveAnnotations]);
202 +
203 + // ── Keyboard nav ───────────────────────────────────────────────────────────
99 204 const goToPage = useCallback((num: number) => {
100 205 setPageNum(Math.max(1, Math.min(num, totalPages)));
101 206 }, [totalPages]);
102 207
103 208 useEffect(() => {
104 209 const handleKey = (e: KeyboardEvent) => {
105 − if (e.target instanceof HTMLInputElement) return;
210 + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
106 211 if (e.key === "ArrowLeft" || e.key === "ArrowUp") { e.preventDefault(); goToPage(pageNum - 1); }
107 212 if (e.key === "ArrowRight" || e.key === "ArrowDown") { e.preventDefault(); goToPage(pageNum + 1); }
108 213 };
@@ -110,15 +215,83 @@ export default function PdfPreview({ file }: PreviewPluginProps) {
110 215 return () => window.removeEventListener("keydown", handleKey);
111 216 }, [pageNum, goToPage]);
112 217
218 + // ── Mouse handlers on overlay ──────────────────────────────────────────────
219 + const getRelativePos = (e: React.MouseEvent<SVGSVGElement>) => {
220 + const rect = e.currentTarget.getBoundingClientRect();
221 + return {
222 + x: (e.clientX - rect.left) / rect.width * canvasDims.w,
223 + y: (e.clientY - rect.top) / rect.height * canvasDims.h,
224 + };
225 + };
226 +
227 + const handleMouseDown = useCallback((e: React.MouseEvent<SVGSVGElement>) => {
228 + if (!isEditing) return;
229 + const pos = getRelativePos(e);
230 +
231 + if (tool === "highlight") {
232 + drawingRef.current = { startX: pos.x, startY: pos.y };
233 + setDrawRect({ x: pos.x, y: pos.y, w: 0, h: 0 });
234 + e.preventDefault();
235 + }
236 +
237 + if (tool === "note") {
238 + addAnnotation({ id: uid(), type: "note", x: pos.x, y: pos.y, text: "" });
239 + }
240 +
241 + if (tool === "pen") {
242 + penRef.current = `M ${pos.x} ${pos.y}`;
243 + setPenPath(penRef.current);
244 + e.preventDefault();
245 + }
246 + // eslint-disable-next-line react-hooks/exhaustive-deps
247 + }, [isEditing, tool, canvasDims, addAnnotation]);
248 +
249 + const handleMouseMove = useCallback((e: React.MouseEvent<SVGSVGElement>) => {
250 + if (!isEditing) return;
251 + const pos = getRelativePos(e);
252 +
253 + if (tool === "highlight" && drawingRef.current) {
254 + const dx = pos.x - drawingRef.current.startX;
255 + const dy = pos.y - drawingRef.current.startY;
256 + setDrawRect({
257 + x: dx < 0 ? pos.x : drawingRef.current.startX,
258 + y: dy < 0 ? pos.y : drawingRef.current.startY,
259 + w: Math.abs(dx),
260 + h: Math.abs(dy),
261 + });
262 + }
263 +
264 + if (tool === "pen" && penRef.current) {
265 + penRef.current += ` L ${pos.x} ${pos.y}`;
266 + setPenPath(penRef.current);
267 + }
268 + // eslint-disable-next-line react-hooks/exhaustive-deps
269 + }, [isEditing, tool, canvasDims]);
270 +
271 + const handleMouseUp = useCallback(() => {
272 + if (!isEditing) return;
273 +
274 + if (tool === "highlight" && drawingRef.current && drawRect && drawRect.w > 5 && drawRect.h > 5) {
275 + addAnnotation({ id: uid(), type: "highlight", ...drawRect, color: "#fef08a" });
276 + }
277 + drawingRef.current = null;
278 + setDrawRect(null);
279 +
280 + if (tool === "pen" && penRef.current) {
281 + addAnnotation({ id: uid(), type: "pen", path: penRef.current, color: "#ef4444" });
282 + penRef.current = null;
283 + setPenPath(null);
284 + }
285 + }, [isEditing, tool, drawRect, addAnnotation]);
286 +
287 + // ── Current page annotations ───────────────────────────────────────────────
288 + const currentAnnotations = pageAnnotations[pageNum] ?? [];
289 +
290 + // ── Error fallback ─────────────────────────────────────────────────────────
113 291 if (error) {
114 − // Fallback to browser's built-in PDF viewer via iframe
115 292 return (
116 293 <div className="w-full h-full">
117 − <iframe
118 − src={`/api/files/${file.id}/preview`}
119 − className="w-full h-full border-0"
120 − title={file.name}
121 − />
294 + <iframe src={`/api/files/${file.id}/preview`} className="w-full h-full border-0" title={file.name} />
122 295 </div>
123 296 );
124 297 }
@@ -126,7 +299,8 @@ export default function PdfPreview({ file }: PreviewPluginProps) {
126 299 return (
127 300 <div className="flex flex-col h-full">
128 301 {/* Toolbar */}
129 − <div className="flex items-center gap-2 px-3 py-2 border-b border-white/10 bg-white/5 flex-shrink-0">
302 + <div className="flex items-center gap-2 px-3 py-2 border-b border-white/10 bg-white/5 flex-shrink-0 flex-wrap">
303 + {/* Navigation */}
130 304 <button
131 305 onClick={() => goToPage(pageNum - 1)}
132 306 disabled={pageNum <= 1}
@@ -155,44 +329,178 @@ export default function PdfPreview({ file }: PreviewPluginProps) {
155 329
156 330 <div className="w-px h-4 bg-white/10 mx-1" />
157 331
158 − <button
159 − onClick={() => setScale((s) => Math.min(s + 0.2, 3))}
160 − className="p-1.5 text-gray-400 hover:text-white transition-colors rounded hover:bg-white/10"
161 − title="Zoom in"
162 − >
332 + {/* Zoom */}
333 + <button onClick={() => setScale((s) => Math.min(s + 0.2, 3))} className="p-1.5 text-gray-400 hover:text-white transition-colors rounded hover:bg-white/10">
163 334 <ZoomIn className="w-4 h-4" />
164 335 </button>
165 − <span className="text-xs text-gray-500 min-w-[3rem] text-center">
166 − {Math.round(scale * 100)}%
167 − </span>
168 − <button
169 − onClick={() => setScale((s) => Math.max(s - 0.2, 0.4))}
170 − className="p-1.5 text-gray-400 hover:text-white transition-colors rounded hover:bg-white/10"
171 − title="Zoom out"
172 − >
336 + <span className="text-xs text-gray-500 min-w-[3rem] text-center">{Math.round(scale * 100)}%</span>
337 + <button onClick={() => setScale((s) => Math.max(s - 0.2, 0.4))} className="p-1.5 text-gray-400 hover:text-white transition-colors rounded hover:bg-white/10">
173 338 <ZoomOut className="w-4 h-4" />
174 339 </button>
175 −
176 − <button
177 − onClick={() => setScale(1.2)}
178 − className="text-[10px] text-gray-500 hover:text-white px-1.5 py-0.5 rounded hover:bg-white/10 transition-colors"
179 − >
340 + <button onClick={() => setScale(1.2)} className="text-[10px] text-gray-500 hover:text-white px-1.5 py-0.5 rounded hover:bg-white/10 transition-colors">
180 341 Reset
181 342 </button>
343 +
344 + {/* Annotation tools (only in edit mode) */}
345 + {isEditing && (
346 + <>
347 + <div className="w-px h-4 bg-white/10 mx-1" />
348 + <span className="text-[10px] text-gray-500 mr-1">Annoter :</span>
349 +
350 + {([
351 + { id: "pointer", Icon: MousePointer, label: "Sélection" },
352 + { id: "highlight", Icon: Highlighter, label: "Surligner" },
353 + { id: "note", Icon: StickyNote, label: "Note" },
354 + { id: "pen", Icon: Pen, label: "Dessin" },
355 + ] as const).map(({ id, Icon, label }) => (
356 + <button
357 + key={id}
358 + onClick={() => setTool(id)}
359 + title={label}
360 + className={`p-1.5 rounded transition-colors text-xs flex items-center gap-1 ${
361 + tool === id
362 + ? "bg-yellow-500/20 text-yellow-300 ring-1 ring-yellow-400/50"
363 + : "text-gray-400 hover:text-white hover:bg-white/10"
364 + }`}
365 + >
366 + <Icon className="w-3.5 h-3.5" />
367 + </button>
368 + ))}
369 +
370 + {currentAnnotations.length > 0 && (
371 + <span className="text-[10px] text-gray-500 ml-1">
372 + {currentAnnotations.length} annot.
373 + </span>
374 + )}
375 + </>
376 + )}
182 377 </div>
183 378
184 − {/* Canvas */}
379 + {/* Canvas + Overlay */}
185 380 <div ref={containerRef} className="flex-1 overflow-auto flex justify-center p-4 bg-gray-900/50">
186 381 {loading ? (
187 382 <div className="flex items-center justify-center h-full">
188 383 <div className="w-6 h-6 border-2 border-white/20 border-t-white rounded-full animate-spin" />
189 384 </div>
190 385 ) : (
191 − <canvas
192 − ref={canvasRef}
193 − className="shadow-2xl max-w-full"
194 − style={{ background: "white" }}
195 − />
386 + <div className="relative inline-block" style={{ lineHeight: 0 }}>
387 + <canvas
388 + ref={canvasRef}
389 + className="shadow-2xl max-w-full block"
390 + style={{ background: "white" }}
391 + />
392 +
393 + {/* SVG Annotation Overlay */}
394 + <svg
395 + ref={overlayRef}
396 + className={`absolute inset-0 w-full h-full ${isEditing && tool !== "pointer" ? "cursor-crosshair" : "cursor-default"}`}
397 + viewBox={`0 0 ${canvasDims.w} ${canvasDims.h}`}
398 + preserveAspectRatio="none"
399 + onMouseDown={handleMouseDown}
400 + onMouseMove={handleMouseMove}
401 + onMouseUp={handleMouseUp}
402 + >
403 + {/* Render saved annotations */}
404 + {currentAnnotations.map((ann) => {
405 + if (ann.type === "highlight") {
406 + return (
407 + <g key={ann.id}>
408 + <rect
409 + x={ann.x} y={ann.y} width={ann.w} height={ann.h}
410 + fill={ann.color}
411 + fillOpacity={0.35}
412 + stroke={ann.color}
413 + strokeWidth={1}
414 + strokeOpacity={0.6}
415 + className="cursor-pointer"
416 + onClick={(e) => {
417 + if (isEditing && tool === "pointer") {
418 + e.stopPropagation();
419 + deleteAnnotation(ann.id);
420 + }
421 + }}
422 + />
423 + {isEditing && tool === "pointer" && (
424 + <foreignObject x={ann.x + ann.w - 12} y={ann.y - 12} width={16} height={16}>
425 + <button
426 + title="Supprimer"
427 + onClick={(e) => { e.stopPropagation(); deleteAnnotation(ann.id); }}
428 + className="w-4 h-4 bg-red-500 text-white rounded-full text-[8px] flex items-center justify-center hover:bg-red-600"
429 + >
430 + ×
431 + </button>
432 + </foreignObject>
433 + )}
434 + </g>
435 + );
436 + }
437 +
438 + if (ann.type === "pen") {
439 + return (
440 + <g key={ann.id}>
441 + <path
442 + d={ann.path}
443 + fill="none"
444 + stroke={ann.color}
445 + strokeWidth={2}
446 + strokeLinecap="round"
447 + strokeLinejoin="round"
448 + opacity={0.8}
449 + />
450 + </g>
451 + );
452 + }
453 +
454 + if (ann.type === "note") {
455 + return (
456 + <foreignObject key={ann.id} x={ann.x} y={ann.y} width={160} height={90}>
457 + <div className="relative">
458 + <textarea
459 + className="w-full h-full bg-yellow-100 text-gray-800 text-[10px] p-1.5 rounded shadow-md resize-none border border-yellow-300 outline-none"
460 + placeholder="Note..."
461 + value={ann.text}
462 + onChange={(e) => {
463 + if (isEditing) updateNoteText(ann.id, e.target.value);
464 + }}
465 + readOnly={!isEditing}
466 + rows={4}
467 + />
468 + {isEditing && (
469 + <button
470 + onClick={() => deleteAnnotation(ann.id)}
471 + className="absolute -top-1.5 -right-1.5 w-4 h-4 bg-red-500 text-white rounded-full text-[9px] flex items-center justify-center hover:bg-red-600 z-10"
472 + >
473 + <Trash2 className="w-2.5 h-2.5" />
474 + </button>
475 + )}
476 + </div>
477 + </foreignObject>
478 + );
479 + }
480 +
481 + return null;
482 + })}
483 +
484 + {/* Live drawing preview — highlight rect */}
485 + {drawRect && drawRect.w > 0 && (
486 + <rect
487 + x={drawRect.x} y={drawRect.y} width={drawRect.w} height={drawRect.h}
488 + fill="#fef08a" fillOpacity={0.35}
489 + stroke="#ca8a04" strokeWidth={1} strokeDasharray="4 2"
490 + />
491 + )}
492 +
493 + {/* Live pen path preview */}
494 + {penPath && (
495 + <path
496 + d={penPath}
497 + fill="none" stroke="#ef4444" strokeWidth={2}
498 + strokeLinecap="round" strokeLinejoin="round"
499 + opacity={0.8}
500 + />
501 + )}
502 + </svg>
503 + </div>
196 504 )}
197 505 </div>
198 506 </div>
modified components/preview/plugins/WordPreview.tsx +48 −18
@@ -1,27 +1,47 @@
1 1 "use client";
2 2
3 −import { useState, useEffect } from "react";
3 +import { useState, useEffect, useRef } from "react";
4 4 import { Loader2, Download, FileText } from "lucide-react";
5 +import dynamic from "next/dynamic";
5 6 import type { PreviewPluginProps } from "./types";
6 7
7 −export default function WordPreview({ file }: PreviewPluginProps) {
8 +// Load Tiptap editor only on demand (heavy bundle)
9 +const WordTiptapEditor = dynamic(() => import("./WordTiptapEditor"), {
10 + ssr: false,
11 + loading: () => (
12 + <div className="flex flex-col items-center justify-center h-full gap-3 text-gray-400">
13 + <Loader2 className="w-8 h-8 animate-spin text-blue-400" />
14 + <p className="text-sm">Chargement de l&apos;éditeur…</p>
15 + </div>
16 + ),
17 +});
18 +
19 +export default function WordPreview({ file, isEditing, onSave }: PreviewPluginProps) {
8 20 const [html, setHtml] = useState<string | null>(null);
9 21 const [error, setError] = useState(false);
10 22 const [loading, setLoading] = useState(true);
23 + // Incremented when returning from edit mode to force re-fetch from server
24 + const [fetchKey, setFetchKey] = useState(0);
25 + const prevEditingRef = useRef(isEditing);
11 26
27 + // When switching from edit → view, re-fetch to show saved content
12 28 useEffect(() => {
13 − let cancelled = false;
29 + if (!isEditing && prevEditingRef.current) {
30 + setFetchKey((k) => k + 1);
31 + }
32 + prevEditingRef.current = isEditing;
33 + }, [isEditing]);
14 34
35 + useEffect(() => {
36 + let cancelled = false;
15 37 const convert = async () => {
16 38 setLoading(true);
17 39 setError(false);
40 + setHtml(null);
18 41 try {
19 − // Fetch the raw file bytes
20 − const res = await fetch(`/api/files/${file.id}/download`);
42 + const res = await fetch(`/api/files/${file.id}/download?t=${Date.now()}`);
21 43 if (!res.ok) throw new Error("Download failed");
22 44 const arrayBuffer = await res.arrayBuffer();
23 −
24 − // mammoth converts docx → clean HTML
25 45 const mammoth = await import("mammoth");
26 46 const result = await mammoth.convertToHtml(
27 47 { arrayBuffer },
@@ -35,21 +55,36 @@ export default function WordPreview({ file }: PreviewPluginProps) {
35 55 ],
36 56 }
37 57 );
38 −
39 − if (!cancelled) {
40 − setHtml(result.value || "<p>Document vide.</p>");
41 − }
58 + if (!cancelled) setHtml(result.value || "<p>Document vide.</p>");
42 59 } catch {
43 60 if (!cancelled) setError(true);
44 61 } finally {
45 62 if (!cancelled) setLoading(false);
46 63 }
47 64 };
48 −
49 65 convert();
50 66 return () => { cancelled = true; };
51 − }, [file.id]);
67 + }, [file.id, fetchKey]);
52 68
69 + // Edit mode — render Tiptap editor once html is ready
70 + if (isEditing) {
71 + if (loading) {
72 + return (
73 + <div className="flex flex-col items-center justify-center h-full gap-3 text-gray-400">
74 + <Loader2 className="w-8 h-8 animate-spin text-blue-400" />
75 + <p className="text-sm">Chargement du document…</p>
76 + </div>
77 + );
78 + }
79 + return (
80 + <WordTiptapEditor
81 + initialHtml={html ?? "<p>Document vide.</p>"}
82 + onSave={onSave ? async (blob, v) => onSave(blob, v) : undefined}
83 + />
84 + );
85 + }
86 +
87 + // Read mode
53 88 if (loading) {
54 89 return (
55 90 <div className="flex flex-col items-center justify-center h-full gap-3 text-gray-400">
@@ -77,10 +112,8 @@ export default function WordPreview({ file }: PreviewPluginProps) {
77 112
78 113 return (
79 114 <div className="h-full overflow-auto bg-gray-50 dark:bg-[#1a1a1a]">
80 − {/* Page-like container */}
81 115 <div className="max-w-3xl mx-auto my-6 px-4 sm:px-6">
82 116 <div className="bg-white dark:bg-[#242424] shadow-md rounded-sm px-8 py-10 sm:px-14 sm:py-12 min-h-[500px]">
83 − {/* Download link */}
84 117 <div className="flex items-center justify-between mb-6 pb-4 border-b border-gray-100 dark:border-white/10">
85 118 <div className="flex items-center gap-2">
86 119 <FileText className="w-4 h-4 text-blue-500" />
@@ -96,8 +129,6 @@ export default function WordPreview({ file }: PreviewPluginProps) {
96 129 Télécharger
97 130 </button>
98 131 </div>
99 −
100 − {/* Rendered Word content */}
101 132 <div
102 133 className="word-content prose prose-sm sm:prose dark:prose-invert max-w-none"
103 134 dangerouslySetInnerHTML={{ __html: html ?? "" }}
@@ -105,7 +136,6 @@ export default function WordPreview({ file }: PreviewPluginProps) {
105 136 </div>
106 137 </div>
107 138
108 − {/* Scoped styles for Word content */}
109 139 <style jsx global>{`
110 140 .word-content h1 { font-size: 1.6rem; font-weight: 700; margin-bottom: 0.5rem; margin-top: 1.5rem; }
111 141 .word-content h2 { font-size: 1.3rem; font-weight: 600; margin-bottom: 0.4rem; margin-top: 1.2rem; }
added components/preview/plugins/WordTiptapEditor.tsx +174 −0
@@ -0,0 +1,174 @@
1 +"use client";
2 +
3 +// This component is imported dynamically from WordPreview (ssr: false already set there)
4 +// so we can safely import Tiptap dependencies at the top level.
5 +import { useEditor, EditorContent } from "@tiptap/react";
6 +import StarterKit from "@tiptap/starter-kit";
7 +import UnderlineExt from "@tiptap/extension-underline";
8 +import TextAlign from "@tiptap/extension-text-align";
9 +import Highlight from "@tiptap/extension-highlight";
10 +import {
11 + Bold,
12 + Italic,
13 + Underline,
14 + List,
15 + AlignLeft,
16 + AlignCenter,
17 + AlignRight,
18 + Save,
19 + Type,
20 + Loader2,
21 +} from "lucide-react";
22 +import { useCallback, useState } from "react";
23 +
24 +interface WordTiptapEditorProps {
25 + initialHtml: string;
26 + onSave?: (blob: Blob, asNewVersion?: boolean) => Promise<void>;
27 +}
28 +
29 +export default function WordTiptapEditor({ initialHtml, onSave }: WordTiptapEditorProps) {
30 + const [saving, setSaving] = useState(false);
31 +
32 + const editor = useEditor({
33 + immediatelyRender: false,
34 + extensions: [
35 + StarterKit,
36 + UnderlineExt,
37 + TextAlign.configure({ types: ["heading", "paragraph"] }),
38 + Highlight,
39 + ],
40 + content: initialHtml,
41 + editorProps: {
42 + attributes: {
43 + class: "word-content prose prose-sm sm:prose dark:prose-invert max-w-none focus:outline-none min-h-[400px] tiptap-editor",
44 + },
45 + },
46 + });
47 +
48 + const handleSave = useCallback(async () => {
49 + if (!editor || !onSave) return;
50 + setSaving(true);
51 + try {
52 + const html = editor.getHTML();
53 + const fullHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>${html}</body></html>`;
54 + // Dynamically import browser-compatible dist bundle (avoids Node fs dep)
55 + const htmlDocx = await import("html-docx-js/dist/html-docx");
56 + const blob = htmlDocx.asBlob(fullHtml, {
57 + orientation: "portrait",
58 + margins: { top: 720, bottom: 720, left: 1080, right: 1080 },
59 + });
60 + await onSave(blob, true);
61 + } finally {
62 + setSaving(false);
63 + }
64 + }, [editor, onSave]);
65 +
66 + const btn = (active: boolean, onClick: () => void, title: string, children: React.ReactNode) => (
67 + <button
68 + onMouseDown={(e) => { e.preventDefault(); onClick(); }}
69 + title={title}
70 + className={`p-1.5 rounded transition-colors min-w-[32px] min-h-[32px] flex items-center justify-center ${
71 + active
72 + ? "bg-blue-100 dark:bg-blue-900/40 text-blue-600 dark:text-blue-400"
73 + : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-white/10"
74 + }`}
75 + >
76 + {children}
77 + </button>
78 + );
79 +
80 + if (!editor) return (
81 + <div className="flex items-center justify-center h-full gap-3">
82 + <Loader2 className="w-6 h-6 animate-spin text-blue-400" />
83 + </div>
84 + );
85 +
86 + return (
87 + <div className="absolute inset-0 flex flex-col bg-gray-50 dark:bg-[#1a1a1a]">
88 + {/* Toolbar — sticky so it never scrolls out of view */}
89 + <div className="flex items-center gap-0.5 px-3 py-2 bg-white dark:bg-[#242424] border-b border-gray-200 dark:border-white/10 overflow-x-auto shrink-0 flex-nowrap">
90 + {btn(editor.isActive("bold"), () => editor.chain().focus().toggleBold().run(), "Gras", <Bold className="w-3.5 h-3.5" />)}
91 + {btn(editor.isActive("italic"), () => editor.chain().focus().toggleItalic().run(), "Italique", <Italic className="w-3.5 h-3.5" />)}
92 + {btn(editor.isActive("underline"), () => editor.chain().focus().toggleUnderline().run(), "Souligné", <Underline className="w-3.5 h-3.5" />)}
93 +
94 + <div className="w-px h-5 bg-gray-200 dark:bg-white/10 mx-1" />
95 +
96 + {btn(
97 + editor.isActive("heading", { level: 1 }),
98 + () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
99 + "Titre 1",
100 + <span className="text-xs font-bold">H1</span>
101 + )}
102 + {btn(
103 + editor.isActive("heading", { level: 2 }),
104 + () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
105 + "Titre 2",
106 + <span className="text-xs font-bold">H2</span>
107 + )}
108 + {btn(
109 + editor.isActive("heading", { level: 3 }),
110 + () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
111 + "Titre 3",
112 + <span className="text-xs font-bold">H3</span>
113 + )}
114 + {btn(false, () => editor.chain().focus().setParagraph().run(), "Paragraphe", <Type className="w-3.5 h-3.5" />)}
115 +
116 + <div className="w-px h-5 bg-gray-200 dark:bg-white/10 mx-1" />
117 +
118 + {btn(editor.isActive("bulletList"), () => editor.chain().focus().toggleBulletList().run(), "Liste à puces", <List className="w-3.5 h-3.5" />)}
119 +
120 + <div className="w-px h-5 bg-gray-200 dark:bg-white/10 mx-1" />
121 +
122 + {btn(editor.isActive({ textAlign: "left" }), () => editor.chain().focus().setTextAlign("left").run(), "Gauche", <AlignLeft className="w-3.5 h-3.5" />)}
123 + {btn(editor.isActive({ textAlign: "center" }), () => editor.chain().focus().setTextAlign("center").run(), "Centré", <AlignCenter className="w-3.5 h-3.5" />)}
124 + {btn(editor.isActive({ textAlign: "right" }), () => editor.chain().focus().setTextAlign("right").run(), "Droite", <AlignRight className="w-3.5 h-3.5" />)}
125 +
126 + <div className="flex-1" />
127 +
128 + <button
129 + onClick={handleSave}
130 + disabled={saving}
131 + className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors disabled:opacity-60"
132 + >
133 + {saving ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
134 + Sauvegarder
135 + </button>
136 + </div>
137 +
138 + {/* Editor content area */}
139 + <div className="flex-1 overflow-auto py-6">
140 + <div className="max-w-3xl mx-auto px-4 sm:px-6">
141 + <div className="bg-white dark:bg-[#242424] shadow-md rounded-sm px-8 py-10 sm:px-14 sm:py-12 min-h-[500px]">
142 + <EditorContent editor={editor} />
143 + </div>
144 + </div>
145 + </div>
146 +
147 + <style jsx global>{`
148 + .tiptap-editor { min-height: 400px; outline: none; }
149 + .tiptap-editor p.is-editor-empty:first-child::before {
150 + content: attr(data-placeholder);
151 + color: #9ca3af;
152 + pointer-events: none;
153 + float: left;
154 + height: 0;
155 + }
156 + .word-content h1 { font-size: 1.6rem; font-weight: 700; margin-bottom: 0.5rem; margin-top: 1.5rem; }
157 + .word-content h2 { font-size: 1.3rem; font-weight: 600; margin-bottom: 0.4rem; margin-top: 1.2rem; }
158 + .word-content h3 { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.3rem; margin-top: 1rem; }
159 + .word-content p { margin-bottom: 0.6rem; line-height: 1.7; }
160 + .word-content ul, .word-content ol { padding-left: 1.5rem; margin-bottom: 0.6rem; }
161 + .word-content li { margin-bottom: 0.2rem; }
162 + .word-content table { border-collapse: collapse; width: 100%; margin-bottom: 1rem; }
163 + .word-content td, .word-content th { border: 1px solid #d1d5db; padding: 0.4rem 0.6rem; text-align: left; }
164 + .word-content th { background: #f3f4f6; font-weight: 600; }
165 + .dark .word-content td, .dark .word-content th { border-color: rgba(255,255,255,0.15); }
166 + .dark .word-content th { background: rgba(255,255,255,0.08); }
167 + .word-content strong { font-weight: 700; }
168 + .word-content em { font-style: italic; }
169 + .word-content a { color: #3b82f6; text-decoration: underline; }
170 + .word-content img { max-width: 100%; height: auto; margin: 0.5rem 0; }
171 + `}</style>
172 + </div>
173 + );
174 +}
modified components/preview/plugins/index.ts +19 −0
@@ -12,6 +12,7 @@ const CsvJsonPreview = dynamic(() => import("./CsvJsonPreview"), { ssr: false })
12 12 const MarkdownPreview = dynamic(() => import("./MarkdownPreview"), { ssr: false });
13 13 const ArchivePreview = dynamic(() => import("./ArchivePreview"), { ssr: false });
14 14 const WordPreview = dynamic(() => import("./WordPreview"), { ssr: false });
15 +const ExcelPreview = dynamic(() => import("./ExcelPreview"), { ssr: false });
15 16 const FallbackPreview = dynamic(() => import("./FallbackPreview"), { ssr: false });
16 17
17 18 const WORD_MIMES = [
@@ -20,6 +21,14 @@ const WORD_MIMES = [
20 21 "application/vnd.oasis.opendocument.text", // .odt
21 22 ];
22 23
24 +const EXCEL_MIMES = [
25 + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx
26 + "application/vnd.ms-excel", // .xls
27 + "application/vnd.ms-excel.sheet.macroEnabled.12", // .xlsm
28 + "application/vnd.ms-excel.sheet.binary.macroEnabled.12", // .xlsb
29 + "application/vnd.oasis.opendocument.spreadsheet", // .ods
30 +];
31 +
23 32 const plugins: PreviewPlugin[] = [
24 33 {
25 34 id: "image",
@@ -48,6 +57,7 @@ const plugins: PreviewPlugin[] = [
48 57 canHandle: (mime) => mime === "application/pdf",
49 58 priority: 10,
50 59 component: PdfPreview as any,
60 + supportsEdit: true,
51 61 },
52 62 {
53 63 id: "csv-json",
@@ -85,6 +95,15 @@ const plugins: PreviewPlugin[] = [
85 95 canHandle: (mime) => WORD_MIMES.includes(mime),
86 96 priority: 15,
87 97 component: WordPreview as any,
98 + supportsEdit: true,
99 + },
100 + {
101 + id: "excel",
102 + name: "Excel",
103 + canHandle: (mime) => EXCEL_MIMES.includes(mime),
104 + priority: 15,
105 + component: ExcelPreview as any,
106 + supportsEdit: true,
88 107 },
89 108 {
90 109 id: "archive",
modified package-lock.json +1390 −4
@@ -31,10 +31,17 @@
31 31 "@codemirror/theme-one-dark": "^6.1.3",
32 32 "@codemirror/view": "^6.39.14",
33 33 "@prisma/client": "^5.22.0",
34 + "@tiptap/extension-highlight": "^3.20.0",
35 + "@tiptap/extension-text-align": "^3.20.0",
36 + "@tiptap/extension-underline": "^3.20.0",
37 + "@tiptap/pm": "^3.20.0",
38 + "@tiptap/react": "^3.20.0",
39 + "@tiptap/starter-kit": "^3.20.0",
34 40 "archiver": "^7.0.1",
35 41 "bcryptjs": "^3.0.3",
36 42 "diff": "^8.0.3",
37 43 "framer-motion": "^12.34.0",
44 + "html-docx-js": "^0.3.1",
38 45 "iron-session": "^8.0.4",
39 46 "jszip": "^3.10.1",
40 47 "lucide-react": "^0.564.0",
@@ -54,14 +61,17 @@
54 61 "react-hot-toast": "^2.6.0",
55 62 "react-markdown": "^10.1.0",
56 63 "react-syntax-highlighter": "^16.1.0",
64 + "remark-gfm": "^4.0.1",
57 65 "sharp": "^0.34.5",
58 66 "uuid": "^13.0.0",
59 − "wavesurfer.js": "^7.12.1"
67 + "wavesurfer.js": "^7.12.1",
68 + "xlsx": "^0.18.5"
60 69 },
61 70 "devDependencies": {
62 71 "@types/archiver": "^7.0.0",
63 72 "@types/bcryptjs": "^2.4.6",
64 73 "@types/diff": "^7.0.2",
74 + "@types/html-docx-js": "^0.3.4",
65 75 "@types/mime-types": "^3.0.1",
66 76 "@types/node": "^20",
67 77 "@types/papaparse": "^5.5.2",
@@ -919,6 +929,34 @@
919 929 "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
920 930 }
921 931 },
932 + "node_modules/@floating-ui/core": {
933 + "version": "1.7.4",
934 + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz",
935 + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==",
936 + "license": "MIT",
937 + "optional": true,
938 + "dependencies": {
939 + "@floating-ui/utils": "^0.2.10"
940 + }
941 + },
942 + "node_modules/@floating-ui/dom": {
943 + "version": "1.7.5",
944 + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz",
945 + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==",
946 + "license": "MIT",
947 + "optional": true,
948 + "dependencies": {
949 + "@floating-ui/core": "^1.7.4",
950 + "@floating-ui/utils": "^0.2.10"
951 + }
952 + },
953 + "node_modules/@floating-ui/utils": {
954 + "version": "0.2.10",
955 + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
956 + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
957 + "license": "MIT",
958 + "optional": true
959 + },
922 960 "node_modules/@humanwhocodes/config-array": {
923 961 "version": "0.13.0",
924 962 "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -2296,6 +2334,12 @@
2296 2334 "@prisma/debug": "5.22.0"
2297 2335 }
2298 2336 },
2337 + "node_modules/@remirror/core-constants": {
2338 + "version": "3.0.0",
2339 + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
2340 + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
2341 + "license": "MIT"
2342 + },
2299 2343 "node_modules/@rtsao/scc": {
2300 2344 "version": "1.1.0",
2301 2345 "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -2335,6 +2379,466 @@
2335 2379 "tslib": "^2.4.0"
2336 2380 }
2337 2381 },
2382 + "node_modules/@tiptap/core": {
2383 + "version": "3.20.0",
2384 + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.0.tgz",
2385 + "integrity": "sha512-aC9aROgia/SpJqhsXFiX9TsligL8d+oeoI8W3u00WI45s0VfsqjgeKQLDLF7Tu7hC+7F02teC84SAHuup003VQ==",
2386 + "license": "MIT",
2387 + "funding": {
2388 + "type": "github",
2389 + "url": "https://github.com/sponsors/ueberdosis"
2390 + },
2391 + "peerDependencies": {
2392 + "@tiptap/pm": "^3.20.0"
2393 + }
2394 + },
2395 + "node_modules/@tiptap/extension-blockquote": {
2396 + "version": "3.20.0",
2397 + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.20.0.tgz",
2398 + "integrity": "sha512-LQzn6aGtL4WXz2+rYshl/7/VnP2qJTpD7fWL96GXAzhqviPEY1bJES7poqJb3MU/gzl8VJUVzVzU1VoVfUKlbA==",
2399 + "license": "MIT",
2400 + "funding": {
2401 + "type": "github",
2402 + "url": "https://github.com/sponsors/ueberdosis"
2403 + },
2404 + "peerDependencies": {
2405 + "@tiptap/core": "^3.20.0"
2406 + }
2407 + },
2408 + "node_modules/@tiptap/extension-bold": {
2409 + "version": "3.20.0",
2410 + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.20.0.tgz",
2411 + "integrity": "sha512-sQklEWiyf58yDjiHtm5vmkVjfIc/cBuSusmCsQ0q9vGYnEF1iOHKhGpvnCeEXNeqF3fiJQRlquzt/6ymle3Iwg==",
2412 + "license": "MIT",
2413 + "funding": {
2414 + "type": "github",
2415 + "url": "https://github.com/sponsors/ueberdosis"
2416 + },
2417 + "peerDependencies": {
2418 + "@tiptap/core": "^3.20.0"
2419 + }
2420 + },
2421 + "node_modules/@tiptap/extension-bubble-menu": {
2422 + "version": "3.20.0",
2423 + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.20.0.tgz",
2424 + "integrity": "sha512-MDosUfs8Tj+nwg8RC+wTMWGkLJORXmbR6YZgbiX4hrc7G90Gopdd6kj6ht5/T8t7dLLaX7N0+DEHdUEPGED7dw==",
2425 + "license": "MIT",
2426 + "optional": true,
2427 + "dependencies": {
2428 + "@floating-ui/dom": "^1.0.0"
2429 + },
2430 + "funding": {
2431 + "type": "github",
2432 + "url": "https://github.com/sponsors/ueberdosis"
2433 + },
2434 + "peerDependencies": {
2435 + "@tiptap/core": "^3.20.0",
2436 + "@tiptap/pm": "^3.20.0"
2437 + }
2438 + },
2439 + "node_modules/@tiptap/extension-bullet-list": {
2440 + "version": "3.20.0",
2441 + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.0.tgz",
2442 + "integrity": "sha512-OcKMeopBbqWzhSi6o8nNz0aayogg1sfOAhto3NxJu3Ya32dwBFqmHXSYM6uW4jOphNvVPyjiq9aNRh3qTdd1dw==",
2443 + "license": "MIT",
2444 + "funding": {
2445 + "type": "github",
2446 + "url": "https://github.com/sponsors/ueberdosis"
2447 + },
2448 + "peerDependencies": {
2449 + "@tiptap/extension-list": "^3.20.0"
2450 + }
2451 + },
2452 + "node_modules/@tiptap/extension-code": {
2453 + "version": "3.20.0",
2454 + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.20.0.tgz",
2455 + "integrity": "sha512-TYDWFeSQ9umiyrqsT6VecbuhL8XIHkUhO+gEk0sVvH67ZLwjFDhAIIgWIr1/dbIGPcvMZM19E7xUUhAdIaXaOQ==",
2456 + "license": "MIT",
2457 + "funding": {
2458 + "type": "github",
2459 + "url": "https://github.com/sponsors/ueberdosis"
2460 + },
2461 + "peerDependencies": {
2462 + "@tiptap/core": "^3.20.0"
2463 + }
2464 + },
2465 + "node_modules/@tiptap/extension-code-block": {
2466 + "version": "3.20.0",
2467 + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.0.tgz",
2468 + "integrity": "sha512-lBbmNek14aCjrHcBcq3PRqWfNLvC6bcRa2Osc6e/LtmXlcpype4f6n+Yx+WZ+f2uUh0UmDRCz7BEyUETEsDmlQ==",
2469 + "license": "MIT",
2470 + "funding": {
2471 + "type": "github",
2472 + "url": "https://github.com/sponsors/ueberdosis"
2473 + },
2474 + "peerDependencies": {
2475 + "@tiptap/core": "^3.20.0",
2476 + "@tiptap/pm": "^3.20.0"
2477 + }
2478 + },
2479 + "node_modules/@tiptap/extension-document": {
2480 + "version": "3.20.0",
2481 + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.20.0.tgz",
2482 + "integrity": "sha512-oJfLIG3vAtZo/wg29WiBcyWt22KUgddpP8wqtCE+kY5Dw8znLR9ehNmVWlSWJA5OJUMO0ntAHx4bBT+I2MBd5w==",
2483 + "license": "MIT",
2484 + "funding": {
2485 + "type": "github",
2486 + "url": "https://github.com/sponsors/ueberdosis"
2487 + },
2488 + "peerDependencies": {
2489 + "@tiptap/core": "^3.20.0"
2490 + }
2491 + },
2492 + "node_modules/@tiptap/extension-dropcursor": {
2493 + "version": "3.20.0",
2494 + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.0.tgz",
2495 + "integrity": "sha512-d+cxplRlktVgZPwatnc34IArlppM0IFKS1J5wLk+ba1jidizsbMVh45tP/BTK2flhyfRqcNoB5R0TArhUpbkNQ==",
2496 + "license": "MIT",
2497 + "funding": {
2498 + "type": "github",
2499 + "url": "https://github.com/sponsors/ueberdosis"
2500 + },
2501 + "peerDependencies": {
2502 + "@tiptap/extensions": "^3.20.0"
2503 + }
2504 + },
2505 + "node_modules/@tiptap/extension-floating-menu": {
2506 + "version": "3.20.0",
2507 + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.20.0.tgz",
2508 + "integrity": "sha512-rYs4Bv5pVjqZ/2vvR6oe7ammZapkAwN51As/WDbemvYDjfOGRqK58qGauUjYZiDzPOEIzI2mxGwsZ4eJhPW4Ig==",
2509 + "license": "MIT",
2510 + "optional": true,
2511 + "funding": {
2512 + "type": "github",
2513 + "url": "https://github.com/sponsors/ueberdosis"
2514 + },
2515 + "peerDependencies": {
2516 + "@floating-ui/dom": "^1.0.0",
2517 + "@tiptap/core": "^3.20.0",
2518 + "@tiptap/pm": "^3.20.0"
2519 + }
2520 + },
2521 + "node_modules/@tiptap/extension-gapcursor": {
2522 + "version": "3.20.0",
2523 + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.0.tgz",
2524 + "integrity": "sha512-P/LasfvG9/qFq43ZAlNbAnPnXC+/RJf49buTrhtFvI9Zg0+Lbpjx1oh6oMHB19T88Y28KtrckfFZ8aTSUWDq6w==",
2525 + "license": "MIT",
2526 + "funding": {
2527 + "type": "github",
2528 + "url": "https://github.com/sponsors/ueberdosis"
2529 + },
2530 + "peerDependencies": {
2531 + "@tiptap/extensions": "^3.20.0"
2532 + }
2533 + },
2534 + "node_modules/@tiptap/extension-hard-break": {
2535 + "version": "3.20.0",
2536 + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.20.0.tgz",
2537 + "integrity": "sha512-rqvhMOw4f+XQmEthncbvDjgLH6fz8L9splnKZC7OeS0eX8b0qd7+xI1u5kyxF3KA2Z0BnigES++jjWuecqV6mA==",
2538 + "license": "MIT",
2539 + "funding": {
2540 + "type": "github",
2541 + "url": "https://github.com/sponsors/ueberdosis"
2542 + },
2543 + "peerDependencies": {
2544 + "@tiptap/core": "^3.20.0"
2545 + }
2546 + },
2547 + "node_modules/@tiptap/extension-heading": {
2548 + "version": "3.20.0",
2549 + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.20.0.tgz",
2550 + "integrity": "sha512-JgJhurnCe3eN6a0lEsNQM/46R1bcwzwWWZEFDSb1P9dR8+t1/5v7cMZWsSInpD7R4/74iJn0+M5hcXLwCmBmYA==",
2551 + "license": "MIT",
2552 + "funding": {
2553 + "type": "github",
2554 + "url": "https://github.com/sponsors/ueberdosis"
2555 + },
2556 + "peerDependencies": {
2557 + "@tiptap/core": "^3.20.0"
2558 + }
2559 + },
2560 + "node_modules/@tiptap/extension-highlight": {
2561 + "version": "3.20.0",
2562 + "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.20.0.tgz",
2563 + "integrity": "sha512-ANL1wFz0s1ScNR4uBfO0s6Sz+qqGp2u6ynrCVk6TCT3d10CQ+gD1gSDTrVRC3CtlMKtHHH4fYrFAq284+J0gKA==",
2564 + "license": "MIT",
2565 + "funding": {
2566 + "type": "github",
2567 + "url": "https://github.com/sponsors/ueberdosis"
2568 + },
2569 + "peerDependencies": {
2570 + "@tiptap/core": "^3.20.0"
2571 + }
2572 + },
2573 + "node_modules/@tiptap/extension-horizontal-rule": {
2574 + "version": "3.20.0",
2575 + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.0.tgz",
2576 + "integrity": "sha512-6uvcutFMv+9wPZgptDkbRDjAm3YVxlibmkhWD5GuaWwS9L/yUtobpI3GycujRSUZ8D3q6Q9J7LqpmQtQRTalWA==",
2577 + "license": "MIT",
2578 + "funding": {
2579 + "type": "github",
2580 + "url": "https://github.com/sponsors/ueberdosis"
2581 + },
2582 + "peerDependencies": {
2583 + "@tiptap/core": "^3.20.0",
2584 + "@tiptap/pm": "^3.20.0"
2585 + }
2586 + },
2587 + "node_modules/@tiptap/extension-italic": {
2588 + "version": "3.20.0",
2589 + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.20.0.tgz",
2590 + "integrity": "sha512-/DhnKQF8yN8RxtuL8abZ28wd5281EaGoE2Oha35zXSOF1vNYnbyt8Ymkv/7u1BcWEWTvRPgaju0YCGXisPRLYw==",
2591 + "license": "MIT",
2592 + "funding": {
2593 + "type": "github",
2594 + "url": "https://github.com/sponsors/ueberdosis"
2595 + },
2596 + "peerDependencies": {
2597 + "@tiptap/core": "^3.20.0"
2598 + }
2599 + },
2600 + "node_modules/@tiptap/extension-link": {
2601 + "version": "3.20.0",
2602 + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.20.0.tgz",
2603 + "integrity": "sha512-qI/5A+R0ZWBxo/8HxSn1uOyr7odr3xHBZ/gzOR1GUJaZqjlJxkWFX0RtXMbLKEGEvT25o345cF7b0wFznEh8qA==",
2604 + "license": "MIT",
2605 + "dependencies": {
2606 + "linkifyjs": "^4.3.2"
2607 + },
2608 + "funding": {
2609 + "type": "github",
2610 + "url": "https://github.com/sponsors/ueberdosis"
2611 + },
2612 + "peerDependencies": {
2613 + "@tiptap/core": "^3.20.0",
2614 + "@tiptap/pm": "^3.20.0"
2615 + }
2616 + },
2617 + "node_modules/@tiptap/extension-list": {
2618 + "version": "3.20.0",
2619 + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.0.tgz",
2620 + "integrity": "sha512-+V0/gsVWAv+7vcY0MAe6D52LYTIicMSHw00wz3ISZgprSb2yQhJ4+4gurOnUrQ4Du3AnRQvxPROaofwxIQ66WQ==",
2621 + "license": "MIT",
2622 + "funding": {
2623 + "type": "github",
2624 + "url": "https://github.com/sponsors/ueberdosis"
2625 + },
2626 + "peerDependencies": {
2627 + "@tiptap/core": "^3.20.0",
2628 + "@tiptap/pm": "^3.20.0"
2629 + }
2630 + },
2631 + "node_modules/@tiptap/extension-list-item": {
2632 + "version": "3.20.0",
2633 + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.20.0.tgz",
2634 + "integrity": "sha512-qEtjaaGPuqaFB4VpLrGDoIe9RHnckxPfu6d3rc22ap6TAHCDyRv05CEyJogqccnFceG/v5WN4znUBER8RWnWHA==",
2635 + "license": "MIT",
2636 + "funding": {
2637 + "type": "github",
2638 + "url": "https://github.com/sponsors/ueberdosis"
2639 + },
2640 + "peerDependencies": {
2641 + "@tiptap/extension-list": "^3.20.0"
2642 + }
2643 + },
2644 + "node_modules/@tiptap/extension-list-keymap": {
2645 + "version": "3.20.0",
2646 + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.0.tgz",
2647 + "integrity": "sha512-Z4GvKy04Ms4cLFN+CY6wXswd36xYsT2p/YL0V89LYFMZTerOeTjFYlndzn6svqL8NV1PRT5Diw4WTTxJSmcJPA==",
2648 + "license": "MIT",
2649 + "funding": {
2650 + "type": "github",
2651 + "url": "https://github.com/sponsors/ueberdosis"
2652 + },
2653 + "peerDependencies": {
2654 + "@tiptap/extension-list": "^3.20.0"
2655 + }
2656 + },
2657 + "node_modules/@tiptap/extension-ordered-list": {
2658 + "version": "3.20.0",
2659 + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.0.tgz",
2660 + "integrity": "sha512-jVKnJvrizLk7etwBMfyoj6H2GE4M+PD4k7Bwp6Bh1ohBWtfIA1TlngdS842Mx5i1VB2e3UWIwr8ZH46gl6cwMA==",
2661 + "license": "MIT",
2662 + "funding": {
2663 + "type": "github",
2664 + "url": "https://github.com/sponsors/ueberdosis"
2665 + },
2666 + "peerDependencies": {
2667 + "@tiptap/extension-list": "^3.20.0"
2668 + }
2669 + },
2670 + "node_modules/@tiptap/extension-paragraph": {
2671 + "version": "3.20.0",
2672 + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.20.0.tgz",
2673 + "integrity": "sha512-mM99zK4+RnEXIMCv6akfNATAs0Iija6FgyFA9J9NZ6N4o8y9QiNLLa6HjLpAC+W+VoCgQIekyoF/Q9ftxmAYDQ==",
2674 + "license": "MIT",
2675 + "funding": {
2676 + "type": "github",
2677 + "url": "https://github.com/sponsors/ueberdosis"
2678 + },
2679 + "peerDependencies": {
2680 + "@tiptap/core": "^3.20.0"
2681 + }
2682 + },
2683 + "node_modules/@tiptap/extension-strike": {
2684 + "version": "3.20.0",
2685 + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.20.0.tgz",
2686 + "integrity": "sha512-0vcTZRRAiDfon3VM1mHBr9EFmTkkUXMhm0Xtdtn0bGe+sIqufyi+hUYTEw93EQOD9XNsPkrud6jzQNYpX2H3AQ==",
2687 + "license": "MIT",
2688 + "funding": {
2689 + "type": "github",
2690 + "url": "https://github.com/sponsors/ueberdosis"
2691 + },
2692 + "peerDependencies": {
2693 + "@tiptap/core": "^3.20.0"
2694 + }
2695 + },
2696 + "node_modules/@tiptap/extension-text": {
2697 + "version": "3.20.0",
2698 + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.20.0.tgz",
2699 + "integrity": "sha512-tf8bE8tSaOEWabCzPm71xwiUhyMFKqY9jkP5af3Kr1/F45jzZFIQAYZooHI/+zCHRrgJ99MQHKHe1ZNvODrKHQ==",
2700 + "license": "MIT",
2701 + "funding": {
2702 + "type": "github",
2703 + "url": "https://github.com/sponsors/ueberdosis"
2704 + },
2705 + "peerDependencies": {
2706 + "@tiptap/core": "^3.20.0"
2707 + }
2708 + },
2709 + "node_modules/@tiptap/extension-text-align": {
2710 + "version": "3.20.0",
2711 + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.20.0.tgz",
2712 + "integrity": "sha512-4s0r+bovtH6yeGDUD+Ui8j5WOV5koB5P6AuzOMqoLwaFGRSkKf64ly6DXjjmjIgnYCLZN/XO6llaQKVVdvad2g==",
2713 + "license": "MIT",
2714 + "funding": {
2715 + "type": "github",
2716 + "url": "https://github.com/sponsors/ueberdosis"
2717 + },
2718 + "peerDependencies": {
2719 + "@tiptap/core": "^3.20.0"
2720 + }
2721 + },
2722 + "node_modules/@tiptap/extension-underline": {
2723 + "version": "3.20.0",
2724 + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.20.0.tgz",
2725 + "integrity": "sha512-LzNXuy2jwR/y+ymoUqC72TiGzbOCjioIjsDu0MNYpHuHqTWPK5aV9Mh0nbZcYFy/7fPlV1q0W139EbJeYBZEAQ==",
2726 + "license": "MIT",
2727 + "funding": {
2728 + "type": "github",
2729 + "url": "https://github.com/sponsors/ueberdosis"
2730 + },
2731 + "peerDependencies": {
2732 + "@tiptap/core": "^3.20.0"
2733 + }
2734 + },
2735 + "node_modules/@tiptap/extensions": {
2736 + "version": "3.20.0",
2737 + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.0.tgz",
2738 + "integrity": "sha512-HIsXX942w3nbxEQBlMAAR/aa6qiMBEP7CsSMxaxmTIVAmW35p6yUASw6GdV1u0o3lCZjXq2OSRMTskzIqi5uLg==",
2739 + "license": "MIT",
2740 + "funding": {
2741 + "type": "github",
2742 + "url": "https://github.com/sponsors/ueberdosis"
2743 + },
2744 + "peerDependencies": {
2745 + "@tiptap/core": "^3.20.0",
2746 + "@tiptap/pm": "^3.20.0"
2747 + }
2748 + },
2749 + "node_modules/@tiptap/pm": {
2750 + "version": "3.20.0",
2751 + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.0.tgz",
2752 + "integrity": "sha512-jn+2KnQZn+b+VXr8EFOJKsnjVNaA4diAEr6FOazupMt8W8ro1hfpYtZ25JL87Kao/WbMze55sd8M8BDXLUKu1A==",
2753 + "license": "MIT",
2754 + "dependencies": {
2755 + "prosemirror-changeset": "^2.3.0",
2756 + "prosemirror-collab": "^1.3.1",
2757 + "prosemirror-commands": "^1.6.2",
2758 + "prosemirror-dropcursor": "^1.8.1",
2759 + "prosemirror-gapcursor": "^1.3.2",
2760 + "prosemirror-history": "^1.4.1",
2761 + "prosemirror-inputrules": "^1.4.0",
2762 + "prosemirror-keymap": "^1.2.2",
2763 + "prosemirror-markdown": "^1.13.1",
2764 + "prosemirror-menu": "^1.2.4",
2765 + "prosemirror-model": "^1.24.1",
2766 + "prosemirror-schema-basic": "^1.2.3",
2767 + "prosemirror-schema-list": "^1.5.0",
2768 + "prosemirror-state": "^1.4.3",
2769 + "prosemirror-tables": "^1.6.4",
2770 + "prosemirror-trailing-node": "^3.0.0",
2771 + "prosemirror-transform": "^1.10.2",
2772 + "prosemirror-view": "^1.38.1"
2773 + },
2774 + "funding": {
2775 + "type": "github",
2776 + "url": "https://github.com/sponsors/ueberdosis"
2777 + }
2778 + },
2779 + "node_modules/@tiptap/react": {
2780 + "version": "3.20.0",
2781 + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.20.0.tgz",
2782 + "integrity": "sha512-jFLNzkmn18zqefJwPje0PPd9VhZ7Oy28YHiSvSc7YpBnQIbuN/HIxZ2lrOsKyEHta0WjRZjfU5X1pGxlbcGwOA==",
2783 + "license": "MIT",
2784 + "dependencies": {
2785 + "@types/use-sync-external-store": "^0.0.6",
2786 + "fast-equals": "^5.3.3",
2787 + "use-sync-external-store": "^1.4.0"
2788 + },
2789 + "funding": {
2790 + "type": "github",
2791 + "url": "https://github.com/sponsors/ueberdosis"
2792 + },
2793 + "optionalDependencies": {
2794 + "@tiptap/extension-bubble-menu": "^3.20.0",
2795 + "@tiptap/extension-floating-menu": "^3.20.0"
2796 + },
2797 + "peerDependencies": {
2798 + "@tiptap/core": "^3.20.0",
2799 + "@tiptap/pm": "^3.20.0",
2800 + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
2801 + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
2802 + "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
2803 + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
2804 + }
2805 + },
2806 + "node_modules/@tiptap/starter-kit": {
2807 + "version": "3.20.0",
2808 + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.20.0.tgz",
2809 + "integrity": "sha512-W4+1re35pDNY/7rpXVg+OKo/Fa4Gfrn08Bq3E3fzlJw6gjE3tYU8dY9x9vC2rK9pd9NOp7Af11qCFDaWpohXkw==",
2810 + "license": "MIT",
2811 + "dependencies": {
2812 + "@tiptap/core": "^3.20.0",
2813 + "@tiptap/extension-blockquote": "^3.20.0",
2814 + "@tiptap/extension-bold": "^3.20.0",
2815 + "@tiptap/extension-bullet-list": "^3.20.0",
2816 + "@tiptap/extension-code": "^3.20.0",
2817 + "@tiptap/extension-code-block": "^3.20.0",
2818 + "@tiptap/extension-document": "^3.20.0",
2819 + "@tiptap/extension-dropcursor": "^3.20.0",
2820 + "@tiptap/extension-gapcursor": "^3.20.0",
2821 + "@tiptap/extension-hard-break": "^3.20.0",
2822 + "@tiptap/extension-heading": "^3.20.0",
2823 + "@tiptap/extension-horizontal-rule": "^3.20.0",
2824 + "@tiptap/extension-italic": "^3.20.0",
2825 + "@tiptap/extension-link": "^3.20.0",
2826 + "@tiptap/extension-list": "^3.20.0",
2827 + "@tiptap/extension-list-item": "^3.20.0",
2828 + "@tiptap/extension-list-keymap": "^3.20.0",
2829 + "@tiptap/extension-ordered-list": "^3.20.0",
2830 + "@tiptap/extension-paragraph": "^3.20.0",
2831 + "@tiptap/extension-strike": "^3.20.0",
2832 + "@tiptap/extension-text": "^3.20.0",
2833 + "@tiptap/extension-underline": "^3.20.0",
2834 + "@tiptap/extensions": "^3.20.0",
2835 + "@tiptap/pm": "^3.20.0"
2836 + },
2837 + "funding": {
2838 + "type": "github",
2839 + "url": "https://github.com/sponsors/ueberdosis"
2840 + }
2841 + },
2338 2842 "node_modules/@tybys/wasm-util": {
2339 2843 "version": "0.10.1",
2340 2844 "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -2403,6 +2907,16 @@
2403 2907 "@types/unist": "*"
2404 2908 }
2405 2909 },
2910 + "node_modules/@types/html-docx-js": {
2911 + "version": "0.3.4",
2912 + "resolved": "https://registry.npmjs.org/@types/html-docx-js/-/html-docx-js-0.3.4.tgz",
2913 + "integrity": "sha512-+t/4SxNnEq+0xbSUG7LrOOfG/y12aTXBjYKtOYtigVRIIOCEmK7jh0pSUeW5ATvI934274hkzdBg57t5RDrZow==",
2914 + "dev": true,
2915 + "license": "MIT",
2916 + "dependencies": {
2917 + "@types/node": "*"
2918 + }
2919 + },
2406 2920 "node_modules/@types/json5": {
2407 2921 "version": "0.0.29",
2408 2922 "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -2410,6 +2924,22 @@
2410 2924 "dev": true,
2411 2925 "license": "MIT"
2412 2926 },
2927 + "node_modules/@types/linkify-it": {
2928 + "version": "5.0.0",
2929 + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
2930 + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
2931 + "license": "MIT"
2932 + },
2933 + "node_modules/@types/markdown-it": {
2934 + "version": "14.1.2",
2935 + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
2936 + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
2937 + "license": "MIT",
2938 + "dependencies": {
2939 + "@types/linkify-it": "^5",
2940 + "@types/mdurl": "^2"
2941 + }
2942 + },
2413 2943 "node_modules/@types/mdast": {
2414 2944 "version": "4.0.4",
2415 2945 "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
@@ -2419,6 +2949,12 @@
2419 2949 "@types/unist": "*"
2420 2950 }
2421 2951 },
2952 + "node_modules/@types/mdurl": {
2953 + "version": "2.0.0",
2954 + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
2955 + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
2956 + "license": "MIT"
2957 + },
2422 2958 "node_modules/@types/mime-types": {
2423 2959 "version": "3.0.1",
2424 2960 "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz",
@@ -2488,7 +3024,6 @@
2488 3024 "version": "18.3.7",
2489 3025 "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
2490 3026 "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
2491 − "dev": true,
2492 3027 "license": "MIT",
2493 3028 "peerDependencies": {
2494 3029 "@types/react": "^18.0.0"
@@ -2530,6 +3065,12 @@
2530 3065 "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
2531 3066 "license": "MIT"
2532 3067 },
3068 + "node_modules/@types/use-sync-external-store": {
3069 + "version": "0.0.6",
3070 + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
3071 + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
3072 + "license": "MIT"
3073 + },
2533 3074 "node_modules/@types/uuid": {
2534 3075 "version": "10.0.0",
2535 3076 "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
@@ -3125,6 +3666,15 @@
3125 3666 "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
3126 3667 }
3127 3668 },
3669 + "node_modules/adler-32": {
3670 + "version": "1.3.1",
3671 + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
3672 + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
3673 + "license": "Apache-2.0",
3674 + "engines": {
3675 + "node": ">=0.8"
3676 + }
3677 + },
3128 3678 "node_modules/ajv": {
3129 3679 "version": "6.12.6",
3130 3680 "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -3324,7 +3874,6 @@
3324 3874 "version": "2.0.1",
3325 3875 "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
3326 3876 "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
3327 − "dev": true,
3328 3877 "license": "Python-2.0"
3329 3878 },
3330 3879 "node_modules/aria-query": {
@@ -3820,6 +4369,19 @@
3820 4369 "url": "https://github.com/sponsors/wooorm"
3821 4370 }
3822 4371 },
4372 + "node_modules/cfb": {
4373 + "version": "1.2.2",
4374 + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
4375 + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
4376 + "license": "Apache-2.0",
4377 + "dependencies": {
4378 + "adler-32": "~1.3.0",
4379 + "crc-32": "~1.2.0"
4380 + },
4381 + "engines": {
4382 + "node": ">=0.8"
4383 + }
4384 + },
3823 4385 "node_modules/chalk": {
3824 4386 "version": "4.1.2",
3825 4387 "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -3966,6 +4528,15 @@
3966 4528 "node": ">=8"
3967 4529 }
3968 4530 },
4531 + "node_modules/codepage": {
4532 + "version": "1.15.0",
4533 + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
4534 + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
4535 + "license": "Apache-2.0",
4536 + "engines": {
4537 + "node": ">=0.8"
4538 + }
4539 + },
3969 4540 "node_modules/color-convert": {
3970 4541 "version": "2.0.1",
3971 4542 "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -4454,6 +5025,18 @@
4454 5025 "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
4455 5026 "license": "MIT"
4456 5027 },
5028 + "node_modules/entities": {
5029 + "version": "4.5.0",
5030 + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
5031 + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
5032 + "license": "BSD-2-Clause",
5033 + "engines": {
5034 + "node": ">=0.12"
5035 + },
5036 + "funding": {
5037 + "url": "https://github.com/fb55/entities?sponsor=1"
5038 + }
5039 + },
4457 5040 "node_modules/es-abstract": {
4458 5041 "version": "1.24.1",
4459 5042 "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
@@ -4677,7 +5260,6 @@
4677 5260 "version": "4.0.0",
4678 5261 "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
4679 5262 "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
4680 − "dev": true,
4681 5263 "license": "MIT",
4682 5264 "engines": {
4683 5265 "node": ">=10"
@@ -5184,6 +5766,15 @@
5184 5766 "dev": true,
5185 5767 "license": "MIT"
5186 5768 },
5769 + "node_modules/fast-equals": {
5770 + "version": "5.4.0",
5771 + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
5772 + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
5773 + "license": "MIT",
5774 + "engines": {
5775 + "node": ">=6.0.0"
5776 + }
5777 + },
5187 5778 "node_modules/fast-fifo": {
5188 5779 "version": "1.3.2",
5189 5780 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
@@ -5374,6 +5965,15 @@
5374 5965 "node": ">=0.4.x"
5375 5966 }
5376 5967 },
5968 + "node_modules/frac": {
5969 + "version": "1.1.2",
5970 + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
5971 + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
5972 + "license": "Apache-2.0",
5973 + "engines": {
5974 + "node": ">=0.8"
5975 + }
5976 + },
5377 5977 "node_modules/framer-motion": {
5378 5978 "version": "12.34.0",
5379 5979 "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.0.tgz",
@@ -5859,6 +6459,45 @@
5859 6459 "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
5860 6460 "license": "CC0-1.0"
5861 6461 },
6462 + "node_modules/html-docx-js": {
6463 + "version": "0.3.1",
6464 + "resolved": "https://registry.npmjs.org/html-docx-js/-/html-docx-js-0.3.1.tgz",
6465 + "integrity": "sha512-QSrMiRhxesqxYCa3f+2Z3ttIHPzSjDOL1tCOmIDIEET7HdabxXND6tAbsFMXAgRG4RADQ3wbl74ydMmjidaDPA==",
6466 + "license": "MIT",
6467 + "dependencies": {
6468 + "jszip": "^2.3.0",
6469 + "lodash.escape": "^3.0.0",
6470 + "lodash.merge": "^3.2.0"
6471 + }
6472 + },
6473 + "node_modules/html-docx-js/node_modules/jszip": {
6474 + "version": "2.7.0",
6475 + "resolved": "https://registry.npmjs.org/jszip/-/jszip-2.7.0.tgz",
6476 + "integrity": "sha512-JIsRKRVC3gTRo2vM4Wy9WBC3TRcfnIZU8k65Phi3izkvPH975FowRYtKGT6PxevA0XnJ/yO8b0QwV0ydVyQwfw==",
6477 + "license": "(MIT OR GPL-3.0)",
6478 + "dependencies": {
6479 + "pako": "~1.0.2"
6480 + }
6481 + },
6482 + "node_modules/html-docx-js/node_modules/lodash.merge": {
6483 + "version": "3.3.2",
6484 + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-3.3.2.tgz",
6485 + "integrity": "sha512-ZgGZpRhWLjivGUbjtApZR4HyLv/UAyoYqESVYkK4aLBJVHRrbFpG+GNnE9JPijliME4LkKM0SFI/WyOiBiv1+w==",
6486 + "license": "MIT",
6487 + "dependencies": {
6488 + "lodash._arraycopy": "^3.0.0",
6489 + "lodash._arrayeach": "^3.0.0",
6490 + "lodash._createassigner": "^3.0.0",
6491 + "lodash._getnative": "^3.0.0",
6492 + "lodash.isarguments": "^3.0.0",
6493 + "lodash.isarray": "^3.0.0",
6494 + "lodash.isplainobject": "^3.0.0",
6495 + "lodash.istypedarray": "^3.0.0",
6496 + "lodash.keys": "^3.0.0",
6497 + "lodash.keysin": "^3.0.0",
6498 + "lodash.toplainobject": "^3.0.0"
6499 + }
6500 + },
5862 6501 "node_modules/html-url-attributes": {
5863 6502 "version": "3.0.1",
5864 6503 "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@@ -6735,6 +7374,21 @@
6735 7374 "dev": true,
6736 7375 "license": "MIT"
6737 7376 },
7377 + "node_modules/linkify-it": {
7378 + "version": "5.0.0",
7379 + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
7380 + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
7381 + "license": "MIT",
7382 + "dependencies": {
7383 + "uc.micro": "^2.0.0"
7384 + }
7385 + },
7386 + "node_modules/linkifyjs": {
7387 + "version": "4.3.2",
7388 + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
7389 + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
7390 + "license": "MIT"
7391 + },
6738 7392 "node_modules/locate-path": {
6739 7393 "version": "6.0.0",
6740 7394 "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -6757,6 +7411,124 @@
6757 7411 "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
6758 7412 "license": "MIT"
6759 7413 },
7414 + "node_modules/lodash._arraycopy": {
7415 + "version": "3.0.0",
7416 + "resolved": "https://registry.npmjs.org/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz",
7417 + "integrity": "sha512-RHShTDnPKP7aWxlvXKiDT6IX2jCs6YZLCtNhOru/OX2Q/tzX295vVBK5oX1ECtN+2r86S0Ogy8ykP1sgCZAN0A==",
7418 + "license": "MIT"
7419 + },
7420 + "node_modules/lodash._arrayeach": {
7421 + "version": "3.0.0",
7422 + "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz",
7423 + "integrity": "sha512-Mn7HidOVcl3mkQtbPsuKR0Fj0N6Q6DQB77CtYncZcJc0bx5qv2q4Gl6a0LC1AN+GSxpnBDNnK3CKEm9XNA4zqQ==",
7424 + "license": "MIT"
7425 + },
7426 + "node_modules/lodash._basecopy": {
7427 + "version": "3.0.1",
7428 + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
7429 + "integrity": "sha512-rFR6Vpm4HeCK1WPGvjZSJ+7yik8d8PVUdCJx5rT2pogG4Ve/2ZS7kfmO5l5T2o5V2mqlNIfSF5MZlr1+xOoYQQ==",
7430 + "license": "MIT"
7431 + },
7432 + "node_modules/lodash._basefor": {
7433 + "version": "3.0.3",
7434 + "resolved": "https://registry.npmjs.org/lodash._basefor/-/lodash._basefor-3.0.3.tgz",
7435 + "integrity": "sha512-6bc3b8grkpMgDcVJv9JYZAk/mHgcqMljzm7OsbmcE2FGUMmmLQTPHlh/dFqR8LA0GQ7z4K67JSotVKu5058v1A==",
7436 + "license": "MIT"
7437 + },
7438 + "node_modules/lodash._bindcallback": {
7439 + "version": "3.0.1",
7440 + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz",
7441 + "integrity": "sha512-2wlI0JRAGX8WEf4Gm1p/mv/SZ+jLijpj0jyaE/AXeuQphzCgD8ZQW4oSpoN8JAopujOFGU3KMuq7qfHBWlGpjQ==",
7442 + "license": "MIT"
7443 + },
7444 + "node_modules/lodash._createassigner": {
7445 + "version": "3.1.1",
7446 + "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz",
7447 + "integrity": "sha512-LziVL7IDnJjQeeV95Wvhw6G28Z8Q6da87LWKOPWmzBLv4u6FAT/x5v00pyGW0u38UoogNF2JnD3bGgZZDaNEBw==",
7448 + "license": "MIT",
7449 + "dependencies": {
7450 + "lodash._bindcallback": "^3.0.0",
7451 + "lodash._isiterateecall": "^3.0.0",
7452 + "lodash.restparam": "^3.0.0"
7453 + }
7454 + },
7455 + "node_modules/lodash._getnative": {
7456 + "version": "3.9.1",
7457 + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
7458 + "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==",
7459 + "license": "MIT"
7460 + },
7461 + "node_modules/lodash._isiterateecall": {
7462 + "version": "3.0.9",
7463 + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz",
7464 + "integrity": "sha512-De+ZbrMu6eThFti/CSzhRvTKMgQToLxbij58LMfM8JnYDNSOjkjTCIaa8ixglOeGh2nyPlakbt5bJWJ7gvpYlQ==",
7465 + "license": "MIT"
7466 + },
7467 + "node_modules/lodash._root": {
7468 + "version": "3.0.1",
7469 + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz",
7470 + "integrity": "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==",
7471 + "license": "MIT"
7472 + },
7473 + "node_modules/lodash.escape": {
7474 + "version": "3.2.0",
7475 + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz",
7476 + "integrity": "sha512-n1PZMXgaaDWZDSvuNZ/8XOcYO2hOKDqZel5adtR30VKQAtoWs/5AOeFA0vPV8moiPzlqe7F4cP2tzpFewQyelQ==",
7477 + "license": "MIT",
7478 + "dependencies": {
7479 + "lodash._root": "^3.0.0"
7480 + }
7481 + },
7482 + "node_modules/lodash.isarguments": {
7483 + "version": "3.1.0",
7484 + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
7485 + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
7486 + "license": "MIT"
7487 + },
7488 + "node_modules/lodash.isarray": {
7489 + "version": "3.0.4",
7490 + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
7491 + "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==",
7492 + "license": "MIT"
7493 + },
7494 + "node_modules/lodash.isplainobject": {
7495 + "version": "3.2.0",
7496 + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-3.2.0.tgz",
7497 + "integrity": "sha512-P4wZnho5curNqeEq/x292Pb57e1v+woR7DJ84DURelKB46lby8aDEGVobSaYtzHdQBWQrJSdxcCwjlGOvvdIyg==",
7498 + "license": "MIT",
7499 + "dependencies": {
7500 + "lodash._basefor": "^3.0.0",
7501 + "lodash.isarguments": "^3.0.0",
7502 + "lodash.keysin": "^3.0.0"
7503 + }
7504 + },
7505 + "node_modules/lodash.istypedarray": {
7506 + "version": "3.0.6",
7507 + "resolved": "https://registry.npmjs.org/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz",
7508 + "integrity": "sha512-lGWJ6N8AA3KSv+ZZxlTdn4f6A7kMfpJboeyvbFdE7IU9YAgweODqmOgdUHOA+c6lVWeVLysdaxciFXi+foVsWw==",
7509 + "license": "MIT"
7510 + },
7511 + "node_modules/lodash.keys": {
7512 + "version": "3.1.2",
7513 + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
7514 + "integrity": "sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==",
7515 + "license": "MIT",
7516 + "dependencies": {
7517 + "lodash._getnative": "^3.0.0",
7518 + "lodash.isarguments": "^3.0.0",
7519 + "lodash.isarray": "^3.0.0"
7520 + }
7521 + },
7522 + "node_modules/lodash.keysin": {
7523 + "version": "3.0.8",
7524 + "resolved": "https://registry.npmjs.org/lodash.keysin/-/lodash.keysin-3.0.8.tgz",
7525 + "integrity": "sha512-YDB/5xkL3fBKFMDaC+cfGV00pbiJ6XoJIfRmBhv7aR6wWtbCW6IzkiWnTfkiHTF6ALD7ff83dAtB3OEaSoyQPg==",
7526 + "license": "MIT",
7527 + "dependencies": {
7528 + "lodash.isarguments": "^3.0.0",
7529 + "lodash.isarray": "^3.0.0"
7530 + }
7531 + },
6760 7532 "node_modules/lodash.merge": {
6761 7533 "version": "4.6.2",
6762 7534 "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -6764,6 +7536,22 @@
6764 7536 "dev": true,
6765 7537 "license": "MIT"
6766 7538 },
7539 + "node_modules/lodash.restparam": {
7540 + "version": "3.6.1",
7541 + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
7542 + "integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==",
7543 + "license": "MIT"
7544 + },
7545 + "node_modules/lodash.toplainobject": {
7546 + "version": "3.0.0",
7547 + "resolved": "https://registry.npmjs.org/lodash.toplainobject/-/lodash.toplainobject-3.0.0.tgz",
7548 + "integrity": "sha512-wMI0Ju1bvSmnBS3EcRRH/3zDnZOPpDtMtNDzbbNMKuTrEpALsf+sPyMeogmv63Y11qZQO7H1xFzohIEGRMjPYA==",
7549 + "license": "MIT",
7550 + "dependencies": {
7551 + "lodash._basecopy": "^3.0.0",
7552 + "lodash.keysin": "^3.0.0"
7553 + }
7554 + },
6767 7555 "node_modules/longest-streak": {
6768 7556 "version": "3.1.0",
6769 7557 "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -6859,6 +7647,33 @@
6859 7647 "sprintf-js": "~1.0.2"
6860 7648 }
6861 7649 },
7650 + "node_modules/markdown-it": {
7651 + "version": "14.1.1",
7652 + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
7653 + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
7654 + "license": "MIT",
7655 + "dependencies": {
7656 + "argparse": "^2.0.1",
7657 + "entities": "^4.4.0",
7658 + "linkify-it": "^5.0.0",
7659 + "mdurl": "^2.0.0",
7660 + "punycode.js": "^2.3.1",
7661 + "uc.micro": "^2.1.0"
7662 + },
7663 + "bin": {
7664 + "markdown-it": "bin/markdown-it.mjs"
7665 + }
7666 + },
7667 + "node_modules/markdown-table": {
7668 + "version": "3.0.4",
7669 + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
7670 + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
7671 + "license": "MIT",
7672 + "funding": {
7673 + "type": "github",
7674 + "url": "https://github.com/sponsors/wooorm"
7675 + }
7676 + },
6862 7677 "node_modules/math-intrinsics": {
6863 7678 "version": "1.1.0",
6864 7679 "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -6869,6 +7684,34 @@
6869 7684 "node": ">= 0.4"
6870 7685 }
6871 7686 },
7687 + "node_modules/mdast-util-find-and-replace": {
7688 + "version": "3.0.2",
7689 + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
7690 + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
7691 + "license": "MIT",
7692 + "dependencies": {
7693 + "@types/mdast": "^4.0.0",
7694 + "escape-string-regexp": "^5.0.0",
7695 + "unist-util-is": "^6.0.0",
7696 + "unist-util-visit-parents": "^6.0.0"
7697 + },
7698 + "funding": {
7699 + "type": "opencollective",
7700 + "url": "https://opencollective.com/unified"
7701 + }
7702 + },
7703 + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
7704 + "version": "5.0.0",
7705 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
7706 + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
7707 + "license": "MIT",
7708 + "engines": {
7709 + "node": ">=12"
7710 + },
7711 + "funding": {
7712 + "url": "https://github.com/sponsors/sindresorhus"
7713 + }
7714 + },
6872 7715 "node_modules/mdast-util-from-markdown": {
6873 7716 "version": "2.0.2",
6874 7717 "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
@@ -6893,6 +7736,107 @@
6893 7736 "url": "https://opencollective.com/unified"
6894 7737 }
6895 7738 },
7739 + "node_modules/mdast-util-gfm": {
7740 + "version": "3.1.0",
7741 + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
7742 + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
7743 + "license": "MIT",
7744 + "dependencies": {
7745 + "mdast-util-from-markdown": "^2.0.0",
7746 + "mdast-util-gfm-autolink-literal": "^2.0.0",
7747 + "mdast-util-gfm-footnote": "^2.0.0",
7748 + "mdast-util-gfm-strikethrough": "^2.0.0",
7749 + "mdast-util-gfm-table": "^2.0.0",
7750 + "mdast-util-gfm-task-list-item": "^2.0.0",
7751 + "mdast-util-to-markdown": "^2.0.0"
7752 + },
7753 + "funding": {
7754 + "type": "opencollective",
7755 + "url": "https://opencollective.com/unified"
7756 + }
7757 + },
7758 + "node_modules/mdast-util-gfm-autolink-literal": {
7759 + "version": "2.0.1",
7760 + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
7761 + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
7762 + "license": "MIT",
7763 + "dependencies": {
7764 + "@types/mdast": "^4.0.0",
7765 + "ccount": "^2.0.0",
7766 + "devlop": "^1.0.0",
7767 + "mdast-util-find-and-replace": "^3.0.0",
7768 + "micromark-util-character": "^2.0.0"
7769 + },
7770 + "funding": {
7771 + "type": "opencollective",
7772 + "url": "https://opencollective.com/unified"
7773 + }
7774 + },
7775 + "node_modules/mdast-util-gfm-footnote": {
7776 + "version": "2.1.0",
7777 + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
7778 + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
7779 + "license": "MIT",
7780 + "dependencies": {
7781 + "@types/mdast": "^4.0.0",
7782 + "devlop": "^1.1.0",
7783 + "mdast-util-from-markdown": "^2.0.0",
7784 + "mdast-util-to-markdown": "^2.0.0",
7785 + "micromark-util-normalize-identifier": "^2.0.0"
7786 + },
7787 + "funding": {
7788 + "type": "opencollective",
7789 + "url": "https://opencollective.com/unified"
7790 + }
7791 + },
7792 + "node_modules/mdast-util-gfm-strikethrough": {
7793 + "version": "2.0.0",
7794 + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
7795 + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
7796 + "license": "MIT",
7797 + "dependencies": {
7798 + "@types/mdast": "^4.0.0",
7799 + "mdast-util-from-markdown": "^2.0.0",
7800 + "mdast-util-to-markdown": "^2.0.0"
7801 + },
7802 + "funding": {
7803 + "type": "opencollective",
7804 + "url": "https://opencollective.com/unified"
7805 + }
7806 + },
7807 + "node_modules/mdast-util-gfm-table": {
7808 + "version": "2.0.0",
7809 + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
7810 + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
7811 + "license": "MIT",
7812 + "dependencies": {
7813 + "@types/mdast": "^4.0.0",
7814 + "devlop": "^1.0.0",
7815 + "markdown-table": "^3.0.0",
7816 + "mdast-util-from-markdown": "^2.0.0",
7817 + "mdast-util-to-markdown": "^2.0.0"
7818 + },
7819 + "funding": {
7820 + "type": "opencollective",
7821 + "url": "https://opencollective.com/unified"
7822 + }
7823 + },
7824 + "node_modules/mdast-util-gfm-task-list-item": {
7825 + "version": "2.0.0",
7826 + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
7827 + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
7828 + "license": "MIT",
7829 + "dependencies": {
7830 + "@types/mdast": "^4.0.0",
7831 + "devlop": "^1.0.0",
7832 + "mdast-util-from-markdown": "^2.0.0",
7833 + "mdast-util-to-markdown": "^2.0.0"
7834 + },
7835 + "funding": {
7836 + "type": "opencollective",
7837 + "url": "https://opencollective.com/unified"
7838 + }
7839 + },
6896 7840 "node_modules/mdast-util-mdx-expression": {
6897 7841 "version": "2.0.1",
6898 7842 "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
@@ -7022,6 +7966,12 @@
7022 7966 "url": "https://opencollective.com/unified"
7023 7967 }
7024 7968 },
7969 + "node_modules/mdurl": {
7970 + "version": "2.0.0",
7971 + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
7972 + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
7973 + "license": "MIT"
7974 + },
7025 7975 "node_modules/merge2": {
7026 7976 "version": "1.4.1",
7027 7977 "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -7101,6 +8051,127 @@
7101 8051 "micromark-util-types": "^2.0.0"
7102 8052 }
7103 8053 },
8054 + "node_modules/micromark-extension-gfm": {
8055 + "version": "3.0.0",
8056 + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
8057 + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
8058 + "license": "MIT",
8059 + "dependencies": {
8060 + "micromark-extension-gfm-autolink-literal": "^2.0.0",
8061 + "micromark-extension-gfm-footnote": "^2.0.0",
8062 + "micromark-extension-gfm-strikethrough": "^2.0.0",
8063 + "micromark-extension-gfm-table": "^2.0.0",
8064 + "micromark-extension-gfm-tagfilter": "^2.0.0",
8065 + "micromark-extension-gfm-task-list-item": "^2.0.0",
8066 + "micromark-util-combine-extensions": "^2.0.0",
8067 + "micromark-util-types": "^2.0.0"
8068 + },
8069 + "funding": {
8070 + "type": "opencollective",
8071 + "url": "https://opencollective.com/unified"
8072 + }
8073 + },
8074 + "node_modules/micromark-extension-gfm-autolink-literal": {
8075 + "version": "2.1.0",
8076 + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
8077 + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
8078 + "license": "MIT",
8079 + "dependencies": {
8080 + "micromark-util-character": "^2.0.0",
8081 + "micromark-util-sanitize-uri": "^2.0.0",
8082 + "micromark-util-symbol": "^2.0.0",
8083 + "micromark-util-types": "^2.0.0"
8084 + },
8085 + "funding": {
8086 + "type": "opencollective",
8087 + "url": "https://opencollective.com/unified"
8088 + }
8089 + },
8090 + "node_modules/micromark-extension-gfm-footnote": {
8091 + "version": "2.1.0",
8092 + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
8093 + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
8094 + "license": "MIT",
8095 + "dependencies": {
8096 + "devlop": "^1.0.0",
8097 + "micromark-core-commonmark": "^2.0.0",
8098 + "micromark-factory-space": "^2.0.0",
8099 + "micromark-util-character": "^2.0.0",
8100 + "micromark-util-normalize-identifier": "^2.0.0",
8101 + "micromark-util-sanitize-uri": "^2.0.0",
8102 + "micromark-util-symbol": "^2.0.0",
8103 + "micromark-util-types": "^2.0.0"
8104 + },
8105 + "funding": {
8106 + "type": "opencollective",
8107 + "url": "https://opencollective.com/unified"
8108 + }
8109 + },
8110 + "node_modules/micromark-extension-gfm-strikethrough": {
8111 + "version": "2.1.0",
8112 + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
8113 + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
8114 + "license": "MIT",
8115 + "dependencies": {
8116 + "devlop": "^1.0.0",
8117 + "micromark-util-chunked": "^2.0.0",
8118 + "micromark-util-classify-character": "^2.0.0",
8119 + "micromark-util-resolve-all": "^2.0.0",
8120 + "micromark-util-symbol": "^2.0.0",
8121 + "micromark-util-types": "^2.0.0"
8122 + },
8123 + "funding": {
8124 + "type": "opencollective",
8125 + "url": "https://opencollective.com/unified"
8126 + }
8127 + },
8128 + "node_modules/micromark-extension-gfm-table": {
8129 + "version": "2.1.1",
8130 + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
8131 + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
8132 + "license": "MIT",
8133 + "dependencies": {
8134 + "devlop": "^1.0.0",
8135 + "micromark-factory-space": "^2.0.0",
8136 + "micromark-util-character": "^2.0.0",
8137 + "micromark-util-symbol": "^2.0.0",
8138 + "micromark-util-types": "^2.0.0"
8139 + },
8140 + "funding": {
8141 + "type": "opencollective",
8142 + "url": "https://opencollective.com/unified"
8143 + }
8144 + },
8145 + "node_modules/micromark-extension-gfm-tagfilter": {
8146 + "version": "2.0.0",
8147 + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
8148 + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
8149 + "license": "MIT",
8150 + "dependencies": {
8151 + "micromark-util-types": "^2.0.0"
8152 + },
8153 + "funding": {
8154 + "type": "opencollective",
8155 + "url": "https://opencollective.com/unified"
8156 + }
8157 + },
8158 + "node_modules/micromark-extension-gfm-task-list-item": {
8159 + "version": "2.1.0",
8160 + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
8161 + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
8162 + "license": "MIT",
8163 + "dependencies": {
8164 + "devlop": "^1.0.0",
8165 + "micromark-factory-space": "^2.0.0",
8166 + "micromark-util-character": "^2.0.0",
8167 + "micromark-util-symbol": "^2.0.0",
8168 + "micromark-util-types": "^2.0.0"
8169 + },
8170 + "funding": {
8171 + "type": "opencollective",
8172 + "url": "https://opencollective.com/unified"
8173 + }
8174 + },
7104 8175 "node_modules/micromark-factory-destination": {
7105 8176 "version": "2.0.1",
7106 8177 "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
@@ -7879,6 +8950,12 @@
7879 8950 "node": ">= 0.8.0"
7880 8951 }
7881 8952 },
8953 + "node_modules/orderedmap": {
8954 + "version": "2.1.1",
8955 + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
8956 + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
8957 + "license": "MIT"
8958 + },
7882 8959 "node_modules/otplib": {
7883 8960 "version": "13.3.0",
7884 8961 "resolved": "https://registry.npmjs.org/otplib/-/otplib-13.3.0.tgz",
@@ -8378,6 +9455,201 @@
8378 9455 "url": "https://github.com/sponsors/wooorm"
8379 9456 }
8380 9457 },
9458 + "node_modules/prosemirror-changeset": {
9459 + "version": "2.4.0",
9460 + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
9461 + "integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==",
9462 + "license": "MIT",
9463 + "dependencies": {
9464 + "prosemirror-transform": "^1.0.0"
9465 + }
9466 + },
9467 + "node_modules/prosemirror-collab": {
9468 + "version": "1.3.1",
9469 + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
9470 + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
9471 + "license": "MIT",
9472 + "dependencies": {
9473 + "prosemirror-state": "^1.0.0"
9474 + }
9475 + },
9476 + "node_modules/prosemirror-commands": {
9477 + "version": "1.7.1",
9478 + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
9479 + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
9480 + "license": "MIT",
9481 + "dependencies": {
9482 + "prosemirror-model": "^1.0.0",
9483 + "prosemirror-state": "^1.0.0",
9484 + "prosemirror-transform": "^1.10.2"
9485 + }
9486 + },
9487 + "node_modules/prosemirror-dropcursor": {
9488 + "version": "1.8.2",
9489 + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
9490 + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
9491 + "license": "MIT",
9492 + "dependencies": {
9493 + "prosemirror-state": "^1.0.0",
9494 + "prosemirror-transform": "^1.1.0",
9495 + "prosemirror-view": "^1.1.0"
9496 + }
9497 + },
9498 + "node_modules/prosemirror-gapcursor": {
9499 + "version": "1.4.0",
9500 + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz",
9501 + "integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==",
9502 + "license": "MIT",
9503 + "dependencies": {
9504 + "prosemirror-keymap": "^1.0.0",
9505 + "prosemirror-model": "^1.0.0",
9506 + "prosemirror-state": "^1.0.0",
9507 + "prosemirror-view": "^1.0.0"
9508 + }
9509 + },
9510 + "node_modules/prosemirror-history": {
9511 + "version": "1.5.0",
9512 + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
9513 + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
9514 + "license": "MIT",
9515 + "dependencies": {
9516 + "prosemirror-state": "^1.2.2",
9517 + "prosemirror-transform": "^1.0.0",
9518 + "prosemirror-view": "^1.31.0",
9519 + "rope-sequence": "^1.3.0"
9520 + }
9521 + },
9522 + "node_modules/prosemirror-inputrules": {
9523 + "version": "1.5.1",
9524 + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
9525 + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
9526 + "license": "MIT",
9527 + "dependencies": {
9528 + "prosemirror-state": "^1.0.0",
9529 + "prosemirror-transform": "^1.0.0"
9530 + }
9531 + },
9532 + "node_modules/prosemirror-keymap": {
9533 + "version": "1.2.3",
9534 + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
9535 + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
9536 + "license": "MIT",
9537 + "dependencies": {
9538 + "prosemirror-state": "^1.0.0",
9539 + "w3c-keyname": "^2.2.0"
9540 + }
9541 + },
9542 + "node_modules/prosemirror-markdown": {
9543 + "version": "1.13.4",
9544 + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz",
9545 + "integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==",
9546 + "license": "MIT",
9547 + "dependencies": {
9548 + "@types/markdown-it": "^14.0.0",
9549 + "markdown-it": "^14.0.0",
9550 + "prosemirror-model": "^1.25.0"
9551 + }
9552 + },
9553 + "node_modules/prosemirror-menu": {
9554 + "version": "1.3.0",
9555 + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz",
9556 + "integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==",
9557 + "license": "MIT",
9558 + "dependencies": {
9559 + "crelt": "^1.0.0",
9560 + "prosemirror-commands": "^1.0.0",
9561 + "prosemirror-history": "^1.0.0",
9562 + "prosemirror-state": "^1.0.0"
9563 + }
9564 + },
9565 + "node_modules/prosemirror-model": {
9566 + "version": "1.25.4",
9567 + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
9568 + "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
9569 + "license": "MIT",
9570 + "dependencies": {
9571 + "orderedmap": "^2.0.0"
9572 + }
9573 + },
9574 + "node_modules/prosemirror-schema-basic": {
9575 + "version": "1.2.4",
9576 + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
9577 + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
9578 + "license": "MIT",
9579 + "dependencies": {
9580 + "prosemirror-model": "^1.25.0"
9581 + }
9582 + },
9583 + "node_modules/prosemirror-schema-list": {
9584 + "version": "1.5.1",
9585 + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
9586 + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
9587 + "license": "MIT",
9588 + "dependencies": {
9589 + "prosemirror-model": "^1.0.0",
9590 + "prosemirror-state": "^1.0.0",
9591 + "prosemirror-transform": "^1.7.3"
9592 + }
9593 + },
9594 + "node_modules/prosemirror-state": {
9595 + "version": "1.4.4",
9596 + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
9597 + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
9598 + "license": "MIT",
9599 + "dependencies": {
9600 + "prosemirror-model": "^1.0.0",
9601 + "prosemirror-transform": "^1.0.0",
9602 + "prosemirror-view": "^1.27.0"
9603 + }
9604 + },
9605 + "node_modules/prosemirror-tables": {
9606 + "version": "1.8.5",
9607 + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
9608 + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
9609 + "license": "MIT",
9610 + "dependencies": {
9611 + "prosemirror-keymap": "^1.2.3",
9612 + "prosemirror-model": "^1.25.4",
9613 + "prosemirror-state": "^1.4.4",
9614 + "prosemirror-transform": "^1.10.5",
9615 + "prosemirror-view": "^1.41.4"
9616 + }
9617 + },
9618 + "node_modules/prosemirror-trailing-node": {
9619 + "version": "3.0.0",
9620 + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
9621 + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
9622 + "license": "MIT",
9623 + "dependencies": {
9624 + "@remirror/core-constants": "3.0.0",
9625 + "escape-string-regexp": "^4.0.0"
9626 + },
9627 + "peerDependencies": {
9628 + "prosemirror-model": "^1.22.1",
9629 + "prosemirror-state": "^1.4.2",
9630 + "prosemirror-view": "^1.33.8"
9631 + }
9632 + },
9633 + "node_modules/prosemirror-transform": {
9634 + "version": "1.11.0",
9635 + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz",
9636 + "integrity": "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==",
9637 + "license": "MIT",
9638 + "dependencies": {
9639 + "prosemirror-model": "^1.21.0"
9640 + }
9641 + },
9642 + "node_modules/prosemirror-view": {
9643 + "version": "1.41.6",
9644 + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.6.tgz",
9645 + "integrity": "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==",
9646 + "license": "MIT",
9647 + "dependencies": {
9648 + "prosemirror-model": "^1.20.0",
9649 + "prosemirror-state": "^1.0.0",
9650 + "prosemirror-transform": "^1.1.0"
9651 + }
9652 + },
8381 9653 "node_modules/punycode": {
8382 9654 "version": "2.3.1",
8383 9655 "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -8388,6 +9660,15 @@
8388 9660 "node": ">=6"
8389 9661 }
8390 9662 },
9663 + "node_modules/punycode.js": {
9664 + "version": "2.3.1",
9665 + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
9666 + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
9667 + "license": "MIT",
9668 + "engines": {
9669 + "node": ">=6"
9670 + }
9671 + },
8391 9672 "node_modules/qrcode": {
8392 9673 "version": "1.5.4",
8393 9674 "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
@@ -8678,6 +9959,24 @@
8678 9959 "url": "https://github.com/sponsors/ljharb"
8679 9960 }
8680 9961 },
9962 + "node_modules/remark-gfm": {
9963 + "version": "4.0.1",
9964 + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
9965 + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
9966 + "license": "MIT",
9967 + "dependencies": {
9968 + "@types/mdast": "^4.0.0",
9969 + "mdast-util-gfm": "^3.0.0",
9970 + "micromark-extension-gfm": "^3.0.0",
9971 + "remark-parse": "^11.0.0",
9972 + "remark-stringify": "^11.0.0",
9973 + "unified": "^11.0.0"
9974 + },
9975 + "funding": {
9976 + "type": "opencollective",
9977 + "url": "https://opencollective.com/unified"
9978 + }
9979 + },
8681 9980 "node_modules/remark-parse": {
8682 9981 "version": "11.0.0",
8683 9982 "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
@@ -8711,6 +10010,21 @@
8711 10010 "url": "https://opencollective.com/unified"
8712 10011 }
8713 10012 },
10013 + "node_modules/remark-stringify": {
10014 + "version": "11.0.0",
10015 + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
10016 + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
10017 + "license": "MIT",
10018 + "dependencies": {
10019 + "@types/mdast": "^4.0.0",
10020 + "mdast-util-to-markdown": "^2.0.0",
10021 + "unified": "^11.0.0"
10022 + },
10023 + "funding": {
10024 + "type": "opencollective",
10025 + "url": "https://opencollective.com/unified"
10026 + }
10027 + },
8714 10028 "node_modules/require-directory": {
8715 10029 "version": "2.1.1",
8716 10030 "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -8817,6 +10131,12 @@
8817 10131 "url": "https://github.com/sponsors/isaacs"
8818 10132 }
8819 10133 },
10134 + "node_modules/rope-sequence": {
10135 + "version": "1.3.4",
10136 + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
10137 + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
10138 + "license": "MIT"
10139 + },
8820 10140 "node_modules/run-parallel": {
8821 10141 "version": "1.2.0",
8822 10142 "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -9162,6 +10482,18 @@
9162 10482 "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
9163 10483 "license": "BSD-3-Clause"
9164 10484 },
10485 + "node_modules/ssf": {
10486 + "version": "0.11.2",
10487 + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
10488 + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
10489 + "license": "Apache-2.0",
10490 + "dependencies": {
10491 + "frac": "~1.1.2"
10492 + },
10493 + "engines": {
10494 + "node": ">=0.8"
10495 + }
10496 + },
9165 10497 "node_modules/stable-hash": {
9166 10498 "version": "0.0.5",
9167 10499 "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -9927,6 +11259,12 @@
9927 11259 "node": ">=14.17"
9928 11260 }
9929 11261 },
11262 + "node_modules/uc.micro": {
11263 + "version": "2.1.0",
11264 + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
11265 + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
11266 + "license": "MIT"
11267 + },
9930 11268 "node_modules/unbox-primitive": {
9931 11269 "version": "1.1.0",
9932 11270 "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -10097,6 +11435,15 @@
10097 11435 "punycode": "^2.1.0"
10098 11436 }
10099 11437 },
11438 + "node_modules/use-sync-external-store": {
11439 + "version": "1.6.0",
11440 + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
11441 + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
11442 + "license": "MIT",
11443 + "peerDependencies": {
11444 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
11445 + }
11446 + },
10100 11447 "node_modules/util-deprecate": {
10101 11448 "version": "1.0.2",
10102 11449 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -10266,6 +11613,24 @@
10266 11613 "url": "https://github.com/sponsors/ljharb"
10267 11614 }
10268 11615 },
11616 + "node_modules/wmf": {
11617 + "version": "1.0.2",
11618 + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
11619 + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
11620 + "license": "Apache-2.0",
11621 + "engines": {
11622 + "node": ">=0.8"
11623 + }
11624 + },
11625 + "node_modules/word": {
11626 + "version": "0.3.0",
11627 + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
11628 + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
11629 + "license": "Apache-2.0",
11630 + "engines": {
11631 + "node": ">=0.8"
11632 + }
11633 + },
10269 11634 "node_modules/word-wrap": {
10270 11635 "version": "1.2.5",
10271 11636 "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -10377,6 +11742,27 @@
10377 11742 "dev": true,
10378 11743 "license": "ISC"
10379 11744 },
11745 + "node_modules/xlsx": {
11746 + "version": "0.18.5",
11747 + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
11748 + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
11749 + "license": "Apache-2.0",
11750 + "dependencies": {
11751 + "adler-32": "~1.3.0",
11752 + "cfb": "~1.2.1",
11753 + "codepage": "~1.15.0",
11754 + "crc-32": "~1.2.1",
11755 + "ssf": "~0.11.2",
11756 + "wmf": "~1.0.1",
11757 + "word": "~0.3.0"
11758 + },
11759 + "bin": {
11760 + "xlsx": "bin/xlsx.njs"
11761 + },
11762 + "engines": {
11763 + "node": ">=0.8"
11764 + }
11765 + },
10380 11766 "node_modules/xmlbuilder": {
10381 11767 "version": "10.1.1",
10382 11768 "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
modified package.json +13 −3
@@ -3,9 +3,9 @@
3 3 "version": "1.0.0",
4 4 "private": true,
5 5 "scripts": {
6 − "dev": "next dev",
6 + "dev": "tsx server.ts",
7 7 "build": "next build",
8 − "start": "next start",
8 + "start": "NODE_ENV=production tsx server.ts",
9 9 "lint": "next lint",
10 10 "db:generate": "prisma generate",
11 11 "db:push": "prisma db push",
@@ -40,10 +40,17 @@
40 40 "@codemirror/theme-one-dark": "^6.1.3",
41 41 "@codemirror/view": "^6.39.14",
42 42 "@prisma/client": "^5.22.0",
43 + "@tiptap/extension-highlight": "^3.20.0",
44 + "@tiptap/extension-text-align": "^3.20.0",
45 + "@tiptap/extension-underline": "^3.20.0",
46 + "@tiptap/pm": "^3.20.0",
47 + "@tiptap/react": "^3.20.0",
48 + "@tiptap/starter-kit": "^3.20.0",
43 49 "archiver": "^7.0.1",
44 50 "bcryptjs": "^3.0.3",
45 51 "diff": "^8.0.3",
46 52 "framer-motion": "^12.34.0",
53 + "html-docx-js": "^0.3.1",
47 54 "iron-session": "^8.0.4",
48 55 "jszip": "^3.10.1",
49 56 "lucide-react": "^0.564.0",
@@ -63,14 +70,17 @@
63 70 "react-hot-toast": "^2.6.0",
64 71 "react-markdown": "^10.1.0",
65 72 "react-syntax-highlighter": "^16.1.0",
73 + "remark-gfm": "^4.0.1",
66 74 "sharp": "^0.34.5",
67 75 "uuid": "^13.0.0",
68 − "wavesurfer.js": "^7.12.1"
76 + "wavesurfer.js": "^7.12.1",
77 + "xlsx": "^0.18.5"
69 78 },
70 79 "devDependencies": {
71 80 "@types/archiver": "^7.0.0",
72 81 "@types/bcryptjs": "^2.4.6",
73 82 "@types/diff": "^7.0.2",
83 + "@types/html-docx-js": "^0.3.4",
74 84 "@types/mime-types": "^3.0.1",
75 85 "@types/node": "^20",
76 86 "@types/papaparse": "^5.5.2",
added types/html-docx-js.d.ts +15 −0
@@ -0,0 +1,15 @@
1 +declare module "html-docx-js/dist/html-docx" {
2 + interface HtmlDocxOptions {
3 + orientation?: "landscape" | "portrait";
4 + margins?: {
5 + top?: number;
6 + bottom?: number;
7 + left?: number;
8 + right?: number;
9 + header?: number;
10 + footer?: number;
11 + gutter?: number;
12 + };
13 + }
14 + export function asBlob(html: string, options?: HtmlDocxOptions): Blob;
15 +}
16