deepspace 0.3.10 → 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/cli.js +331 -115
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +199 -16
- package/dist/index.js +354 -166
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +242 -73
- package/dist/server.js +985 -374
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +322 -73
- package/dist/worker.js +961 -343
- package/dist/worker.js.map +1 -1
- package/features/admin-page/feature.json +29 -0
- package/features/admin-page/src/AdminPage.tsx +292 -0
- package/features/admin-page/src/admin-schema.ts +20 -0
- package/features/ai-chat/feature.json +41 -0
- package/features/ai-chat/src/AiChatPage.tsx +692 -0
- package/features/ai-chat/src/ChatPanel.tsx +1098 -0
- package/features/ai-chat/src/ai-chat-schema.ts +11 -0
- package/features/canvas/feature.json +45 -0
- package/features/canvas/src/CanvasEditorPage.tsx +87 -0
- package/features/canvas/src/CanvasListPage.tsx +202 -0
- package/features/canvas/src/canvas-schema.ts +33 -0
- package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
- package/features/canvas/src/components/CanvasView.tsx +369 -0
- package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
- package/features/cron/feature.json +33 -0
- package/features/cron/src/CronLogPage.tsx +100 -0
- package/features/cron/src/cron.ts +25 -0
- package/features/docs/feature.json +96 -0
- package/features/docs/src/DocsEditorPage.tsx +1012 -0
- package/features/docs/src/DocsListPage.tsx +951 -0
- package/features/docs/src/DocsOutlinePanel.tsx +68 -0
- package/features/docs/src/DocsPresence.tsx +138 -0
- package/features/docs/src/InviteDialog.tsx +336 -0
- package/features/docs/src/LibrarySidebar.tsx +464 -0
- package/features/docs/src/docs-favorites.ts +18 -0
- package/features/docs/src/docs-library-types.ts +15 -0
- package/features/docs/src/docs-schema.ts +80 -0
- package/features/docs/src/docs-ui.css +309 -0
- package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
- package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
- package/features/docs/src/editor/useDocEditor.ts +206 -0
- package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
- package/features/file-attachments/feature.json +42 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
- package/features/file-attachments/src/attachments-constants.ts +9 -0
- package/features/file-attachments/src/attachments-schema.ts +45 -0
- package/features/file-attachments/src/useFilePreview.ts +471 -0
- package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
- package/features/file-manager/feature.json +24 -0
- package/features/file-manager/src/FileManagerPage.tsx +330 -0
- package/features/integration-test/feature.json +18 -0
- package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
- package/features/integration-test/src/integration.spec.ts +42 -0
- package/features/items/feature.json +33 -0
- package/features/items/src/ItemsPage.tsx +325 -0
- package/features/items/src/items-constants.ts +10 -0
- package/features/items/src/items-schema.ts +27 -0
- package/features/kanban/feature.json +18 -0
- package/features/kanban/src/Kanban.tsx +283 -0
- package/features/landing/feature.json +62 -0
- package/features/landing/src/LandingPage.tsx +627 -0
- package/features/landing/src/primitives.tsx +389 -0
- package/features/landing/src/sections/FAQSection.tsx +110 -0
- package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
- package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
- package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
- package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
- package/features/landing/src/sections/StatsSection.tsx +31 -0
- package/features/landing/src/sections/TeamSection.tsx +86 -0
- package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
- package/features/landing/src/sections/VideoSection.tsx +71 -0
- package/features/leaderboard/feature.json +33 -0
- package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
- package/features/leaderboard/src/leaderboard-constants.ts +21 -0
- package/features/leaderboard/src/leaderboard-schema.ts +36 -0
- package/features/messaging/feature.json +111 -0
- package/features/messaging/src/ChatMultiPage.tsx +300 -0
- package/features/messaging/src/ChatPage.tsx +75 -0
- package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
- package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
- package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
- package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
- package/features/messaging/src/components/chat/MessageList.tsx +248 -0
- package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
- package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
- package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
- package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
- package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
- package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
- package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
- package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
- package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
- package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
- package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
- package/features/messaging/src/hooks/useChatChannel.ts +64 -0
- package/features/messaging/src/hooks/useLongPress.ts +74 -0
- package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
- package/features/messaging/src/messaging-schema.ts +25 -0
- package/features/presence-test/feature.json +21 -0
- package/features/presence-test/src/PresenceTestPage.tsx +120 -0
- package/features/sidebar/feature.json +19 -0
- package/features/sidebar/src/AppSidebar.tsx +558 -0
- package/features/tasks/feature.json +33 -0
- package/features/tasks/src/TasksPage.tsx +598 -0
- package/features/tasks/src/tasks-constants.ts +43 -0
- package/features/tasks/src/tasks-schema.ts +56 -0
- package/features/testing/feature.json +49 -0
- package/features/testing/src/TestPage.tsx +145 -0
- package/features/testing/src/cpu-burn-action.ts +51 -0
- package/features/testing/src/subrequest-burn-action.ts +53 -0
- package/features/testing/src/test-schema.ts +23 -0
- package/features/topbar/feature.json +18 -0
- package/features/topbar/src/Topbar.tsx +353 -0
- package/features/tree/feature.json +26 -0
- package/features/tree/src/TreeSidebar.tsx +607 -0
- package/features/tree/src/tree-constants.ts +68 -0
- package/features/tree/src/tree-hooks.ts +258 -0
- package/package.json +4 -2
- package/scripts/add-feature.cjs +654 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocsListPage — docs2-style library: sidebar, folders, favorites, grid/list.
|
|
3
|
+
* New docs default to “Untitled Document”; rename in editor (no modal).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo, useState, useCallback, useEffect, useRef } from 'react'
|
|
7
|
+
import { useNavigate } from 'react-router-dom'
|
|
8
|
+
import type { RecordData } from 'deepspace'
|
|
9
|
+
import { useUser } from 'deepspace'
|
|
10
|
+
import { useQuery } from 'deepspace'
|
|
11
|
+
import { useMutations } from 'deepspace'
|
|
12
|
+
import { ROLES, type Role } from 'deepspace'
|
|
13
|
+
import {
|
|
14
|
+
FileText,
|
|
15
|
+
Plus,
|
|
16
|
+
Trash2,
|
|
17
|
+
LayoutGrid,
|
|
18
|
+
LayoutList,
|
|
19
|
+
SortAsc,
|
|
20
|
+
Search,
|
|
21
|
+
Star,
|
|
22
|
+
Folder,
|
|
23
|
+
ChevronRight,
|
|
24
|
+
MoreVertical,
|
|
25
|
+
Pencil,
|
|
26
|
+
} from 'lucide-react'
|
|
27
|
+
import { LibrarySidebar, readSidebarCollapsed, writeSidebarCollapsed } from './LibrarySidebar'
|
|
28
|
+
import { getFavorites, saveFavorites } from './docs-favorites'
|
|
29
|
+
import type { DocFolderFields, LibraryNavSelection } from './docs-library-types'
|
|
30
|
+
import './docs-ui.css'
|
|
31
|
+
|
|
32
|
+
interface DocumentFields {
|
|
33
|
+
title: string
|
|
34
|
+
ownerId: string
|
|
35
|
+
collaborators?: string
|
|
36
|
+
editors?: string
|
|
37
|
+
folderId?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type SortOption = 'lastEdited' | 'titleAZ' | 'titleZA'
|
|
41
|
+
type ViewMode = 'grid' | 'list'
|
|
42
|
+
|
|
43
|
+
const SORT_OPTIONS: { value: SortOption; label: string }[] = [
|
|
44
|
+
{ value: 'lastEdited', label: 'Last edited' },
|
|
45
|
+
{ value: 'titleAZ', label: 'Title A–Z' },
|
|
46
|
+
{ value: 'titleZA', label: 'Title Z–A' },
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
const UNTITLED = 'Untitled Document'
|
|
50
|
+
|
|
51
|
+
function greetingForTime(): string {
|
|
52
|
+
const h = new Date().getHours()
|
|
53
|
+
if (h < 12) return 'Good morning'
|
|
54
|
+
if (h < 17) return 'Good afternoon'
|
|
55
|
+
return 'Good evening'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatDate(iso: string): string {
|
|
59
|
+
return new Date(iso).toLocaleDateString(undefined, {
|
|
60
|
+
month: 'short',
|
|
61
|
+
day: 'numeric',
|
|
62
|
+
year: 'numeric',
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseIdList(raw: string | undefined): string[] {
|
|
67
|
+
if (!raw) return []
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(raw)
|
|
70
|
+
return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === 'string') : []
|
|
71
|
+
} catch {
|
|
72
|
+
return []
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default function DocsListPage() {
|
|
77
|
+
const { user } = useUser()
|
|
78
|
+
const navigate = useNavigate()
|
|
79
|
+
const userRole = (user?.role ?? ROLES.VIEWER) as Role
|
|
80
|
+
const canCreate = userRole === ROLES.MEMBER || userRole === ROLES.ADMIN
|
|
81
|
+
|
|
82
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
83
|
+
const [sortBy, setSortBy] = useState<SortOption>('lastEdited')
|
|
84
|
+
const [viewMode, setViewMode] = useState<ViewMode>('grid')
|
|
85
|
+
const [sortOpen, setSortOpen] = useState(false)
|
|
86
|
+
const [favorites, setFavorites] = useState<Set<string>>(() => getFavorites())
|
|
87
|
+
const [libraryNav, setLibraryNav] = useState<LibraryNavSelection>({ kind: 'all' })
|
|
88
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState(readSidebarCollapsed)
|
|
89
|
+
const [renamingId, setRenamingId] = useState<string | null>(null)
|
|
90
|
+
const [renameValue, setRenameValue] = useState('')
|
|
91
|
+
const [folderRenamingId, setFolderRenamingId] = useState<string | null>(null)
|
|
92
|
+
const [folderRenameValue, setFolderRenameValue] = useState('')
|
|
93
|
+
const [moveMenuId, setMoveMenuId] = useState<string | null>(null)
|
|
94
|
+
const [actionsMenuId, setActionsMenuId] = useState<string | null>(null)
|
|
95
|
+
|
|
96
|
+
const { records: documents, status } = useQuery<DocumentFields>('documents', {
|
|
97
|
+
orderBy: 'createdAt',
|
|
98
|
+
orderDir: 'desc',
|
|
99
|
+
})
|
|
100
|
+
const { create, remove, put } = useMutations<DocumentFields>('documents')
|
|
101
|
+
|
|
102
|
+
const { records: folderRecords } = useQuery<DocFolderFields>('doc_folders', {
|
|
103
|
+
orderBy: 'createdAt',
|
|
104
|
+
orderDir: 'asc',
|
|
105
|
+
})
|
|
106
|
+
const {
|
|
107
|
+
create: createFolder,
|
|
108
|
+
put: putFolder,
|
|
109
|
+
remove: removeFolder,
|
|
110
|
+
} = useMutations<DocFolderFields>('doc_folders')
|
|
111
|
+
|
|
112
|
+
const myFolders = useMemo(
|
|
113
|
+
() => (folderRecords ?? []).filter((f) => f.data.ownerId === user?.id),
|
|
114
|
+
[folderRecords, user?.id],
|
|
115
|
+
)
|
|
116
|
+
const sortedFolders = useMemo(
|
|
117
|
+
() => [...myFolders].sort((a, b) => (a.data.name ?? '').localeCompare(b.data.name ?? '')),
|
|
118
|
+
[myFolders],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (libraryNav.kind !== 'folder') return
|
|
123
|
+
if (!sortedFolders.some((f) => f.recordId === libraryNav.folderId)) {
|
|
124
|
+
setLibraryNav({ kind: 'all' })
|
|
125
|
+
}
|
|
126
|
+
}, [libraryNav, sortedFolders])
|
|
127
|
+
|
|
128
|
+
const displayFirstName = user?.name?.trim().split(/\s+/)[0] ?? 'there'
|
|
129
|
+
|
|
130
|
+
const isOwnedDocument = useCallback(
|
|
131
|
+
(d: RecordData<DocumentFields>) => d.data.ownerId === user?.id,
|
|
132
|
+
[user?.id],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const isSharedWithCurrentUser = useCallback(
|
|
136
|
+
(d: RecordData<DocumentFields>) =>
|
|
137
|
+
Boolean(
|
|
138
|
+
user?.id &&
|
|
139
|
+
d.data.ownerId !== user.id &&
|
|
140
|
+
parseIdList(d.data.collaborators).includes(user.id),
|
|
141
|
+
),
|
|
142
|
+
[user?.id],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
const ownedDocumentCount = useMemo(
|
|
146
|
+
() => (documents ?? []).filter(isOwnedDocument).length,
|
|
147
|
+
[documents, isOwnedDocument],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
const listHeading = useMemo(() => {
|
|
151
|
+
if (libraryNav.kind === 'shared') return 'Shared with me'
|
|
152
|
+
if (libraryNav.kind === 'favorites') return 'Favorites'
|
|
153
|
+
if (libraryNav.kind === 'uncategorized') return 'Uncategorized'
|
|
154
|
+
if (libraryNav.kind === 'folder') {
|
|
155
|
+
return (
|
|
156
|
+
sortedFolders.find((f) => f.recordId === libraryNav.folderId)?.data.name?.trim() || 'Folder'
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
return 'My Documents'
|
|
160
|
+
}, [libraryNav, sortedFolders])
|
|
161
|
+
|
|
162
|
+
const matchesNav = useCallback(
|
|
163
|
+
(d: RecordData<DocumentFields>) => {
|
|
164
|
+
const fid = d.data.folderId ?? ''
|
|
165
|
+
if (libraryNav.kind === 'all') return isOwnedDocument(d)
|
|
166
|
+
if (libraryNav.kind === 'shared') return isSharedWithCurrentUser(d)
|
|
167
|
+
if (libraryNav.kind === 'favorites') return favorites.has(d.recordId)
|
|
168
|
+
if (libraryNav.kind === 'uncategorized') return isOwnedDocument(d) && fid === ''
|
|
169
|
+
if (libraryNav.kind === 'folder') return isOwnedDocument(d) && fid === libraryNav.folderId
|
|
170
|
+
return true
|
|
171
|
+
},
|
|
172
|
+
[favorites, isOwnedDocument, isSharedWithCurrentUser, libraryNav],
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
const filteredSorted = useMemo(() => {
|
|
176
|
+
let rows = (documents ?? []).filter(matchesNav)
|
|
177
|
+
const q = searchQuery.trim().toLowerCase()
|
|
178
|
+
if (q) rows = rows.filter((d) => d.data.title.toLowerCase().includes(q))
|
|
179
|
+
switch (sortBy) {
|
|
180
|
+
case 'lastEdited':
|
|
181
|
+
rows.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
182
|
+
break
|
|
183
|
+
case 'titleAZ':
|
|
184
|
+
rows.sort((a, b) => a.data.title.localeCompare(b.data.title))
|
|
185
|
+
break
|
|
186
|
+
case 'titleZA':
|
|
187
|
+
rows.sort((a, b) => b.data.title.localeCompare(a.data.title))
|
|
188
|
+
break
|
|
189
|
+
default:
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
return rows
|
|
193
|
+
}, [documents, matchesNav, searchQuery, sortBy])
|
|
194
|
+
|
|
195
|
+
const toggleSidebar = useCallback(() => {
|
|
196
|
+
setSidebarCollapsed((c) => {
|
|
197
|
+
const n = !c
|
|
198
|
+
writeSidebarCollapsed(n)
|
|
199
|
+
return n
|
|
200
|
+
})
|
|
201
|
+
}, [])
|
|
202
|
+
|
|
203
|
+
const toggleFavorite = useCallback((contentId: string) => {
|
|
204
|
+
setFavorites((prev) => {
|
|
205
|
+
const next = new Set(prev)
|
|
206
|
+
if (next.has(contentId)) next.delete(contentId)
|
|
207
|
+
else next.add(contentId)
|
|
208
|
+
saveFavorites(next)
|
|
209
|
+
return next
|
|
210
|
+
})
|
|
211
|
+
}, [])
|
|
212
|
+
|
|
213
|
+
const folderIdForNew = libraryNav.kind === 'folder' ? libraryNav.folderId : ''
|
|
214
|
+
|
|
215
|
+
const handleNewDocument = useCallback(async () => {
|
|
216
|
+
if (!user || !canCreate) return
|
|
217
|
+
const id = await create({
|
|
218
|
+
title: UNTITLED,
|
|
219
|
+
ownerId: user.id,
|
|
220
|
+
collaborators: '[]',
|
|
221
|
+
editors: '[]',
|
|
222
|
+
folderId: folderIdForNew,
|
|
223
|
+
})
|
|
224
|
+
navigate(`/docs/${id}`)
|
|
225
|
+
}, [canCreate, create, folderIdForNew, navigate, user])
|
|
226
|
+
|
|
227
|
+
const handleDelete = useCallback(
|
|
228
|
+
async (docId: string) => {
|
|
229
|
+
if (confirm('Delete this document?')) await remove(docId)
|
|
230
|
+
},
|
|
231
|
+
[remove],
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const handleRenameCommit = useCallback(
|
|
235
|
+
async (docId: string) => {
|
|
236
|
+
const rec = documents.find((d) => d.recordId === docId)
|
|
237
|
+
const t = renameValue.trim()
|
|
238
|
+
setRenamingId(null)
|
|
239
|
+
if (!rec || !t || t === rec.data.title) return
|
|
240
|
+
await put(docId, { ...rec.data, title: t }).catch(() => {})
|
|
241
|
+
},
|
|
242
|
+
[documents, put, renameValue],
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
const handleCreateFolder = useCallback(
|
|
246
|
+
async (name: string) => {
|
|
247
|
+
if (!user) return
|
|
248
|
+
await createFolder({ name, ownerId: user.id })
|
|
249
|
+
},
|
|
250
|
+
[createFolder, user],
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
const handleDeleteFolder = useCallback(
|
|
254
|
+
async (folderId: string) => {
|
|
255
|
+
const owned = (documents ?? []).filter(
|
|
256
|
+
(d) => d.data.ownerId === user?.id && (d.data.folderId ?? '') === folderId,
|
|
257
|
+
)
|
|
258
|
+
await Promise.all(owned.map((d) => put(d.recordId, { ...d.data, folderId: '' })))
|
|
259
|
+
await removeFolder(folderId)
|
|
260
|
+
setFolderRenamingId((id) => (id === folderId ? null : id))
|
|
261
|
+
setLibraryNav((nav) =>
|
|
262
|
+
nav.kind === 'folder' && nav.folderId === folderId ? { kind: 'all' } : nav,
|
|
263
|
+
)
|
|
264
|
+
},
|
|
265
|
+
[documents, put, removeFolder, user?.id],
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
const commitRenameFolderRef = useRef<() => Promise<void>>(async () => {})
|
|
269
|
+
const commitRenameFolder = useCallback(async () => {
|
|
270
|
+
const id = folderRenamingId
|
|
271
|
+
const trimmed = folderRenameValue.trim()
|
|
272
|
+
setFolderRenamingId(null)
|
|
273
|
+
if (!id || !trimmed) return
|
|
274
|
+
const folder = sortedFolders.find((f) => f.recordId === id)
|
|
275
|
+
if (!folder || trimmed === (folder.data.name ?? '').trim()) return
|
|
276
|
+
await putFolder(id, { ...folder.data, name: trimmed }).catch(() => {})
|
|
277
|
+
}, [folderRenameValue, folderRenamingId, putFolder, sortedFolders])
|
|
278
|
+
|
|
279
|
+
commitRenameFolderRef.current = commitRenameFolder
|
|
280
|
+
|
|
281
|
+
const handleMoveDoc = useCallback(
|
|
282
|
+
async (docId: string, folderId: string) => {
|
|
283
|
+
const rec = documents.find((d) => d.recordId === docId)
|
|
284
|
+
if (!rec) return
|
|
285
|
+
await put(docId, { ...rec.data, folderId })
|
|
286
|
+
setMoveMenuId(null)
|
|
287
|
+
},
|
|
288
|
+
[documents, put],
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
const isLoading = status === 'loading'
|
|
292
|
+
|
|
293
|
+
const accentBtn =
|
|
294
|
+
'inline-flex h-9 items-center gap-2 rounded-lg px-4 text-[12px] font-bold text-white shadow-sm transition-opacity hover:opacity-90'
|
|
295
|
+
const ghostBtn =
|
|
296
|
+
'inline-flex h-9 items-center gap-2 rounded-lg border px-4 text-[12px] font-semibold shadow-sm transition-colors'
|
|
297
|
+
|
|
298
|
+
const blankWhen =
|
|
299
|
+
canCreate &&
|
|
300
|
+
libraryNav.kind !== 'favorites' &&
|
|
301
|
+
(libraryNav.kind === 'all' ||
|
|
302
|
+
libraryNav.kind === 'uncategorized' ||
|
|
303
|
+
libraryNav.kind === 'folder')
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<div
|
|
307
|
+
className="docs-feature-scope flex min-h-full overflow-hidden"
|
|
308
|
+
style={{ backgroundColor: 'var(--docs-el-bg)', color: 'var(--docs-el-text)' }}
|
|
309
|
+
>
|
|
310
|
+
{user ? (
|
|
311
|
+
<LibrarySidebar
|
|
312
|
+
selection={libraryNav}
|
|
313
|
+
onSelect={setLibraryNav}
|
|
314
|
+
folders={myFolders}
|
|
315
|
+
collapsed={sidebarCollapsed}
|
|
316
|
+
onToggleCollapsed={toggleSidebar}
|
|
317
|
+
onCreateFolder={handleCreateFolder}
|
|
318
|
+
onDeleteFolder={handleDeleteFolder}
|
|
319
|
+
onStartRenameFolder={(f) => {
|
|
320
|
+
setFolderRenamingId(f.recordId)
|
|
321
|
+
setFolderRenameValue(f.data.name ?? '')
|
|
322
|
+
}}
|
|
323
|
+
renamingFolderId={folderRenamingId}
|
|
324
|
+
renameFolderValue={folderRenameValue}
|
|
325
|
+
setRenameFolderValue={setFolderRenameValue}
|
|
326
|
+
onCommitRenameFolder={() => commitRenameFolderRef.current()}
|
|
327
|
+
onCancelRenameFolder={() => setFolderRenamingId(null)}
|
|
328
|
+
/>
|
|
329
|
+
) : null}
|
|
330
|
+
|
|
331
|
+
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto">
|
|
332
|
+
<div className="px-6 pb-10 pt-9 md:px-12 md:pb-12 md:pt-10 lg:px-16 lg:pb-16 lg:pt-12">
|
|
333
|
+
<div className="mb-1 flex min-w-0 items-center justify-between gap-4">
|
|
334
|
+
<h1
|
|
335
|
+
className="min-w-0 text-3xl font-bold leading-tight tracking-tight sm:text-4xl"
|
|
336
|
+
style={{ color: 'var(--docs-el-text)' }}
|
|
337
|
+
>
|
|
338
|
+
{greetingForTime()}, {displayFirstName}
|
|
339
|
+
</h1>
|
|
340
|
+
</div>
|
|
341
|
+
<p className="mb-8 text-[13px] font-medium" style={{ color: 'var(--docs-el-muted)' }}>
|
|
342
|
+
{ownedDocumentCount} {ownedDocumentCount === 1 ? 'document' : 'documents'} in your
|
|
343
|
+
documents.
|
|
344
|
+
</p>
|
|
345
|
+
|
|
346
|
+
<div className="mb-8 flex w-full min-w-0 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
347
|
+
<div className="relative min-w-0 max-w-md flex-1">
|
|
348
|
+
<Search
|
|
349
|
+
className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2"
|
|
350
|
+
style={{ color: 'var(--docs-el-muted)' }}
|
|
351
|
+
strokeWidth={2}
|
|
352
|
+
aria-hidden
|
|
353
|
+
/>
|
|
354
|
+
<input
|
|
355
|
+
type="search"
|
|
356
|
+
value={searchQuery}
|
|
357
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
358
|
+
placeholder="Search your library..."
|
|
359
|
+
className="h-auto w-full rounded-lg py-2 pl-10 pr-10 text-[13px] shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/25"
|
|
360
|
+
style={{
|
|
361
|
+
backgroundColor: 'var(--docs-el-surface)',
|
|
362
|
+
borderWidth: 1,
|
|
363
|
+
borderStyle: 'solid',
|
|
364
|
+
borderColor: 'var(--docs-el-line)',
|
|
365
|
+
color: 'var(--docs-el-text)',
|
|
366
|
+
}}
|
|
367
|
+
/>
|
|
368
|
+
{searchQuery ? (
|
|
369
|
+
<button
|
|
370
|
+
type="button"
|
|
371
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-md px-2 py-1 text-[11px] font-medium"
|
|
372
|
+
style={{ color: 'var(--docs-el-muted)' }}
|
|
373
|
+
onClick={() => setSearchQuery('')}
|
|
374
|
+
>
|
|
375
|
+
Clear
|
|
376
|
+
</button>
|
|
377
|
+
) : null}
|
|
378
|
+
</div>
|
|
379
|
+
<div className="flex shrink-0 flex-wrap items-center justify-end gap-3 sm:ml-auto">
|
|
380
|
+
{canCreate ? (
|
|
381
|
+
<button
|
|
382
|
+
type="button"
|
|
383
|
+
onClick={() => void handleNewDocument()}
|
|
384
|
+
className={accentBtn}
|
|
385
|
+
style={{ backgroundColor: 'var(--docs-el-accent)' }}
|
|
386
|
+
>
|
|
387
|
+
<Plus className="h-4 w-4" strokeWidth={2.5} />
|
|
388
|
+
New Document
|
|
389
|
+
</button>
|
|
390
|
+
) : null}
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
{canCreate && sortedFolders.length > 0 && (
|
|
395
|
+
<div className="mb-10 grid grid-cols-1 gap-3 md:grid-cols-4">
|
|
396
|
+
{sortedFolders.map((folder) => {
|
|
397
|
+
const selected =
|
|
398
|
+
libraryNav.kind === 'folder' && libraryNav.folderId === folder.recordId
|
|
399
|
+
return (
|
|
400
|
+
<button
|
|
401
|
+
key={folder.recordId}
|
|
402
|
+
type="button"
|
|
403
|
+
onClick={() => setLibraryNav({ kind: 'folder', folderId: folder.recordId })}
|
|
404
|
+
className="group flex min-w-0 items-center rounded-xl border p-3.5 text-left shadow-sm transition-all"
|
|
405
|
+
style={{
|
|
406
|
+
borderColor: selected
|
|
407
|
+
? 'color-mix(in srgb, var(--docs-el-accent) 50%, transparent)'
|
|
408
|
+
: 'var(--docs-el-line)',
|
|
409
|
+
backgroundColor: 'var(--docs-el-surface)',
|
|
410
|
+
boxShadow: selected
|
|
411
|
+
? '0 0 0 2px color-mix(in srgb, var(--docs-el-accent) 15%, transparent)'
|
|
412
|
+
: undefined,
|
|
413
|
+
}}
|
|
414
|
+
>
|
|
415
|
+
<div
|
|
416
|
+
className="rounded-lg p-2 transition-all group-hover:bg-[color-mix(in_srgb,var(--docs-el-accent)_5%,transparent)]"
|
|
417
|
+
style={{
|
|
418
|
+
backgroundColor: 'var(--docs-el-bg)',
|
|
419
|
+
color: 'var(--docs-el-muted)',
|
|
420
|
+
}}
|
|
421
|
+
>
|
|
422
|
+
<Folder className="h-4 w-4" />
|
|
423
|
+
</div>
|
|
424
|
+
<span className="min-w-0 flex-1 truncate pl-3 text-[13px] font-semibold">
|
|
425
|
+
{folder.data.name}
|
|
426
|
+
</span>
|
|
427
|
+
<ChevronRight className="h-3 w-3 shrink-0 opacity-50" />
|
|
428
|
+
</button>
|
|
429
|
+
)
|
|
430
|
+
})}
|
|
431
|
+
</div>
|
|
432
|
+
)}
|
|
433
|
+
|
|
434
|
+
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
435
|
+
<h2
|
|
436
|
+
className="text-[11px] font-bold uppercase tracking-widest"
|
|
437
|
+
style={{ color: 'var(--docs-el-muted)' }}
|
|
438
|
+
>
|
|
439
|
+
{listHeading}
|
|
440
|
+
</h2>
|
|
441
|
+
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
|
442
|
+
<div className="relative">
|
|
443
|
+
<button
|
|
444
|
+
type="button"
|
|
445
|
+
onClick={() => setSortOpen(!sortOpen)}
|
|
446
|
+
className={ghostBtn}
|
|
447
|
+
style={{
|
|
448
|
+
borderColor: 'var(--docs-el-line)',
|
|
449
|
+
backgroundColor: 'var(--docs-el-surface)',
|
|
450
|
+
color: 'var(--docs-el-muted)',
|
|
451
|
+
}}
|
|
452
|
+
>
|
|
453
|
+
<SortAsc className="h-3.5 w-3.5" />
|
|
454
|
+
{SORT_OPTIONS.find((o) => o.value === sortBy)?.label}
|
|
455
|
+
</button>
|
|
456
|
+
{sortOpen ? (
|
|
457
|
+
<>
|
|
458
|
+
<button
|
|
459
|
+
type="button"
|
|
460
|
+
className="fixed inset-0 z-40 cursor-default bg-transparent"
|
|
461
|
+
aria-label="Close menu"
|
|
462
|
+
onClick={() => setSortOpen(false)}
|
|
463
|
+
/>
|
|
464
|
+
<div
|
|
465
|
+
className="absolute right-0 top-full z-50 mt-1 min-w-[168px] rounded-lg border py-1 shadow-lg"
|
|
466
|
+
style={{
|
|
467
|
+
borderColor: 'var(--docs-el-line)',
|
|
468
|
+
backgroundColor: 'var(--docs-el-surface)',
|
|
469
|
+
}}
|
|
470
|
+
>
|
|
471
|
+
{SORT_OPTIONS.map((opt) => (
|
|
472
|
+
<button
|
|
473
|
+
key={opt.value}
|
|
474
|
+
type="button"
|
|
475
|
+
onClick={() => {
|
|
476
|
+
setSortBy(opt.value)
|
|
477
|
+
setSortOpen(false)
|
|
478
|
+
}}
|
|
479
|
+
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-black/[0.03] dark:hover:bg-white/[0.05]"
|
|
480
|
+
style={{
|
|
481
|
+
color:
|
|
482
|
+
sortBy === opt.value
|
|
483
|
+
? 'var(--docs-el-accent)'
|
|
484
|
+
: 'var(--docs-el-text)',
|
|
485
|
+
fontWeight: sortBy === opt.value ? 600 : 400,
|
|
486
|
+
}}
|
|
487
|
+
>
|
|
488
|
+
{opt.label}
|
|
489
|
+
</button>
|
|
490
|
+
))}
|
|
491
|
+
</div>
|
|
492
|
+
</>
|
|
493
|
+
) : null}
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<div
|
|
497
|
+
className="flex items-center rounded-lg p-0.5"
|
|
498
|
+
style={{
|
|
499
|
+
borderWidth: 1,
|
|
500
|
+
borderStyle: 'solid',
|
|
501
|
+
borderColor: 'var(--docs-el-line)',
|
|
502
|
+
backgroundColor: 'color-mix(in srgb, var(--docs-el-text) 5%, transparent)',
|
|
503
|
+
}}
|
|
504
|
+
>
|
|
505
|
+
<button
|
|
506
|
+
type="button"
|
|
507
|
+
onClick={() => setViewMode('grid')}
|
|
508
|
+
className="rounded-md p-1.5 transition-all"
|
|
509
|
+
style={
|
|
510
|
+
viewMode === 'grid'
|
|
511
|
+
? {
|
|
512
|
+
backgroundColor: 'var(--docs-el-surface)',
|
|
513
|
+
color: 'var(--docs-el-text)',
|
|
514
|
+
boxShadow: '0 1px 2px rgba(0,0,0,0.06)',
|
|
515
|
+
}
|
|
516
|
+
: { color: 'var(--docs-el-muted)' }
|
|
517
|
+
}
|
|
518
|
+
title="Grid view"
|
|
519
|
+
>
|
|
520
|
+
<LayoutGrid className="h-[13px] w-[13px]" />
|
|
521
|
+
</button>
|
|
522
|
+
<button
|
|
523
|
+
type="button"
|
|
524
|
+
onClick={() => setViewMode('list')}
|
|
525
|
+
className="rounded-md p-1.5 transition-all"
|
|
526
|
+
style={
|
|
527
|
+
viewMode === 'list'
|
|
528
|
+
? {
|
|
529
|
+
backgroundColor: 'var(--docs-el-surface)',
|
|
530
|
+
color: 'var(--docs-el-text)',
|
|
531
|
+
boxShadow: '0 1px 2px rgba(0,0,0,0.06)',
|
|
532
|
+
}
|
|
533
|
+
: { color: 'var(--docs-el-muted)' }
|
|
534
|
+
}
|
|
535
|
+
title="List view"
|
|
536
|
+
>
|
|
537
|
+
<LayoutList className="h-[13px] w-[13px]" />
|
|
538
|
+
</button>
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
{isLoading ? (
|
|
544
|
+
<div className="flex items-center justify-center py-20">
|
|
545
|
+
<div
|
|
546
|
+
className="h-10 w-10 animate-spin rounded-full border-2"
|
|
547
|
+
style={{
|
|
548
|
+
borderColor: 'var(--docs-el-line)',
|
|
549
|
+
borderTopColor: 'var(--docs-el-accent)',
|
|
550
|
+
}}
|
|
551
|
+
/>
|
|
552
|
+
</div>
|
|
553
|
+
) : filteredSorted.length === 0 ? (
|
|
554
|
+
<div className="flex flex-col items-center justify-center px-4 py-16 text-center">
|
|
555
|
+
<div
|
|
556
|
+
className="mb-5 flex h-16 w-16 items-center justify-center rounded-2xl"
|
|
557
|
+
style={{
|
|
558
|
+
backgroundColor: 'var(--docs-el-surface)',
|
|
559
|
+
boxShadow:
|
|
560
|
+
'0 0 0 1px var(--docs-el-line), 0 1px 2px color-mix(in srgb, var(--docs-el-text) 8%, transparent)',
|
|
561
|
+
}}
|
|
562
|
+
>
|
|
563
|
+
<FileText
|
|
564
|
+
className="h-8 w-8"
|
|
565
|
+
strokeWidth={1.5}
|
|
566
|
+
style={{ color: 'var(--docs-el-muted)' }}
|
|
567
|
+
aria-hidden
|
|
568
|
+
/>
|
|
569
|
+
</div>
|
|
570
|
+
<h3 className="mb-2 text-lg font-semibold" style={{ color: 'var(--docs-el-text)' }}>
|
|
571
|
+
{searchQuery ? 'No matches' : 'No documents yet'}
|
|
572
|
+
</h3>
|
|
573
|
+
<p className="mb-6 max-w-sm text-sm" style={{ color: 'var(--docs-el-muted)' }}>
|
|
574
|
+
{canCreate && !searchQuery
|
|
575
|
+
? 'Create your first document with New Document above'
|
|
576
|
+
: searchQuery
|
|
577
|
+
? 'Try a different search'
|
|
578
|
+
: 'No documents yet'}
|
|
579
|
+
</p>
|
|
580
|
+
</div>
|
|
581
|
+
) : viewMode === 'grid' ? (
|
|
582
|
+
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5">
|
|
583
|
+
{blankWhen ? (
|
|
584
|
+
<button
|
|
585
|
+
type="button"
|
|
586
|
+
onClick={() => void handleNewDocument()}
|
|
587
|
+
className="group w-full cursor-pointer rounded-xl border-2 border-dashed p-4 text-left shadow-sm transition-all hover:shadow-md"
|
|
588
|
+
style={{
|
|
589
|
+
borderColor: 'var(--docs-el-line)',
|
|
590
|
+
backgroundColor: 'color-mix(in srgb, var(--docs-el-surface) 20%, transparent)',
|
|
591
|
+
}}
|
|
592
|
+
>
|
|
593
|
+
<div
|
|
594
|
+
className="relative mb-4 aspect-[3/4] w-full overflow-hidden rounded-lg border shadow-sm"
|
|
595
|
+
style={{
|
|
596
|
+
borderColor: 'var(--docs-el-line)',
|
|
597
|
+
backgroundColor: 'var(--docs-el-bg)',
|
|
598
|
+
}}
|
|
599
|
+
>
|
|
600
|
+
<div className="flex h-full w-full items-center justify-center">
|
|
601
|
+
<div
|
|
602
|
+
className="flex h-10 w-10 items-center justify-center rounded-full ring-1 transition-colors group-hover:text-[color:var(--docs-el-accent)]"
|
|
603
|
+
style={{
|
|
604
|
+
backgroundColor: 'var(--docs-el-surface)',
|
|
605
|
+
color: 'var(--docs-el-muted)',
|
|
606
|
+
boxShadow: '0 0 0 1px var(--docs-el-line)',
|
|
607
|
+
}}
|
|
608
|
+
>
|
|
609
|
+
<Plus className="h-5 w-5" strokeWidth={2.5} />
|
|
610
|
+
</div>
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
<h3
|
|
614
|
+
className="truncate text-[13px] font-semibold leading-snug transition-colors group-hover:text-[color:var(--docs-el-accent)]"
|
|
615
|
+
style={{ color: 'var(--docs-el-text)' }}
|
|
616
|
+
>
|
|
617
|
+
Blank doc
|
|
618
|
+
</h3>
|
|
619
|
+
<p
|
|
620
|
+
className="mt-0.5 text-[10px] font-medium"
|
|
621
|
+
style={{ color: 'var(--docs-el-muted)' }}
|
|
622
|
+
>
|
|
623
|
+
Creates “{UNTITLED}” — rename in editor
|
|
624
|
+
</p>
|
|
625
|
+
</button>
|
|
626
|
+
) : null}
|
|
627
|
+
|
|
628
|
+
{filteredSorted.map((doc) => {
|
|
629
|
+
const isOwner = doc.data.ownerId === user?.id
|
|
630
|
+
const isFav = favorites.has(doc.recordId)
|
|
631
|
+
const dateLabel = formatDate(doc.createdAt)
|
|
632
|
+
const fid = doc.data.folderId ?? ''
|
|
633
|
+
|
|
634
|
+
return (
|
|
635
|
+
<div
|
|
636
|
+
key={doc.recordId}
|
|
637
|
+
role="button"
|
|
638
|
+
tabIndex={0}
|
|
639
|
+
className="group relative cursor-pointer rounded-xl border p-4 shadow-sm transition-all hover:-translate-y-0.5 hover:shadow-md"
|
|
640
|
+
style={{
|
|
641
|
+
borderColor: 'var(--docs-el-line)',
|
|
642
|
+
backgroundColor: 'var(--docs-el-surface)',
|
|
643
|
+
}}
|
|
644
|
+
onClick={() => navigate(`/docs/${doc.recordId}`)}
|
|
645
|
+
onKeyDown={(e) => {
|
|
646
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
647
|
+
e.preventDefault()
|
|
648
|
+
navigate(`/docs/${doc.recordId}`)
|
|
649
|
+
}
|
|
650
|
+
}}
|
|
651
|
+
>
|
|
652
|
+
<div
|
|
653
|
+
className="relative mb-4 aspect-[3/4] w-full overflow-hidden rounded-lg border shadow-sm"
|
|
654
|
+
style={{
|
|
655
|
+
borderColor: 'var(--docs-el-line)',
|
|
656
|
+
backgroundColor: 'var(--docs-el-bg)',
|
|
657
|
+
}}
|
|
658
|
+
>
|
|
659
|
+
<div className="flex h-full w-full items-center justify-center">
|
|
660
|
+
<FileText className="h-10 w-10 opacity-[0.18]" strokeWidth={1.25} />
|
|
661
|
+
</div>
|
|
662
|
+
</div>
|
|
663
|
+
<div className="flex items-start justify-between gap-2">
|
|
664
|
+
<div className="min-w-0 flex-1">
|
|
665
|
+
{renamingId === doc.recordId ? (
|
|
666
|
+
<input
|
|
667
|
+
value={renameValue}
|
|
668
|
+
onChange={(e) => setRenameValue(e.target.value)}
|
|
669
|
+
onBlur={() => void handleRenameCommit(doc.recordId)}
|
|
670
|
+
onKeyDown={(e) => {
|
|
671
|
+
if (e.key === 'Enter') void handleRenameCommit(doc.recordId)
|
|
672
|
+
if (e.key === 'Escape') setRenamingId(null)
|
|
673
|
+
}}
|
|
674
|
+
onClick={(e) => e.stopPropagation()}
|
|
675
|
+
className="mb-0.5 w-full rounded-md border px-2 py-1 text-[13px] font-semibold outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
676
|
+
style={{
|
|
677
|
+
borderColor: 'var(--docs-el-line)',
|
|
678
|
+
backgroundColor: 'var(--docs-el-bg)',
|
|
679
|
+
color: 'var(--docs-el-text)',
|
|
680
|
+
}}
|
|
681
|
+
autoFocus
|
|
682
|
+
/>
|
|
683
|
+
) : (
|
|
684
|
+
<h3
|
|
685
|
+
className="truncate text-[13px] font-semibold leading-snug transition-colors group-hover:text-[color:var(--docs-el-accent)]"
|
|
686
|
+
style={{ color: 'var(--docs-el-text)' }}
|
|
687
|
+
>
|
|
688
|
+
{doc.data.title}
|
|
689
|
+
</h3>
|
|
690
|
+
)}
|
|
691
|
+
<p
|
|
692
|
+
className="mt-0.5 text-[10px] font-medium leading-4 tracking-tight"
|
|
693
|
+
style={{ color: 'var(--docs-el-muted)' }}
|
|
694
|
+
>
|
|
695
|
+
{dateLabel}
|
|
696
|
+
{isOwner ? ' · You' : ''}
|
|
697
|
+
{!isOwner ? ' · Shared with you' : ''}
|
|
698
|
+
{fid
|
|
699
|
+
? ` · ${sortedFolders.find((f) => f.recordId === fid)?.data.name ?? 'Folder'}`
|
|
700
|
+
: ''}
|
|
701
|
+
</p>
|
|
702
|
+
</div>
|
|
703
|
+
{isOwner ? (
|
|
704
|
+
<div className="flex shrink-0 items-center gap-0.5">
|
|
705
|
+
<button
|
|
706
|
+
type="button"
|
|
707
|
+
title={isFav ? 'Remove favorite' : 'Add favorite'}
|
|
708
|
+
onClick={(e) => {
|
|
709
|
+
e.stopPropagation()
|
|
710
|
+
toggleFavorite(doc.recordId)
|
|
711
|
+
}}
|
|
712
|
+
className="rounded-lg p-1.5 transition-colors hover:bg-black/[0.05] dark:hover:bg-white/[0.06]"
|
|
713
|
+
style={{ color: 'var(--docs-el-muted)' }}
|
|
714
|
+
>
|
|
715
|
+
<Star
|
|
716
|
+
className={`h-4 w-4 ${isFav ? 'fill-yellow-500 text-yellow-500' : ''}`}
|
|
717
|
+
/>
|
|
718
|
+
</button>
|
|
719
|
+
<div className="relative">
|
|
720
|
+
<button
|
|
721
|
+
type="button"
|
|
722
|
+
title="More"
|
|
723
|
+
onClick={(e) => {
|
|
724
|
+
e.stopPropagation()
|
|
725
|
+
setActionsMenuId((id) =>
|
|
726
|
+
id === doc.recordId ? null : doc.recordId,
|
|
727
|
+
)
|
|
728
|
+
setMoveMenuId(null)
|
|
729
|
+
}}
|
|
730
|
+
className="rounded-lg p-1.5"
|
|
731
|
+
style={{ color: 'var(--docs-el-muted)' }}
|
|
732
|
+
>
|
|
733
|
+
<MoreVertical className="h-4 w-4" />
|
|
734
|
+
</button>
|
|
735
|
+
{actionsMenuId === doc.recordId ? (
|
|
736
|
+
<>
|
|
737
|
+
<button
|
|
738
|
+
type="button"
|
|
739
|
+
className="fixed inset-0 z-40 cursor-default bg-transparent"
|
|
740
|
+
aria-label="Close"
|
|
741
|
+
onClick={(e) => {
|
|
742
|
+
e.stopPropagation()
|
|
743
|
+
setActionsMenuId(null)
|
|
744
|
+
}}
|
|
745
|
+
/>
|
|
746
|
+
<div
|
|
747
|
+
className="absolute right-0 top-full z-50 mt-1 min-w-[160px] rounded-lg border py-1 shadow-lg"
|
|
748
|
+
style={{
|
|
749
|
+
borderColor: 'var(--docs-el-line)',
|
|
750
|
+
backgroundColor: 'var(--docs-el-surface)',
|
|
751
|
+
color: 'var(--docs-el-text)',
|
|
752
|
+
}}
|
|
753
|
+
onClick={(e) => e.stopPropagation()}
|
|
754
|
+
>
|
|
755
|
+
<button
|
|
756
|
+
type="button"
|
|
757
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-black/[0.04] dark:hover:bg-white/[0.06]"
|
|
758
|
+
onClick={() => {
|
|
759
|
+
setRenamingId(doc.recordId)
|
|
760
|
+
setRenameValue(doc.data.title)
|
|
761
|
+
setActionsMenuId(null)
|
|
762
|
+
}}
|
|
763
|
+
>
|
|
764
|
+
<Pencil className="h-3.5 w-3.5" /> Rename
|
|
765
|
+
</button>
|
|
766
|
+
<button
|
|
767
|
+
type="button"
|
|
768
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-black/[0.04] dark:hover:bg-white/[0.06]"
|
|
769
|
+
onClick={() => {
|
|
770
|
+
setMoveMenuId(
|
|
771
|
+
moveMenuId === doc.recordId ? null : doc.recordId,
|
|
772
|
+
)
|
|
773
|
+
}}
|
|
774
|
+
>
|
|
775
|
+
<Folder className="h-3.5 w-3.5" /> Move to…
|
|
776
|
+
</button>
|
|
777
|
+
{moveMenuId === doc.recordId ? (
|
|
778
|
+
<div
|
|
779
|
+
className="border-t px-2 py-1"
|
|
780
|
+
style={{ borderColor: 'var(--docs-el-line)' }}
|
|
781
|
+
>
|
|
782
|
+
<button
|
|
783
|
+
type="button"
|
|
784
|
+
className="block w-full rounded px-2 py-1 text-left text-xs hover:bg-black/[0.04]"
|
|
785
|
+
disabled={fid === ''}
|
|
786
|
+
onClick={() => void handleMoveDoc(doc.recordId, '')}
|
|
787
|
+
>
|
|
788
|
+
Uncategorized
|
|
789
|
+
</button>
|
|
790
|
+
{sortedFolders.map((f) => (
|
|
791
|
+
<button
|
|
792
|
+
key={f.recordId}
|
|
793
|
+
type="button"
|
|
794
|
+
disabled={fid === f.recordId}
|
|
795
|
+
className="block w-full rounded px-2 py-1 text-left text-xs hover:bg-black/[0.04] disabled:opacity-40"
|
|
796
|
+
onClick={() =>
|
|
797
|
+
void handleMoveDoc(doc.recordId, f.recordId)
|
|
798
|
+
}
|
|
799
|
+
>
|
|
800
|
+
{f.data.name}
|
|
801
|
+
</button>
|
|
802
|
+
))}
|
|
803
|
+
</div>
|
|
804
|
+
) : null}
|
|
805
|
+
<button
|
|
806
|
+
type="button"
|
|
807
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-red-600 hover:bg-red-500/10"
|
|
808
|
+
onClick={() => {
|
|
809
|
+
setActionsMenuId(null)
|
|
810
|
+
void handleDelete(doc.recordId)
|
|
811
|
+
}}
|
|
812
|
+
>
|
|
813
|
+
<Trash2 className="h-3.5 w-3.5" /> Delete
|
|
814
|
+
</button>
|
|
815
|
+
</div>
|
|
816
|
+
</>
|
|
817
|
+
) : null}
|
|
818
|
+
</div>
|
|
819
|
+
</div>
|
|
820
|
+
) : null}
|
|
821
|
+
</div>
|
|
822
|
+
</div>
|
|
823
|
+
)
|
|
824
|
+
})}
|
|
825
|
+
</div>
|
|
826
|
+
) : (
|
|
827
|
+
<div
|
|
828
|
+
className="overflow-hidden rounded-xl border shadow-sm"
|
|
829
|
+
style={{
|
|
830
|
+
borderColor: 'var(--docs-el-line)',
|
|
831
|
+
backgroundColor: 'var(--docs-el-surface)',
|
|
832
|
+
}}
|
|
833
|
+
>
|
|
834
|
+
<div
|
|
835
|
+
className="flex items-center gap-4 border-b px-4 py-2 text-[10px] font-bold uppercase tracking-widest"
|
|
836
|
+
style={{
|
|
837
|
+
borderColor: 'var(--docs-el-line)',
|
|
838
|
+
backgroundColor: 'color-mix(in srgb, var(--docs-el-text) 3%, transparent)',
|
|
839
|
+
color: 'var(--docs-el-muted)',
|
|
840
|
+
}}
|
|
841
|
+
>
|
|
842
|
+
<div className="min-w-0 flex-1">Name</div>
|
|
843
|
+
<div className="hidden w-28 shrink-0 md:block">Folder</div>
|
|
844
|
+
<div className="hidden w-36 shrink-0 text-right md:block">Modified</div>
|
|
845
|
+
<div className="w-24 shrink-0" />
|
|
846
|
+
</div>
|
|
847
|
+
{filteredSorted.map((doc) => {
|
|
848
|
+
const isOwner = doc.data.ownerId === user?.id
|
|
849
|
+
const isFav = favorites.has(doc.recordId)
|
|
850
|
+
const dateLabel = formatDate(doc.createdAt)
|
|
851
|
+
const fid = doc.data.folderId ?? ''
|
|
852
|
+
const folderLabel = fid
|
|
853
|
+
? (sortedFolders.find((f) => f.recordId === fid)?.data.name ?? '—')
|
|
854
|
+
: '—'
|
|
855
|
+
|
|
856
|
+
return (
|
|
857
|
+
<div
|
|
858
|
+
key={doc.recordId}
|
|
859
|
+
role="button"
|
|
860
|
+
tabIndex={0}
|
|
861
|
+
className="group flex cursor-pointer items-center gap-4 border-b px-4 py-3 transition-colors last:border-b-0 hover:bg-black/[0.02] dark:hover:bg-white/[0.03]"
|
|
862
|
+
style={{ borderColor: 'var(--docs-el-line)' }}
|
|
863
|
+
onClick={() => navigate(`/docs/${doc.recordId}`)}
|
|
864
|
+
onKeyDown={(e) => {
|
|
865
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
866
|
+
e.preventDefault()
|
|
867
|
+
navigate(`/docs/${doc.recordId}`)
|
|
868
|
+
}
|
|
869
|
+
}}
|
|
870
|
+
>
|
|
871
|
+
<div className="flex min-w-0 flex-1 items-center gap-4">
|
|
872
|
+
<div
|
|
873
|
+
className="hidden h-14 w-11 shrink-0 overflow-hidden rounded-md border sm:block"
|
|
874
|
+
style={{
|
|
875
|
+
borderColor: 'var(--docs-el-line)',
|
|
876
|
+
backgroundColor: 'var(--docs-el-bg)',
|
|
877
|
+
}}
|
|
878
|
+
>
|
|
879
|
+
<div className="flex h-full w-full items-center justify-center">
|
|
880
|
+
<FileText className="h-5 w-5 opacity-25" />
|
|
881
|
+
</div>
|
|
882
|
+
</div>
|
|
883
|
+
<div className="min-w-0 flex-1">
|
|
884
|
+
<h4
|
|
885
|
+
className="line-clamp-2 text-[13px] font-semibold leading-snug transition-colors group-hover:text-[color:var(--docs-el-accent)]"
|
|
886
|
+
style={{ color: 'var(--docs-el-text)' }}
|
|
887
|
+
>
|
|
888
|
+
{doc.data.title}
|
|
889
|
+
</h4>
|
|
890
|
+
<p
|
|
891
|
+
className="mt-0.5 text-[10px] font-medium md:hidden"
|
|
892
|
+
style={{ color: 'var(--docs-el-muted)' }}
|
|
893
|
+
>
|
|
894
|
+
{dateLabel}
|
|
895
|
+
{isOwner ? ' · You' : ' · Shared with you'}
|
|
896
|
+
{folderLabel !== '—' ? ` · ${folderLabel}` : ''}
|
|
897
|
+
</p>
|
|
898
|
+
</div>
|
|
899
|
+
</div>
|
|
900
|
+
<div
|
|
901
|
+
className="hidden w-28 shrink-0 truncate text-[12px] md:block"
|
|
902
|
+
style={{ color: 'var(--docs-el-muted)' }}
|
|
903
|
+
>
|
|
904
|
+
{folderLabel}
|
|
905
|
+
</div>
|
|
906
|
+
<div
|
|
907
|
+
className="hidden w-36 shrink-0 text-right text-[12px] md:block"
|
|
908
|
+
style={{ color: 'var(--docs-el-muted)' }}
|
|
909
|
+
>
|
|
910
|
+
{dateLabel}
|
|
911
|
+
</div>
|
|
912
|
+
<div className="flex w-24 shrink-0 justify-end gap-1">
|
|
913
|
+
{isOwner ? (
|
|
914
|
+
<>
|
|
915
|
+
<button
|
|
916
|
+
type="button"
|
|
917
|
+
onClick={(e) => {
|
|
918
|
+
e.stopPropagation()
|
|
919
|
+
toggleFavorite(doc.recordId)
|
|
920
|
+
}}
|
|
921
|
+
className="rounded-lg p-1.5"
|
|
922
|
+
style={{ color: 'var(--docs-el-muted)' }}
|
|
923
|
+
>
|
|
924
|
+
<Star
|
|
925
|
+
className={`h-4 w-4 ${isFav ? 'fill-yellow-500 text-yellow-500' : ''}`}
|
|
926
|
+
/>
|
|
927
|
+
</button>
|
|
928
|
+
<button
|
|
929
|
+
type="button"
|
|
930
|
+
onClick={(e) => {
|
|
931
|
+
e.stopPropagation()
|
|
932
|
+
void handleDelete(doc.recordId)
|
|
933
|
+
}}
|
|
934
|
+
className="rounded-lg p-1.5 opacity-0 transition-opacity group-hover:opacity-100"
|
|
935
|
+
style={{ color: 'var(--docs-el-muted)' }}
|
|
936
|
+
>
|
|
937
|
+
<Trash2 className="h-4 w-4" />
|
|
938
|
+
</button>
|
|
939
|
+
</>
|
|
940
|
+
) : null}
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
)
|
|
944
|
+
})}
|
|
945
|
+
</div>
|
|
946
|
+
)}
|
|
947
|
+
</div>
|
|
948
|
+
</div>
|
|
949
|
+
</div>
|
|
950
|
+
)
|
|
951
|
+
}
|