react-embed-docs 0.2.0 → 0.4.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/dist/client/components/DocumentEdit.d.ts.map +1 -1
- package/dist/client/components/DocumentEdit.js +332 -6
- package/dist/client/components/DocumentList.d.ts.map +1 -1
- package/dist/client/components/DocumentList.js +3 -1
- package/dist/client/components/DocumentProvider.d.ts +1 -1
- package/dist/client/components/DocumentProvider.d.ts.map +1 -1
- package/dist/client/components/DocumentProvider.js +5 -1
- package/dist/client/components/DocumentView.js +2 -2
- package/dist/client/components/EmojiPicker.d.ts.map +1 -1
- package/dist/client/components/EmojiPicker.js +24 -23
- package/dist/client/components/Layout.d.ts.map +1 -1
- package/dist/client/components/Layout.js +4 -2
- package/dist/client/components/Sidebar.js +1 -1
- package/dist/client/locales/en.json +3 -0
- package/dist/client/locales/ru.json +3 -0
- package/package.json +2 -1
- package/dist/client/components/DocsLayout.d.ts +0 -20
- package/dist/client/components/DocsLayout.d.ts.map +0 -1
- package/dist/client/components/DocsLayout.js +0 -387
- package/dist/client/providers/DocumentProvider.d.ts +0 -1
- package/dist/client/providers/DocumentProvider.d.ts.map +0 -1
- package/dist/client/providers/DocumentProvider.js +0 -1
|
@@ -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;
|
|
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"}
|
|
@@ -7,8 +7,8 @@ import { Eye, ImageIcon, Loader2, Save, X } from 'lucide-react';
|
|
|
7
7
|
import { useEffect, useState } from 'react';
|
|
8
8
|
import { useCreateDocumentMutation, useDocumentQuery, useUpdateDocumentMutation, } from '../hooks/useDocsQuery.js';
|
|
9
9
|
import { useFileUpload } from '../hooks/useFileUpload.js';
|
|
10
|
-
import { blockNoteTheme } from '../lib/blocknoteTheme.js';
|
|
11
10
|
import { useTranslation } from '../hooks/useTranslation.js';
|
|
11
|
+
import { blockNoteTheme } from '../lib/blocknoteTheme.js';
|
|
12
12
|
import { useDocument } from './DocumentProvider.js';
|
|
13
13
|
import { EmojiPicker } from './EmojiPicker.js';
|
|
14
14
|
// Default initial content for new documents
|
|
@@ -18,9 +18,314 @@ const getDefaultContent = () => [
|
|
|
18
18
|
content: '',
|
|
19
19
|
},
|
|
20
20
|
];
|
|
21
|
+
// Transliteration map for common non-Latin characters (consolidated, no duplicates)
|
|
22
|
+
const transliterationMap = {
|
|
23
|
+
// Cyrillic
|
|
24
|
+
а: 'a',
|
|
25
|
+
б: 'b',
|
|
26
|
+
в: 'v',
|
|
27
|
+
г: 'g',
|
|
28
|
+
д: 'd',
|
|
29
|
+
е: 'e',
|
|
30
|
+
ё: 'yo',
|
|
31
|
+
ж: 'zh',
|
|
32
|
+
з: 'z',
|
|
33
|
+
и: 'i',
|
|
34
|
+
й: 'y',
|
|
35
|
+
к: 'k',
|
|
36
|
+
л: 'l',
|
|
37
|
+
м: 'm',
|
|
38
|
+
н: 'n',
|
|
39
|
+
о: 'o',
|
|
40
|
+
п: 'p',
|
|
41
|
+
р: 'r',
|
|
42
|
+
с: 's',
|
|
43
|
+
т: 't',
|
|
44
|
+
у: 'u',
|
|
45
|
+
ф: 'f',
|
|
46
|
+
х: 'h',
|
|
47
|
+
ц: 'ts',
|
|
48
|
+
ч: 'ch',
|
|
49
|
+
ш: 'sh',
|
|
50
|
+
щ: 'sch',
|
|
51
|
+
ъ: '',
|
|
52
|
+
ы: 'y',
|
|
53
|
+
ь: '',
|
|
54
|
+
э: 'e',
|
|
55
|
+
ю: 'yu',
|
|
56
|
+
я: 'ya',
|
|
57
|
+
А: 'A',
|
|
58
|
+
Б: 'B',
|
|
59
|
+
В: 'V',
|
|
60
|
+
Г: 'G',
|
|
61
|
+
Д: 'D',
|
|
62
|
+
Е: 'E',
|
|
63
|
+
Ё: 'Yo',
|
|
64
|
+
Ж: 'Zh',
|
|
65
|
+
З: 'Z',
|
|
66
|
+
И: 'I',
|
|
67
|
+
Й: 'Y',
|
|
68
|
+
К: 'K',
|
|
69
|
+
Л: 'L',
|
|
70
|
+
М: 'M',
|
|
71
|
+
Н: 'N',
|
|
72
|
+
О: 'O',
|
|
73
|
+
П: 'P',
|
|
74
|
+
Р: 'R',
|
|
75
|
+
С: 'S',
|
|
76
|
+
Т: 'T',
|
|
77
|
+
У: 'U',
|
|
78
|
+
Ф: 'F',
|
|
79
|
+
Х: 'H',
|
|
80
|
+
Ц: 'Ts',
|
|
81
|
+
Ч: 'Ch',
|
|
82
|
+
Ш: 'Sh',
|
|
83
|
+
Щ: 'Sch',
|
|
84
|
+
Ъ: '',
|
|
85
|
+
Ы: 'Y',
|
|
86
|
+
Ь: '',
|
|
87
|
+
Э: 'E',
|
|
88
|
+
Ю: 'Yu',
|
|
89
|
+
Я: 'Ya',
|
|
90
|
+
// Latin Extended
|
|
91
|
+
ä: 'ae',
|
|
92
|
+
ö: 'oe',
|
|
93
|
+
ü: 'ue',
|
|
94
|
+
ß: 'ss',
|
|
95
|
+
Ä: 'Ae',
|
|
96
|
+
Ö: 'Oe',
|
|
97
|
+
Ü: 'Ue',
|
|
98
|
+
à: 'a',
|
|
99
|
+
â: 'a',
|
|
100
|
+
æ: 'ae',
|
|
101
|
+
ç: 'c',
|
|
102
|
+
è: 'e',
|
|
103
|
+
é: 'e',
|
|
104
|
+
ê: 'e',
|
|
105
|
+
ë: 'e',
|
|
106
|
+
î: 'i',
|
|
107
|
+
ï: 'i',
|
|
108
|
+
ô: 'o',
|
|
109
|
+
œ: 'oe',
|
|
110
|
+
ù: 'u',
|
|
111
|
+
û: 'u',
|
|
112
|
+
ÿ: 'y',
|
|
113
|
+
À: 'A',
|
|
114
|
+
Â: 'A',
|
|
115
|
+
Æ: 'Ae',
|
|
116
|
+
Ç: 'C',
|
|
117
|
+
È: 'E',
|
|
118
|
+
É: 'E',
|
|
119
|
+
Ê: 'E',
|
|
120
|
+
Ë: 'E',
|
|
121
|
+
Î: 'I',
|
|
122
|
+
Ï: 'I',
|
|
123
|
+
Ô: 'O',
|
|
124
|
+
Œ: 'Oe',
|
|
125
|
+
Ù: 'U',
|
|
126
|
+
Û: 'U',
|
|
127
|
+
Ÿ: 'Y',
|
|
128
|
+
á: 'a',
|
|
129
|
+
í: 'i',
|
|
130
|
+
ó: 'o',
|
|
131
|
+
ú: 'u',
|
|
132
|
+
ñ: 'n',
|
|
133
|
+
Á: 'A',
|
|
134
|
+
Í: 'I',
|
|
135
|
+
Ó: 'O',
|
|
136
|
+
Ú: 'U',
|
|
137
|
+
Ñ: 'N',
|
|
138
|
+
ã: 'a',
|
|
139
|
+
õ: 'o',
|
|
140
|
+
Ã: 'A',
|
|
141
|
+
Õ: 'O',
|
|
142
|
+
ì: 'i',
|
|
143
|
+
ò: 'o',
|
|
144
|
+
Ì: 'I',
|
|
145
|
+
Ò: 'O',
|
|
146
|
+
ą: 'a',
|
|
147
|
+
ć: 'c',
|
|
148
|
+
ę: 'e',
|
|
149
|
+
ł: 'l',
|
|
150
|
+
ń: 'n',
|
|
151
|
+
ś: 's',
|
|
152
|
+
ź: 'z',
|
|
153
|
+
ż: 'z',
|
|
154
|
+
Ą: 'A',
|
|
155
|
+
Ć: 'C',
|
|
156
|
+
Ę: 'E',
|
|
157
|
+
Ł: 'L',
|
|
158
|
+
Ń: 'N',
|
|
159
|
+
Ś: 'S',
|
|
160
|
+
Ź: 'Z',
|
|
161
|
+
Ż: 'Z',
|
|
162
|
+
č: 'c',
|
|
163
|
+
ď: 'd',
|
|
164
|
+
ě: 'e',
|
|
165
|
+
ň: 'n',
|
|
166
|
+
ř: 'r',
|
|
167
|
+
š: 's',
|
|
168
|
+
ť: 't',
|
|
169
|
+
ů: 'u',
|
|
170
|
+
ž: 'z',
|
|
171
|
+
Č: 'C',
|
|
172
|
+
Ď: 'D',
|
|
173
|
+
Ě: 'E',
|
|
174
|
+
Ň: 'N',
|
|
175
|
+
Ř: 'R',
|
|
176
|
+
Š: 'S',
|
|
177
|
+
Ť: 'T',
|
|
178
|
+
Ů: 'U',
|
|
179
|
+
Ž: 'Z',
|
|
180
|
+
ș: 's',
|
|
181
|
+
ț: 't',
|
|
182
|
+
Ș: 'S',
|
|
183
|
+
Ț: 'T',
|
|
184
|
+
ő: 'o',
|
|
185
|
+
ű: 'u',
|
|
186
|
+
Ő: 'O',
|
|
187
|
+
Ű: 'U',
|
|
188
|
+
ğ: 'g',
|
|
189
|
+
ı: 'i',
|
|
190
|
+
ş: 's',
|
|
191
|
+
Ğ: 'G',
|
|
192
|
+
İ: 'I',
|
|
193
|
+
Ş: 'S',
|
|
194
|
+
'¿': '',
|
|
195
|
+
'¡': '',
|
|
196
|
+
// Greek
|
|
197
|
+
α: 'a',
|
|
198
|
+
β: 'b',
|
|
199
|
+
γ: 'g',
|
|
200
|
+
δ: 'd',
|
|
201
|
+
ε: 'e',
|
|
202
|
+
ζ: 'z',
|
|
203
|
+
η: 'i',
|
|
204
|
+
θ: 'th',
|
|
205
|
+
ι: 'i',
|
|
206
|
+
κ: 'k',
|
|
207
|
+
λ: 'l',
|
|
208
|
+
μ: 'm',
|
|
209
|
+
ν: 'n',
|
|
210
|
+
ξ: 'x',
|
|
211
|
+
ο: 'o',
|
|
212
|
+
π: 'p',
|
|
213
|
+
ρ: 'r',
|
|
214
|
+
σ: 's',
|
|
215
|
+
ς: 's',
|
|
216
|
+
τ: 't',
|
|
217
|
+
υ: 'y',
|
|
218
|
+
φ: 'f',
|
|
219
|
+
χ: 'ch',
|
|
220
|
+
ψ: 'ps',
|
|
221
|
+
ω: 'o',
|
|
222
|
+
ά: 'a',
|
|
223
|
+
έ: 'e',
|
|
224
|
+
ή: 'i',
|
|
225
|
+
ί: 'i',
|
|
226
|
+
ό: 'o',
|
|
227
|
+
ύ: 'y',
|
|
228
|
+
ώ: 'o',
|
|
229
|
+
Α: 'A',
|
|
230
|
+
Β: 'B',
|
|
231
|
+
Γ: 'G',
|
|
232
|
+
Δ: 'D',
|
|
233
|
+
Ε: 'E',
|
|
234
|
+
Ζ: 'Z',
|
|
235
|
+
Η: 'I',
|
|
236
|
+
Θ: 'Th',
|
|
237
|
+
Ι: 'I',
|
|
238
|
+
Κ: 'K',
|
|
239
|
+
Λ: 'L',
|
|
240
|
+
Μ: 'M',
|
|
241
|
+
Ν: 'N',
|
|
242
|
+
Ξ: 'X',
|
|
243
|
+
Ο: 'O',
|
|
244
|
+
Π: 'P',
|
|
245
|
+
Ρ: 'R',
|
|
246
|
+
Σ: 'S',
|
|
247
|
+
Τ: 'T',
|
|
248
|
+
Υ: 'Y',
|
|
249
|
+
Φ: 'F',
|
|
250
|
+
Χ: 'Ch',
|
|
251
|
+
Ψ: 'Ps',
|
|
252
|
+
Ω: 'O',
|
|
253
|
+
Ά: 'A',
|
|
254
|
+
Έ: 'E',
|
|
255
|
+
Ή: 'I',
|
|
256
|
+
Ί: 'I',
|
|
257
|
+
Ό: 'O',
|
|
258
|
+
Ύ: 'Y',
|
|
259
|
+
Ώ: 'O',
|
|
260
|
+
// Arabic
|
|
261
|
+
ا: 'a',
|
|
262
|
+
ب: 'b',
|
|
263
|
+
ت: 't',
|
|
264
|
+
ث: 'th',
|
|
265
|
+
ج: 'j',
|
|
266
|
+
ح: 'h',
|
|
267
|
+
خ: 'kh',
|
|
268
|
+
د: 'd',
|
|
269
|
+
ذ: 'dh',
|
|
270
|
+
ر: 'r',
|
|
271
|
+
ز: 'z',
|
|
272
|
+
س: 's',
|
|
273
|
+
ش: 'sh',
|
|
274
|
+
ص: 's',
|
|
275
|
+
ض: 'd',
|
|
276
|
+
ط: 't',
|
|
277
|
+
ظ: 'z',
|
|
278
|
+
ع: 'a',
|
|
279
|
+
غ: 'gh',
|
|
280
|
+
ف: 'f',
|
|
281
|
+
ق: 'q',
|
|
282
|
+
ك: 'k',
|
|
283
|
+
ل: 'l',
|
|
284
|
+
م: 'm',
|
|
285
|
+
ن: 'n',
|
|
286
|
+
ه: 'h',
|
|
287
|
+
و: 'w',
|
|
288
|
+
ي: 'y',
|
|
289
|
+
// Hebrew
|
|
290
|
+
א: 'a',
|
|
291
|
+
ב: 'b',
|
|
292
|
+
ג: 'g',
|
|
293
|
+
ד: 'd',
|
|
294
|
+
ה: 'h',
|
|
295
|
+
ו: 'v',
|
|
296
|
+
ז: 'z',
|
|
297
|
+
ח: 'ch',
|
|
298
|
+
ט: 't',
|
|
299
|
+
י: 'y',
|
|
300
|
+
כ: 'k',
|
|
301
|
+
ך: 'k',
|
|
302
|
+
ל: 'l',
|
|
303
|
+
מ: 'm',
|
|
304
|
+
ם: 'm',
|
|
305
|
+
נ: 'n',
|
|
306
|
+
ן: 'n',
|
|
307
|
+
ס: 's',
|
|
308
|
+
ע: 'a',
|
|
309
|
+
פ: 'p',
|
|
310
|
+
ף: 'p',
|
|
311
|
+
צ: 'ts',
|
|
312
|
+
ץ: 'ts',
|
|
313
|
+
ק: 'k',
|
|
314
|
+
ר: 'r',
|
|
315
|
+
ש: 'sh',
|
|
316
|
+
ת: 't',
|
|
317
|
+
};
|
|
318
|
+
// Transliterate non-Latin characters to Latin
|
|
319
|
+
const transliterate = (text) => {
|
|
320
|
+
return text
|
|
321
|
+
.split('')
|
|
322
|
+
.map((char) => transliterationMap[char] ?? char)
|
|
323
|
+
.join('');
|
|
324
|
+
};
|
|
21
325
|
// Generate slug from title
|
|
22
326
|
const generateSlug = (title) => {
|
|
23
|
-
|
|
327
|
+
const transliterated = transliterate(title);
|
|
328
|
+
return (transliterated
|
|
24
329
|
.toLowerCase()
|
|
25
330
|
.replace(/[^a-z0-9]+/g, '-')
|
|
26
331
|
.replace(/^-+|-+$/g, '')
|
|
@@ -37,6 +342,8 @@ export function DocumentEdit({}) {
|
|
|
37
342
|
const updateMutation = useUpdateDocumentMutation();
|
|
38
343
|
// Local state
|
|
39
344
|
const [title, setTitle] = useState('');
|
|
345
|
+
const [slug, setSlug] = useState('');
|
|
346
|
+
const [isSlugManuallyEdited, setIsSlugManuallyEdited] = useState(false);
|
|
40
347
|
const [emoji, setEmoji] = useState(undefined);
|
|
41
348
|
const [cover, setCover] = useState(null);
|
|
42
349
|
const [isSaving, setIsSaving] = useState(false);
|
|
@@ -54,6 +361,7 @@ export function DocumentEdit({}) {
|
|
|
54
361
|
useEffect(() => {
|
|
55
362
|
if (existingDoc && !hasLoaded) {
|
|
56
363
|
setTitle(existingDoc.title);
|
|
364
|
+
setSlug(existingDoc.slug);
|
|
57
365
|
if (existingDoc.emoji)
|
|
58
366
|
setEmoji(existingDoc.emoji);
|
|
59
367
|
if (existingDoc.cover)
|
|
@@ -72,6 +380,12 @@ export function DocumentEdit({}) {
|
|
|
72
380
|
setHasLoaded(true);
|
|
73
381
|
}
|
|
74
382
|
}, [existingDoc, editor, hasLoaded]);
|
|
383
|
+
// Auto-generate slug from title for new documents (only when not manually edited)
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
if (isNewDocument && !isSlugManuallyEdited && title) {
|
|
386
|
+
setSlug(generateSlug(title));
|
|
387
|
+
}
|
|
388
|
+
}, [title, isNewDocument, isSlugManuallyEdited]);
|
|
75
389
|
const handleView = async () => {
|
|
76
390
|
if (!existingDoc)
|
|
77
391
|
return;
|
|
@@ -83,15 +397,19 @@ export function DocumentEdit({}) {
|
|
|
83
397
|
alert(t('editor.titleRequired'));
|
|
84
398
|
return;
|
|
85
399
|
}
|
|
400
|
+
if (!slug.trim()) {
|
|
401
|
+
alert(t('editor.slugRequired'));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
86
404
|
setIsSaving(true);
|
|
87
405
|
try {
|
|
88
406
|
const content = editor.document;
|
|
89
|
-
const
|
|
407
|
+
const finalSlug = slug.trim() || generateSlug(title);
|
|
90
408
|
if (!existingDoc) {
|
|
91
409
|
// Create new document (with parentId if it's a child document)
|
|
92
410
|
const newDoc = await createMutation.mutateAsync({
|
|
93
411
|
title,
|
|
94
|
-
slug,
|
|
412
|
+
slug: finalSlug,
|
|
95
413
|
content,
|
|
96
414
|
isPublished: true,
|
|
97
415
|
parentSlugs: params.slugs,
|
|
@@ -106,7 +424,7 @@ export function DocumentEdit({}) {
|
|
|
106
424
|
id: existingDoc.id,
|
|
107
425
|
data: {
|
|
108
426
|
title,
|
|
109
|
-
slug,
|
|
427
|
+
slug: finalSlug,
|
|
110
428
|
content,
|
|
111
429
|
emoji,
|
|
112
430
|
cover,
|
|
@@ -151,5 +469,13 @@ export function DocumentEdit({}) {
|
|
|
151
469
|
}
|
|
152
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 ||
|
|
153
471
|
createMutation.isPending ||
|
|
154
|
-
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-
|
|
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) => {
|
|
473
|
+
setSlug(e.target.value);
|
|
474
|
+
setIsSlugManuallyEdited(true);
|
|
475
|
+
}, onBlur: (e) => {
|
|
476
|
+
if (e.target.value.trim() === '') {
|
|
477
|
+
setIsSlugManuallyEdited(false);
|
|
478
|
+
setSlug(generateSlug(title));
|
|
479
|
+
}
|
|
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" }) }) })] }));
|
|
155
481
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DocumentList.d.ts","sourceRoot":"","sources":["../../../src/client/components/DocumentList.tsx"],"names":[],"mappings":"AAkBA,UAAU,iBAAiB;CAAG;AAgC9B,wBAAgB,YAAY,CAAC,EAAE,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"DocumentList.d.ts","sourceRoot":"","sources":["../../../src/client/components/DocumentList.tsx"],"names":[],"mappings":"AAkBA,UAAU,iBAAiB;CAAG;AAgC9B,wBAAgB,YAAY,CAAC,EAAE,EAAE,iBAAiB,2CAmIjD"}
|
|
@@ -41,7 +41,9 @@ export function DocumentList({}) {
|
|
|
41
41
|
// Separate documents into folders (documents with children) and regular documents
|
|
42
42
|
const folders = documents.filter((doc) => documents.some((d) => d.parentId === doc.id));
|
|
43
43
|
const regularDocuments = documents.filter((doc) => !doc.parentId && !folders.some((f) => f.id === doc.id));
|
|
44
|
-
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: t('docList.welcome') }), _jsx("p", { className: "text-gray-500", children: t('docList.welcomeDescription') })] }), folders.length > 0 && (_jsxs("section", { children: [_jsx("h3", { className: "text-lg font-semibold mb-4", children: t('docList.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, ' ', t('docList.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: t('docList.newFolder') })] })] })] })), _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
|
|
44
|
+
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: t('docList.welcome') }), _jsx("p", { className: "text-gray-500", children: t('docList.welcomeDescription') })] }), folders.length > 0 && (_jsxs("section", { children: [_jsx("h3", { className: "text-lg font-semibold mb-4", children: t('docList.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, ' ', t('docList.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: t('docList.newFolder') })] })] })] })), _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
|
|
45
|
+
? t('docList.documents')
|
|
46
|
+
: t('docList.recentDocuments') }), _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" }), t('docList.newDocument')] })] }), 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: t('docList.noDocuments') }), _jsx("p", { className: "text-sm text-gray-500 mb-4", children: t('docList.getStarted') }), _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" }), t('docList.createDocument')] })] }) })) : (_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: [t('docList.updated'), ' ', doc.updatedAt
|
|
45
47
|
? formatDistanceToNow(new Date(doc.updatedAt), t)
|
|
46
48
|
: 'N/A'] })] })] }), _jsx("button", { className: "px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 rounded-md transition-colors", children: t('docList.open') })] }, doc.id))) }))] })] }));
|
|
47
49
|
}
|
|
@@ -14,7 +14,7 @@ export interface DocumentContextValue {
|
|
|
14
14
|
path: string;
|
|
15
15
|
locale: string;
|
|
16
16
|
params: Params;
|
|
17
|
-
onCreate: () => void;
|
|
17
|
+
onCreate: (slug?: string) => void;
|
|
18
18
|
onOpen: (path: string) => void;
|
|
19
19
|
onEdit: (slug: string) => void;
|
|
20
20
|
onDelete: (doc: DocumentSummary) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DocumentProvider.d.ts","sourceRoot":"","sources":["../../../src/client/components/DocumentProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAiB,SAAS,EAA2B,MAAM,OAAO,CAAA;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAEvC;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,CAAA;AAE5C;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,oBAAoB;IACpB,KAAK,EAAE,aAAa,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"DocumentProvider.d.ts","sourceRoot":"","sources":["../../../src/client/components/DocumentProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAiB,SAAS,EAA2B,MAAM,OAAO,CAAA;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAEvC;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,CAAA;AAE5C;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,oBAAoB;IACpB,KAAK,EAAE,aAAa,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,QAAQ,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,CAAA;CACzC;AAOD;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,SAAS,CAAA;IACnB,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI,CAAA;CACrB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,MAAM,EACN,KAAe,EACf,IAAI,EACJ,UAAU,EACV,MAAa,GACd,EAAE,qBAAqB,GAAG,GAAG,CAAC,OAAO,CAgDrC;AAED;;;GAGG;AACH,wBAAgB,WAAW,IAAI,oBAAoB,CAQlD"}
|
|
@@ -16,7 +16,11 @@ export function DocumentProvider({ children, params, theme = 'light', path, onNa
|
|
|
16
16
|
const onEdit = useCallback((slug) => {
|
|
17
17
|
onNavigate?.(`${slug}/edit`);
|
|
18
18
|
}, [onNavigate]);
|
|
19
|
-
const onCreate = useCallback(() => {
|
|
19
|
+
const onCreate = useCallback((slug) => {
|
|
20
|
+
if (slug) {
|
|
21
|
+
onNavigate?.(`${slug}`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
20
24
|
onNavigate?.(`${path}/new`);
|
|
21
25
|
}, [onNavigate]);
|
|
22
26
|
const onDelete = useCallback((doc) => {
|
|
@@ -6,8 +6,8 @@ import { useCreateBlockNote } from '@blocknote/react';
|
|
|
6
6
|
import { Clock, Edit, Loader2, User } from 'lucide-react';
|
|
7
7
|
import { useCallback, useEffect, useState } from 'react';
|
|
8
8
|
import { useDocumentQuery } from '../hooks/useDocsQuery.js';
|
|
9
|
-
import { blockNoteTheme } from '../lib/blocknoteTheme.js';
|
|
10
9
|
import { useTranslation } from '../hooks/useTranslation.js';
|
|
10
|
+
import { blockNoteTheme } from '../lib/blocknoteTheme.js';
|
|
11
11
|
import { useDocument } from './DocumentProvider.js';
|
|
12
12
|
import { ExportButton } from './ExportButton.js';
|
|
13
13
|
// Default content when document has no content
|
|
@@ -18,7 +18,7 @@ const getDefaultContent = () => [
|
|
|
18
18
|
},
|
|
19
19
|
];
|
|
20
20
|
export function DocumentView({}) {
|
|
21
|
-
const { params,
|
|
21
|
+
const { params, onEdit, theme } = useDocument();
|
|
22
22
|
const { t } = useTranslation();
|
|
23
23
|
const { data: doc, isLoading, error } = useDocumentQuery(params.documentSlug);
|
|
24
24
|
const [hasLoaded, setHasLoaded] = useState(false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmojiPicker.d.ts","sourceRoot":"","sources":["../../../src/client/components/EmojiPicker.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"EmojiPicker.d.ts","sourceRoot":"","sources":["../../../src/client/components/EmojiPicker.tsx"],"names":[],"mappings":"AAUA,UAAU,gBAAgB;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,gBAAgB,2CAuF3E"}
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import EmojiPickerLib, { EmojiStyle, Theme, } from 'emoji-picker-react';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
5
|
import { useTranslation } from '../hooks/useTranslation.js';
|
|
4
|
-
import { useState, useRef, useEffect } from 'react';
|
|
5
|
-
// Popular emojis for documents
|
|
6
|
-
const POPULAR_EMOJIS = [
|
|
7
|
-
'📄', '📝', '📊', '📈', '📉', '📑', '📋', '📁', '📂', '🗂️',
|
|
8
|
-
'📦', '📮', '📯', '📰', '📱', '💻', '🖥️', '📀', '💿', '📼',
|
|
9
|
-
'🌸', '🌺', '🌻', '🌼', '🌷', '🌹', '🌵', '🌲', '🌳', '🌴',
|
|
10
|
-
'🍏', '🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🫐',
|
|
11
|
-
'⚽', '🏀', '🏈', '⚾', '🥎', '🎾', '🏐', '🏉', '🥏', '🎱',
|
|
12
|
-
'🚗', '🚕', '🚙', '🚌', '🚎', '🏎️', '🚓', '🚑', '🚒', '🚐',
|
|
13
|
-
'❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '❣️',
|
|
14
|
-
'⭐', '🌟', '✨', '💫', '⚡', '🔥', '💥', '☄️', '☀️', '🌤️',
|
|
15
|
-
'🔴', '🟠', '🟡', '🟢', '🔵', '🟣', '⚫', '⚪', '🟤', '🔘',
|
|
16
|
-
'✅', '❌', '⭕', '🚫', '💯', '💢', '♨️', '🚷', '🚯', '🚳',
|
|
17
|
-
];
|
|
18
6
|
export function EmojiPicker({ value, onChange, className }) {
|
|
19
7
|
const { t } = useTranslation();
|
|
20
8
|
const [isOpen, setIsOpen] = useState(false);
|
|
9
|
+
const [theme, setTheme] = useState(Theme.LIGHT);
|
|
21
10
|
const dropdownRef = useRef(null);
|
|
11
|
+
// Detect dark mode
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
14
|
+
const updateTheme = () => {
|
|
15
|
+
setTheme(mediaQuery.matches ? Theme.DARK : Theme.LIGHT);
|
|
16
|
+
};
|
|
17
|
+
updateTheme();
|
|
18
|
+
mediaQuery.addEventListener('change', updateTheme);
|
|
19
|
+
return () => mediaQuery.removeEventListener('change', updateTheme);
|
|
20
|
+
}, []);
|
|
22
21
|
// Close dropdown when clicking outside
|
|
23
22
|
useEffect(() => {
|
|
24
23
|
function handleClickOutside(event) {
|
|
25
|
-
if (dropdownRef.current &&
|
|
24
|
+
if (dropdownRef.current &&
|
|
25
|
+
!dropdownRef.current.contains(event.target)) {
|
|
26
26
|
setIsOpen(false);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -33,18 +33,19 @@ export function EmojiPicker({ value, onChange, className }) {
|
|
|
33
33
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
34
34
|
};
|
|
35
35
|
}, [isOpen]);
|
|
36
|
+
const handleEmojiClick = (emojiData) => {
|
|
37
|
+
onChange(emojiData.emoji);
|
|
38
|
+
setIsOpen(false);
|
|
39
|
+
};
|
|
36
40
|
return (_jsxs("div", { ref: dropdownRef, className: "relative", children: [_jsx("button", { onClick: () => setIsOpen(!isOpen), className: [
|
|
37
41
|
'h-10 w-10 rounded-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900',
|
|
38
42
|
'hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center justify-center text-xl transition-colors',
|
|
39
43
|
className,
|
|
40
|
-
].join(' '), children: value || '📄' }), isOpen && (_jsxs("div", { className: "absolute top-full left-0 mt-1 z-50
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
'h-8 w-8 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center justify-center text-lg transition-colors',
|
|
45
|
-
value === emoji && 'bg-gray-100 dark:bg-gray-800',
|
|
46
|
-
].join(' '), children: emoji }, emoji))) }), value && (_jsx("button", { onClick: () => {
|
|
44
|
+
].join(' '), children: value || '📄' }), isOpen && (_jsxs("div", { className: "absolute top-full left-0 mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg overflow-hidden", children: [_jsx(EmojiPickerLib, { open: true, onEmojiClick: handleEmojiClick, theme: theme, width: 600, height: 400, emojiStyle: EmojiStyle.NATIVE, autoFocusSearch: true, skinTonesDisabled: false, searchPlaceholder: t('emoji.search') || 'Search emoji...', previewConfig: {
|
|
45
|
+
showPreview: true,
|
|
46
|
+
defaultCaption: t('emoji.select') || 'Select an emoji',
|
|
47
|
+
} }), value && (_jsx("button", { onClick: () => {
|
|
47
48
|
onChange('');
|
|
48
49
|
setIsOpen(false);
|
|
49
|
-
}, className: "
|
|
50
|
+
}, className: "w-full py-2 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 border-t border-gray-200 dark:border-gray-700 transition-colors", children: t('emoji.remove') }))] }))] }));
|
|
50
51
|
}
|
|
@@ -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,
|
|
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"}
|
|
@@ -154,7 +154,9 @@ export function Layout({ children, userAvatar }) {
|
|
|
154
154
|
document.addEventListener('mousedown', handleClickOutside);
|
|
155
155
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
156
156
|
}, []);
|
|
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
|
|
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
|
|
158
|
+
? 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) => {
|
|
158
160
|
setHeaderSearchQuery(e.target.value);
|
|
159
161
|
setShowSearchResults(e.target.value.length > 0);
|
|
160
162
|
}, onFocus: () => {
|
|
@@ -170,5 +172,5 @@ export function Layout({ children, userAvatar }) {
|
|
|
170
172
|
setHeaderSearchQuery('');
|
|
171
173
|
setShowSearchResults(false);
|
|
172
174
|
}, 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));
|
|
173
|
-
}) })) }))] }), _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 })] })] }));
|
|
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 })] })] }));
|
|
174
176
|
}
|
|
@@ -172,7 +172,7 @@ export function Sidebar({ isOpen, onToggle, documents, currentDocId }) {
|
|
|
172
172
|
return (_jsxs("aside", { className: [
|
|
173
173
|
'border-r border-border flex flex-col transition-all duration-200 ease-in-out overflow-hidden',
|
|
174
174
|
isOpen ? 'w-64 opacity-100' : 'w-0 opacity-0',
|
|
175
|
-
].join(' '), children: [_jsx("div", { className: "p-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h2", { className: "font-semibold text-sm text-muted-foreground", children: t('sidebar.documents') }), _jsx("button", { className: "p-2 hover:bg-primary rounded-md transition-colors cursor-pointer", onClick: onCreate, children: _jsx(Plus, { className: "h-4 w-4" }) })] }) }), _jsxs("div", { className: "flex-1 overflow-y-auto p-2 space-y-1 relative", children: [_jsxs(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragStart: handleDragStart, onDragOver: handleDragOver, onDragEnd: handleDragEnd, onDragCancel: handleDragCancel, children: [flattenedTree
|
|
175
|
+
].join(' '), children: [_jsx("div", { className: "p-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h2", { className: "font-semibold text-sm text-muted-foreground", children: t('sidebar.documents') }), _jsx("button", { className: "p-2 hover:bg-primary rounded-md transition-colors cursor-pointer", onClick: () => onCreate('new'), children: _jsx(Plus, { className: "h-4 w-4" }) })] }) }), _jsxs("div", { className: "flex-1 overflow-y-auto p-2 space-y-1 relative", children: [_jsxs(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragStart: handleDragStart, onDragOver: handleDragOver, onDragEnd: handleDragEnd, onDragCancel: handleDragCancel, children: [flattenedTree
|
|
176
176
|
// .filter((doc) => doc.id !== activeId)
|
|
177
177
|
.map((doc) => (_jsx(SortableTreeItem, { id: doc.id, doc: doc, isExpanded: expandedFolders.has(doc.id), isCurrent: doc.id === resolvedCurrentDocId, dropIndicator: dropIndicator?.id === doc.id ? dropIndicator.position : null, onToggle: toggleFolder, onOpen: onOpen }, 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: t('sidebar.noDocuments') }))] })] }));
|
|
178
178
|
}
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
"breadcrumbs.loading": "Loading...",
|
|
11
11
|
"breadcrumbs.error": "Error loading path",
|
|
12
12
|
"emoji.remove": "Remove emoji",
|
|
13
|
+
"emoji.search": "Search emoji...",
|
|
14
|
+
"emoji.select": "Select an emoji",
|
|
13
15
|
"editor.titlePlaceholder": "Document title",
|
|
14
16
|
"editor.titleRequired": "Please enter a document title",
|
|
17
|
+
"editor.slugRequired": "Please enter a document slug",
|
|
15
18
|
"editor.saveFailed": "Failed to save document. Please try again.",
|
|
16
19
|
"editor.view": "View",
|
|
17
20
|
"editor.saving": "Saving...",
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
"breadcrumbs.loading": "Загрузка...",
|
|
11
11
|
"breadcrumbs.error": "Ошибка загрузки пути",
|
|
12
12
|
"emoji.remove": "Убрать эмодзи",
|
|
13
|
+
"emoji.search": "Поиск эмодзи...",
|
|
14
|
+
"emoji.select": "Выберите эмодзи",
|
|
13
15
|
"editor.titlePlaceholder": "Название документа",
|
|
14
16
|
"editor.titleRequired": "Пожалуйста, введите название документа",
|
|
17
|
+
"editor.slugRequired": "Пожалуйста, введите slug документа",
|
|
15
18
|
"editor.saveFailed": "Не удалось сохранить документ. Попробуйте ещё раз.",
|
|
16
19
|
"editor.view": "Просмотр",
|
|
17
20
|
"editor.saving": "Сохранение...",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-embed-docs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"@dnd-kit/core": "^6.3.1"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
+
"emoji-picker-react": "^4.18.0",
|
|
57
58
|
"hono": "^4.7.0",
|
|
58
59
|
"lucide-react": "^0.487.0",
|
|
59
60
|
"nanoid": "^5.1.5",
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { DocumentSummary } from '../../shared/types.js';
|
|
2
|
-
interface DocsLayoutProps {
|
|
3
|
-
theme?: 'light' | 'dark';
|
|
4
|
-
children: React.ReactNode;
|
|
5
|
-
currentDocId?: string;
|
|
6
|
-
/**
|
|
7
|
-
* Called when navigating to a document.
|
|
8
|
-
* `path` is the full path from root to the document (e.g., "/docs/parent/child")
|
|
9
|
-
*/
|
|
10
|
-
onNavigate?: (path: string) => void;
|
|
11
|
-
/**
|
|
12
|
-
* Base path for document URLs (default: "/docs")
|
|
13
|
-
*/
|
|
14
|
-
basePath?: string;
|
|
15
|
-
userAvatar?: React.ReactNode;
|
|
16
|
-
onSearch?: (query: string) => DocumentSummary[];
|
|
17
|
-
}
|
|
18
|
-
export declare function DocsLayout({ children, currentDocId, onNavigate, basePath, userAvatar, onSearch, }: DocsLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
-
export {};
|
|
20
|
-
//# sourceMappingURL=DocsLayout.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"DocsLayout.d.ts","sourceRoot":"","sources":["../../../src/client/components/DocsLayout.tsx"],"names":[],"mappings":"AAoCA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAavD,UAAU,eAAe;IACvB,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IACxB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC5B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,eAAe,EAAE,CAAA;CAChD;AAsQD,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,QAAkB,EAClB,UAAU,EACV,QAAQ,GACT,EAAE,eAAe,2CAgcjB"}
|
|
@@ -1,387 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
//# sourceMappingURL=DocumentProvider.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"DocumentProvider.d.ts","sourceRoot":"","sources":["../../../src/client/providers/DocumentProvider.tsx"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";
|