deepspace 0.10.1 → 0.10.3
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/CHANGELOG.md +14 -0
- package/dist/chunk-ZJ43EK3N.js +1751 -0
- package/dist/chunk-ZJ43EK3N.js.map +1 -0
- package/dist/cli.js +848 -650
- package/dist/cli.js.map +1 -1
- package/dist/documentation-client-core.d.ts +18 -0
- package/dist/documentation-client-core.js +395 -0
- package/dist/documentation-client-core.js.map +1 -0
- package/dist/documentation-default-renderer.cjs +124 -0
- package/dist/documentation-react.d.ts +2 -0
- package/dist/documentation-react.js +35 -0
- package/dist/documentation-react.js.map +1 -0
- package/dist/documentation-runtime.js +71 -0
- package/dist/documentation-server-core.d.ts +11 -0
- package/dist/documentation-server-core.js +48 -0
- package/dist/documentation-server-core.js.map +1 -0
- package/dist/documentation.d.ts +367 -0
- package/dist/documentation.js +4122 -0
- package/dist/documentation.js.map +1 -0
- package/dist/feature-installer.js +145 -0
- package/dist/feature-installer.js.map +1 -1
- package/dist/fonts/geist-mono-OFL.txt +93 -0
- package/dist/fonts/geist-mono-variable.woff2 +0 -0
- package/dist/fonts/inter-OFL.txt +92 -0
- package/dist/fonts/inter-variable.woff2 +0 -0
- package/dist/index.d.ts +177 -1
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -1
- package/dist/public-BGRX_Cjd.d.ts +234 -0
- package/dist/schema.d.ts +186 -0
- package/dist/schema.js +610 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.d.ts +83 -4
- package/dist/server.js +296 -1
- package/dist/server.js.map +1 -1
- package/dist/testing.d.ts +5 -8
- package/dist/testing.js +48 -14
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +249 -5
- package/dist/worker.js +1438 -19
- package/dist/worker.js.map +1 -1
- package/features/admin-page/src/admin-schema.ts +1 -1
- package/features/ai-chat/feature.json +2 -2
- package/features/ai-chat/src/ChatPanel.tsx +6 -11
- package/features/ai-chat/src/ai-chat-schema.ts +1 -2
- package/features/canvas/src/canvas-schema.ts +1 -1
- package/features/documentation/content/index.mdx +14 -0
- package/features/documentation/content/quickstart.mdx +12 -0
- package/features/documentation/documentation.json +18 -0
- package/features/documentation/feature.json +68 -0
- package/features/{docs → documents}/feature.json +27 -27
- package/features/{docs/src/DocsEditorPage.tsx → documents/src/DocumentsEditorPage.tsx} +74 -74
- package/features/{docs/src/DocsListPage.tsx → documents/src/DocumentsListPage.tsx} +102 -102
- package/features/{docs/src/DocsOutlinePanel.tsx → documents/src/DocumentsOutlinePanel.tsx} +9 -9
- package/features/{docs/src/DocsPresence.tsx → documents/src/DocumentsPresence.tsx} +18 -18
- package/features/{docs → documents}/src/InviteDialog.tsx +21 -21
- package/features/{docs → documents}/src/LibrarySidebar.tsx +25 -25
- package/features/documents/src/__tests__/documents-presence-access.test.ts +71 -0
- package/features/{docs/src/docs-library-types.ts → documents/src/documents-library-types.ts} +3 -3
- package/features/{docs/src/docs-schema.ts → documents/src/documents-schema.ts} +6 -6
- package/features/{docs/src/docs-ui.css → documents/src/documents-ui.css} +89 -89
- package/features/{docs → documents}/src/editor/DocEditorSurface.tsx +11 -11
- package/features/{docs/src/editor/DocsTiptapToolbar.tsx → documents/src/editor/DocumentsTiptapToolbar.tsx} +21 -21
- package/features/{docs → documents}/src/editor/useDocEditor.ts +4 -4
- package/features/{docs/src/use-docs-presence-access.ts → documents/src/use-documents-presence-access.ts} +51 -51
- package/features/file-attachments/src/attachments-schema.ts +1 -1
- package/features/items/src/items-schema.ts +1 -1
- package/features/leaderboard/src/leaderboard-schema.ts +1 -1
- package/features/messaging/src/ChatMultiPage.tsx +1 -1
- package/features/messaging/src/ChatPage.tsx +1 -1
- package/features/messaging/src/messaging-schema.ts +2 -2
- package/features/tasks/src/tasks-schema.ts +1 -1
- package/features/testing/src/test-schema.ts +2 -2
- package/package.json +26 -1
- package/features/docs/src/__tests__/docs-presence-access.test.ts +0 -71
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react'
|
|
2
2
|
|
|
3
|
-
export interface
|
|
3
|
+
export interface DocumentsPresenceParticipant {
|
|
4
4
|
clientId: number
|
|
5
5
|
userId: string
|
|
6
6
|
name: string
|
|
@@ -12,15 +12,15 @@ export interface DocsPresenceParticipant {
|
|
|
12
12
|
isSelf?: boolean
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export interface
|
|
16
|
-
participants:
|
|
15
|
+
export interface DocumentsPresenceProps {
|
|
16
|
+
participants: DocumentsPresenceParticipant[]
|
|
17
17
|
typingNames: string[]
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const MAX_VISIBLE_AVATARS = 3
|
|
21
21
|
|
|
22
22
|
/** Stable React keys (Yjs clientId is not used for PresenceRoom peers). */
|
|
23
|
-
function participantKey(p:
|
|
23
|
+
function participantKey(p: DocumentsPresenceParticipant) {
|
|
24
24
|
return `${p.userId}:${p.isSelf ? 'self' : 'peer'}`
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -37,14 +37,14 @@ function typingText(names: string[]) {
|
|
|
37
37
|
return `${names[0]}, ${names[1]}, and ${names.length - 2} more are typing...`
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function ParticipantAvatar({ participant, index }: { participant:
|
|
40
|
+
function ParticipantAvatar({ participant, index }: { participant: DocumentsPresenceParticipant; index: number }) {
|
|
41
41
|
const label = `${participant.name}${participant.isSelf ? ' (you)' : ''} - ${
|
|
42
42
|
participant.mode === 'edit' ? 'Editing' : 'Viewing'
|
|
43
43
|
}`
|
|
44
44
|
|
|
45
45
|
return (
|
|
46
46
|
<span
|
|
47
|
-
className="
|
|
47
|
+
className="documents-presence-avatar"
|
|
48
48
|
style={{ zIndex: 20 - index }}
|
|
49
49
|
title={label}
|
|
50
50
|
aria-label={label}
|
|
@@ -58,7 +58,7 @@ function ParticipantAvatar({ participant, index }: { participant: DocsPresencePa
|
|
|
58
58
|
)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export function
|
|
61
|
+
export function DocumentsPresence({ participants, typingNames }: DocumentsPresenceProps) {
|
|
62
62
|
const [open, setOpen] = useState(false)
|
|
63
63
|
const visibleParticipants = participants.slice(0, MAX_VISIBLE_AVATARS)
|
|
64
64
|
const hiddenCount = Math.max(0, participants.length - MAX_VISIBLE_AVATARS)
|
|
@@ -72,12 +72,12 @@ export function DocsPresence({ participants, typingNames }: DocsPresenceProps) {
|
|
|
72
72
|
}, [participants.length])
|
|
73
73
|
|
|
74
74
|
return (
|
|
75
|
-
<div className="relative flex shrink-0 items-center gap-2" data-testid="
|
|
75
|
+
<div className="relative flex shrink-0 items-center gap-2" data-testid="documents-presence">
|
|
76
76
|
{message ? (
|
|
77
77
|
<span
|
|
78
78
|
className="hidden max-w-[220px] truncate text-xs sm:inline"
|
|
79
|
-
style={{ color: 'var(--
|
|
80
|
-
data-testid="
|
|
79
|
+
style={{ color: 'var(--documents-el-muted)' }}
|
|
80
|
+
data-testid="documents-typing-indicator"
|
|
81
81
|
>
|
|
82
82
|
{message}
|
|
83
83
|
</span>
|
|
@@ -90,11 +90,11 @@ export function DocsPresence({ participants, typingNames }: DocsPresenceProps) {
|
|
|
90
90
|
{hiddenCount > 0 ? (
|
|
91
91
|
<button
|
|
92
92
|
type="button"
|
|
93
|
-
className="
|
|
93
|
+
className="documents-presence-more"
|
|
94
94
|
onClick={() => setOpen((value) => !value)}
|
|
95
95
|
aria-expanded={open}
|
|
96
96
|
aria-label={`View ${hiddenCount} more collaborator${hiddenCount === 1 ? '' : 's'}`}
|
|
97
|
-
data-testid="
|
|
97
|
+
data-testid="documents-presence-more"
|
|
98
98
|
>
|
|
99
99
|
+{hiddenCount}
|
|
100
100
|
</button>
|
|
@@ -105,13 +105,13 @@ export function DocsPresence({ participants, typingNames }: DocsPresenceProps) {
|
|
|
105
105
|
<div
|
|
106
106
|
className="absolute right-0 top-9 z-50 w-64 rounded-xl border p-2 shadow-xl"
|
|
107
107
|
style={{
|
|
108
|
-
borderColor: 'var(--
|
|
109
|
-
backgroundColor: 'var(--
|
|
110
|
-
color: 'var(--
|
|
108
|
+
borderColor: 'var(--documents-el-line)',
|
|
109
|
+
backgroundColor: 'var(--documents-el-surface)',
|
|
110
|
+
color: 'var(--documents-el-text)',
|
|
111
111
|
}}
|
|
112
|
-
data-testid="
|
|
112
|
+
data-testid="documents-presence-popover"
|
|
113
113
|
>
|
|
114
|
-
<p className="px-2 pb-2 text-xs font-medium" style={{ color: 'var(--
|
|
114
|
+
<p className="px-2 pb-2 text-xs font-medium" style={{ color: 'var(--documents-el-muted)' }}>
|
|
115
115
|
{heading}
|
|
116
116
|
</p>
|
|
117
117
|
<ul className="max-h-72 overflow-y-auto">
|
|
@@ -123,7 +123,7 @@ export function DocsPresence({ participants, typingNames }: DocsPresenceProps) {
|
|
|
123
123
|
{participant.name}
|
|
124
124
|
{participant.isSelf ? ' (you)' : ''}
|
|
125
125
|
</p>
|
|
126
|
-
<p className="truncate text-xs" style={{ color: 'var(--
|
|
126
|
+
<p className="truncate text-xs" style={{ color: 'var(--documents-el-muted)' }}>
|
|
127
127
|
{participant.mode === 'edit' ? 'Editing' : 'Viewing'}
|
|
128
128
|
{participant.typing ? ' - typing' : ''}
|
|
129
129
|
</p>
|
|
@@ -3,7 +3,7 @@ import type { RecordData } from 'deepspace'
|
|
|
3
3
|
import { useMutations, useQuery, useUser } from 'deepspace'
|
|
4
4
|
import { Mail, ShieldCheck, UserMinus, Users } from 'lucide-react'
|
|
5
5
|
import { Modal, useToast } from '@/components/ui'
|
|
6
|
-
import {
|
|
6
|
+
import { parseDocumentsIdList, type DocumentsDocumentFields, type InviteAclDiff } from './documents-library-types'
|
|
7
7
|
|
|
8
8
|
interface UserFields {
|
|
9
9
|
email?: string
|
|
@@ -16,7 +16,7 @@ type InviteRole = 'viewer' | 'editor'
|
|
|
16
16
|
interface InviteDialogProps {
|
|
17
17
|
open: boolean
|
|
18
18
|
onOpenChange: (open: boolean) => void
|
|
19
|
-
doc: RecordData<
|
|
19
|
+
doc: RecordData<DocumentsDocumentFields>
|
|
20
20
|
isOwner: boolean
|
|
21
21
|
/**
|
|
22
22
|
* Called after a save mutation that changed the ACL. The parent uses it to
|
|
@@ -43,17 +43,17 @@ function initialsFor(name: string): string {
|
|
|
43
43
|
export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }: InviteDialogProps) {
|
|
44
44
|
const { user } = useUser()
|
|
45
45
|
const { records: users } = useQuery<UserFields>('users')
|
|
46
|
-
const { put } = useMutations<
|
|
46
|
+
const { put } = useMutations<DocumentsDocumentFields>('documents')
|
|
47
47
|
const toast = useToast()
|
|
48
48
|
const [email, setEmail] = useState('')
|
|
49
49
|
const [role, setRole] = useState<InviteRole>('editor')
|
|
50
50
|
const [saving, setSaving] = useState(false)
|
|
51
51
|
|
|
52
52
|
const collaborators = useMemo(
|
|
53
|
-
() =>
|
|
53
|
+
() => parseDocumentsIdList(doc.data.collaborators),
|
|
54
54
|
[doc.data.collaborators],
|
|
55
55
|
)
|
|
56
|
-
const editors = useMemo(() =>
|
|
56
|
+
const editors = useMemo(() => parseDocumentsIdList(doc.data.editors), [doc.data.editors])
|
|
57
57
|
|
|
58
58
|
const ownerRecord = useMemo(
|
|
59
59
|
() => users.find((u) => u.recordId === doc.data.ownerId),
|
|
@@ -71,8 +71,8 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
71
71
|
if (!isOwner) return null
|
|
72
72
|
|
|
73
73
|
const saveAccess = async (nextCollaborators: string[], nextEditors: string[]) => {
|
|
74
|
-
const prevCollaborators =
|
|
75
|
-
const prevEditors =
|
|
74
|
+
const prevCollaborators = parseDocumentsIdList(doc.data.collaborators)
|
|
75
|
+
const prevEditors = parseDocumentsIdList(doc.data.editors)
|
|
76
76
|
const nextCollabList = uniqueIds(nextCollaborators)
|
|
77
77
|
const nextCollabSet = new Set(nextCollabList)
|
|
78
78
|
const nextEditorList = uniqueIds(nextEditors).filter((id) => nextCollabSet.has(id))
|
|
@@ -151,7 +151,7 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
151
151
|
'Owner'
|
|
152
152
|
|
|
153
153
|
return (
|
|
154
|
-
<Modal open={open} onClose={() => onOpenChange(false)} size="lg" className="
|
|
154
|
+
<Modal open={open} onClose={() => onOpenChange(false)} size="lg" className="documents-feature-scope">
|
|
155
155
|
<Modal.Header>
|
|
156
156
|
<Modal.Title>Share document</Modal.Title>
|
|
157
157
|
<Modal.Description>
|
|
@@ -160,10 +160,10 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
160
160
|
</Modal.Header>
|
|
161
161
|
|
|
162
162
|
<Modal.Body className="space-y-5">
|
|
163
|
-
<div className="rounded-xl border p-3" style={{ borderColor: 'var(--
|
|
163
|
+
<div className="rounded-xl border p-3" style={{ borderColor: 'var(--documents-el-line)' }}>
|
|
164
164
|
<label
|
|
165
165
|
className="mb-2 block text-xs font-semibold uppercase tracking-wide"
|
|
166
|
-
style={{ color: 'var(--
|
|
166
|
+
style={{ color: 'var(--documents-el-muted)' }}
|
|
167
167
|
>
|
|
168
168
|
Add people
|
|
169
169
|
</label>
|
|
@@ -171,7 +171,7 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
171
171
|
<div className="relative min-w-0 flex-1">
|
|
172
172
|
<Mail
|
|
173
173
|
className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2"
|
|
174
|
-
style={{ color: 'var(--
|
|
174
|
+
style={{ color: 'var(--documents-el-muted)' }}
|
|
175
175
|
/>
|
|
176
176
|
<input
|
|
177
177
|
value={email}
|
|
@@ -181,14 +181,14 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
181
181
|
}}
|
|
182
182
|
placeholder="person@gmail.com"
|
|
183
183
|
className="h-10 w-full rounded-lg border bg-transparent pl-9 pr-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
184
|
-
style={{ borderColor: 'var(--
|
|
184
|
+
style={{ borderColor: 'var(--documents-el-line)', color: 'var(--documents-el-text)' }}
|
|
185
185
|
/>
|
|
186
186
|
</div>
|
|
187
187
|
<select
|
|
188
188
|
value={role}
|
|
189
189
|
onChange={(e) => setRole(e.target.value as InviteRole)}
|
|
190
190
|
className="h-10 rounded-lg border bg-transparent px-3 text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
191
|
-
style={{ borderColor: 'var(--
|
|
191
|
+
style={{ borderColor: 'var(--documents-el-line)', color: 'var(--documents-el-text)' }}
|
|
192
192
|
>
|
|
193
193
|
<option value="editor">Editor</option>
|
|
194
194
|
<option value="viewer">Viewer</option>
|
|
@@ -198,7 +198,7 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
198
198
|
onClick={() => void addInvite()}
|
|
199
199
|
disabled={saving || !email.trim()}
|
|
200
200
|
className="h-10 rounded-lg px-4 text-sm font-semibold text-white transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
|
|
201
|
-
style={{ backgroundColor: 'var(--
|
|
201
|
+
style={{ backgroundColor: 'var(--documents-el-accent)' }}
|
|
202
202
|
>
|
|
203
203
|
Add
|
|
204
204
|
</button>
|
|
@@ -207,21 +207,21 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
207
207
|
|
|
208
208
|
<div>
|
|
209
209
|
<div className="mb-3 flex items-center gap-2">
|
|
210
|
-
<Users className="h-4 w-4" style={{ color: 'var(--
|
|
210
|
+
<Users className="h-4 w-4" style={{ color: 'var(--documents-el-muted)' }} />
|
|
211
211
|
<h3 className="text-sm font-semibold">People with access</h3>
|
|
212
212
|
</div>
|
|
213
213
|
|
|
214
214
|
<div className="space-y-2">
|
|
215
215
|
<div
|
|
216
216
|
className="flex items-center gap-3 rounded-xl border p-3"
|
|
217
|
-
style={{ borderColor: 'var(--
|
|
217
|
+
style={{ borderColor: 'var(--documents-el-line)' }}
|
|
218
218
|
>
|
|
219
219
|
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-black/5 text-xs font-bold dark:bg-white/10">
|
|
220
220
|
{initialsFor(ownerName)}
|
|
221
221
|
</div>
|
|
222
222
|
<div className="min-w-0 flex-1">
|
|
223
223
|
<div className="truncate text-sm font-medium">{ownerName}</div>
|
|
224
|
-
<div className="truncate text-xs" style={{ color: 'var(--
|
|
224
|
+
<div className="truncate text-xs" style={{ color: 'var(--documents-el-muted)' }}>
|
|
225
225
|
{ownerRecord?.data.email ?? user?.email ?? 'Owner'}
|
|
226
226
|
</div>
|
|
227
227
|
</div>
|
|
@@ -238,7 +238,7 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
238
238
|
<div
|
|
239
239
|
key={u.recordId}
|
|
240
240
|
className="flex items-center gap-3 rounded-xl border p-3"
|
|
241
|
-
style={{ borderColor: 'var(--
|
|
241
|
+
style={{ borderColor: 'var(--documents-el-line)' }}
|
|
242
242
|
>
|
|
243
243
|
{u.data.imageUrl ? (
|
|
244
244
|
<img
|
|
@@ -253,7 +253,7 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
253
253
|
)}
|
|
254
254
|
<div className="min-w-0 flex-1">
|
|
255
255
|
<div className="truncate text-sm font-medium">{name}</div>
|
|
256
|
-
<div className="truncate text-xs" style={{ color: 'var(--
|
|
256
|
+
<div className="truncate text-xs" style={{ color: 'var(--documents-el-muted)' }}>
|
|
257
257
|
{u.data.email ?? 'No email'}
|
|
258
258
|
</div>
|
|
259
259
|
</div>
|
|
@@ -264,7 +264,7 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
264
264
|
}
|
|
265
265
|
disabled={saving}
|
|
266
266
|
className="h-8 rounded-lg border bg-transparent px-2 text-xs font-medium outline-none"
|
|
267
|
-
style={{ borderColor: 'var(--
|
|
267
|
+
style={{ borderColor: 'var(--documents-el-line)', color: 'var(--documents-el-text)' }}
|
|
268
268
|
>
|
|
269
269
|
<option value="editor">Editor</option>
|
|
270
270
|
<option value="viewer">Viewer</option>
|
|
@@ -285,7 +285,7 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
285
285
|
{collaboratorRecords.length === 0 ? (
|
|
286
286
|
<p
|
|
287
287
|
className="rounded-xl border p-3 text-sm"
|
|
288
|
-
style={{ color: 'var(--
|
|
288
|
+
style={{ color: 'var(--documents-el-muted)' }}
|
|
289
289
|
>
|
|
290
290
|
Only the owner can access this document.
|
|
291
291
|
</p>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Etheris-style library sidebar — nav + folders (
|
|
2
|
+
* Etheris-style library sidebar — nav + folders (documents2-aligned UI).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
@@ -23,9 +23,9 @@ import {
|
|
|
23
23
|
Trash2,
|
|
24
24
|
Users,
|
|
25
25
|
} from 'lucide-react'
|
|
26
|
-
import type { DocFolderFields, LibraryNavSelection } from './
|
|
26
|
+
import type { DocFolderFields, LibraryNavSelection } from './documents-library-types'
|
|
27
27
|
|
|
28
|
-
const SIDEBAR_COLLAPSED_KEY = 'deepspace-
|
|
28
|
+
const SIDEBAR_COLLAPSED_KEY = 'deepspace-documents-library-sidebar-collapsed'
|
|
29
29
|
|
|
30
30
|
export function readSidebarCollapsed(): boolean {
|
|
31
31
|
if (typeof window === 'undefined') return false
|
|
@@ -70,8 +70,8 @@ function NavRow({
|
|
|
70
70
|
title={collapsed ? label : undefined}
|
|
71
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
72
|
active
|
|
73
|
-
? 'bg-[color:var(--
|
|
74
|
-
: 'text-[color:var(--
|
|
73
|
+
? 'bg-[color:var(--documents-el-accent)] font-medium text-white shadow-sm'
|
|
74
|
+
: 'text-[color:var(--documents-el-text)]/80 transition-colors hover:bg-black/[0.04] hover:text-[color:var(--documents-el-text)] dark:hover:bg-white/[0.06]'
|
|
75
75
|
}`}
|
|
76
76
|
>
|
|
77
77
|
<span className="flex h-9 w-10 shrink-0 items-center justify-center" aria-hidden>
|
|
@@ -170,9 +170,9 @@ function SidebarFolderRenameField({
|
|
|
170
170
|
data-testid={testId}
|
|
171
171
|
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"
|
|
172
172
|
style={{
|
|
173
|
-
borderColor: 'var(--
|
|
174
|
-
backgroundColor: 'var(--
|
|
175
|
-
color: 'var(--
|
|
173
|
+
borderColor: 'var(--documents-el-line)',
|
|
174
|
+
backgroundColor: 'var(--documents-el-bg)',
|
|
175
|
+
color: 'var(--documents-el-text)',
|
|
176
176
|
}}
|
|
177
177
|
/>
|
|
178
178
|
)
|
|
@@ -249,8 +249,8 @@ export function LibrarySidebar({
|
|
|
249
249
|
collapsed ? 'w-16' : 'w-64'
|
|
250
250
|
}`}
|
|
251
251
|
style={{
|
|
252
|
-
borderColor: 'var(--
|
|
253
|
-
backgroundColor: 'color-mix(in srgb, var(--
|
|
252
|
+
borderColor: 'var(--documents-el-line)',
|
|
253
|
+
backgroundColor: 'color-mix(in srgb, var(--documents-el-surface) 92%, transparent)',
|
|
254
254
|
}}
|
|
255
255
|
>
|
|
256
256
|
<div className="flex h-full min-h-0 flex-col">
|
|
@@ -258,7 +258,7 @@ export function LibrarySidebar({
|
|
|
258
258
|
className={`relative flex min-h-14 w-full min-w-0 items-center overflow-hidden border-b py-3 pl-3.5 ${
|
|
259
259
|
collapsed ? 'pr-2.5' : 'pr-12'
|
|
260
260
|
}`}
|
|
261
|
-
style={{ borderColor: 'var(--
|
|
261
|
+
style={{ borderColor: 'var(--documents-el-line)' }}
|
|
262
262
|
>
|
|
263
263
|
<div
|
|
264
264
|
className={`flex min-w-0 flex-1 items-center gap-2.5 overflow-hidden ${
|
|
@@ -270,10 +270,10 @@ export function LibrarySidebar({
|
|
|
270
270
|
<FileText className="text-blue-600 dark:text-blue-400" size={26} strokeWidth={2} />
|
|
271
271
|
</div>
|
|
272
272
|
<div className="min-w-0">
|
|
273
|
-
<span className="block truncate text-base font-bold leading-tight tracking-tight text-[color:var(--
|
|
273
|
+
<span className="block truncate text-base font-bold leading-tight tracking-tight text-[color:var(--documents-el-text)]">
|
|
274
274
|
Documents
|
|
275
275
|
</span>
|
|
276
|
-
<span className="block truncate text-[10px] font-semibold uppercase tracking-widest text-[color:var(--
|
|
276
|
+
<span className="block truncate text-[10px] font-semibold uppercase tracking-widest text-[color:var(--documents-el-muted)]/75">
|
|
277
277
|
Workspace
|
|
278
278
|
</span>
|
|
279
279
|
</div>
|
|
@@ -282,8 +282,8 @@ export function LibrarySidebar({
|
|
|
282
282
|
type="button"
|
|
283
283
|
data-testid="library-sidebar-collapse"
|
|
284
284
|
onClick={onToggleCollapsed}
|
|
285
|
-
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(--
|
|
286
|
-
style={{ color: 'var(--
|
|
285
|
+
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(--documents-el-text)] dark:hover:bg-white/[0.06]"
|
|
286
|
+
style={{ color: 'var(--documents-el-muted)' }}
|
|
287
287
|
title={collapsed ? 'Show sidebar' : 'Hide sidebar'}
|
|
288
288
|
aria-expanded={!collapsed}
|
|
289
289
|
>
|
|
@@ -328,9 +328,9 @@ export function LibrarySidebar({
|
|
|
328
328
|
collapsed={collapsed}
|
|
329
329
|
/>
|
|
330
330
|
|
|
331
|
-
<div className="mt-8 border-t pt-3" style={{ borderColor: 'var(--
|
|
331
|
+
<div className="mt-8 border-t pt-3" style={{ borderColor: 'var(--documents-el-line)' }}>
|
|
332
332
|
<span
|
|
333
|
-
className={`mb-2 block pl-2.5 pr-0 text-[10px] font-bold uppercase tracking-widest text-[color:var(--
|
|
333
|
+
className={`mb-2 block pl-2.5 pr-0 text-[10px] font-bold uppercase tracking-widest text-[color:var(--documents-el-muted)]/80 ${
|
|
334
334
|
collapsed ? 'pointer-events-none select-none opacity-0' : ''
|
|
335
335
|
}`}
|
|
336
336
|
aria-hidden={collapsed}
|
|
@@ -342,10 +342,10 @@ export function LibrarySidebar({
|
|
|
342
342
|
renamingFolderId === f.recordId ? (
|
|
343
343
|
<div
|
|
344
344
|
key={f.recordId}
|
|
345
|
-
className="group flex w-full min-w-0 items-center gap-0.5 rounded-md bg-[color:var(--
|
|
345
|
+
className="group flex w-full min-w-0 items-center gap-0.5 rounded-md bg-[color:var(--documents-el-accent)]/10 py-0.5 pl-0.5 dark:bg-[color:var(--documents-el-accent)]/15"
|
|
346
346
|
>
|
|
347
347
|
<span
|
|
348
|
-
className="flex h-9 w-10 shrink-0 items-center justify-center text-[color:var(--
|
|
348
|
+
className="flex h-9 w-10 shrink-0 items-center justify-center text-[color:var(--documents-el-muted)]"
|
|
349
349
|
aria-hidden
|
|
350
350
|
>
|
|
351
351
|
<Folder size={14} strokeWidth={1.8} />
|
|
@@ -383,7 +383,7 @@ export function LibrarySidebar({
|
|
|
383
383
|
e.stopPropagation()
|
|
384
384
|
onStartRenameFolder(f)
|
|
385
385
|
}}
|
|
386
|
-
className="rounded p-1 text-[color:var(--
|
|
386
|
+
className="rounded p-1 text-[color:var(--documents-el-muted)] opacity-0 transition-all hover:bg-black/[0.04] hover:text-[color:var(--documents-el-accent)] group-hover:opacity-100 dark:hover:bg-white/[0.06]"
|
|
387
387
|
title="Rename folder"
|
|
388
388
|
aria-label={`Rename folder ${f.data.name}`}
|
|
389
389
|
>
|
|
@@ -402,7 +402,7 @@ export function LibrarySidebar({
|
|
|
402
402
|
void onDeleteFolder(f.recordId)
|
|
403
403
|
}
|
|
404
404
|
}}
|
|
405
|
-
className="rounded p-1 text-[color:var(--
|
|
405
|
+
className="rounded p-1 text-[color:var(--documents-el-muted)] opacity-0 transition-all hover:bg-red-500/10 hover:text-red-600 group-hover:opacity-100"
|
|
406
406
|
title="Delete folder"
|
|
407
407
|
aria-label={`Delete folder ${f.data.name}`}
|
|
408
408
|
>
|
|
@@ -432,9 +432,9 @@ export function LibrarySidebar({
|
|
|
432
432
|
data-testid="new-folder-input"
|
|
433
433
|
className="w-full rounded-md border px-2 py-1.5 text-[13px] outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
434
434
|
style={{
|
|
435
|
-
borderColor: 'var(--
|
|
436
|
-
backgroundColor: 'var(--
|
|
437
|
-
color: 'var(--
|
|
435
|
+
borderColor: 'var(--documents-el-line)',
|
|
436
|
+
backgroundColor: 'var(--documents-el-bg)',
|
|
437
|
+
color: 'var(--documents-el-text)',
|
|
438
438
|
}}
|
|
439
439
|
/>
|
|
440
440
|
</div>
|
|
@@ -451,7 +451,7 @@ export function LibrarySidebar({
|
|
|
451
451
|
if (name) void onCreateFolder(name)
|
|
452
452
|
}}
|
|
453
453
|
title={collapsed ? 'New folder' : undefined}
|
|
454
|
-
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(--
|
|
454
|
+
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(--documents-el-muted)] transition-colors hover:bg-black/[0.04] hover:text-[color:var(--documents-el-accent)] dark:hover:bg-white/[0.06]"
|
|
455
455
|
>
|
|
456
456
|
<span className="flex h-9 w-10 shrink-0 items-center justify-center" aria-hidden>
|
|
457
457
|
<FolderPlus className="h-3.5 w-3.5 shrink-0" />
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
deriveDocumentsAccessChange,
|
|
4
|
+
detectDocumentsAclEvent,
|
|
5
|
+
isDocumentsOwner,
|
|
6
|
+
resolveDocumentsAccessRole,
|
|
7
|
+
} from '../use-documents-presence-access'
|
|
8
|
+
|
|
9
|
+
describe('document access policy', () => {
|
|
10
|
+
const document = {
|
|
11
|
+
data: {
|
|
12
|
+
ownerId: 'owner',
|
|
13
|
+
collaborators: JSON.stringify(['editor', 'viewer']),
|
|
14
|
+
editors: JSON.stringify(['editor']),
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
it('resolves owner, editor, viewer, and no-access roles from the document ACL', () => {
|
|
19
|
+
expect(resolveDocumentsAccessRole(document, 'owner')).toBe('owner')
|
|
20
|
+
expect(resolveDocumentsAccessRole(document, 'editor')).toBe('editor')
|
|
21
|
+
expect(resolveDocumentsAccessRole(document, 'viewer')).toBe('viewer')
|
|
22
|
+
expect(resolveDocumentsAccessRole(document, 'outsider')).toBe('none')
|
|
23
|
+
expect(resolveDocumentsAccessRole(document, null)).toBe('none')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('does not expose owner controls while both the document and user are loading', () => {
|
|
27
|
+
expect(isDocumentsOwner(undefined, undefined)).toBe(false)
|
|
28
|
+
expect(isDocumentsOwner(document, undefined)).toBe(false)
|
|
29
|
+
expect(isDocumentsOwner(undefined, 'owner')).toBe(false)
|
|
30
|
+
expect(isDocumentsOwner(document, 'owner')).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('treats malformed ACL fields as empty lists', () => {
|
|
34
|
+
const malformed = {
|
|
35
|
+
data: { ownerId: 'owner', collaborators: '{broken', editors: JSON.stringify('editor') },
|
|
36
|
+
}
|
|
37
|
+
expect(resolveDocumentsAccessRole(malformed, 'editor')).toBe('none')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('derives only actionable transitions from the session baseline', () => {
|
|
41
|
+
expect(deriveDocumentsAccessChange('editor', 'viewer', null)).toBe('downgrade')
|
|
42
|
+
expect(deriveDocumentsAccessChange('viewer', 'editor', null)).toBe('upgrade')
|
|
43
|
+
expect(deriveDocumentsAccessChange('viewer', 'none', null)).toBe('revoked')
|
|
44
|
+
expect(deriveDocumentsAccessChange('owner', 'owner', null)).toBeNull()
|
|
45
|
+
expect(deriveDocumentsAccessChange(null, 'viewer', { kind: 'downgrade', at: 20 })).toBe('downgrade')
|
|
46
|
+
expect(deriveDocumentsAccessChange('editor', 'viewer', { kind: 'revoked', at: 20 })).toBe('revoked')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('accepts only a fresh ACL signal addressed to the current user', () => {
|
|
50
|
+
const state = {
|
|
51
|
+
aclSignal: {
|
|
52
|
+
at: 20,
|
|
53
|
+
removed: ['removed-user'],
|
|
54
|
+
demoted: ['demoted-user'],
|
|
55
|
+
promoted: ['promoted-user'],
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
expect(detectDocumentsAclEvent(state, 'removed-user', 10)).toEqual({ kind: 'revoked', at: 20 })
|
|
60
|
+
expect(detectDocumentsAclEvent(state, 'demoted-user', 10)).toEqual({
|
|
61
|
+
kind: 'downgrade',
|
|
62
|
+
at: 20,
|
|
63
|
+
})
|
|
64
|
+
expect(detectDocumentsAclEvent(state, 'promoted-user', 10)).toEqual({
|
|
65
|
+
kind: 'upgrade',
|
|
66
|
+
at: 20,
|
|
67
|
+
})
|
|
68
|
+
expect(detectDocumentsAclEvent(state, 'other-user', 10)).toBeNull()
|
|
69
|
+
expect(detectDocumentsAclEvent(state, 'removed-user', 20)).toBeNull()
|
|
70
|
+
})
|
|
71
|
+
})
|
package/features/{docs/src/docs-library-types.ts → documents/src/documents-library-types.ts}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/** Shared document, ACL, folder, and navigation model for the
|
|
1
|
+
/** Shared document, ACL, folder, and navigation model for the Documents feature. */
|
|
2
2
|
|
|
3
|
-
export interface
|
|
3
|
+
export interface DocumentsDocumentFields {
|
|
4
4
|
title: string
|
|
5
5
|
ownerId: string
|
|
6
6
|
collaborators?: string
|
|
@@ -29,7 +29,7 @@ export type LibraryNavSelection =
|
|
|
29
29
|
| { kind: 'uncategorized' }
|
|
30
30
|
| { kind: 'folder'; folderId: string }
|
|
31
31
|
|
|
32
|
-
export function
|
|
32
|
+
export function parseDocumentsIdList(raw: string | undefined): string[] {
|
|
33
33
|
if (!raw) return []
|
|
34
34
|
try {
|
|
35
35
|
const parsed: unknown = JSON.parse(raw)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Documents feature — record schemas
|
|
3
3
|
*
|
|
4
|
-
* `documents`: collaborative
|
|
4
|
+
* `documents`: collaborative documents with optional folder placement.
|
|
5
5
|
* `doc_folders`: user-owned folders for library organization.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { CollectionSchema } from 'deepspace/
|
|
8
|
+
import type { CollectionSchema } from 'deepspace/schema'
|
|
9
9
|
|
|
10
10
|
const folderPermissions = {
|
|
11
11
|
viewer: {
|
|
@@ -40,7 +40,7 @@ export const docFoldersSchema: CollectionSchema = {
|
|
|
40
40
|
permissions: folderPermissions,
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export const
|
|
43
|
+
export const documentsSchema: CollectionSchema = {
|
|
44
44
|
name: 'documents',
|
|
45
45
|
columns: [
|
|
46
46
|
{ name: 'title', storage: 'text', interpretation: 'plain', required: true },
|
|
@@ -76,5 +76,5 @@ export const docsSchema: CollectionSchema = {
|
|
|
76
76
|
},
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** Pass into `schemas` with spread: `[...
|
|
80
|
-
export const
|
|
79
|
+
/** Pass into `schemas` with spread: `[...documentsCollections, ...]` */
|
|
80
|
+
export const documentsCollections: CollectionSchema[] = [documentsSchema, docFoldersSchema]
|