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.
Files changed (120) hide show
  1. package/dist/cli.js +96 -18
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.d.ts +160 -1
  4. package/dist/index.js +283 -126
  5. package/dist/index.js.map +1 -1
  6. package/dist/server.d.ts +172 -2
  7. package/dist/server.js +581 -1
  8. package/dist/server.js.map +1 -1
  9. package/dist/worker.d.ts +252 -2
  10. package/dist/worker.js +588 -1
  11. package/dist/worker.js.map +1 -1
  12. package/features/admin-page/feature.json +29 -0
  13. package/features/admin-page/src/AdminPage.tsx +292 -0
  14. package/features/admin-page/src/admin-schema.ts +20 -0
  15. package/features/ai-chat/feature.json +41 -0
  16. package/features/ai-chat/src/AiChatPage.tsx +692 -0
  17. package/features/ai-chat/src/ChatPanel.tsx +1098 -0
  18. package/features/ai-chat/src/ai-chat-schema.ts +11 -0
  19. package/features/canvas/feature.json +45 -0
  20. package/features/canvas/src/CanvasEditorPage.tsx +87 -0
  21. package/features/canvas/src/CanvasListPage.tsx +202 -0
  22. package/features/canvas/src/canvas-schema.ts +33 -0
  23. package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
  24. package/features/canvas/src/components/CanvasView.tsx +369 -0
  25. package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
  26. package/features/cron/feature.json +33 -0
  27. package/features/cron/src/CronLogPage.tsx +100 -0
  28. package/features/cron/src/cron.ts +25 -0
  29. package/features/docs/feature.json +96 -0
  30. package/features/docs/src/DocsEditorPage.tsx +1012 -0
  31. package/features/docs/src/DocsListPage.tsx +951 -0
  32. package/features/docs/src/DocsOutlinePanel.tsx +68 -0
  33. package/features/docs/src/DocsPresence.tsx +138 -0
  34. package/features/docs/src/InviteDialog.tsx +336 -0
  35. package/features/docs/src/LibrarySidebar.tsx +464 -0
  36. package/features/docs/src/docs-favorites.ts +18 -0
  37. package/features/docs/src/docs-library-types.ts +15 -0
  38. package/features/docs/src/docs-schema.ts +80 -0
  39. package/features/docs/src/docs-ui.css +309 -0
  40. package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
  41. package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
  42. package/features/docs/src/editor/useDocEditor.ts +206 -0
  43. package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
  44. package/features/file-attachments/feature.json +42 -0
  45. package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
  46. package/features/file-attachments/src/attachments-constants.ts +9 -0
  47. package/features/file-attachments/src/attachments-schema.ts +45 -0
  48. package/features/file-attachments/src/useFilePreview.ts +471 -0
  49. package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
  50. package/features/file-manager/feature.json +24 -0
  51. package/features/file-manager/src/FileManagerPage.tsx +330 -0
  52. package/features/integration-test/feature.json +18 -0
  53. package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
  54. package/features/integration-test/src/integration.spec.ts +42 -0
  55. package/features/items/feature.json +33 -0
  56. package/features/items/src/ItemsPage.tsx +325 -0
  57. package/features/items/src/items-constants.ts +10 -0
  58. package/features/items/src/items-schema.ts +27 -0
  59. package/features/kanban/feature.json +18 -0
  60. package/features/kanban/src/Kanban.tsx +283 -0
  61. package/features/landing/feature.json +62 -0
  62. package/features/landing/src/LandingPage.tsx +627 -0
  63. package/features/landing/src/primitives.tsx +389 -0
  64. package/features/landing/src/sections/FAQSection.tsx +110 -0
  65. package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
  66. package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
  67. package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
  68. package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
  69. package/features/landing/src/sections/StatsSection.tsx +31 -0
  70. package/features/landing/src/sections/TeamSection.tsx +86 -0
  71. package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
  72. package/features/landing/src/sections/VideoSection.tsx +71 -0
  73. package/features/leaderboard/feature.json +33 -0
  74. package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
  75. package/features/leaderboard/src/leaderboard-constants.ts +21 -0
  76. package/features/leaderboard/src/leaderboard-schema.ts +36 -0
  77. package/features/messaging/feature.json +111 -0
  78. package/features/messaging/src/ChatMultiPage.tsx +300 -0
  79. package/features/messaging/src/ChatPage.tsx +75 -0
  80. package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
  81. package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
  82. package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
  83. package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
  84. package/features/messaging/src/components/chat/MessageList.tsx +248 -0
  85. package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
  86. package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
  87. package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
  88. package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
  89. package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
  90. package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
  91. package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
  92. package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
  93. package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
  94. package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
  95. package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
  96. package/features/messaging/src/hooks/useChatChannel.ts +64 -0
  97. package/features/messaging/src/hooks/useLongPress.ts +74 -0
  98. package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
  99. package/features/messaging/src/messaging-schema.ts +25 -0
  100. package/features/presence-test/feature.json +21 -0
  101. package/features/presence-test/src/PresenceTestPage.tsx +120 -0
  102. package/features/sidebar/feature.json +19 -0
  103. package/features/sidebar/src/AppSidebar.tsx +558 -0
  104. package/features/tasks/feature.json +33 -0
  105. package/features/tasks/src/TasksPage.tsx +598 -0
  106. package/features/tasks/src/tasks-constants.ts +43 -0
  107. package/features/tasks/src/tasks-schema.ts +56 -0
  108. package/features/testing/feature.json +49 -0
  109. package/features/testing/src/TestPage.tsx +145 -0
  110. package/features/testing/src/cpu-burn-action.ts +51 -0
  111. package/features/testing/src/subrequest-burn-action.ts +53 -0
  112. package/features/testing/src/test-schema.ts +23 -0
  113. package/features/topbar/feature.json +18 -0
  114. package/features/topbar/src/Topbar.tsx +353 -0
  115. package/features/tree/feature.json +26 -0
  116. package/features/tree/src/TreeSidebar.tsx +607 -0
  117. package/features/tree/src/tree-constants.ts +68 -0
  118. package/features/tree/src/tree-hooks.ts +258 -0
  119. package/package.json +4 -2
  120. 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]