react-embed-docs 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -306,6 +306,24 @@ module.exports = {
306
306
 
307
307
  All components support dark mode through Tailwind's `dark:` variants. The components automatically adapt when you have `class="dark"` on your HTML element.
308
308
 
309
+ ### Mobile / Responsive
310
+
311
+ The components are fully responsive and mobile-friendly. For optimal mobile experience, ensure your HTML includes the viewport meta tag:
312
+
313
+ ```html
314
+ <head>
315
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
316
+ </head>
317
+ ```
318
+
319
+ **Mobile features:**
320
+ - Hamburger menu for sidebar navigation on screens < 768px
321
+ - Responsive typography that scales down on mobile
322
+ - Touch-friendly button and input sizes (min 44px touch targets)
323
+ - Collapsible sidebar that becomes an overlay on mobile
324
+ - Optimized search dropdown for small screens
325
+ - Flexible layouts that stack vertically on mobile
326
+
309
327
  ## Configuration
310
328
 
311
329
  ### Custom API Prefix
@@ -1 +1 @@
1
- {"version":3,"file":"DocumentEdit.d.ts","sourceRoot":"","sources":["../../../src/client/components/DocumentEdit.tsx"],"names":[],"mappings":"AAIA,OAAO,8BAA8B,CAAA;AAgBrC,UAAU,iBAAiB;CAAG;AAwU9B,wBAAgB,YAAY,CAAC,EAAE,EAAE,iBAAiB,2CA2RjD"}
1
+ {"version":3,"file":"DocumentEdit.d.ts","sourceRoot":"","sources":["../../../src/client/components/DocumentEdit.tsx"],"names":[],"mappings":"AAIA,OAAO,8BAA8B,CAAA;AAgBrC,UAAU,iBAAiB;CAAG;AAwU9B,wBAAgB,YAAY,CAAC,EAAE,EAAE,iBAAiB,2CAqYjD"}
@@ -4,7 +4,7 @@ import { BlockNoteView } from '@blocknote/mantine';
4
4
  import '@blocknote/mantine/style.css';
5
5
  import { useCreateBlockNote } from '@blocknote/react';
6
6
  import { Eye, ImageIcon, Loader2, Save, X } from 'lucide-react';
7
- import { useEffect, useState } from 'react';
7
+ import { useCallback, useEffect, useState } from 'react';
8
8
  import { useCreateDocumentMutation, useDocumentQuery, useUpdateDocumentMutation, } from '../hooks/useDocsQuery.js';
9
9
  import { useFileUpload } from '../hooks/useFileUpload.js';
10
10
  import { useTranslation } from '../hooks/useTranslation.js';
@@ -346,17 +346,94 @@ export function DocumentEdit({}) {
346
346
  const [isSlugManuallyEdited, setIsSlugManuallyEdited] = useState(false);
347
347
  const [emoji, setEmoji] = useState(undefined);
348
348
  const [cover, setCover] = useState(null);
349
+ const [previousCover, setPreviousCover] = useState(null);
349
350
  const [isSaving, setIsSaving] = useState(false);
350
351
  const [hasLoaded, setHasLoaded] = useState(false);
351
- // File upload hook
352
+ // Track image URLs for cleanup when deleted
353
+ const [previousImageUrls, setPreviousImageUrls] = useState(new Set());
354
+ // Helper to extract image URLs from blocks
355
+ const extractImageUrls = useCallback((blocks) => {
356
+ const urls = [];
357
+ for (const block of blocks) {
358
+ if (block.type === 'image' && block.props?.url) {
359
+ urls.push(block.props.url);
360
+ }
361
+ // Check nested content
362
+ if (block.children && block.children.length > 0) {
363
+ urls.push(...extractImageUrls(block.children));
364
+ }
365
+ }
366
+ return urls;
367
+ }, []);
368
+ // File upload hook for cover
352
369
  const { isUploading: isUploadingCover, uploadFile: uploadCover } = useFileUpload({
353
370
  maxSize: 5 * 1024 * 1024, // 5MB
354
371
  acceptedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
355
372
  });
356
- // Initialize editor
373
+ // File upload hook for editor images
374
+ const { uploadFile: uploadEditorFile } = useFileUpload({
375
+ maxSize: 5 * 1024 * 1024, // 5MB
376
+ acceptedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'],
377
+ });
378
+ // Upload file handler for BlockNote editor
379
+ const handleUploadFile = useCallback(async (file) => {
380
+ const result = await uploadEditorFile(file);
381
+ return result.url;
382
+ }, [uploadEditorFile]);
383
+ // Initialize editor with file upload support
357
384
  const editor = useCreateBlockNote({
358
385
  initialContent: getDefaultContent(),
386
+ uploadFile: handleUploadFile,
359
387
  });
388
+ // Helper to extract file ID from URL
389
+ const extractFileIdFromUrl = useCallback((url) => {
390
+ // URL format: /api/docs/files/{id}
391
+ const match = url.match(/\/api\/docs\/files\/([a-zA-Z0-9_-]+)/);
392
+ return match?.[1] ?? null;
393
+ }, []);
394
+ // Soft delete file from server
395
+ const softDeleteFile = useCallback(async (fileId) => {
396
+ try {
397
+ const res = await fetch(`/api/docs/files/${fileId}`, {
398
+ method: 'DELETE',
399
+ });
400
+ if (!res.ok) {
401
+ console.error(`Failed to soft delete file ${fileId}:`, res.statusText);
402
+ }
403
+ }
404
+ catch (error) {
405
+ console.error(`Error soft deleting file ${fileId}:`, error);
406
+ }
407
+ }, []);
408
+ // Monitor editor changes and soft delete removed images
409
+ useEffect(() => {
410
+ if (!editor)
411
+ return;
412
+ const unsubscribe = editor.onChange(() => {
413
+ const currentBlocks = editor.document;
414
+ const currentImageUrls = new Set(extractImageUrls(currentBlocks));
415
+ // Find images that were in previous but not in current
416
+ for (const url of previousImageUrls) {
417
+ if (!currentImageUrls.has(url)) {
418
+ const fileId = extractFileIdFromUrl(url);
419
+ if (fileId) {
420
+ softDeleteFile(fileId);
421
+ }
422
+ }
423
+ }
424
+ setPreviousImageUrls(currentImageUrls);
425
+ });
426
+ return () => {
427
+ unsubscribe();
428
+ };
429
+ }, [editor, extractImageUrls, extractFileIdFromUrl, softDeleteFile, previousImageUrls]);
430
+ // Initialize previousImageUrls when document loads
431
+ useEffect(() => {
432
+ if (editor && hasLoaded && existingDoc) {
433
+ const urls = new Set(extractImageUrls(editor.document));
434
+ setPreviousImageUrls(urls);
435
+ }
436
+ }, [editor, hasLoaded, existingDoc, extractImageUrls]);
360
437
  // Load existing document data when available
361
438
  useEffect(() => {
362
439
  if (existingDoc && !hasLoaded) {
@@ -364,8 +441,10 @@ export function DocumentEdit({}) {
364
441
  setSlug(existingDoc.slug);
365
442
  if (existingDoc.emoji)
366
443
  setEmoji(existingDoc.emoji);
367
- if (existingDoc.cover)
444
+ if (existingDoc.cover) {
368
445
  setCover(existingDoc.cover);
446
+ setPreviousCover(existingDoc.cover);
447
+ }
369
448
  if (editor && existingDoc.content) {
370
449
  try {
371
450
  const content = typeof existingDoc.content === 'string'
@@ -405,6 +484,13 @@ export function DocumentEdit({}) {
405
484
  try {
406
485
  const content = editor.document;
407
486
  const finalSlug = slug.trim() || generateSlug(title);
487
+ // Check if cover was removed and soft delete the old file
488
+ if (previousCover && !cover) {
489
+ const fileId = extractFileIdFromUrl(previousCover);
490
+ if (fileId) {
491
+ await softDeleteFile(fileId);
492
+ }
493
+ }
408
494
  if (!existingDoc) {
409
495
  // Create new document (with parentId if it's a child document)
410
496
  const newDoc = await createMutation.mutateAsync({
@@ -431,6 +517,8 @@ export function DocumentEdit({}) {
431
517
  },
432
518
  });
433
519
  }
520
+ // Update previousCover after successful save
521
+ setPreviousCover(cover);
434
522
  }
435
523
  catch (error) {
436
524
  console.error('Failed to save document:', error);
@@ -467,9 +555,9 @@ export function DocumentEdit({}) {
467
555
  if (!isNewDocument && isLoadingDoc) {
468
556
  return (_jsx("div", { className: "flex items-center justify-center min-h-100", children: _jsx(Loader2, { className: "h-8 w-8 animate-spin text-gray-400" }) }));
469
557
  }
470
- return (_jsxs("div", { className: "mx-auto h-full flex flex-col", children: [_jsxs("div", { className: "mb-6 shrink-0", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex items-center gap-2 flex-1", children: [_jsx(EmojiPicker, { value: emoji, onChange: setEmoji }), _jsx("input", { type: "text", value: title, onChange: (e) => setTitle(e.target.value), placeholder: t('editor.titlePlaceholder'), className: "text-2xl font-bold bg-transparent border-none outline-none px-0 flex-1 placeholder:text-gray-400" })] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [!isNewDocument && existingDoc && (_jsxs("button", { onClick: handleView, className: "px-3 py-1.5 text-sm border border-border rounded-md hover:bg-gray-50 flex items-center gap-2 transition-colors", children: [_jsx(Eye, { className: "h-4 w-4" }), t('editor.view')] })), _jsx("button", { onClick: handleSave, disabled: isSaving || createMutation.isPending || updateMutation.isPending, className: "px-3 py-1.5 text-sm bg-primary text-white rounded-md hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 transition-colors", children: isSaving ||
558
+ return (_jsxs("div", { className: "mx-auto h-full flex flex-col max-w-full sm:max-w-[90%] lg:max-w-[80%]", children: [_jsxs("div", { className: "mb-4 sm:mb-6 shrink-0", children: [_jsxs("div", { className: "flex flex-col sm:flex-row sm:items-start justify-between gap-3 sm:gap-4", children: [_jsxs("div", { className: "flex items-center gap-2 flex-1 min-w-0", children: [_jsx(EmojiPicker, { value: emoji, onChange: setEmoji }), _jsx("input", { type: "text", value: title, onChange: (e) => setTitle(e.target.value), placeholder: t('editor.titlePlaceholder'), className: "text-xl sm:text-2xl font-bold bg-transparent border-none outline-none px-0 flex-1 placeholder:text-gray-400 min-w-0" })] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [!isNewDocument && existingDoc && (_jsxs("button", { onClick: handleView, className: "px-2 sm:px-3 py-1.5 text-sm border border-border rounded-md hover:bg-gray-50 flex items-center gap-1 sm:gap-2 transition-colors", children: [_jsx(Eye, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('editor.view') })] })), _jsx("button", { onClick: handleSave, disabled: isSaving || createMutation.isPending || updateMutation.isPending, className: "px-2 sm:px-3 py-1.5 text-sm bg-primary text-white rounded-md hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1 sm:gap-2 transition-colors", children: isSaving ||
471
559
  createMutation.isPending ||
472
- updateMutation.isPending ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), t('editor.saving')] })) : (_jsxs(_Fragment, { children: [_jsx(Save, { className: "h-4 w-4" }), isNewDocument ? t('editor.create') : t('editor.save')] })) })] })] }), _jsxs("div", { className: "text-sm text-gray-500 mt-2 flex items-center gap-2", children: [_jsxs("span", { className: "shrink-0", children: [t('editor.slug'), ":"] }), _jsx("input", { type: "text", value: slug, onChange: (e) => {
560
+ updateMutation.isPending ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), _jsx("span", { className: "hidden sm:inline", children: t('editor.saving') })] })) : (_jsxs(_Fragment, { children: [_jsx(Save, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: isNewDocument ? t('editor.create') : t('editor.save') })] })) })] })] }), _jsxs("div", { className: "text-sm text-gray-500 mt-2 flex items-center gap-2 flex-wrap", children: [_jsxs("span", { className: "shrink-0", children: [t('editor.slug'), ":"] }), _jsx("input", { type: "text", value: slug, onChange: (e) => {
473
561
  setSlug(e.target.value);
474
562
  setIsSlugManuallyEdited(true);
475
563
  }, onBlur: (e) => {
@@ -477,5 +565,5 @@ export function DocumentEdit({}) {
477
565
  setIsSlugManuallyEdited(false);
478
566
  setSlug(generateSlug(title));
479
567
  }
480
- }, placeholder: generateSlug(title), className: "bg-transparent border-none outline-none px-0 flex-1 text-gray-500 placeholder:text-gray-400 font-mono text-sm" })] })] }), _jsx("div", { className: "mb-6 shrink-0", children: cover ? (_jsxs("div", { className: "relative w-full h-80 rounded-lg overflow-hidden bg-gray-100", children: [_jsx("img", { src: cover, alt: t('editor.coverAlt'), className: "w-full h-full object-cover" }), _jsx("button", { onClick: () => setCover(null), className: "absolute top-2 right-2 p-1.5 bg-secondary hover:bg-white rounded-md shadow-sm transition-colors", title: t('editor.removeCover'), children: _jsx(X, { className: "h-4 w-4" }) })] })) : (_jsxs("div", { onDrop: handleDrop, onDragOver: handleDragOver, className: "relative w-full h-32 border-2 border-dashed border-border rounded-lg bg-secondary hover:bg-gray-100 transition-colors cursor-pointer", children: [_jsx("input", { type: "file", accept: "image/jpeg,image/png,image/gif,image/webp", onChange: handleFileInput, className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer" }), _jsx("div", { className: "flex flex-col items-center justify-center h-full gap-2 text-gray-400", children: isUploadingCover ? (_jsx(Loader2, { className: "h-6 w-6 animate-spin" })) : (_jsxs(_Fragment, { children: [_jsx(ImageIcon, { className: "h-6 w-6" }), _jsx("span", { className: "text-sm", children: t('editor.coverDropzone') }), _jsx("span", { className: "text-xs text-gray-400", children: t('editor.coverFormats') })] })) })] })) }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: _jsx("div", { className: "h-full", children: _jsx(BlockNoteView, { editor: editor, theme: blockNoteTheme[theme], className: "h-full" }) }) })] }));
568
+ }, placeholder: generateSlug(title), className: "bg-transparent border-none outline-none px-0 flex-1 text-gray-500 placeholder:text-gray-400 font-mono text-sm min-w-0" })] })] }), _jsx("div", { className: "mb-4 sm:mb-6 shrink-0", children: cover ? (_jsxs("div", { className: "relative w-full h-48 sm:h-64 lg:h-80 rounded-lg overflow-hidden bg-gray-100", children: [_jsx("img", { src: cover, alt: t('editor.coverAlt'), className: "w-full h-full object-cover" }), _jsx("button", { onClick: () => setCover(null), className: "absolute top-2 right-2 p-1.5 bg-secondary hover:bg-white rounded-md shadow-sm transition-colors", title: t('editor.removeCover'), children: _jsx(X, { className: "h-4 w-4" }) })] })) : (_jsxs("div", { onDrop: handleDrop, onDragOver: handleDragOver, className: "relative w-full h-24 sm:h-32 border-2 border-dashed border-border rounded-lg bg-secondary hover:bg-gray-100 transition-colors cursor-pointer", children: [_jsx("input", { type: "file", accept: "image/jpeg,image/png,image/gif,image/webp", onChange: handleFileInput, className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer" }), _jsx("div", { className: "flex flex-col items-center justify-center h-full gap-1 sm:gap-2 text-gray-400", children: isUploadingCover ? (_jsx(Loader2, { className: "h-5 sm:h-6 w-5 sm:w-6 animate-spin" })) : (_jsxs(_Fragment, { children: [_jsx(ImageIcon, { className: "h-5 sm:h-6 w-5 sm:w-6" }), _jsx("span", { className: "text-xs sm:text-sm", children: t('editor.coverDropzone') }), _jsx("span", { className: "text-xs text-gray-400 hidden sm:inline", children: t('editor.coverFormats') })] })) })] })) }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: _jsx("div", { className: "h-full", children: _jsx(BlockNoteView, { editor: editor, theme: blockNoteTheme[theme], className: "h-full" }) }) })] }));
481
569
  }
@@ -54,7 +54,7 @@ export function DocumentView({}) {
54
54
  if (error || !doc) {
55
55
  return (_jsx("div", { className: "flex items-center justify-center min-h-[400px]", children: _jsx("p", { className: "text-red-500", children: t('view.loadFailed') }) }));
56
56
  }
57
- return (_jsxs("div", { className: "max-w-[80%] mx-auto", children: [_jsxs("div", { className: "mb-8", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 mb-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [doc.emoji && (_jsx("span", { className: "text-5xl leading-none", role: "img", "aria-label": "document emoji", children: doc.emoji })), _jsx("h1", { className: "text-3xl font-bold", children: doc.title })] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [doc && (_jsx(ExportButton, { documentId: doc.id, documentTitle: doc.title })), _jsxs("button", { onClick: handleEdit, className: "px-3 py-1.5 text-sm bg-secondary text-white rounded-md hover:bg-primary flex items-center gap-2 transition-colors", children: [_jsx(Edit, { className: "h-4 w-4" }), t('view.edit')] })] })] }), _jsxs("div", { className: "flex items-center gap-6 text-sm text-gray-500", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(User, { className: "h-4 w-4" }), _jsxs("span", { children: [t('view.authorId'), ": ", doc.authorId] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Clock, { className: "h-4 w-4" }), _jsxs("span", { children: [t('view.updated'), ' ', doc.updatedAt
57
+ return (_jsxs("div", { className: "max-w-full sm:max-w-[90%] lg:max-w-[80%] mx-auto", children: [_jsxs("div", { className: "mb-6 sm:mb-8", children: [_jsxs("div", { className: "flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [doc.emoji && (_jsx("span", { className: "text-4xl sm:text-5xl leading-none", role: "img", "aria-label": "document emoji", children: doc.emoji })), _jsx("h1", { className: "text-2xl sm:text-3xl font-bold", children: doc.title })] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [doc && (_jsx(ExportButton, { documentId: doc.id, documentTitle: doc.title })), _jsxs("button", { onClick: handleEdit, className: "px-3 py-1.5 text-sm bg-secondary text-white rounded-md hover:bg-primary flex items-center gap-2 transition-colors", children: [_jsx(Edit, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('view.edit') })] })] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-4 sm:gap-6 text-sm text-gray-500", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(User, { className: "h-4 w-4" }), _jsxs("span", { children: [t('view.authorId'), ": ", doc.authorId] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Clock, { className: "h-4 w-4" }), _jsxs("span", { children: [t('view.updated'), ' ', doc.updatedAt
58
58
  ? new Date(doc.updatedAt).toLocaleDateString()
59
- : 'N/A'] })] })] })] }), doc.cover && (_jsx("div", { className: "mb-8 -mx-4 sm:-mx-8 lg:-mx-12", children: _jsx("div", { className: "relative w-full max-h-80 overflow-hidden", children: _jsx("img", { src: doc.cover, alt: doc.title, className: "w-full h-full object-cover" }) }) })), _jsx("div", { children: _jsx(BlockNoteView, { editor: editor, editable: false, theme: blockNoteTheme[theme], className: "[&_.bn-editor]:p-0 [&_.bn-editor]:px-0 [&_.bn-container]:max-w-none [&_.bn-editor]:!px-0" }) })] }));
59
+ : 'N/A'] })] })] })] }), doc.cover && (_jsx("div", { className: "mb-6 sm:mb-8 -mx-4 sm:-mx-8 lg:-mx-12", children: _jsx("div", { className: "relative w-full max-h-60 sm:max-h-80 overflow-hidden", children: _jsx("img", { src: doc.cover, alt: doc.title, className: "w-full h-full object-cover" }) }) })), _jsx("div", { children: _jsx(BlockNoteView, { editor: editor, editable: false, theme: blockNoteTheme[theme], className: "[&_.bn-editor]:p-0 [&_.bn-editor]:px-0 [&_.bn-container]:max-w-none [&_.bn-editor]:!px-0" }) })] }));
60
60
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Layout.d.ts","sourceRoot":"","sources":["../../../src/client/components/Layout.tsx"],"names":[],"mappings":"AAiBA,UAAU,eAAe;IACvB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AAmJD,wBAAgB,MAAM,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,eAAe,2CAmK/D"}
1
+ {"version":3,"file":"Layout.d.ts","sourceRoot":"","sources":["../../../src/client/components/Layout.tsx"],"names":[],"mappings":"AAmBA,UAAU,eAAe;IACvB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AAmJD,wBAAgB,MAAM,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,eAAe,2CAoO/D"}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { FileText, PanelLeftCloseIcon, PanelLeftOpenIcon, Plus, Search, } from 'lucide-react';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { FileText, Menu, PanelLeftCloseIcon, PanelLeftOpenIcon, Plus, Search, X, } from 'lucide-react';
4
4
  import { useEffect, useRef, useState } from 'react';
5
5
  import { useDocumentsQuery } from '../hooks/useDocsQuery.js';
6
6
  import { useTranslation } from '../hooks/useTranslation.js';
@@ -133,6 +133,8 @@ export function Layout({ children, userAvatar }) {
133
133
  const { t } = useTranslation();
134
134
  const [expandedFolders, setExpandedFolders] = useState(new Set());
135
135
  const [isSidebarOpen, setIsSidebarOpen] = useState(true);
136
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
137
+ const [isMobile, setIsMobile] = useState(false);
136
138
  // Header search state
137
139
  const [headerSearchQuery, setHeaderSearchQuery] = useState('');
138
140
  const [showSearchResults, setShowSearchResults] = useState(false);
@@ -140,6 +142,22 @@ export function Layout({ children, userAvatar }) {
140
142
  const searchInputRef = useRef(null);
141
143
  const { data } = useDocumentsQuery();
142
144
  const documents = data?.documents ?? [];
145
+ // Mobile detection
146
+ useEffect(() => {
147
+ const checkMobile = () => {
148
+ setIsMobile(window.innerWidth < 768);
149
+ if (window.innerWidth < 768) {
150
+ setIsSidebarOpen(false);
151
+ }
152
+ };
153
+ checkMobile();
154
+ window.addEventListener('resize', checkMobile);
155
+ return () => window.removeEventListener('resize', checkMobile);
156
+ }, []);
157
+ // Close mobile menu on document change
158
+ useEffect(() => {
159
+ setIsMobileMenuOpen(false);
160
+ }, [params.documentSlug]);
143
161
  // Search query for header search
144
162
  const { data: searchResultsData } = useDocumentsQuery(debouncedSearchQuery.length > 0 ? { search: debouncedSearchQuery } : {});
145
163
  const searchResults = searchResultsData?.documents ?? [];
@@ -154,9 +172,9 @@ export function Layout({ children, userAvatar }) {
154
172
  document.addEventListener('mousedown', handleClickOutside);
155
173
  return () => document.removeEventListener('mousedown', handleClickOutside);
156
174
  }, []);
157
- return (_jsxs("div", { className: "flex h-screen w-full", children: [_jsx(Sidebar, { isOpen: isSidebarOpen, onToggle: setIsSidebarOpen, documents: documents }), _jsxs("main", { className: "flex-1 flex flex-col min-w-0 overflow-hidden", children: [params.mode === 'view' && (_jsxs("header", { className: "h-16 border-b border-border flex items-center justify-between px-6 shrink-0", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("button", { onClick: () => setIsSidebarOpen(!isSidebarOpen), className: "p-2 hover:bg-primary rounded-md transition-colors text-muted-foreground", title: isSidebarOpen
175
+ return (_jsxs("div", { className: "flex h-screen w-full", children: [!isMobile && (_jsx(Sidebar, { isOpen: isSidebarOpen, onToggle: setIsSidebarOpen, documents: documents })), isMobile && isMobileMenuOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 bg-black/50 z-40", onClick: () => setIsMobileMenuOpen(false) }), _jsxs("div", { className: "fixed left-0 top-0 h-full w-64 z-50 bg-background border-r border-border", children: [_jsxs("div", { className: "p-4 border-b border-border flex items-center justify-between", children: [_jsx("h2", { className: "font-semibold text-sm text-muted-foreground", children: t('sidebar.documents') }), _jsx("button", { onClick: () => setIsMobileMenuOpen(false), className: "p-2 hover:bg-primary rounded-md transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] }), _jsx(Sidebar, { isOpen: true, onToggle: () => { }, documents: documents, currentDocId: params.documentSlug })] })] })), _jsxs("main", { className: "flex-1 flex flex-col min-w-0 overflow-hidden", children: [params.mode === 'view' && (_jsxs("header", { className: "h-16 border-b border-border flex items-center justify-between px-4 sm:px-6 shrink-0", children: [_jsxs("div", { className: "flex items-center gap-3", children: [isMobile && (_jsx("button", { onClick: () => setIsMobileMenuOpen(true), className: "p-2 hover:bg-primary rounded-md transition-colors text-muted-foreground", title: t('layout.openSidebar'), children: _jsx(Menu, { className: "h-6 w-6" }) })), !isMobile && (_jsx("button", { onClick: () => setIsSidebarOpen(!isSidebarOpen), className: "p-2 hover:bg-primary rounded-md transition-colors text-muted-foreground", title: isSidebarOpen
158
176
  ? t('layout.closeSidebar')
159
- : t('layout.openSidebar'), children: isSidebarOpen ? (_jsx(PanelLeftCloseIcon, { className: "h-6 w-6" })) : (_jsx(PanelLeftOpenIcon, { className: "h-6 w-6" })) }), _jsx(Breadcrumbs, { homeLabel: t('sidebar.documents') })] }), _jsxs("div", { className: "flex items-center gap-4 py-2", children: [_jsxs("div", { ref: searchInputRef, className: "relative w-64", children: [_jsx(Search, { className: "absolute left-2 top-2.5 h-4 w-4 z-10" }), _jsx("input", { type: "text", placeholder: t('layout.searchPlaceholder'), className: "w-full pl-8 pr-3 py-2 text-sm border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent", value: headerSearchQuery, onChange: (e) => {
177
+ : t('layout.openSidebar'), children: isSidebarOpen ? (_jsx(PanelLeftCloseIcon, { className: "h-6 w-6" })) : (_jsx(PanelLeftOpenIcon, { className: "h-6 w-6" })) })), _jsx(Breadcrumbs, { homeLabel: t('sidebar.documents') })] }), _jsxs("div", { className: "flex items-center gap-2 sm:gap-4 py-2", children: [_jsxs("div", { ref: searchInputRef, className: "relative w-40 sm:w-64", children: [_jsx(Search, { className: "absolute left-2 top-2.5 h-4 w-4 z-10" }), _jsx("input", { type: "text", placeholder: t('layout.searchPlaceholder'), className: "w-full pl-8 pr-3 py-2 text-sm border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent", value: headerSearchQuery, onChange: (e) => {
160
178
  setHeaderSearchQuery(e.target.value);
161
179
  setShowSearchResults(e.target.value.length > 0);
162
180
  }, onFocus: () => {
@@ -172,5 +190,5 @@ export function Layout({ children, userAvatar }) {
172
190
  setHeaderSearchQuery('');
173
191
  setShowSearchResults(false);
174
192
  }, className: "w-full px-3 py-2 text-left hover:bg-primary hover:text-primary-foreground transition-colors", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("div", { className: "shrink-0 mt-0.5", children: doc.emoji ? (_jsx("span", { className: "text-base", children: doc.emoji })) : (_jsx(FileText, { className: "h-4 w-4 text-muted-foreground" })) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "font-medium text-sm truncate", children: doc.title }), breadcrumb && (_jsx("div", { className: "text-xs text-muted-foreground truncate", children: breadcrumb }))] })] }) }, doc.id));
175
- }) })) }))] }), _jsxs("button", { onClick: () => onCreate(), className: "px-4 py-2 bg-secondary text-white text-sm rounded-md hover:bg-gray-800 flex items-center gap-2 transition-colors", children: [_jsx(Plus, { className: "h-4 w-4" }), t('layout.create')] }), userAvatar] })] })), _jsx("div", { className: "flex-1 overflow-auto p-6", children: children })] })] }));
193
+ }) })) }))] }), _jsxs("button", { onClick: () => onCreate(), className: "px-3 sm:px-4 py-2 bg-secondary text-white text-sm rounded-md hover:bg-gray-800 flex items-center gap-2 transition-colors", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('layout.create') })] }), userAvatar] })] })), _jsx("div", { className: "flex-1 overflow-auto p-4 sm:p-6", children: children })] })] }));
176
194
  }
@@ -17,25 +17,50 @@ export declare class FilesService {
17
17
  */
18
18
  upload(data: InsertFileUpload): Promise<File>;
19
19
  /**
20
- * Get a file by its ID
20
+ * Get a file by its ID (excludes soft-deleted files)
21
21
  * @param id - The file ID
22
22
  * @returns The file record or undefined if not found
23
23
  */
24
24
  getById(id: string): Promise<File | undefined>;
25
25
  /**
26
- * Delete a file by its ID
26
+ * Soft delete a file by its ID (sets deletedAt timestamp)
27
27
  * @param id - The file ID to delete
28
28
  * @throws Error if deletion fails
29
29
  */
30
30
  delete(id: string): Promise<void>;
31
31
  /**
32
- * List all files with optional pagination
33
- * @param options - Pagination options
32
+ * Hard delete a file by its ID (permanent deletion)
33
+ * @param id - The file ID to delete
34
+ * @throws Error if deletion fails
35
+ */
36
+ hardDelete(id: string): Promise<void>;
37
+ /**
38
+ * Restore a soft-deleted file
39
+ * @param id - The file ID to restore
40
+ * @throws Error if restoration fails
41
+ */
42
+ restore(id: string): Promise<void>;
43
+ /**
44
+ * List all non-deleted files with optional pagination
45
+ * @param options - Pagination options and includeDeleted flag
34
46
  * @returns Array of files and total count
35
47
  */
36
48
  list(options?: {
37
49
  limit?: number;
38
50
  offset?: number;
51
+ includeDeleted?: boolean;
52
+ }): Promise<{
53
+ files: File[];
54
+ total: number;
55
+ }>;
56
+ /**
57
+ * List soft-deleted files (for cleanup purposes)
58
+ * @param options - Pagination options
59
+ * @returns Array of deleted files and total count
60
+ */
61
+ listDeleted(options?: {
62
+ limit?: number;
63
+ offset?: number;
39
64
  }): Promise<{
40
65
  files: File[];
41
66
  total: number;
@@ -1 +1 @@
1
- {"version":3,"file":"FilesService.d.ts","sourceRoot":"","sources":["../../src/server/FilesService.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAA;AAC5B,OAAO,EAEL,KAAK,IAAI,EACV,MAAM,aAAa,CAAA;AAEpB;;;;GAIG;AACH,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEnC;;;;;OAKG;IACG,MAAM,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBnD;;;;OAIG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC;IAQpD;;;;OAIG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvC;;;;OAIG;IACG,IAAI,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAkBzG"}
1
+ {"version":3,"file":"FilesService.d.ts","sourceRoot":"","sources":["../../src/server/FilesService.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAA;AAC5B,OAAO,EAEL,KAAK,IAAI,EACV,MAAM,aAAa,CAAA;AAEpB;;;;GAIG;AACH,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEnC;;;;;OAKG;IACG,MAAM,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBnD;;;;OAIG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC;IAapD;;;;OAIG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYvC;;;;OAIG;IACG,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW3C;;;;OAIG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxC;;;;OAIG;IACG,IAAI,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAwBlI;;;;OAIG;IACG,WAAW,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAoBhH"}
@@ -1,4 +1,4 @@
1
- import { eq, sql } from 'drizzle-orm';
1
+ import { eq, isNull, sql } from 'drizzle-orm';
2
2
  import { nanoid } from 'nanoid';
3
3
  import { filesTable } from './schema.js';
4
4
  /**
@@ -32,7 +32,7 @@ export class FilesService {
32
32
  return result;
33
33
  }
34
34
  /**
35
- * Get a file by its ID
35
+ * Get a file by its ID (excludes soft-deleted files)
36
36
  * @param id - The file ID
37
37
  * @returns The file record or undefined if not found
38
38
  */
@@ -40,14 +40,33 @@ export class FilesService {
40
40
  const file = await this.db.query.filesTable.findFirst({
41
41
  where: eq(filesTable.id, id),
42
42
  });
43
+ // Return undefined if file is soft-deleted
44
+ if (file?.deletedAt) {
45
+ return undefined;
46
+ }
43
47
  return file;
44
48
  }
45
49
  /**
46
- * Delete a file by its ID
50
+ * Soft delete a file by its ID (sets deletedAt timestamp)
47
51
  * @param id - The file ID to delete
48
52
  * @throws Error if deletion fails
49
53
  */
50
54
  async delete(id) {
55
+ const [result] = await this.db
56
+ .update(filesTable)
57
+ .set({ deletedAt: new Date() })
58
+ .where(eq(filesTable.id, id))
59
+ .returning();
60
+ if (!result) {
61
+ throw new Error('File not found');
62
+ }
63
+ }
64
+ /**
65
+ * Hard delete a file by its ID (permanent deletion)
66
+ * @param id - The file ID to delete
67
+ * @throws Error if deletion fails
68
+ */
69
+ async hardDelete(id) {
51
70
  const [result] = await this.db
52
71
  .delete(filesTable)
53
72
  .where(eq(filesTable.id, id))
@@ -57,21 +76,61 @@ export class FilesService {
57
76
  }
58
77
  }
59
78
  /**
60
- * List all files with optional pagination
61
- * @param options - Pagination options
79
+ * Restore a soft-deleted file
80
+ * @param id - The file ID to restore
81
+ * @throws Error if restoration fails
82
+ */
83
+ async restore(id) {
84
+ const [result] = await this.db
85
+ .update(filesTable)
86
+ .set({ deletedAt: null })
87
+ .where(eq(filesTable.id, id))
88
+ .returning();
89
+ if (!result) {
90
+ throw new Error('File not found');
91
+ }
92
+ }
93
+ /**
94
+ * List all non-deleted files with optional pagination
95
+ * @param options - Pagination options and includeDeleted flag
62
96
  * @returns Array of files and total count
63
97
  */
64
98
  async list(options = {}) {
65
- const { limit = 50, offset = 0 } = options;
99
+ const { limit = 50, offset = 0, includeDeleted = false } = options;
100
+ // Build where condition based on includeDeleted flag
101
+ const whereCondition = includeDeleted ? undefined : isNull(filesTable.deletedAt);
66
102
  const files = await this.db.query.filesTable.findMany({
103
+ where: whereCondition,
67
104
  limit,
68
105
  offset,
69
106
  orderBy: (files, { desc }) => [desc(files.createdAt)],
70
107
  });
71
- // Get total count
108
+ // Get total count (filtered by deleted status)
109
+ const totalResult = await this.db
110
+ .select({ count: sql `count(*)::int` })
111
+ .from(filesTable)
112
+ .where(whereCondition ?? sql `TRUE`);
113
+ const total = Number(totalResult[0]?.count ?? 0);
114
+ return { files, total };
115
+ }
116
+ /**
117
+ * List soft-deleted files (for cleanup purposes)
118
+ * @param options - Pagination options
119
+ * @returns Array of deleted files and total count
120
+ */
121
+ async listDeleted(options = {}) {
122
+ const { limit = 50, offset = 0 } = options;
123
+ const files = await this.db.query.filesTable.findMany({
124
+ where: sql `${filesTable.deletedAt} IS NOT NULL`,
125
+ limit,
126
+ offset,
127
+ orderBy: (files, { desc }) => [desc(files.deletedAt)],
128
+ });
129
+ // Get total count of deleted files
72
130
  const totalResult = await this.db
73
131
  .select({ count: sql `count(*)::int` })
74
- .from(filesTable);
132
+ .from(filesTable)
133
+ .where(sql `${filesTable.deletedAt} IS NOT NULL`);
75
134
  const total = Number(totalResult[0]?.count ?? 0);
76
135
  return { files, total };
77
136
  }
@@ -307,6 +307,18 @@ export declare const filesTable: import("drizzle-orm/pg-core").PgTableWithColumn
307
307
  enumValues: undefined;
308
308
  baseColumn: never;
309
309
  }, {}, {}>;
310
+ deletedAt: import("drizzle-orm/pg-core").PgColumn<{
311
+ name: "deletedAt";
312
+ tableName: "files";
313
+ dataType: "date";
314
+ columnType: "PgTimestamp";
315
+ data: Date;
316
+ driverParam: string;
317
+ notNull: false;
318
+ hasDefault: false;
319
+ enumValues: undefined;
320
+ baseColumn: never;
321
+ }, {}, {}>;
310
322
  };
311
323
  dialect: "pg";
312
324
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAErD;;;GAGG;AACH,eAAO,MAAM,eAAe,gDAAmB,CAAA;AAE/C;;;GAGG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2CxB,CAAA;AAEH;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;EAM3B,CAAA;AAEH;;;GAGG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAcpB,CAAA;AAEH;;;GAGG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuB/B,CAAA;AAEH;;;GAGG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAa/B,CAAA;AAEH;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAC,YAAY,CAAA;AAC/D,MAAM,MAAM,QAAQ,GAAG,OAAO,cAAc,CAAC,YAAY,CAAA;AACzD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;AAEpD,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,YAAY,CAAA;AACvD,MAAM,MAAM,IAAI,GAAG,OAAO,UAAU,CAAC,YAAY,CAAA;AAEjD,MAAM,MAAM,qBAAqB,GAAG,OAAO,qBAAqB,CAAC,YAAY,CAAA;AAC7E,MAAM,MAAM,eAAe,GAAG,OAAO,qBAAqB,CAAC,YAAY,CAAA;AAEvE,MAAM,MAAM,sBAAsB,GAAG,OAAO,qBAAqB,CAAC,YAAY,CAAA;AAC9E,MAAM,MAAM,gBAAgB,GAAG,OAAO,qBAAqB,CAAC,YAAY,CAAA"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAErD;;;GAGG;AACH,eAAO,MAAM,eAAe,gDAAmB,CAAA;AAE/C;;;GAGG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2CxB,CAAA;AAEH;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;EAM3B,CAAA;AAEH;;;GAGG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiBpB,CAAA;AAEH;;;GAGG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuB/B,CAAA;AAEH;;;GAGG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAa/B,CAAA;AAEH;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAC,YAAY,CAAA;AAC/D,MAAM,MAAM,QAAQ,GAAG,OAAO,cAAc,CAAC,YAAY,CAAA;AACzD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;AAEpD,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,YAAY,CAAA;AACvD,MAAM,MAAM,IAAI,GAAG,OAAO,UAAU,CAAC,YAAY,CAAA;AAEjD,MAAM,MAAM,qBAAqB,GAAG,OAAO,qBAAqB,CAAC,YAAY,CAAA;AAC7E,MAAM,MAAM,eAAe,GAAG,OAAO,qBAAqB,CAAC,YAAY,CAAA;AAEvE,MAAM,MAAM,sBAAsB,GAAG,OAAO,qBAAqB,CAAC,YAAY,CAAA;AAC9E,MAAM,MAAM,gBAAgB,GAAG,OAAO,qBAAqB,CAAC,YAAY,CAAA"}
@@ -75,11 +75,14 @@ export const filesTable = docsTableSchema.table('files', {
75
75
  createdAt: timestamp('createdAt')
76
76
  .default(sql `NOW()`)
77
77
  .notNull(),
78
+ deletedAt: timestamp('deletedAt'), // Soft delete for files
78
79
  }, (table) => ({
79
80
  // Index for file lookups
80
81
  filenameIdx: index('filename_idx').on(table.filename),
81
82
  // Index for mime type filtering
82
83
  mimeTypeIdx: index('mime_type_idx').on(table.mimeType),
84
+ // Index for soft delete queries
85
+ filesDeletedAtIdx: index('files_deleted_at_idx').on(table.deletedAt),
83
86
  }));
84
87
  /**
85
88
  * Document versions table
@@ -0,0 +1,2 @@
1
+ ALTER TABLE "docs"."files" ADD COLUMN "deletedAt" timestamp;--> statement-breakpoint
2
+ CREATE INDEX IF NOT EXISTS "files_deleted_at_idx" ON "docs"."files" USING btree ("deletedAt");
@@ -0,0 +1,595 @@
1
+ {
2
+ "id": "cd711252-7226-4bb5-a58d-0867586c7463",
3
+ "prevId": "31106a8d-1ede-402b-9721-ead67be66c4d",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "docs.document_presence": {
8
+ "name": "document_presence",
9
+ "schema": "docs",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "uuid",
14
+ "primaryKey": true,
15
+ "notNull": true,
16
+ "default": "gen_random_uuid()"
17
+ },
18
+ "document_id": {
19
+ "name": "document_id",
20
+ "type": "varchar(21)",
21
+ "primaryKey": false,
22
+ "notNull": true
23
+ },
24
+ "user_id": {
25
+ "name": "user_id",
26
+ "type": "varchar(255)",
27
+ "primaryKey": false,
28
+ "notNull": true
29
+ },
30
+ "user_name": {
31
+ "name": "user_name",
32
+ "type": "varchar(255)",
33
+ "primaryKey": false,
34
+ "notNull": false
35
+ },
36
+ "user_color": {
37
+ "name": "user_color",
38
+ "type": "varchar(7)",
39
+ "primaryKey": false,
40
+ "notNull": false
41
+ },
42
+ "cursor_position": {
43
+ "name": "cursor_position",
44
+ "type": "jsonb",
45
+ "primaryKey": false,
46
+ "notNull": false
47
+ },
48
+ "selection": {
49
+ "name": "selection",
50
+ "type": "jsonb",
51
+ "primaryKey": false,
52
+ "notNull": false
53
+ },
54
+ "last_seen_at": {
55
+ "name": "last_seen_at",
56
+ "type": "timestamp",
57
+ "primaryKey": false,
58
+ "notNull": true,
59
+ "default": "NOW()"
60
+ }
61
+ },
62
+ "indexes": {
63
+ "presence_idx": {
64
+ "name": "presence_idx",
65
+ "columns": [
66
+ {
67
+ "expression": "document_id",
68
+ "isExpression": false,
69
+ "asc": true,
70
+ "nulls": "last"
71
+ },
72
+ {
73
+ "expression": "user_id",
74
+ "isExpression": false,
75
+ "asc": true,
76
+ "nulls": "last"
77
+ }
78
+ ],
79
+ "isUnique": false,
80
+ "concurrently": false,
81
+ "method": "btree",
82
+ "with": {}
83
+ },
84
+ "last_seen_idx": {
85
+ "name": "last_seen_idx",
86
+ "columns": [
87
+ {
88
+ "expression": "last_seen_at",
89
+ "isExpression": false,
90
+ "asc": true,
91
+ "nulls": "last"
92
+ }
93
+ ],
94
+ "isUnique": false,
95
+ "concurrently": false,
96
+ "method": "btree",
97
+ "with": {}
98
+ }
99
+ },
100
+ "foreignKeys": {
101
+ "document_presence_document_id_documents_id_fk": {
102
+ "name": "document_presence_document_id_documents_id_fk",
103
+ "tableFrom": "document_presence",
104
+ "tableTo": "documents",
105
+ "schemaTo": "docs",
106
+ "columnsFrom": [
107
+ "document_id"
108
+ ],
109
+ "columnsTo": [
110
+ "id"
111
+ ],
112
+ "onDelete": "no action",
113
+ "onUpdate": "no action"
114
+ }
115
+ },
116
+ "compositePrimaryKeys": {},
117
+ "uniqueConstraints": {}
118
+ },
119
+ "docs.document_versions": {
120
+ "name": "document_versions",
121
+ "schema": "docs",
122
+ "columns": {
123
+ "id": {
124
+ "name": "id",
125
+ "type": "uuid",
126
+ "primaryKey": true,
127
+ "notNull": true,
128
+ "default": "gen_random_uuid()"
129
+ },
130
+ "document_id": {
131
+ "name": "document_id",
132
+ "type": "varchar(21)",
133
+ "primaryKey": false,
134
+ "notNull": true
135
+ },
136
+ "version_number": {
137
+ "name": "version_number",
138
+ "type": "integer",
139
+ "primaryKey": false,
140
+ "notNull": true
141
+ },
142
+ "change_description": {
143
+ "name": "change_description",
144
+ "type": "text",
145
+ "primaryKey": false,
146
+ "notNull": false
147
+ },
148
+ "title": {
149
+ "name": "title",
150
+ "type": "varchar(255)",
151
+ "primaryKey": false,
152
+ "notNull": true
153
+ },
154
+ "content": {
155
+ "name": "content",
156
+ "type": "jsonb",
157
+ "primaryKey": false,
158
+ "notNull": true
159
+ },
160
+ "emoji": {
161
+ "name": "emoji",
162
+ "type": "varchar(10)",
163
+ "primaryKey": false,
164
+ "notNull": false
165
+ },
166
+ "cover": {
167
+ "name": "cover",
168
+ "type": "varchar(500)",
169
+ "primaryKey": false,
170
+ "notNull": false
171
+ },
172
+ "ydoc_state": {
173
+ "name": "ydoc_state",
174
+ "type": "text",
175
+ "primaryKey": false,
176
+ "notNull": false
177
+ },
178
+ "created_by": {
179
+ "name": "created_by",
180
+ "type": "varchar(255)",
181
+ "primaryKey": false,
182
+ "notNull": false
183
+ },
184
+ "created_by_name": {
185
+ "name": "created_by_name",
186
+ "type": "varchar(255)",
187
+ "primaryKey": false,
188
+ "notNull": false
189
+ },
190
+ "created_at": {
191
+ "name": "created_at",
192
+ "type": "timestamp",
193
+ "primaryKey": false,
194
+ "notNull": true,
195
+ "default": "NOW()"
196
+ }
197
+ },
198
+ "indexes": {
199
+ "document_version_idx": {
200
+ "name": "document_version_idx",
201
+ "columns": [
202
+ {
203
+ "expression": "document_id",
204
+ "isExpression": false,
205
+ "asc": true,
206
+ "nulls": "last"
207
+ },
208
+ {
209
+ "expression": "version_number",
210
+ "isExpression": false,
211
+ "asc": true,
212
+ "nulls": "last"
213
+ }
214
+ ],
215
+ "isUnique": false,
216
+ "concurrently": false,
217
+ "method": "btree",
218
+ "with": {}
219
+ },
220
+ "version_created_at_idx": {
221
+ "name": "version_created_at_idx",
222
+ "columns": [
223
+ {
224
+ "expression": "created_at",
225
+ "isExpression": false,
226
+ "asc": true,
227
+ "nulls": "last"
228
+ }
229
+ ],
230
+ "isUnique": false,
231
+ "concurrently": false,
232
+ "method": "btree",
233
+ "with": {}
234
+ }
235
+ },
236
+ "foreignKeys": {
237
+ "document_versions_document_id_documents_id_fk": {
238
+ "name": "document_versions_document_id_documents_id_fk",
239
+ "tableFrom": "document_versions",
240
+ "tableTo": "documents",
241
+ "schemaTo": "docs",
242
+ "columnsFrom": [
243
+ "document_id"
244
+ ],
245
+ "columnsTo": [
246
+ "id"
247
+ ],
248
+ "onDelete": "no action",
249
+ "onUpdate": "no action"
250
+ }
251
+ },
252
+ "compositePrimaryKeys": {},
253
+ "uniqueConstraints": {}
254
+ },
255
+ "docs.documents": {
256
+ "name": "documents",
257
+ "schema": "docs",
258
+ "columns": {
259
+ "id": {
260
+ "name": "id",
261
+ "type": "varchar(21)",
262
+ "primaryKey": true,
263
+ "notNull": true
264
+ },
265
+ "title": {
266
+ "name": "title",
267
+ "type": "varchar(255)",
268
+ "primaryKey": false,
269
+ "notNull": true
270
+ },
271
+ "slug": {
272
+ "name": "slug",
273
+ "type": "varchar(255)",
274
+ "primaryKey": false,
275
+ "notNull": true
276
+ },
277
+ "content": {
278
+ "name": "content",
279
+ "type": "jsonb",
280
+ "primaryKey": false,
281
+ "notNull": true,
282
+ "default": "'[]'"
283
+ },
284
+ "search_index": {
285
+ "name": "search_index",
286
+ "type": "text",
287
+ "primaryKey": false,
288
+ "notNull": false
289
+ },
290
+ "emoji": {
291
+ "name": "emoji",
292
+ "type": "varchar(10)",
293
+ "primaryKey": false,
294
+ "notNull": false
295
+ },
296
+ "cover": {
297
+ "name": "cover",
298
+ "type": "varchar(500)",
299
+ "primaryKey": false,
300
+ "notNull": false
301
+ },
302
+ "isPublished": {
303
+ "name": "isPublished",
304
+ "type": "boolean",
305
+ "primaryKey": false,
306
+ "notNull": true,
307
+ "default": true
308
+ },
309
+ "parentId": {
310
+ "name": "parentId",
311
+ "type": "varchar(21)",
312
+ "primaryKey": false,
313
+ "notNull": false
314
+ },
315
+ "order": {
316
+ "name": "order",
317
+ "type": "integer",
318
+ "primaryKey": false,
319
+ "notNull": true,
320
+ "default": 0
321
+ },
322
+ "authorId": {
323
+ "name": "authorId",
324
+ "type": "integer",
325
+ "primaryKey": false,
326
+ "notNull": false
327
+ },
328
+ "ydoc_state": {
329
+ "name": "ydoc_state",
330
+ "type": "text",
331
+ "primaryKey": false,
332
+ "notNull": false
333
+ },
334
+ "last_modified_by": {
335
+ "name": "last_modified_by",
336
+ "type": "varchar(255)",
337
+ "primaryKey": false,
338
+ "notNull": false
339
+ },
340
+ "last_modified_at": {
341
+ "name": "last_modified_at",
342
+ "type": "timestamp",
343
+ "primaryKey": false,
344
+ "notNull": false
345
+ },
346
+ "createdAt": {
347
+ "name": "createdAt",
348
+ "type": "timestamp",
349
+ "primaryKey": false,
350
+ "notNull": true,
351
+ "default": "NOW()"
352
+ },
353
+ "updatedAt": {
354
+ "name": "updatedAt",
355
+ "type": "timestamp",
356
+ "primaryKey": false,
357
+ "notNull": true,
358
+ "default": "NOW()"
359
+ },
360
+ "deletedAt": {
361
+ "name": "deletedAt",
362
+ "type": "timestamp",
363
+ "primaryKey": false,
364
+ "notNull": false
365
+ }
366
+ },
367
+ "indexes": {
368
+ "content_search_idx": {
369
+ "name": "content_search_idx",
370
+ "columns": [
371
+ {
372
+ "expression": "content",
373
+ "isExpression": false,
374
+ "asc": true,
375
+ "nulls": "last"
376
+ }
377
+ ],
378
+ "isUnique": false,
379
+ "concurrently": false,
380
+ "method": "gin",
381
+ "with": {}
382
+ },
383
+ "title_search_idx": {
384
+ "name": "title_search_idx",
385
+ "columns": [
386
+ {
387
+ "expression": "title",
388
+ "isExpression": false,
389
+ "asc": true,
390
+ "nulls": "last"
391
+ }
392
+ ],
393
+ "isUnique": false,
394
+ "concurrently": false,
395
+ "method": "btree",
396
+ "with": {}
397
+ },
398
+ "search_index_idx": {
399
+ "name": "search_index_idx",
400
+ "columns": [
401
+ {
402
+ "expression": "search_index",
403
+ "isExpression": false,
404
+ "asc": true,
405
+ "nulls": "last"
406
+ }
407
+ ],
408
+ "isUnique": false,
409
+ "concurrently": false,
410
+ "method": "btree",
411
+ "with": {}
412
+ },
413
+ "deleted_at_idx": {
414
+ "name": "deleted_at_idx",
415
+ "columns": [
416
+ {
417
+ "expression": "deletedAt",
418
+ "isExpression": false,
419
+ "asc": true,
420
+ "nulls": "last"
421
+ }
422
+ ],
423
+ "isUnique": false,
424
+ "concurrently": false,
425
+ "method": "btree",
426
+ "with": {}
427
+ },
428
+ "parent_id_idx": {
429
+ "name": "parent_id_idx",
430
+ "columns": [
431
+ {
432
+ "expression": "parentId",
433
+ "isExpression": false,
434
+ "asc": true,
435
+ "nulls": "last"
436
+ }
437
+ ],
438
+ "isUnique": false,
439
+ "concurrently": false,
440
+ "method": "btree",
441
+ "with": {}
442
+ },
443
+ "order_idx": {
444
+ "name": "order_idx",
445
+ "columns": [
446
+ {
447
+ "expression": "order",
448
+ "isExpression": false,
449
+ "asc": true,
450
+ "nulls": "last"
451
+ }
452
+ ],
453
+ "isUnique": false,
454
+ "concurrently": false,
455
+ "method": "btree",
456
+ "with": {}
457
+ }
458
+ },
459
+ "foreignKeys": {
460
+ "documents_parentId_documents_id_fk": {
461
+ "name": "documents_parentId_documents_id_fk",
462
+ "tableFrom": "documents",
463
+ "tableTo": "documents",
464
+ "schemaTo": "docs",
465
+ "columnsFrom": [
466
+ "parentId"
467
+ ],
468
+ "columnsTo": [
469
+ "id"
470
+ ],
471
+ "onDelete": "no action",
472
+ "onUpdate": "no action"
473
+ }
474
+ },
475
+ "compositePrimaryKeys": {},
476
+ "uniqueConstraints": {
477
+ "documents_slug_unique": {
478
+ "name": "documents_slug_unique",
479
+ "nullsNotDistinct": false,
480
+ "columns": [
481
+ "slug"
482
+ ]
483
+ }
484
+ }
485
+ },
486
+ "docs.files": {
487
+ "name": "files",
488
+ "schema": "docs",
489
+ "columns": {
490
+ "id": {
491
+ "name": "id",
492
+ "type": "varchar(21)",
493
+ "primaryKey": true,
494
+ "notNull": true
495
+ },
496
+ "filename": {
497
+ "name": "filename",
498
+ "type": "varchar(255)",
499
+ "primaryKey": false,
500
+ "notNull": true
501
+ },
502
+ "mimeType": {
503
+ "name": "mimeType",
504
+ "type": "varchar(100)",
505
+ "primaryKey": false,
506
+ "notNull": true
507
+ },
508
+ "size": {
509
+ "name": "size",
510
+ "type": "integer",
511
+ "primaryKey": false,
512
+ "notNull": true
513
+ },
514
+ "content": {
515
+ "name": "content",
516
+ "type": "text",
517
+ "primaryKey": false,
518
+ "notNull": true
519
+ },
520
+ "createdAt": {
521
+ "name": "createdAt",
522
+ "type": "timestamp",
523
+ "primaryKey": false,
524
+ "notNull": true,
525
+ "default": "NOW()"
526
+ },
527
+ "deletedAt": {
528
+ "name": "deletedAt",
529
+ "type": "timestamp",
530
+ "primaryKey": false,
531
+ "notNull": false
532
+ }
533
+ },
534
+ "indexes": {
535
+ "filename_idx": {
536
+ "name": "filename_idx",
537
+ "columns": [
538
+ {
539
+ "expression": "filename",
540
+ "isExpression": false,
541
+ "asc": true,
542
+ "nulls": "last"
543
+ }
544
+ ],
545
+ "isUnique": false,
546
+ "concurrently": false,
547
+ "method": "btree",
548
+ "with": {}
549
+ },
550
+ "mime_type_idx": {
551
+ "name": "mime_type_idx",
552
+ "columns": [
553
+ {
554
+ "expression": "mimeType",
555
+ "isExpression": false,
556
+ "asc": true,
557
+ "nulls": "last"
558
+ }
559
+ ],
560
+ "isUnique": false,
561
+ "concurrently": false,
562
+ "method": "btree",
563
+ "with": {}
564
+ },
565
+ "files_deleted_at_idx": {
566
+ "name": "files_deleted_at_idx",
567
+ "columns": [
568
+ {
569
+ "expression": "deletedAt",
570
+ "isExpression": false,
571
+ "asc": true,
572
+ "nulls": "last"
573
+ }
574
+ ],
575
+ "isUnique": false,
576
+ "concurrently": false,
577
+ "method": "btree",
578
+ "with": {}
579
+ }
580
+ },
581
+ "foreignKeys": {},
582
+ "compositePrimaryKeys": {},
583
+ "uniqueConstraints": {}
584
+ }
585
+ },
586
+ "enums": {},
587
+ "schemas": {
588
+ "docs": "docs"
589
+ },
590
+ "_meta": {
591
+ "columns": {},
592
+ "schemas": {},
593
+ "tables": {}
594
+ }
595
+ }
@@ -8,6 +8,13 @@
8
8
  "when": 1770028583768,
9
9
  "tag": "0000_gray_monster_badoon",
10
10
  "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "7",
15
+ "when": 1770609760944,
16
+ "tag": "0001_omniscient_fallen_one",
17
+ "breakpoints": true
11
18
  }
12
19
  ]
13
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-embed-docs",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Full-stack documentation system with BlockNote editor, hierarchical structure, and file uploads",
5
5
  "type": "module",
6
6
  "main": "./dist/server/index.js",
package/styles/docs.css CHANGED
@@ -978,4 +978,134 @@
978
978
  .docs-content {
979
979
  padding: 1rem;
980
980
  }
981
+
982
+ /* Mobile typography adjustments */
983
+ .prose h1 {
984
+ font-size: 1.75rem;
985
+ }
986
+
987
+ .prose h2 {
988
+ font-size: 1.375rem;
989
+ }
990
+
991
+ .prose h3 {
992
+ font-size: 1.125rem;
993
+ }
994
+
995
+ /* Mobile editor adjustments */
996
+ .docs-editor-container {
997
+ max-width: 100%;
998
+ padding: 0 0.5rem;
999
+ }
1000
+
1001
+ .docs-editor-title-input {
1002
+ font-size: 1.25rem;
1003
+ }
1004
+
1005
+ /* Mobile cover adjustments */
1006
+ .docs-cover-image,
1007
+ .docs-cover-dropzone {
1008
+ max-height: 200px;
1009
+ }
1010
+
1011
+ /* Mobile button sizing */
1012
+ .docs-btn {
1013
+ padding: 0.375rem 0.75rem;
1014
+ font-size: 0.8125rem;
1015
+ }
1016
+
1017
+ /* Mobile tree item touch targets */
1018
+ .docs-tree-item {
1019
+ padding: 0.5rem 0.625rem;
1020
+ min-height: 2.5rem;
1021
+ }
1022
+
1023
+ .docs-tree-item-toggle,
1024
+ .docs-tree-item-drag-handle {
1025
+ min-width: 2rem;
1026
+ min-height: 2rem;
1027
+ display: flex;
1028
+ align-items: center;
1029
+ justify-content: center;
1030
+ }
1031
+
1032
+ /* Always show drag handle on mobile for better touch */
1033
+ .docs-tree-item-drag-handle {
1034
+ opacity: 1;
1035
+ }
1036
+
1037
+ /* Mobile card adjustments */
1038
+ .docs-list-card {
1039
+ padding: 0.875rem;
1040
+ }
1041
+
1042
+ /* Mobile emoji picker */
1043
+ .docs-emoji-picker-dropdown {
1044
+ width: 14rem;
1045
+ left: 50%;
1046
+ transform: translateX(-50%);
1047
+ }
1048
+
1049
+ .docs-emoji-grid {
1050
+ grid-template-columns: repeat(6, 1fr);
1051
+ }
1052
+
1053
+ /* Mobile dropdown adjustments */
1054
+ .docs-dropdown {
1055
+ max-width: calc(100vw - 2rem);
1056
+ }
1057
+
1058
+ /* Mobile view title */
1059
+ .docs-view-heading {
1060
+ font-size: 1.5rem;
1061
+ }
1062
+
1063
+ .docs-view-emoji {
1064
+ font-size: 2rem;
1065
+ }
1066
+ }
1067
+
1068
+ /* Small mobile devices */
1069
+ @media (max-width: 480px) {
1070
+ .docs-container {
1071
+ font-size: 0.875rem;
1072
+ }
1073
+
1074
+ .docs-content {
1075
+ padding: 0.75rem;
1076
+ }
1077
+
1078
+ .prose {
1079
+ line-height: 1.65;
1080
+ }
1081
+
1082
+ .prose p {
1083
+ margin-top: 0.75rem;
1084
+ margin-bottom: 0.75rem;
1085
+ }
1086
+
1087
+ .prose ul,
1088
+ .prose ol {
1089
+ padding-left: 1.25rem;
1090
+ }
1091
+
1092
+ /* Make inputs larger for mobile touch */
1093
+ .docs-input,
1094
+ .docs-sidebar-search-input {
1095
+ min-height: 2.5rem;
1096
+ font-size: 16px; /* Prevent zoom on iOS */
1097
+ }
1098
+
1099
+ /* Touch-friendly buttons */
1100
+ .docs-btn {
1101
+ min-height: 2.25rem;
1102
+ }
1103
+
1104
+ /* Mobile search results */
1105
+ .docs-search-results {
1106
+ max-height: 60vh;
1107
+ left: -1rem;
1108
+ right: -1rem;
1109
+ margin-top: 0.5rem;
1110
+ }
981
1111
  }