deepspace 0.3.9 → 0.3.11
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 +96 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +160 -1
- package/dist/index.js +283 -126
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +172 -2
- package/dist/server.js +581 -1
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +252 -2
- package/dist/worker.js +588 -1
- 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,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Etheris-style library sidebar — nav + folders (docs2-aligned UI).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
type MutableRefObject,
|
|
12
|
+
type ReactNode,
|
|
13
|
+
} from 'react'
|
|
14
|
+
import type { RecordData } from 'deepspace'
|
|
15
|
+
import {
|
|
16
|
+
FileText,
|
|
17
|
+
Folder,
|
|
18
|
+
FolderPlus,
|
|
19
|
+
LayoutGrid,
|
|
20
|
+
PanelLeftClose,
|
|
21
|
+
Pencil,
|
|
22
|
+
Star,
|
|
23
|
+
Trash2,
|
|
24
|
+
Users,
|
|
25
|
+
} from 'lucide-react'
|
|
26
|
+
import type { DocFolderFields, LibraryNavSelection } from './docs-library-types'
|
|
27
|
+
|
|
28
|
+
const SIDEBAR_COLLAPSED_KEY = 'deepspace-docs-library-sidebar-collapsed'
|
|
29
|
+
|
|
30
|
+
export function readSidebarCollapsed(): boolean {
|
|
31
|
+
if (typeof window === 'undefined') return false
|
|
32
|
+
try {
|
|
33
|
+
return window.localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === '1'
|
|
34
|
+
} catch {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function writeSidebarCollapsed(collapsed: boolean): void {
|
|
40
|
+
try {
|
|
41
|
+
window.localStorage.setItem(SIDEBAR_COLLAPSED_KEY, collapsed ? '1' : '0')
|
|
42
|
+
} catch {
|
|
43
|
+
/* ignore */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function NavRow({
|
|
48
|
+
active,
|
|
49
|
+
icon: Icon,
|
|
50
|
+
label,
|
|
51
|
+
onClick,
|
|
52
|
+
testId,
|
|
53
|
+
trailing,
|
|
54
|
+
collapsed,
|
|
55
|
+
}: {
|
|
56
|
+
active: boolean
|
|
57
|
+
icon: typeof FileText
|
|
58
|
+
label: string
|
|
59
|
+
onClick: () => void
|
|
60
|
+
testId?: string
|
|
61
|
+
trailing?: ReactNode
|
|
62
|
+
collapsed?: boolean
|
|
63
|
+
}) {
|
|
64
|
+
return (
|
|
65
|
+
<div className="group flex w-full min-w-0 items-center gap-0.5">
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
data-testid={testId}
|
|
69
|
+
onClick={onClick}
|
|
70
|
+
title={collapsed ? label : undefined}
|
|
71
|
+
className={`flex h-9 min-w-0 flex-1 items-center gap-0 rounded-md pl-0.5 pr-0 text-left text-[13px] ${
|
|
72
|
+
active
|
|
73
|
+
? 'bg-[color:var(--docs-el-accent)] font-medium text-white shadow-sm'
|
|
74
|
+
: 'text-[color:var(--docs-el-text)]/80 transition-colors hover:bg-black/[0.04] hover:text-[color:var(--docs-el-text)] dark:hover:bg-white/[0.06]'
|
|
75
|
+
}`}
|
|
76
|
+
>
|
|
77
|
+
<span className="flex h-9 w-10 shrink-0 items-center justify-center" aria-hidden>
|
|
78
|
+
<Icon size={14} strokeWidth={active ? 2.5 : 1.8} className="shrink-0" />
|
|
79
|
+
</span>
|
|
80
|
+
<span className={collapsed ? 'sr-only' : 'min-w-0 flex-1 truncate pr-2'}>{label}</span>
|
|
81
|
+
</button>
|
|
82
|
+
{!collapsed && trailing}
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const FOLDER_RENAME_BLUR_DEFER_MS = 200
|
|
88
|
+
const FOLDER_RENAME_BLUR_GRACE_MS = 520
|
|
89
|
+
|
|
90
|
+
function SidebarFolderRenameField({
|
|
91
|
+
folderId,
|
|
92
|
+
renameFolderValue,
|
|
93
|
+
setRenameFolderValue,
|
|
94
|
+
onCommitFolderRenameRef,
|
|
95
|
+
onCancelRenameFolder,
|
|
96
|
+
blurTimerRef,
|
|
97
|
+
sessionStartRef,
|
|
98
|
+
testId,
|
|
99
|
+
}: {
|
|
100
|
+
folderId: string
|
|
101
|
+
renameFolderValue: string
|
|
102
|
+
setRenameFolderValue: (v: string) => void
|
|
103
|
+
onCommitFolderRenameRef: MutableRefObject<() => void | Promise<void>>
|
|
104
|
+
onCancelRenameFolder: () => void
|
|
105
|
+
blurTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>
|
|
106
|
+
sessionStartRef: MutableRefObject<number>
|
|
107
|
+
testId: string
|
|
108
|
+
}) {
|
|
109
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
110
|
+
|
|
111
|
+
useLayoutEffect(() => {
|
|
112
|
+
const t = typeof performance !== 'undefined' ? performance.now() : Date.now()
|
|
113
|
+
sessionStartRef.current = t
|
|
114
|
+
const el = inputRef.current
|
|
115
|
+
if (!el) return
|
|
116
|
+
const placeCaretAtEnd = () => {
|
|
117
|
+
const len = el.value.length
|
|
118
|
+
el.setSelectionRange(len, len)
|
|
119
|
+
}
|
|
120
|
+
el.focus({ preventScroll: true })
|
|
121
|
+
placeCaretAtEnd()
|
|
122
|
+
const raf = requestAnimationFrame(() => {
|
|
123
|
+
if (document.activeElement !== el) {
|
|
124
|
+
el.focus({ preventScroll: true })
|
|
125
|
+
placeCaretAtEnd()
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
return () => cancelAnimationFrame(raf)
|
|
129
|
+
}, [folderId])
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<input
|
|
133
|
+
ref={inputRef}
|
|
134
|
+
value={renameFolderValue}
|
|
135
|
+
onChange={(e) => setRenameFolderValue(e.target.value)}
|
|
136
|
+
onFocus={() => {
|
|
137
|
+
if (blurTimerRef.current) {
|
|
138
|
+
clearTimeout(blurTimerRef.current)
|
|
139
|
+
blurTimerRef.current = null
|
|
140
|
+
}
|
|
141
|
+
}}
|
|
142
|
+
onBlur={() => {
|
|
143
|
+
if (blurTimerRef.current) clearTimeout(blurTimerRef.current)
|
|
144
|
+
blurTimerRef.current = setTimeout(() => {
|
|
145
|
+
blurTimerRef.current = null
|
|
146
|
+
const t0 = sessionStartRef.current
|
|
147
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now()
|
|
148
|
+
if (now - t0 < FOLDER_RENAME_BLUR_GRACE_MS) return
|
|
149
|
+
void onCommitFolderRenameRef.current()
|
|
150
|
+
}, FOLDER_RENAME_BLUR_DEFER_MS)
|
|
151
|
+
}}
|
|
152
|
+
onKeyDown={(e) => {
|
|
153
|
+
if (e.key === 'Enter') {
|
|
154
|
+
if (blurTimerRef.current) {
|
|
155
|
+
clearTimeout(blurTimerRef.current)
|
|
156
|
+
blurTimerRef.current = null
|
|
157
|
+
}
|
|
158
|
+
void onCommitFolderRenameRef.current()
|
|
159
|
+
}
|
|
160
|
+
if (e.key === 'Escape') {
|
|
161
|
+
if (blurTimerRef.current) {
|
|
162
|
+
clearTimeout(blurTimerRef.current)
|
|
163
|
+
blurTimerRef.current = null
|
|
164
|
+
}
|
|
165
|
+
onCancelRenameFolder()
|
|
166
|
+
}
|
|
167
|
+
}}
|
|
168
|
+
data-testid={testId}
|
|
169
|
+
className="min-w-0 flex-1 rounded-md border px-2 py-1.5 text-[13px] outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
170
|
+
style={{
|
|
171
|
+
borderColor: 'var(--docs-el-line)',
|
|
172
|
+
backgroundColor: 'var(--docs-el-bg)',
|
|
173
|
+
color: 'var(--docs-el-text)',
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface LibrarySidebarProps {
|
|
180
|
+
selection: LibraryNavSelection
|
|
181
|
+
onSelect: (s: LibraryNavSelection) => void
|
|
182
|
+
folders: RecordData<DocFolderFields>[]
|
|
183
|
+
collapsed: boolean
|
|
184
|
+
onToggleCollapsed: () => void
|
|
185
|
+
onCreateFolder: (name: string) => void | Promise<void>
|
|
186
|
+
onDeleteFolder: (folderId: string) => void | Promise<void>
|
|
187
|
+
onStartRenameFolder: (folder: RecordData<DocFolderFields>) => void
|
|
188
|
+
renamingFolderId: string | null
|
|
189
|
+
renameFolderValue: string
|
|
190
|
+
setRenameFolderValue: (v: string) => void
|
|
191
|
+
onCommitRenameFolder: () => void | Promise<void>
|
|
192
|
+
onCancelRenameFolder: () => void
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function LibrarySidebar({
|
|
196
|
+
selection,
|
|
197
|
+
onSelect,
|
|
198
|
+
folders,
|
|
199
|
+
collapsed,
|
|
200
|
+
onToggleCollapsed,
|
|
201
|
+
onCreateFolder,
|
|
202
|
+
onDeleteFolder,
|
|
203
|
+
onStartRenameFolder,
|
|
204
|
+
renamingFolderId,
|
|
205
|
+
renameFolderValue,
|
|
206
|
+
setRenameFolderValue,
|
|
207
|
+
onCommitRenameFolder,
|
|
208
|
+
onCancelRenameFolder,
|
|
209
|
+
}: LibrarySidebarProps) {
|
|
210
|
+
const [addingFolder, setAddingFolder] = useState(false)
|
|
211
|
+
const [newFolderName, setNewFolderName] = useState('')
|
|
212
|
+
const folderRenameBlurTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
213
|
+
const folderRenameSessionStartRef = useRef(0)
|
|
214
|
+
const onCommitFolderRenameRef = useRef(onCommitRenameFolder)
|
|
215
|
+
onCommitFolderRenameRef.current = onCommitRenameFolder
|
|
216
|
+
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
return () => {
|
|
219
|
+
if (folderRenameBlurTimerRef.current) clearTimeout(folderRenameBlurTimerRef.current)
|
|
220
|
+
}
|
|
221
|
+
}, [])
|
|
222
|
+
|
|
223
|
+
const submitNewFolder = useCallback(async () => {
|
|
224
|
+
const name = newFolderName.trim()
|
|
225
|
+
if (!name) {
|
|
226
|
+
setAddingFolder(false)
|
|
227
|
+
setNewFolderName('')
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
await onCreateFolder(name)
|
|
231
|
+
setNewFolderName('')
|
|
232
|
+
setAddingFolder(false)
|
|
233
|
+
}, [newFolderName, onCreateFolder])
|
|
234
|
+
|
|
235
|
+
const sortedFolders = [...folders].sort((a, b) =>
|
|
236
|
+
(a.data.name ?? '').localeCompare(b.data.name ?? ''),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<aside
|
|
241
|
+
data-testid="library-sidebar"
|
|
242
|
+
className={`relative z-20 box-border shrink-0 overflow-x-clip border-r backdrop-blur-xl transition-[width] duration-200 ease-out will-change-[width] ${
|
|
243
|
+
collapsed ? 'w-16' : 'w-64'
|
|
244
|
+
}`}
|
|
245
|
+
style={{
|
|
246
|
+
borderColor: 'var(--docs-el-line)',
|
|
247
|
+
backgroundColor: 'color-mix(in srgb, var(--docs-el-surface) 92%, transparent)',
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
251
|
+
<div
|
|
252
|
+
className={`relative flex min-h-14 w-full min-w-0 items-center overflow-hidden border-b py-3 pl-3.5 ${
|
|
253
|
+
collapsed ? 'pr-2.5' : 'pr-12'
|
|
254
|
+
}`}
|
|
255
|
+
style={{ borderColor: 'var(--docs-el-line)' }}
|
|
256
|
+
>
|
|
257
|
+
<div
|
|
258
|
+
className={`flex min-w-0 flex-1 items-center gap-2.5 overflow-hidden ${
|
|
259
|
+
collapsed ? 'pointer-events-none select-none opacity-0' : ''
|
|
260
|
+
}`}
|
|
261
|
+
aria-hidden={collapsed}
|
|
262
|
+
>
|
|
263
|
+
<div className="flex h-8 w-9 shrink-0 items-center justify-center" aria-hidden>
|
|
264
|
+
<FileText className="text-blue-600 dark:text-blue-400" size={26} strokeWidth={2} />
|
|
265
|
+
</div>
|
|
266
|
+
<div className="min-w-0">
|
|
267
|
+
<span className="block truncate text-base font-bold leading-tight tracking-tight text-[color:var(--docs-el-text)]">
|
|
268
|
+
Documents
|
|
269
|
+
</span>
|
|
270
|
+
<span className="block truncate text-[10px] font-semibold uppercase tracking-widest text-[color:var(--docs-el-muted)]/75">
|
|
271
|
+
Workspace
|
|
272
|
+
</span>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
<button
|
|
276
|
+
type="button"
|
|
277
|
+
data-testid="library-sidebar-collapse"
|
|
278
|
+
onClick={onToggleCollapsed}
|
|
279
|
+
className="absolute right-2.5 top-1/2 z-10 flex h-8 w-9 -translate-y-1/2 items-center justify-center rounded-md transition-colors hover:bg-black/[0.04] hover:text-[color:var(--docs-el-text)] dark:hover:bg-white/[0.06]"
|
|
280
|
+
style={{ color: 'var(--docs-el-muted)' }}
|
|
281
|
+
title={collapsed ? 'Show sidebar' : 'Hide sidebar'}
|
|
282
|
+
aria-expanded={!collapsed}
|
|
283
|
+
>
|
|
284
|
+
<PanelLeftClose
|
|
285
|
+
className="h-4 w-4"
|
|
286
|
+
style={{ transform: collapsed ? 'scaleX(-1)' : undefined }}
|
|
287
|
+
/>
|
|
288
|
+
</button>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<nav className="flex-1 space-y-0.5 overflow-y-auto py-4 pl-3.5 pr-1.5">
|
|
292
|
+
<NavRow
|
|
293
|
+
active={selection.kind === 'all'}
|
|
294
|
+
icon={LayoutGrid}
|
|
295
|
+
label="Library"
|
|
296
|
+
testId="library-nav-all"
|
|
297
|
+
onClick={() => onSelect({ kind: 'all' })}
|
|
298
|
+
collapsed={collapsed}
|
|
299
|
+
/>
|
|
300
|
+
<NavRow
|
|
301
|
+
active={selection.kind === 'shared'}
|
|
302
|
+
icon={Users}
|
|
303
|
+
label="Shared with me"
|
|
304
|
+
testId="library-nav-shared"
|
|
305
|
+
onClick={() => onSelect({ kind: 'shared' })}
|
|
306
|
+
collapsed={collapsed}
|
|
307
|
+
/>
|
|
308
|
+
<NavRow
|
|
309
|
+
active={selection.kind === 'favorites'}
|
|
310
|
+
icon={Star}
|
|
311
|
+
label="Favorites"
|
|
312
|
+
testId="library-nav-favorites"
|
|
313
|
+
onClick={() => onSelect({ kind: 'favorites' })}
|
|
314
|
+
collapsed={collapsed}
|
|
315
|
+
/>
|
|
316
|
+
<NavRow
|
|
317
|
+
active={selection.kind === 'uncategorized'}
|
|
318
|
+
icon={FileText}
|
|
319
|
+
label="Uncategorized"
|
|
320
|
+
testId="library-nav-uncategorized"
|
|
321
|
+
onClick={() => onSelect({ kind: 'uncategorized' })}
|
|
322
|
+
collapsed={collapsed}
|
|
323
|
+
/>
|
|
324
|
+
|
|
325
|
+
<div className="mt-8 border-t pt-3" style={{ borderColor: 'var(--docs-el-line)' }}>
|
|
326
|
+
<span
|
|
327
|
+
className={`mb-2 block pl-2.5 pr-0 text-[10px] font-bold uppercase tracking-widest text-[color:var(--docs-el-muted)]/80 ${
|
|
328
|
+
collapsed ? 'pointer-events-none select-none opacity-0' : ''
|
|
329
|
+
}`}
|
|
330
|
+
aria-hidden={collapsed}
|
|
331
|
+
>
|
|
332
|
+
Folders
|
|
333
|
+
</span>
|
|
334
|
+
<div className="space-y-0.5">
|
|
335
|
+
{sortedFolders.map((f) =>
|
|
336
|
+
renamingFolderId === f.recordId ? (
|
|
337
|
+
<div
|
|
338
|
+
key={f.recordId}
|
|
339
|
+
className="group flex w-full min-w-0 items-center gap-0.5 rounded-md bg-[color:var(--docs-el-accent)]/10 py-0.5 pl-0.5 dark:bg-[color:var(--docs-el-accent)]/15"
|
|
340
|
+
>
|
|
341
|
+
<span
|
|
342
|
+
className="flex h-9 w-10 shrink-0 items-center justify-center text-[color:var(--docs-el-muted)]"
|
|
343
|
+
aria-hidden
|
|
344
|
+
>
|
|
345
|
+
<Folder size={14} strokeWidth={1.8} />
|
|
346
|
+
</span>
|
|
347
|
+
<SidebarFolderRenameField
|
|
348
|
+
folderId={f.recordId}
|
|
349
|
+
renameFolderValue={renameFolderValue}
|
|
350
|
+
setRenameFolderValue={setRenameFolderValue}
|
|
351
|
+
onCommitFolderRenameRef={onCommitFolderRenameRef}
|
|
352
|
+
onCancelRenameFolder={onCancelRenameFolder}
|
|
353
|
+
blurTimerRef={folderRenameBlurTimerRef}
|
|
354
|
+
sessionStartRef={folderRenameSessionStartRef}
|
|
355
|
+
testId={`rename-folder-input-${f.recordId}`}
|
|
356
|
+
/>
|
|
357
|
+
</div>
|
|
358
|
+
) : (
|
|
359
|
+
<NavRow
|
|
360
|
+
key={f.recordId}
|
|
361
|
+
active={selection.kind === 'folder' && selection.folderId === f.recordId}
|
|
362
|
+
icon={Folder}
|
|
363
|
+
label={f.data.name}
|
|
364
|
+
testId={`library-nav-folder-${f.recordId}`}
|
|
365
|
+
onClick={() => onSelect({ kind: 'folder', folderId: f.recordId })}
|
|
366
|
+
collapsed={collapsed}
|
|
367
|
+
trailing={
|
|
368
|
+
<span className="relative z-[1] flex shrink-0 items-center gap-0.5">
|
|
369
|
+
<button
|
|
370
|
+
type="button"
|
|
371
|
+
data-testid={`rename-folder-${f.recordId}`}
|
|
372
|
+
onMouseDown={(e) => {
|
|
373
|
+
e.preventDefault()
|
|
374
|
+
e.stopPropagation()
|
|
375
|
+
}}
|
|
376
|
+
onClick={(e) => {
|
|
377
|
+
e.stopPropagation()
|
|
378
|
+
onStartRenameFolder(f)
|
|
379
|
+
}}
|
|
380
|
+
className="rounded p-1 text-[color:var(--docs-el-muted)] opacity-0 transition-all hover:bg-black/[0.04] hover:text-[color:var(--docs-el-accent)] group-hover:opacity-100 dark:hover:bg-white/[0.06]"
|
|
381
|
+
title="Rename folder"
|
|
382
|
+
aria-label={`Rename folder ${f.data.name}`}
|
|
383
|
+
>
|
|
384
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
385
|
+
</button>
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
data-testid={`delete-folder-${f.recordId}`}
|
|
389
|
+
onClick={(e) => {
|
|
390
|
+
e.stopPropagation()
|
|
391
|
+
if (
|
|
392
|
+
confirm(
|
|
393
|
+
`Delete folder “${f.data.name}”? Documents inside will move to uncategorized.`,
|
|
394
|
+
)
|
|
395
|
+
) {
|
|
396
|
+
void onDeleteFolder(f.recordId)
|
|
397
|
+
}
|
|
398
|
+
}}
|
|
399
|
+
className="rounded p-1 text-[color:var(--docs-el-muted)] opacity-0 transition-all hover:bg-red-500/10 hover:text-red-600 group-hover:opacity-100"
|
|
400
|
+
title="Delete folder"
|
|
401
|
+
aria-label={`Delete folder ${f.data.name}`}
|
|
402
|
+
>
|
|
403
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
404
|
+
</button>
|
|
405
|
+
</span>
|
|
406
|
+
}
|
|
407
|
+
/>
|
|
408
|
+
),
|
|
409
|
+
)}
|
|
410
|
+
|
|
411
|
+
{addingFolder ? (
|
|
412
|
+
<div className="py-1">
|
|
413
|
+
<input
|
|
414
|
+
autoFocus
|
|
415
|
+
value={newFolderName}
|
|
416
|
+
onChange={(e) => setNewFolderName(e.target.value)}
|
|
417
|
+
onBlur={() => void submitNewFolder()}
|
|
418
|
+
onKeyDown={(e) => {
|
|
419
|
+
if (e.key === 'Enter') void submitNewFolder()
|
|
420
|
+
if (e.key === 'Escape') {
|
|
421
|
+
setAddingFolder(false)
|
|
422
|
+
setNewFolderName('')
|
|
423
|
+
}
|
|
424
|
+
}}
|
|
425
|
+
placeholder="Folder name"
|
|
426
|
+
data-testid="new-folder-input"
|
|
427
|
+
className="w-full rounded-md border px-2 py-1.5 text-[13px] outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
428
|
+
style={{
|
|
429
|
+
borderColor: 'var(--docs-el-line)',
|
|
430
|
+
backgroundColor: 'var(--docs-el-bg)',
|
|
431
|
+
color: 'var(--docs-el-text)',
|
|
432
|
+
}}
|
|
433
|
+
/>
|
|
434
|
+
</div>
|
|
435
|
+
) : (
|
|
436
|
+
<button
|
|
437
|
+
type="button"
|
|
438
|
+
data-testid="new-folder-btn"
|
|
439
|
+
onClick={() => {
|
|
440
|
+
if (!collapsed) {
|
|
441
|
+
setAddingFolder(true)
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
const name = window.prompt('Folder name')?.trim()
|
|
445
|
+
if (name) void onCreateFolder(name)
|
|
446
|
+
}}
|
|
447
|
+
title={collapsed ? 'New folder' : undefined}
|
|
448
|
+
className="flex h-9 w-full min-w-0 items-center gap-0 rounded-md pl-0.5 pr-0 text-left text-[13px] text-[color:var(--docs-el-muted)] transition-colors hover:bg-black/[0.04] hover:text-[color:var(--docs-el-accent)] dark:hover:bg-white/[0.06]"
|
|
449
|
+
>
|
|
450
|
+
<span className="flex h-9 w-10 shrink-0 items-center justify-center" aria-hidden>
|
|
451
|
+
<FolderPlus className="h-3.5 w-3.5 shrink-0" />
|
|
452
|
+
</span>
|
|
453
|
+
<span className={collapsed ? 'sr-only' : 'min-w-0 flex-1 truncate pr-2'}>
|
|
454
|
+
New folder
|
|
455
|
+
</span>
|
|
456
|
+
</button>
|
|
457
|
+
)}
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
</nav>
|
|
461
|
+
</div>
|
|
462
|
+
</aside>
|
|
463
|
+
)
|
|
464
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pin favorites — browser localStorage (same pattern as docs2).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const FAVORITES_KEY = 'deepspace-docs-favorites'
|
|
6
|
+
|
|
7
|
+
export function getFavorites(): Set<string> {
|
|
8
|
+
try {
|
|
9
|
+
const stored = localStorage.getItem(FAVORITES_KEY)
|
|
10
|
+
return stored ? new Set(JSON.parse(stored) as string[]) : new Set()
|
|
11
|
+
} catch {
|
|
12
|
+
return new Set()
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function saveFavorites(favs: Set<string>): void {
|
|
17
|
+
localStorage.setItem(FAVORITES_KEY, JSON.stringify([...favs]))
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library navigation + folder types for docs UI (aligned with docs2).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface DocFolderFields {
|
|
6
|
+
name: string
|
|
7
|
+
ownerId: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type LibraryNavSelection =
|
|
11
|
+
| { kind: 'all' }
|
|
12
|
+
| { kind: 'shared' }
|
|
13
|
+
| { kind: 'favorites' }
|
|
14
|
+
| { kind: 'uncategorized' }
|
|
15
|
+
| { kind: 'folder'; folderId: string }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docs Feature — Record schemas
|
|
3
|
+
*
|
|
4
|
+
* `documents`: collaborative docs with optional folder placement.
|
|
5
|
+
* `doc_folders`: user-owned folders for library organization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CollectionSchema } from 'deepspace/worker'
|
|
9
|
+
|
|
10
|
+
const folderPermissions = {
|
|
11
|
+
viewer: {
|
|
12
|
+
read: true,
|
|
13
|
+
create: false,
|
|
14
|
+
update: false,
|
|
15
|
+
delete: false,
|
|
16
|
+
},
|
|
17
|
+
member: {
|
|
18
|
+
read: true,
|
|
19
|
+
create: true,
|
|
20
|
+
update: true,
|
|
21
|
+
delete: 'own',
|
|
22
|
+
},
|
|
23
|
+
admin: { read: true, create: true, update: true, delete: true },
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
export const docFoldersSchema: CollectionSchema = {
|
|
27
|
+
name: 'doc_folders',
|
|
28
|
+
columns: [
|
|
29
|
+
{ name: 'name', storage: 'text', interpretation: 'plain', required: true },
|
|
30
|
+
{
|
|
31
|
+
name: 'ownerId',
|
|
32
|
+
storage: 'text',
|
|
33
|
+
interpretation: 'plain',
|
|
34
|
+
required: true,
|
|
35
|
+
userBound: true,
|
|
36
|
+
immutable: true,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
ownerField: 'ownerId',
|
|
40
|
+
permissions: folderPermissions,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const docsSchema: CollectionSchema = {
|
|
44
|
+
name: 'documents',
|
|
45
|
+
columns: [
|
|
46
|
+
{ name: 'title', storage: 'text', interpretation: 'plain', required: true },
|
|
47
|
+
{ name: 'content', storage: 'text', interpretation: 'plain' },
|
|
48
|
+
{
|
|
49
|
+
name: 'ownerId',
|
|
50
|
+
storage: 'text',
|
|
51
|
+
interpretation: 'plain',
|
|
52
|
+
required: true,
|
|
53
|
+
userBound: true,
|
|
54
|
+
immutable: true,
|
|
55
|
+
},
|
|
56
|
+
{ name: 'collaborators', storage: 'text', interpretation: 'plain' },
|
|
57
|
+
{ name: 'editors', storage: 'text', interpretation: 'plain' },
|
|
58
|
+
{ name: 'folderId', storage: 'text', interpretation: 'plain' },
|
|
59
|
+
],
|
|
60
|
+
ownerField: 'ownerId',
|
|
61
|
+
collaboratorsField: 'collaborators',
|
|
62
|
+
permissions: {
|
|
63
|
+
viewer: {
|
|
64
|
+
read: 'collaborator',
|
|
65
|
+
create: false,
|
|
66
|
+
update: false,
|
|
67
|
+
delete: false,
|
|
68
|
+
},
|
|
69
|
+
member: {
|
|
70
|
+
read: 'collaborator',
|
|
71
|
+
create: true,
|
|
72
|
+
update: 'own',
|
|
73
|
+
delete: 'own',
|
|
74
|
+
},
|
|
75
|
+
admin: { read: true, create: true, update: true, delete: true },
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Pass into `schemas` with spread: `[...docsCollections, ...]` */
|
|
80
|
+
export const docsCollections: CollectionSchema[] = [docsSchema, docFoldersSchema]
|