deepspace 0.10.1 → 0.10.2

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 (71) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/chunk-T7V4GUIN.js +1638 -0
  3. package/dist/chunk-T7V4GUIN.js.map +1 -0
  4. package/dist/cli.js +731 -324
  5. package/dist/cli.js.map +1 -1
  6. package/dist/documentation-client-core.d.ts +18 -0
  7. package/dist/documentation-client-core.js +395 -0
  8. package/dist/documentation-client-core.js.map +1 -0
  9. package/dist/documentation-default-renderer.cjs +124 -0
  10. package/dist/documentation-react.d.ts +2 -0
  11. package/dist/documentation-react.js +35 -0
  12. package/dist/documentation-react.js.map +1 -0
  13. package/dist/documentation-runtime.js +71 -0
  14. package/dist/documentation-server-core.d.ts +11 -0
  15. package/dist/documentation-server-core.js +48 -0
  16. package/dist/documentation-server-core.js.map +1 -0
  17. package/dist/documentation.d.ts +366 -0
  18. package/dist/documentation.js +3953 -0
  19. package/dist/documentation.js.map +1 -0
  20. package/dist/feature-installer.js +145 -0
  21. package/dist/feature-installer.js.map +1 -1
  22. package/dist/index.d.ts +177 -1
  23. package/dist/index.js +133 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/public-BGRX_Cjd.d.ts +234 -0
  26. package/dist/schema.d.ts +186 -0
  27. package/dist/schema.js +610 -0
  28. package/dist/schema.js.map +1 -0
  29. package/dist/server.d.ts +83 -4
  30. package/dist/server.js +296 -1
  31. package/dist/server.js.map +1 -1
  32. package/dist/testing.d.ts +5 -8
  33. package/dist/testing.js +48 -14
  34. package/dist/testing.js.map +1 -1
  35. package/dist/worker.d.ts +249 -5
  36. package/dist/worker.js +1438 -19
  37. package/dist/worker.js.map +1 -1
  38. package/features/admin-page/src/admin-schema.ts +1 -1
  39. package/features/ai-chat/feature.json +2 -2
  40. package/features/ai-chat/src/ChatPanel.tsx +6 -11
  41. package/features/ai-chat/src/ai-chat-schema.ts +1 -2
  42. package/features/canvas/src/canvas-schema.ts +1 -1
  43. package/features/documentation/content/index.mdx +14 -0
  44. package/features/documentation/content/quickstart.mdx +12 -0
  45. package/features/documentation/documentation.json +18 -0
  46. package/features/documentation/feature.json +68 -0
  47. package/features/{docs → documents}/feature.json +27 -27
  48. package/features/{docs/src/DocsEditorPage.tsx → documents/src/DocumentsEditorPage.tsx} +74 -74
  49. package/features/{docs/src/DocsListPage.tsx → documents/src/DocumentsListPage.tsx} +102 -102
  50. package/features/{docs/src/DocsOutlinePanel.tsx → documents/src/DocumentsOutlinePanel.tsx} +9 -9
  51. package/features/{docs/src/DocsPresence.tsx → documents/src/DocumentsPresence.tsx} +18 -18
  52. package/features/{docs → documents}/src/InviteDialog.tsx +21 -21
  53. package/features/{docs → documents}/src/LibrarySidebar.tsx +25 -25
  54. package/features/documents/src/__tests__/documents-presence-access.test.ts +71 -0
  55. package/features/{docs/src/docs-library-types.ts → documents/src/documents-library-types.ts} +3 -3
  56. package/features/{docs/src/docs-schema.ts → documents/src/documents-schema.ts} +6 -6
  57. package/features/{docs/src/docs-ui.css → documents/src/documents-ui.css} +89 -89
  58. package/features/{docs → documents}/src/editor/DocEditorSurface.tsx +11 -11
  59. package/features/{docs/src/editor/DocsTiptapToolbar.tsx → documents/src/editor/DocumentsTiptapToolbar.tsx} +21 -21
  60. package/features/{docs → documents}/src/editor/useDocEditor.ts +4 -4
  61. package/features/{docs/src/use-docs-presence-access.ts → documents/src/use-documents-presence-access.ts} +51 -51
  62. package/features/file-attachments/src/attachments-schema.ts +1 -1
  63. package/features/items/src/items-schema.ts +1 -1
  64. package/features/leaderboard/src/leaderboard-schema.ts +1 -1
  65. package/features/messaging/src/ChatMultiPage.tsx +1 -1
  66. package/features/messaging/src/ChatPage.tsx +1 -1
  67. package/features/messaging/src/messaging-schema.ts +2 -2
  68. package/features/tasks/src/tasks-schema.ts +1 -1
  69. package/features/testing/src/test-schema.ts +2 -2
  70. package/package.json +26 -1
  71. 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 DocsPresenceParticipant {
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 DocsPresenceProps {
16
- participants: DocsPresenceParticipant[]
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: DocsPresenceParticipant) {
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: DocsPresenceParticipant; index: number }) {
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="docs-presence-avatar"
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 DocsPresence({ participants, typingNames }: DocsPresenceProps) {
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="docs-presence">
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(--docs-el-muted)' }}
80
- data-testid="docs-typing-indicator"
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="docs-presence-more"
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="docs-presence-more"
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(--docs-el-line)',
109
- backgroundColor: 'var(--docs-el-surface)',
110
- color: 'var(--docs-el-text)',
108
+ borderColor: 'var(--documents-el-line)',
109
+ backgroundColor: 'var(--documents-el-surface)',
110
+ color: 'var(--documents-el-text)',
111
111
  }}
112
- data-testid="docs-presence-popover"
112
+ data-testid="documents-presence-popover"
113
113
  >
114
- <p className="px-2 pb-2 text-xs font-medium" style={{ color: 'var(--docs-el-muted)' }}>
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(--docs-el-muted)' }}>
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 { parseDocsIdList, type DocsDocumentFields, type InviteAclDiff } from './docs-library-types'
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<DocsDocumentFields>
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<DocsDocumentFields>('documents')
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
- () => parseDocsIdList(doc.data.collaborators),
53
+ () => parseDocumentsIdList(doc.data.collaborators),
54
54
  [doc.data.collaborators],
55
55
  )
56
- const editors = useMemo(() => parseDocsIdList(doc.data.editors), [doc.data.editors])
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 = parseDocsIdList(doc.data.collaborators)
75
- const prevEditors = parseDocsIdList(doc.data.editors)
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="docs-feature-scope">
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(--docs-el-line)' }}>
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(--docs-el-muted)' }}
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(--docs-el-muted)' }}
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(--docs-el-line)', color: 'var(--docs-el-text)' }}
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(--docs-el-line)', color: 'var(--docs-el-text)' }}
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(--docs-el-accent)' }}
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(--docs-el-muted)' }} />
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(--docs-el-line)' }}
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(--docs-el-muted)' }}>
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(--docs-el-line)' }}
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(--docs-el-muted)' }}>
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(--docs-el-line)', color: 'var(--docs-el-text)' }}
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(--docs-el-muted)' }}
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 (docs2-aligned UI).
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 './docs-library-types'
26
+ import type { DocFolderFields, LibraryNavSelection } from './documents-library-types'
27
27
 
28
- const SIDEBAR_COLLAPSED_KEY = 'deepspace-docs-library-sidebar-collapsed'
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(--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]'
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(--docs-el-line)',
174
- backgroundColor: 'var(--docs-el-bg)',
175
- color: 'var(--docs-el-text)',
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(--docs-el-line)',
253
- backgroundColor: 'color-mix(in srgb, var(--docs-el-surface) 92%, transparent)',
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(--docs-el-line)' }}
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(--docs-el-text)]">
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(--docs-el-muted)]/75">
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(--docs-el-text)] dark:hover:bg-white/[0.06]"
286
- style={{ color: 'var(--docs-el-muted)' }}
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(--docs-el-line)' }}>
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(--docs-el-muted)]/80 ${
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(--docs-el-accent)]/10 py-0.5 pl-0.5 dark:bg-[color:var(--docs-el-accent)]/15"
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(--docs-el-muted)]"
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(--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]"
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(--docs-el-muted)] opacity-0 transition-all hover:bg-red-500/10 hover:text-red-600 group-hover:opacity-100"
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(--docs-el-line)',
436
- backgroundColor: 'var(--docs-el-bg)',
437
- color: 'var(--docs-el-text)',
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(--docs-el-muted)] transition-colors hover:bg-black/[0.04] hover:text-[color:var(--docs-el-accent)] dark:hover:bg-white/[0.06]"
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
+ })
@@ -1,6 +1,6 @@
1
- /** Shared document, ACL, folder, and navigation model for the Docs feature. */
1
+ /** Shared document, ACL, folder, and navigation model for the Documents feature. */
2
2
 
3
- export interface DocsDocumentFields {
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 parseDocsIdList(raw: string | undefined): string[] {
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
- * Docs Feature — Record schemas
2
+ * Documents feature — record schemas
3
3
  *
4
- * `documents`: collaborative docs with optional folder placement.
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/worker'
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 docsSchema: CollectionSchema = {
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: `[...docsCollections, ...]` */
80
- export const docsCollections: CollectionSchema[] = [docsSchema, docFoldersSchema]
79
+ /** Pass into `schemas` with spread: `[...documentsCollections, ...]` */
80
+ export const documentsCollections: CollectionSchema[] = [documentsSchema, docFoldersSchema]