react-embed-docs 0.1.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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +422 -0
  3. package/dist/client/components/Breadcrumbs.d.ts +21 -0
  4. package/dist/client/components/Breadcrumbs.d.ts.map +1 -0
  5. package/dist/client/components/Breadcrumbs.js +123 -0
  6. package/dist/client/components/DocsLayout.d.ts +20 -0
  7. package/dist/client/components/DocsLayout.d.ts.map +1 -0
  8. package/dist/client/components/DocsLayout.js +387 -0
  9. package/dist/client/components/DocumentContent.d.ts +5 -0
  10. package/dist/client/components/DocumentContent.d.ts.map +1 -0
  11. package/dist/client/components/DocumentContent.js +15 -0
  12. package/dist/client/components/DocumentEdit.d.ts +6 -0
  13. package/dist/client/components/DocumentEdit.d.ts.map +1 -0
  14. package/dist/client/components/DocumentEdit.js +153 -0
  15. package/dist/client/components/DocumentList.d.ts +5 -0
  16. package/dist/client/components/DocumentList.d.ts.map +1 -0
  17. package/dist/client/components/DocumentList.js +39 -0
  18. package/dist/client/components/DocumentProvider.d.ts +42 -0
  19. package/dist/client/components/DocumentProvider.d.ts.map +1 -0
  20. package/dist/client/components/DocumentProvider.js +47 -0
  21. package/dist/client/components/DocumentView.d.ts +6 -0
  22. package/dist/client/components/DocumentView.d.ts.map +1 -0
  23. package/dist/client/components/DocumentView.js +58 -0
  24. package/dist/client/components/DragOverlayItem.d.ts +5 -0
  25. package/dist/client/components/DragOverlayItem.d.ts.map +1 -0
  26. package/dist/client/components/DragOverlayItem.js +9 -0
  27. package/dist/client/components/EmojiPicker.d.ts +8 -0
  28. package/dist/client/components/EmojiPicker.d.ts.map +1 -0
  29. package/dist/client/components/EmojiPicker.js +48 -0
  30. package/dist/client/components/ExportButton.d.ts +22 -0
  31. package/dist/client/components/ExportButton.d.ts.map +1 -0
  32. package/dist/client/components/ExportButton.js +97 -0
  33. package/dist/client/components/Layout.d.ts +7 -0
  34. package/dist/client/components/Layout.d.ts.map +1 -0
  35. package/dist/client/components/Layout.js +172 -0
  36. package/dist/client/components/ReactEmbedDocs.d.ts +8 -0
  37. package/dist/client/components/ReactEmbedDocs.d.ts.map +1 -0
  38. package/dist/client/components/ReactEmbedDocs.js +8 -0
  39. package/dist/client/components/SearchInput.d.ts +2 -0
  40. package/dist/client/components/SearchInput.d.ts.map +1 -0
  41. package/dist/client/components/SearchInput.js +7 -0
  42. package/dist/client/components/Sidebar.d.ts +10 -0
  43. package/dist/client/components/Sidebar.d.ts.map +1 -0
  44. package/dist/client/components/Sidebar.js +176 -0
  45. package/dist/client/components/SortableTreeItem.d.ts +13 -0
  46. package/dist/client/components/SortableTreeItem.d.ts.map +1 -0
  47. package/dist/client/components/SortableTreeItem.js +24 -0
  48. package/dist/client/components/VersionHistory.d.ts +14 -0
  49. package/dist/client/components/VersionHistory.d.ts.map +1 -0
  50. package/dist/client/components/VersionHistory.js +102 -0
  51. package/dist/client/hooks/useCollaboration.d.ts +99 -0
  52. package/dist/client/hooks/useCollaboration.d.ts.map +1 -0
  53. package/dist/client/hooks/useCollaboration.js +180 -0
  54. package/dist/client/hooks/useDocsQuery.d.ts +84 -0
  55. package/dist/client/hooks/useDocsQuery.d.ts.map +1 -0
  56. package/dist/client/hooks/useDocsQuery.js +241 -0
  57. package/dist/client/hooks/useExport.d.ts +31 -0
  58. package/dist/client/hooks/useExport.d.ts.map +1 -0
  59. package/dist/client/hooks/useExport.js +66 -0
  60. package/dist/client/hooks/useFileUpload.d.ts +44 -0
  61. package/dist/client/hooks/useFileUpload.d.ts.map +1 -0
  62. package/dist/client/hooks/useFileUpload.js +193 -0
  63. package/dist/client/hooks/useSystemTheme.d.ts +2 -0
  64. package/dist/client/hooks/useSystemTheme.d.ts.map +1 -0
  65. package/dist/client/hooks/useSystemTheme.js +19 -0
  66. package/dist/client/hooks/useVersions.d.ts +105 -0
  67. package/dist/client/hooks/useVersions.d.ts.map +1 -0
  68. package/dist/client/hooks/useVersions.js +129 -0
  69. package/dist/client/index.d.ts +23 -0
  70. package/dist/client/index.d.ts.map +1 -0
  71. package/dist/client/index.js +18 -0
  72. package/dist/client/lib/blocknoteTheme.d.ts +13 -0
  73. package/dist/client/lib/blocknoteTheme.d.ts.map +1 -0
  74. package/dist/client/lib/blocknoteTheme.js +76 -0
  75. package/dist/client/lib/path.d.ts +8 -0
  76. package/dist/client/lib/path.d.ts.map +1 -0
  77. package/dist/client/lib/path.js +30 -0
  78. package/dist/client/providers/DocumentProvider.d.ts +1 -0
  79. package/dist/client/providers/DocumentProvider.d.ts.map +1 -0
  80. package/dist/client/providers/DocumentProvider.js +1 -0
  81. package/dist/server/CollaborationService.d.ts +134 -0
  82. package/dist/server/CollaborationService.d.ts.map +1 -0
  83. package/dist/server/CollaborationService.js +307 -0
  84. package/dist/server/DocsService.d.ts +115 -0
  85. package/dist/server/DocsService.d.ts.map +1 -0
  86. package/dist/server/DocsService.js +512 -0
  87. package/dist/server/ExportService.d.ts +106 -0
  88. package/dist/server/ExportService.d.ts.map +1 -0
  89. package/dist/server/ExportService.js +501 -0
  90. package/dist/server/FilesService.d.ts +44 -0
  91. package/dist/server/FilesService.d.ts.map +1 -0
  92. package/dist/server/FilesService.js +78 -0
  93. package/dist/server/VersioningService.d.ts +112 -0
  94. package/dist/server/VersioningService.d.ts.map +1 -0
  95. package/dist/server/VersioningService.js +264 -0
  96. package/dist/server/db.d.ts +7 -0
  97. package/dist/server/db.d.ts.map +1 -0
  98. package/dist/server/db.js +22 -0
  99. package/dist/server/index.d.ts +55 -0
  100. package/dist/server/index.d.ts.map +1 -0
  101. package/dist/server/index.js +36 -0
  102. package/dist/server/routes.d.ts +9 -0
  103. package/dist/server/routes.d.ts.map +1 -0
  104. package/dist/server/routes.js +483 -0
  105. package/dist/server/schema.d.ts +587 -0
  106. package/dist/server/schema.d.ts.map +1 -0
  107. package/dist/server/schema.js +126 -0
  108. package/dist/shared/types.d.ts +314 -0
  109. package/dist/shared/types.d.ts.map +1 -0
  110. package/dist/shared/types.js +48 -0
  111. package/drizzle/migrations/0000_gray_monster_badoon.sql +88 -0
  112. package/drizzle/migrations/meta/0000_snapshot.json +574 -0
  113. package/drizzle/migrations/meta/_journal.json +13 -0
  114. package/package.json +109 -0
  115. package/styles/docs.css +981 -0
@@ -0,0 +1,387 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { closestCenter, defaultDropAnimationSideEffects, DndContext, DragOverlay, KeyboardSensor, PointerSensor, useSensor, useSensors, } from '@dnd-kit/core';
4
+ import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable';
5
+ import { CSS } from '@dnd-kit/utilities';
6
+ import { ChevronDown, ChevronRight, FileText, Folder, FolderOpen, GripVertical, PanelLeftCloseIcon, PanelLeftOpenIcon, Plus, Search, } from 'lucide-react';
7
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
+ import { useDocumentsQuery, useReorderDocumentMutation, } from '../hooks/useDocsQuery.js';
9
+ import { Breadcrumbs } from './Breadcrumbs.js';
10
+ function SortableTreeItem({ doc, isExpanded, isCurrent, onToggle, onNavigate, }) {
11
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: doc.id });
12
+ const style = {
13
+ transform: CSS.Transform.toString(transform),
14
+ transition,
15
+ opacity: isDragging ? 0.5 : 1,
16
+ paddingLeft: doc.depth * 12 + 8,
17
+ };
18
+ return (_jsxs("div", { ref: setNodeRef, style: style, className: [
19
+ 'flex items-center gap-1 w-full px-2 py-1.5 text-sm rounded-md transition-colors cursor-pointer',
20
+ isCurrent ? 'font-bold' : 'hover:opacity-80',
21
+ isDragging && 'opacity-50',
22
+ ].join(' '), children: [_jsx("div", { ...attributes, ...listeners, className: "cursor-grab active:cursor-grabbing p-0.5 rounded", onClick: (e) => e.stopPropagation(), children: _jsx(GripVertical, { className: "h-4 w-4 text-gray-400" }) }), doc.hasChildren ? (_jsx("button", { onClick: (e) => {
23
+ e.stopPropagation();
24
+ onToggle(doc.id);
25
+ }, className: "p-0.5 hover:bg-gray-200 rounded shrink-0", children: isExpanded ? (_jsx(ChevronDown, { className: "h-4 w-4" })) : (_jsx(ChevronRight, { className: "h-4 w-4" })) })) : (_jsx("div", { className: "w-4" })), doc.emoji ? (_jsx("span", { className: "text-base shrink-0", children: doc.emoji })) : doc.hasChildren ? (isExpanded ? (_jsx(FolderOpen, { className: "h-4 w-4 shrink-0 text-muted" })) : (_jsx(Folder, { className: "h-4 w-4 shrink-0 text-muted" }))) : (_jsx(FileText, { className: "h-4 w-4 shrink-0 text-gray-400" })), _jsx("span", { className: "truncate flex-1", onClick: () => onNavigate(doc.id), children: doc.title })] }));
26
+ }
27
+ // Drag Overlay Item
28
+ function DragOverlayItem({ doc }) {
29
+ return (_jsxs("div", { className: [
30
+ 'flex items-center gap-1 w-full px-2 py-1.5 text-sm text-gray-500 rounded-md shadow-lg',
31
+ 'opacity-80',
32
+ ].join(' '), children: [_jsx(GripVertical, { className: "h-4 w-4 text-gray-400" }), _jsx("div", { className: "w-5" }), doc.emoji ? (_jsx("span", { className: "text-base shrink-0", children: doc.emoji })) : doc.hasChildren ? (_jsx(Folder, { className: "h-4 w-4 shrink-0 text-blue-600" })) : (_jsx(FileText, { className: "h-4 w-4 shrink-0 text-gray-400" })), _jsx("span", { className: "truncate", children: doc.title })] }));
33
+ }
34
+ // Simple debounce hook
35
+ function useDebounce(value, delay) {
36
+ const [debouncedValue, setDebouncedValue] = useState(value);
37
+ useEffect(() => {
38
+ const timer = setTimeout(() => {
39
+ setDebouncedValue(value);
40
+ }, delay);
41
+ return () => clearTimeout(timer);
42
+ }, [value, delay]);
43
+ return debouncedValue;
44
+ }
45
+ // Build breadcrumb path for a document
46
+ function buildBreadcrumb(doc, allDocs) {
47
+ const parts = [doc.title];
48
+ let current = doc;
49
+ while (current.parentId) {
50
+ const parent = allDocs.find((d) => d.id === current.parentId);
51
+ if (!parent)
52
+ break;
53
+ parts.unshift(parent.title);
54
+ current = parent;
55
+ }
56
+ return parts.join(' / ');
57
+ }
58
+ // Build full path from root to document (for URL navigation)
59
+ function buildDocumentPath(docId, allDocs, basePath) {
60
+ const pathParts = [];
61
+ const visited = new Set();
62
+ let currentId = docId;
63
+ while (currentId) {
64
+ // Prevent infinite loops from circular references
65
+ if (visited.has(currentId))
66
+ break;
67
+ visited.add(currentId);
68
+ const doc = allDocs.find((d) => d.id === currentId);
69
+ if (!doc)
70
+ break;
71
+ pathParts.unshift(doc.slug || doc.id);
72
+ currentId = doc.parentId ?? null;
73
+ }
74
+ if (pathParts.length === 0)
75
+ return basePath;
76
+ return `${basePath}/${pathParts.join('/')}`;
77
+ }
78
+ // Get all ancestor IDs of a document (for auto-expanding)
79
+ function getAncestorIds(docId, allDocs) {
80
+ const ancestors = [];
81
+ const visited = new Set();
82
+ let currentId = docId;
83
+ while (currentId) {
84
+ if (visited.has(currentId))
85
+ break;
86
+ visited.add(currentId);
87
+ const doc = allDocs.find((d) => d.id === currentId);
88
+ if (!doc)
89
+ break;
90
+ if (doc.parentId) {
91
+ ancestors.push(doc.parentId);
92
+ currentId = doc.parentId;
93
+ }
94
+ else {
95
+ break;
96
+ }
97
+ }
98
+ return ancestors;
99
+ }
100
+ // Extract text snippets from BlockNote content
101
+ function extractTextSnippets(content, query) {
102
+ if (!content || typeof content !== 'object')
103
+ return [];
104
+ const snippets = [];
105
+ const queryLower = query.toLowerCase();
106
+ const extractFromBlock = (block) => {
107
+ if (!block || typeof block !== 'object')
108
+ return '';
109
+ const b = block;
110
+ // Handle text content
111
+ if (b.content && Array.isArray(b.content)) {
112
+ return b.content
113
+ .map((c) => {
114
+ if (typeof c === 'string')
115
+ return c;
116
+ if (c && typeof c === 'object') {
117
+ const contentItem = c;
118
+ if (contentItem.text && typeof contentItem.text === 'string') {
119
+ return contentItem.text;
120
+ }
121
+ }
122
+ return '';
123
+ })
124
+ .join(' ');
125
+ }
126
+ // Handle children
127
+ if (b.children && Array.isArray(b.children)) {
128
+ return b.children.map(extractFromBlock).join(' ');
129
+ }
130
+ return '';
131
+ };
132
+ const contentObj = content;
133
+ if (contentObj.content && Array.isArray(contentObj.content)) {
134
+ for (const block of contentObj.content) {
135
+ const text = extractFromBlock(block);
136
+ if (text.toLowerCase().includes(queryLower)) {
137
+ // Find the position of the match
138
+ const matchIndex = text.toLowerCase().indexOf(queryLower);
139
+ const start = Math.max(0, matchIndex - 20);
140
+ const end = Math.min(text.length, matchIndex + query.length + 60);
141
+ let snippet = text.slice(start, end);
142
+ // Add ellipsis if truncated
143
+ if (start > 0)
144
+ snippet = '...' + snippet;
145
+ if (end < text.length)
146
+ snippet = snippet + '...';
147
+ snippets.push(snippet);
148
+ if (snippets.length >= 2)
149
+ break; // Limit to 2 snippets
150
+ }
151
+ }
152
+ }
153
+ return snippets;
154
+ }
155
+ export function DocsLayout({ children, currentDocId, onNavigate, basePath = '/docs', userAvatar, onSearch, }) {
156
+ const [expandedFolders, setExpandedFolders] = useState(new Set());
157
+ const [searchQuery, setSearchQuery] = useState('');
158
+ const [activeId, setActiveId] = useState(null);
159
+ const [isSidebarOpen, setIsSidebarOpen] = useState(true);
160
+ // Header search state
161
+ const [headerSearchQuery, setHeaderSearchQuery] = useState('');
162
+ const [showSearchResults, setShowSearchResults] = useState(false);
163
+ const debouncedSearchQuery = useDebounce(headerSearchQuery, 300);
164
+ const searchInputRef = useRef(null);
165
+ const { data } = useDocumentsQuery();
166
+ const reorderMutation = useReorderDocumentMutation();
167
+ const documents = data?.documents ?? [];
168
+ // Search query for header search
169
+ const { data: searchResultsData } = useDocumentsQuery(debouncedSearchQuery.length > 0 ? { search: debouncedSearchQuery } : {});
170
+ const searchResults = searchResultsData?.documents ?? [];
171
+ // Resolve currentDocId (which might be a slug) to actual document ID
172
+ const resolvedCurrentDocId = useMemo(() => {
173
+ if (!currentDocId || documents.length === 0)
174
+ return null;
175
+ // First try to find by ID
176
+ const byId = documents.find((d) => d.id === currentDocId);
177
+ if (byId)
178
+ return byId.id;
179
+ // Then try to find by slug
180
+ const bySlug = documents.find((d) => d.slug === currentDocId);
181
+ if (bySlug)
182
+ return bySlug.id;
183
+ return null;
184
+ }, [currentDocId, documents]);
185
+ // Auto-expand tree branches when current document changes
186
+ useEffect(() => {
187
+ if (!resolvedCurrentDocId || documents.length === 0)
188
+ return;
189
+ const ancestors = getAncestorIds(resolvedCurrentDocId, documents);
190
+ // Expand all ancestors so the current document is visible
191
+ if (ancestors.length > 0) {
192
+ setExpandedFolders((prev) => {
193
+ const newExpanded = new Set(prev);
194
+ ancestors.forEach((id) => newExpanded.add(id));
195
+ return newExpanded;
196
+ });
197
+ }
198
+ }, [resolvedCurrentDocId, documents]);
199
+ // Handle click outside to close search results
200
+ useEffect(() => {
201
+ const handleClickOutside = (event) => {
202
+ if (searchInputRef.current &&
203
+ !searchInputRef.current.contains(event.target)) {
204
+ setShowSearchResults(false);
205
+ }
206
+ };
207
+ document.addEventListener('mousedown', handleClickOutside);
208
+ return () => document.removeEventListener('mousedown', handleClickOutside);
209
+ }, []);
210
+ // Build flattened tree for sortable context
211
+ const flattenedTree = useMemo(() => {
212
+ const items = [];
213
+ const processed = new Set();
214
+ const processDoc = (doc, depth) => {
215
+ if (processed.has(doc.id))
216
+ return;
217
+ processed.add(doc.id);
218
+ const children = documents.filter((d) => d.parentId === doc.id);
219
+ const hasChildren = children.length > 0;
220
+ items.push({ ...doc, depth, hasChildren });
221
+ // If expanded, process children
222
+ if (hasChildren && expandedFolders.has(doc.id)) {
223
+ children
224
+ .sort((a, b) => a.order - b.order)
225
+ .forEach((child) => processDoc(child, depth + 1));
226
+ }
227
+ };
228
+ // Process root documents
229
+ documents
230
+ .filter((d) => !d.parentId)
231
+ .sort((a, b) => a.order - b.order)
232
+ .forEach((doc) => processDoc(doc, 0));
233
+ return items;
234
+ }, [documents, expandedFolders]);
235
+ const getChildren = useCallback((parentId) => documents.filter((d) => d.parentId === parentId), [documents]);
236
+ const toggleFolder = useCallback((folderId) => {
237
+ setExpandedFolders((prev) => {
238
+ const newExpanded = new Set(prev);
239
+ if (newExpanded.has(folderId)) {
240
+ newExpanded.delete(folderId);
241
+ }
242
+ else {
243
+ newExpanded.add(folderId);
244
+ }
245
+ return newExpanded;
246
+ });
247
+ }, []);
248
+ // Navigate with full path
249
+ const handleNavigate = useCallback((docId) => {
250
+ if (!onNavigate)
251
+ return;
252
+ if (docId === 'new') {
253
+ onNavigate(`${basePath}/new`);
254
+ return;
255
+ }
256
+ const path = buildDocumentPath(docId, documents, basePath);
257
+ onNavigate(path);
258
+ }, [onNavigate, documents, basePath]);
259
+ const sensors = useSensors(useSensor(PointerSensor, {
260
+ activationConstraint: {
261
+ distance: 5,
262
+ },
263
+ }), useSensor(KeyboardSensor, {
264
+ coordinateGetter: sortableKeyboardCoordinates,
265
+ }));
266
+ const handleDragStart = useCallback((event) => {
267
+ setActiveId(event.active.id);
268
+ }, []);
269
+ const handleDragOver = useCallback((event) => {
270
+ const { over } = event;
271
+ if (!over)
272
+ return;
273
+ const overId = over.id;
274
+ // Auto-expand folder when hovering over it
275
+ const overDoc = documents.find((d) => d.id === overId);
276
+ if (overDoc) {
277
+ const children = getChildren(overId);
278
+ if (children.length > 0 && !expandedFolders.has(overId)) {
279
+ toggleFolder(overId);
280
+ }
281
+ }
282
+ }, [documents, expandedFolders, getChildren, toggleFolder]);
283
+ const handleDragEnd = useCallback((event) => {
284
+ const { active, over } = event;
285
+ setActiveId(null);
286
+ if (!over)
287
+ return;
288
+ const draggedId = active.id;
289
+ const overId = over.id;
290
+ if (draggedId === overId)
291
+ return;
292
+ const activeDoc = documents.find((d) => d.id === draggedId);
293
+ const overDoc = documents.find((d) => d.id === overId);
294
+ if (!activeDoc || !overDoc)
295
+ return;
296
+ // Calculate new parent and order
297
+ let newParentId = null;
298
+ let newOrder = 0;
299
+ // Determine if we're dropping before, after, or inside the target
300
+ const activeIndex = flattenedTree.findIndex((item) => item.id === draggedId);
301
+ const overIndex = flattenedTree.findIndex((item) => item.id === overId);
302
+ if (activeIndex === -1 || overIndex === -1)
303
+ return;
304
+ if (activeIndex < overIndex) {
305
+ // Dropping after the target
306
+ newParentId = overDoc.parentId || null;
307
+ newOrder = overDoc.order + 5;
308
+ }
309
+ else {
310
+ // Dropping before the target
311
+ newParentId = overDoc.parentId || null;
312
+ newOrder = overDoc.order - 5;
313
+ }
314
+ // Check if dropping on a folder (to make it a child)
315
+ const overHasChildren = getChildren(overId).length > 0;
316
+ if (overHasChildren && activeIndex > overIndex) {
317
+ // If dropping after a folder, make it the first child
318
+ newParentId = overId;
319
+ newOrder = 0;
320
+ }
321
+ // Prevent dropping a parent into its own child
322
+ const isChildOfActive = (docId) => {
323
+ const doc = documents.find((d) => d.id === docId);
324
+ if (!doc)
325
+ return false;
326
+ if (doc.parentId === draggedId)
327
+ return true;
328
+ if (doc.parentId)
329
+ return isChildOfActive(doc.parentId);
330
+ return false;
331
+ };
332
+ if (isChildOfActive(overId)) {
333
+ console.log('Cannot drop a parent into its own child');
334
+ return;
335
+ }
336
+ // Execute the reorder
337
+ reorderMutation.mutate({
338
+ id: draggedId,
339
+ parentId: newParentId,
340
+ order: newOrder,
341
+ });
342
+ }, [documents, flattenedTree, getChildren, reorderMutation]);
343
+ const dropAnimation = {
344
+ sideEffects: defaultDropAnimationSideEffects({
345
+ styles: {
346
+ active: {
347
+ opacity: '0.5',
348
+ },
349
+ },
350
+ }),
351
+ };
352
+ const activeDoc = useMemo(() => {
353
+ if (!activeId)
354
+ return null;
355
+ const doc = documents.find((d) => d.id === activeId);
356
+ if (!doc)
357
+ return null;
358
+ const children = getChildren(doc.id);
359
+ return { ...doc, depth: 0, hasChildren: children.length > 0 };
360
+ }, [activeId, documents, getChildren]);
361
+ return (_jsxs("div", { className: "flex h-screen w-full", children: [_jsxs("aside", { className: [
362
+ 'border-r border-border flex flex-col transition-all duration-200 ease-in-out overflow-hidden',
363
+ isSidebarOpen ? 'w-64 opacity-100' : 'w-0 opacity-0',
364
+ ].join(' '), children: [_jsxs("div", { className: "p-4", children: [_jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("h2", { className: "font-semibold text-sm text-muted-foreground", children: "Documents" }), _jsx("button", { className: "p-2 hover:bg-primary rounded-md transition-colors cursor-pointer", onClick: () => handleNavigate('new'), children: _jsx(Plus, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-2 top-2.5 h-4 w-4 text-gray-400" }), _jsx("input", { type: "text", placeholder: "Search docs...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), className: "w-full pl-8 pr-3 py-2 text-sm border border-gray-300 dark:border-gray-700 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" })] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto p-2 space-y-1", children: [_jsxs(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragStart: handleDragStart, onDragOver: handleDragOver, onDragEnd: handleDragEnd, children: [_jsx(SortableContext, { items: flattenedTree.map((item) => item.id), strategy: verticalListSortingStrategy, children: flattenedTree.map((doc) => (_jsx(SortableTreeItem, { doc: doc, isExpanded: expandedFolders.has(doc.id), isCurrent: doc.id === resolvedCurrentDocId, onToggle: toggleFolder, onNavigate: handleNavigate }, doc.id))) }), _jsx(DragOverlay, { dropAnimation: dropAnimation, children: activeDoc ? _jsx(DragOverlayItem, { doc: activeDoc }) : null })] }), documents.length === 0 && (_jsx("div", { className: "text-sm text-gray-500 text-center py-4", children: "No documents yet" }))] })] }), _jsxs("main", { className: "flex-1 flex flex-col min-w-0 overflow-hidden", children: [_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 ? 'Close sidebar' : 'Open sidebar', children: isSidebarOpen ? (_jsx(PanelLeftCloseIcon, { className: "h-6 w-6" })) : (_jsx(PanelLeftOpenIcon, { className: "h-6 w-6" })) }), _jsx(Breadcrumbs, { docId: resolvedCurrentDocId ?? undefined, documents: documents.map((d) => ({
365
+ id: d.id,
366
+ title: d.title,
367
+ slug: d.slug,
368
+ emoji: d.emoji ?? undefined,
369
+ parentId: d.parentId,
370
+ })), onNavigate: onNavigate, homeLabel: "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: "Search documentation...", 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) => {
371
+ setHeaderSearchQuery(e.target.value);
372
+ setShowSearchResults(e.target.value.length > 0);
373
+ }, onFocus: () => {
374
+ if (headerSearchQuery.length > 0) {
375
+ setShowSearchResults(true);
376
+ }
377
+ } }), showSearchResults && headerSearchQuery.length > 0 && (_jsx("div", { className: "absolute top-full left-0 right-0 mt-1 border border-gray-200 rounded-md shadow-lg max-h-80 overflow-y-auto z-50", children: searchResults.length === 0 ? (_jsxs("div", { className: "px-4 py-3 text-sm text-gray-500", children: ["No results found for \"", headerSearchQuery, "\""] })) : (_jsx("div", { className: "py-1", children: searchResults.map((doc) => {
378
+ const breadcrumb = doc.parentId
379
+ ? buildBreadcrumb(doc, documents)
380
+ : null;
381
+ return (_jsx("button", { onClick: () => {
382
+ handleNavigate(doc.id);
383
+ setHeaderSearchQuery('');
384
+ setShowSearchResults(false);
385
+ }, className: "w-full px-3 py-2 text-left hover:bg-gray-100 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-gray-400" })) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "font-medium text-sm text-gray-900 truncate", children: doc.title }), breadcrumb && (_jsx("div", { className: "text-xs text-gray-500 truncate", children: breadcrumb }))] })] }) }, doc.id));
386
+ }) })) }))] }), _jsxs("button", { onClick: () => handleNavigate('new'), 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" }), "Create"] }), userAvatar] })] }), _jsx("div", { className: "flex-1 overflow-auto p-6", children: children })] })] }));
387
+ }
@@ -0,0 +1,5 @@
1
+ interface IProps {
2
+ }
3
+ export declare function DocumentContent({}: IProps): import("react/jsx-runtime").JSX.Element;
4
+ export {};
5
+ //# sourceMappingURL=DocumentContent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocumentContent.d.ts","sourceRoot":"","sources":["../../../src/client/components/DocumentContent.tsx"],"names":[],"mappings":"AAKA,UAAU,MAAM;CAAG;AAEnB,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,2CASzC"}
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { DocumentEdit } from './DocumentEdit';
3
+ import { DocumentList } from './DocumentList';
4
+ import { useDocument } from './DocumentProvider';
5
+ import { DocumentView } from './DocumentView';
6
+ export function DocumentContent({}) {
7
+ const { params } = useDocument();
8
+ if (params.mode === 'create' || params.mode === 'edit') {
9
+ return _jsx(DocumentEdit, {});
10
+ }
11
+ if (params.documentSlug) {
12
+ return _jsx(DocumentView, {});
13
+ }
14
+ return _jsx(DocumentList, {});
15
+ }
@@ -0,0 +1,6 @@
1
+ import '@blocknote/mantine/style.css';
2
+ interface DocumentEditProps {
3
+ }
4
+ export declare function DocumentEdit({}: DocumentEditProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
6
+ //# sourceMappingURL=DocumentEdit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocumentEdit.d.ts","sourceRoot":"","sources":["../../../src/client/components/DocumentEdit.tsx"],"names":[],"mappings":"AAIA,OAAO,8BAA8B,CAAA;AAerC,UAAU,iBAAiB;CAAG;AAqB9B,wBAAgB,YAAY,CAAC,EAAE,EAAE,iBAAiB,2CA6PjD"}
@@ -0,0 +1,153 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { BlockNoteView } from '@blocknote/mantine';
4
+ import '@blocknote/mantine/style.css';
5
+ import { useCreateBlockNote } from '@blocknote/react';
6
+ import { Eye, ImageIcon, Loader2, Save, X } from 'lucide-react';
7
+ import { useEffect, useState } from 'react';
8
+ import { useCreateDocumentMutation, useDocumentQuery, useUpdateDocumentMutation, } from '../hooks/useDocsQuery.js';
9
+ import { useFileUpload } from '../hooks/useFileUpload.js';
10
+ import { blockNoteTheme } from '../lib/blocknoteTheme.js';
11
+ import { useDocument } from './DocumentProvider.js';
12
+ import { EmojiPicker } from './EmojiPicker.js';
13
+ // Default initial content for new documents
14
+ const getDefaultContent = () => [
15
+ {
16
+ type: 'paragraph',
17
+ content: '',
18
+ },
19
+ ];
20
+ // Generate slug from title
21
+ const generateSlug = (title) => {
22
+ return (title
23
+ .toLowerCase()
24
+ .replace(/[^a-z0-9]+/g, '-')
25
+ .replace(/^-+|-+$/g, '')
26
+ .substring(0, 255) || 'untitled');
27
+ };
28
+ export function DocumentEdit({}) {
29
+ const { onOpen, onEdit, theme, params } = useDocument();
30
+ const isNewDocument = params.documentSlug === undefined;
31
+ // Fetch existing document data if editing
32
+ const { data: existingDoc, isLoading: isLoadingDoc } = useDocumentQuery(params.documentSlug);
33
+ // Mutations
34
+ const createMutation = useCreateDocumentMutation();
35
+ const updateMutation = useUpdateDocumentMutation();
36
+ // Local state
37
+ const [title, setTitle] = useState('');
38
+ const [emoji, setEmoji] = useState(undefined);
39
+ const [cover, setCover] = useState(null);
40
+ const [isSaving, setIsSaving] = useState(false);
41
+ const [hasLoaded, setHasLoaded] = useState(false);
42
+ // File upload hook
43
+ const { isUploading: isUploadingCover, uploadFile: uploadCover } = useFileUpload({
44
+ maxSize: 5 * 1024 * 1024, // 5MB
45
+ acceptedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
46
+ });
47
+ // Initialize editor
48
+ const editor = useCreateBlockNote({
49
+ initialContent: getDefaultContent(),
50
+ });
51
+ // Load existing document data when available
52
+ useEffect(() => {
53
+ if (existingDoc && !hasLoaded) {
54
+ setTitle(existingDoc.title);
55
+ if (existingDoc.emoji)
56
+ setEmoji(existingDoc.emoji);
57
+ if (existingDoc.cover)
58
+ setCover(existingDoc.cover);
59
+ if (editor && existingDoc.content) {
60
+ try {
61
+ const content = typeof existingDoc.content === 'string'
62
+ ? JSON.parse(existingDoc.content)
63
+ : existingDoc.content;
64
+ editor.replaceBlocks(editor.document, content);
65
+ }
66
+ catch (e) {
67
+ console.error('Failed to parse document content:', e);
68
+ }
69
+ }
70
+ setHasLoaded(true);
71
+ }
72
+ }, [existingDoc, editor, hasLoaded]);
73
+ const handleView = async () => {
74
+ if (!existingDoc)
75
+ return;
76
+ await handleSave();
77
+ onOpen(params.slugs.join('/') + '/' + existingDoc.slug);
78
+ };
79
+ const handleSave = async () => {
80
+ if (!title.trim()) {
81
+ alert('Please enter a document title');
82
+ return;
83
+ }
84
+ setIsSaving(true);
85
+ try {
86
+ const content = editor.document;
87
+ const slug = generateSlug(title);
88
+ if (!existingDoc) {
89
+ // Create new document (with parentId if it's a child document)
90
+ const newDoc = await createMutation.mutateAsync({
91
+ title,
92
+ slug,
93
+ content,
94
+ isPublished: true,
95
+ parentSlugs: params.slugs,
96
+ emoji,
97
+ cover,
98
+ });
99
+ onEdit(params.slugs.join('/') + '/' + newDoc.slug);
100
+ }
101
+ else {
102
+ // Update existing document
103
+ const updated = await updateMutation.mutateAsync({
104
+ id: existingDoc.id,
105
+ data: {
106
+ title,
107
+ slug,
108
+ content,
109
+ emoji,
110
+ cover,
111
+ },
112
+ });
113
+ }
114
+ }
115
+ catch (error) {
116
+ console.error('Failed to save document:', error);
117
+ alert('Failed to save document. Please try again.');
118
+ }
119
+ finally {
120
+ setIsSaving(false);
121
+ }
122
+ };
123
+ // Cover upload handlers
124
+ const handleDrop = async (e) => {
125
+ e.preventDefault();
126
+ const file = e.dataTransfer.files[0];
127
+ if (file) {
128
+ const result = await uploadCover(file);
129
+ if (result) {
130
+ setCover(result.url);
131
+ }
132
+ }
133
+ };
134
+ const handleDragOver = (e) => {
135
+ e.preventDefault();
136
+ };
137
+ const handleFileInput = async (e) => {
138
+ const file = e.target.files?.[0];
139
+ if (file) {
140
+ const result = await uploadCover(file);
141
+ if (result) {
142
+ setCover(result.url);
143
+ }
144
+ }
145
+ };
146
+ // Show loading state when fetching existing document
147
+ if (!isNewDocument && isLoadingDoc) {
148
+ 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" }) }));
149
+ }
150
+ 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: "Document title", 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" }), "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 ||
151
+ createMutation.isPending ||
152
+ updateMutation.isPending ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), "Saving..."] })) : (_jsxs(_Fragment, { children: [_jsx(Save, { className: "h-4 w-4" }), isNewDocument ? 'Create' : 'Save'] })) })] })] }), _jsxs("div", { className: "text-sm text-gray-500 mt-1", children: ["Slug: ", generateSlug(title)] })] }), _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: "Cover", 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: "Remove cover", 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: "Drag & drop cover image here or click to upload" }), _jsx("span", { className: "text-xs text-gray-400", children: "JPEG, PNG, GIF, WebP up to 5MB" })] })) })] })) }), _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" }) }) })] }));
153
+ }
@@ -0,0 +1,5 @@
1
+ interface DocumentListProps {
2
+ }
3
+ export declare function DocumentList({}: DocumentListProps): import("react/jsx-runtime").JSX.Element;
4
+ export {};
5
+ //# sourceMappingURL=DocumentList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocumentList.d.ts","sourceRoot":"","sources":["../../../src/client/components/DocumentList.tsx"],"names":[],"mappings":"AAiBA,UAAU,iBAAiB;CAAG;AAuB9B,wBAAgB,YAAY,CAAC,EAAE,EAAE,iBAAiB,2CAiIjD"}
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { FileText, Folder, Plus } from 'lucide-react';
4
+ import { useDocumentsQuery } from '../hooks/useDocsQuery.js';
5
+ import { useDocument } from './DocumentProvider.js';
6
+ function formatDistanceToNow(date) {
7
+ const now = new Date();
8
+ const diffMs = now.getTime() - new Date(date).getTime();
9
+ const diffSec = Math.round(diffMs / 1000);
10
+ const diffMin = Math.round(diffSec / 60);
11
+ const diffHour = Math.round(diffMin / 60);
12
+ const diffDay = Math.round(diffHour / 24);
13
+ if (diffSec < 60) {
14
+ return 'just now';
15
+ }
16
+ else if (diffMin < 60) {
17
+ return `${diffMin} minute${diffMin === 1 ? '' : 's'} ago`;
18
+ }
19
+ else if (diffHour < 24) {
20
+ return `${diffHour} hour${diffHour === 1 ? '' : 's'} ago`;
21
+ }
22
+ else if (diffDay < 7) {
23
+ return `${diffDay} day${diffDay === 1 ? '' : 's'} ago`;
24
+ }
25
+ else {
26
+ return date.toLocaleDateString();
27
+ }
28
+ }
29
+ export function DocumentList({}) {
30
+ const { onOpen, onCreate } = useDocument();
31
+ const { data, isLoading, error } = useDocumentsQuery();
32
+ const documents = data?.documents ?? [];
33
+ // Separate documents into folders (documents with children) and regular documents
34
+ const folders = documents.filter((doc) => documents.some((d) => d.parentId === doc.id));
35
+ const regularDocuments = documents.filter((doc) => !doc.parentId && !folders.some((f) => f.id === doc.id));
36
+ return (_jsxs("div", { className: "space-y-6 max-w-5xl mx-auto", children: [_jsxs("div", { className: "space-y-2", children: [_jsx("h2", { className: "text-2xl font-bold", children: "Welcome to Documentation" }), _jsx("p", { className: "text-gray-500", children: "Browse and manage your documentation. Create new documents or organize them into folders." })] }), folders.length > 0 && (_jsxs("section", { children: [_jsx("h3", { className: "text-lg font-semibold mb-4", children: "Folders" }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4", children: [folders.map((folder) => (_jsxs("div", { onClick: () => onOpen?.(folder.slug), className: "border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition-colors cursor-pointer", children: [_jsxs("div", { className: "flex items-start justify-between mb-2", children: [folder.emoji ? (_jsx("span", { className: "text-2xl", children: folder.emoji })) : (_jsx(Folder, { className: "h-8 w-8 text-blue-600" })), _jsxs("span", { className: "text-xs text-gray-500", children: [documents.filter((d) => d.parentId === folder.id).length, ' ', "docs"] })] }), _jsx("h4", { className: "text-base font-medium", children: folder.title })] }, folder.id))), _jsxs("div", { onClick: onCreate, className: "border border-dashed border-gray-300 rounded-lg p-4 hover:bg-gray-50 transition-colors cursor-pointer", children: [_jsx("div", { className: "flex items-center justify-center h-8 mb-2", children: _jsx(Plus, { className: "h-6 w-6 text-gray-400" }) }), _jsx("h4", { className: "text-base font-medium text-center text-gray-500", children: "New Folder" })] })] })] })), _jsxs("section", { children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsx("h3", { className: "text-lg font-semibold", children: folders.length > 0 ? 'Documents' : 'Recent Documents' }), _jsxs("button", { onClick: onCreate, className: "px-3 py-1.5 text-sm bg-gray-900 text-white rounded-md hover:bg-gray-800 flex items-center gap-2 transition-colors", children: [_jsx(Plus, { className: "h-4 w-4" }), "New Document"] })] }), regularDocuments.length === 0 ? (_jsx("div", { className: "border border-dashed border-gray-300 rounded-lg p-8", children: _jsxs("div", { className: "flex flex-col items-center justify-center text-center", children: [_jsx(FileText, { className: "h-12 w-12 text-gray-400 mb-4" }), _jsx("h4", { className: "text-lg font-medium mb-2", children: "No documents yet" }), _jsx("p", { className: "text-sm text-gray-500 mb-4", children: "Get started by creating your first document" }), _jsxs("button", { onClick: onCreate, className: "px-4 py-2 bg-gray-900 text-white rounded-md hover:bg-gray-800 flex items-center gap-2 transition-colors", children: [_jsx(Plus, { className: "h-4 w-4" }), "Create Document"] })] }) })) : (_jsx("div", { className: "space-y-2", children: regularDocuments.map((doc) => (_jsxs("div", { onClick: () => onOpen?.(doc.slug), className: "flex items-center justify-between p-4 rounded-lg border border-gray-200 bg-white hover:bg-gray-50 transition-colors group cursor-pointer", children: [_jsxs("div", { className: "flex items-center gap-3", children: [doc.emoji ? (_jsx("span", { className: "text-xl", children: doc.emoji })) : (_jsx(FileText, { className: "h-5 w-5 text-gray-400 group-hover:text-gray-600 transition-colors" })), _jsxs("div", { children: [_jsx("h4", { className: "font-medium text-gray-900", children: doc.title }), _jsxs("p", { className: "text-sm text-gray-500", children: ["Updated", ' ', doc.updatedAt
37
+ ? formatDistanceToNow(new Date(doc.updatedAt))
38
+ : 'N/A'] })] })] }), _jsx("button", { className: "px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 rounded-md transition-colors", children: "Open" })] }, doc.id))) }))] })] }));
39
+ }