deepspace 0.7.0 → 0.8.1

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 (63) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +22 -8
  3. package/dist/cli.js +9812 -4674
  4. package/dist/cli.js.map +1 -1
  5. package/dist/feature-installer.js +638 -0
  6. package/dist/feature-installer.js.map +1 -0
  7. package/dist/index.d.ts +107 -117
  8. package/dist/index.js +610 -779
  9. package/dist/index.js.map +1 -1
  10. package/dist/server.d.ts +403 -167
  11. package/dist/server.js +498 -367
  12. package/dist/server.js.map +1 -1
  13. package/dist/testing.d.ts +115 -0
  14. package/dist/testing.js +3 -3
  15. package/dist/testing.js.map +1 -1
  16. package/dist/worker.d.ts +397 -160
  17. package/dist/worker.js +506 -367
  18. package/dist/worker.js.map +1 -1
  19. package/features/ai-chat/feature.json +2 -0
  20. package/features/ai-chat/src/AiChatPage.tsx +2 -3
  21. package/features/ai-chat/src/ChatPanel.messages.tsx +344 -0
  22. package/features/ai-chat/src/ChatPanel.stream.ts +397 -0
  23. package/features/ai-chat/src/ChatPanel.tsx +158 -862
  24. package/features/ai-chat/src/__tests__/ChatPanel.stream.test.ts +163 -0
  25. package/features/canvas/src/CanvasListPage.tsx +1 -1
  26. package/features/docs/feature.json +4 -8
  27. package/features/docs/src/DocsEditorPage.tsx +33 -326
  28. package/features/docs/src/DocsListPage.tsx +33 -29
  29. package/features/docs/src/InviteDialog.tsx +11 -40
  30. package/features/docs/src/__tests__/docs-presence-access.test.ts +71 -0
  31. package/features/docs/src/docs-library-types.ts +28 -3
  32. package/features/docs/src/docs-ui.css +1 -4
  33. package/features/docs/src/use-docs-presence-access.ts +330 -0
  34. package/features/file-attachments/feature.json +2 -2
  35. package/features/file-attachments/src/AttachmentPreview.tsx +428 -0
  36. package/features/file-attachments/src/AttachmentsPage.tsx +100 -462
  37. package/features/file-attachments/src/useFilePreview.ts +54 -68
  38. package/features/file-manager/src/FileManagerPage.tsx +1 -1
  39. package/features/integration-test/src/integration.spec.ts +1 -1
  40. package/features/items/feature.json +1 -5
  41. package/features/items/src/ItemsPage.tsx +207 -135
  42. package/features/landing/feature.json +3 -0
  43. package/features/landing/src/LandingPage.tsx +4 -18
  44. package/features/leaderboard/feature.json +1 -5
  45. package/features/leaderboard/src/LeaderboardPage.tsx +333 -198
  46. package/features/messaging/feature.json +0 -4
  47. package/features/messaging/src/ChatMultiPage.tsx +1 -3
  48. package/features/messaging/src/ChatPage.tsx +1 -3
  49. package/features/messaging/src/components/chat/MessageActionSheet.tsx +40 -16
  50. package/features/search-bar/feature.json +8 -3
  51. package/features/search-bar/src/SearchOverlay.tsx +108 -252
  52. package/features/search-bar/src/__tests__/search-model.test.ts +40 -0
  53. package/features/search-bar/src/search-model.ts +223 -0
  54. package/features/tasks/src/TasksPage.tsx +3 -3
  55. package/features/topbar/src/Topbar.tsx +0 -17
  56. package/package.json +2 -3
  57. package/features/docs/src/docs-favorites.ts +0 -18
  58. package/features/docs/src/use-yjs-room-with-awareness.ts +0 -289
  59. package/features/file-attachments/src/attachments-constants.ts +0 -9
  60. package/features/items/src/items-constants.ts +0 -10
  61. package/features/leaderboard/src/leaderboard-constants.ts +0 -21
  62. package/features/messaging/src/components/chat/ReactionPicker.tsx +0 -2
  63. package/scripts/add-feature.cjs +0 -807
@@ -3,12 +3,12 @@
3
3
  "name": "File Attachments",
4
4
  "category": "data",
5
5
  "description": "File upload with inline previews for images, videos, audio, PDF, code, CSV, DOCX, XLSX",
6
- "details": "Adds a file attachments collection backed by R2 storage with useR2Files. Includes two hooks: useMimeTypeDetection (magic-byte MIME detection + 11 file categories) and useFilePreview (centralized preview with dynamic imports for PDF.js, CodeMirror, PapaParse, docx-preview, ExcelJS). Members can upload and manage their own files, admins can manage all.",
6
+ "details": "Adds a file attachments collection backed by R2 storage with useR2Files. MIME detection and preview loading live in focused hooks; one preview component owns thumbnail and full rendering for PDF.js, CodeMirror, CSV, DOCX, XLSX, audio, video, and images. Members can upload and manage their own files, admins can manage all.",
7
7
  "files": [
8
8
  { "src": "src/attachments-schema.ts", "dest": "src/schemas/attachments-schema.ts" },
9
- { "src": "src/attachments-constants.ts", "dest": "src/constants/attachments-constants.ts" },
10
9
  { "src": "src/useMimeTypeDetection.ts", "dest": "src/hooks/useMimeTypeDetection.ts" },
11
10
  { "src": "src/useFilePreview.ts", "dest": "src/hooks/useFilePreview.ts" },
11
+ { "src": "src/AttachmentPreview.tsx", "dest": "src/components/AttachmentPreview.tsx" },
12
12
  { "src": "src/AttachmentsPage.tsx", "dest": "src/pages/attachments.tsx" }
13
13
  ],
14
14
  "schema": {
@@ -0,0 +1,428 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+
3
+ import { useFilePreview, type XlsxData } from '../hooks/useFilePreview'
4
+ import type { FileCategory } from '../hooks/useMimeTypeDetection'
5
+
6
+ const ICON_PATHS: Record<FileCategory, string> = {
7
+ image:
8
+ 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z',
9
+ video:
10
+ 'M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z',
11
+ audio:
12
+ 'M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2z',
13
+ pdf: 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z',
14
+ document:
15
+ 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
16
+ spreadsheet:
17
+ 'M3 10h18M3 14h18M9 4v16M15 4v16M3 6a2 2 0 012-2h14a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V6z',
18
+ presentation:
19
+ 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
20
+ archive: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4',
21
+ code: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4',
22
+ text: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
23
+ other:
24
+ 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z',
25
+ }
26
+
27
+ function FileIcon({ category }: { category: FileCategory }) {
28
+ return (
29
+ <div
30
+ className="flex h-48 w-full items-center justify-center rounded-lg bg-muted/20"
31
+ role="img"
32
+ aria-label={`${category} file`}
33
+ >
34
+ <svg
35
+ className="h-12 w-12 text-muted-foreground/50"
36
+ fill="none"
37
+ viewBox="0 0 24 24"
38
+ stroke="currentColor"
39
+ aria-hidden="true"
40
+ >
41
+ <path
42
+ strokeLinecap="round"
43
+ strokeLinejoin="round"
44
+ strokeWidth={1.5}
45
+ d={ICON_PATHS[category]}
46
+ />
47
+ </svg>
48
+ </div>
49
+ )
50
+ }
51
+
52
+ export function AttachmentThumbnail({
53
+ url,
54
+ fileName,
55
+ category,
56
+ }: {
57
+ url: string | null
58
+ fileName: string
59
+ category: FileCategory
60
+ }) {
61
+ if (!url) return <FileIcon category={category} />
62
+
63
+ switch (category) {
64
+ case 'image':
65
+ return (
66
+ <img
67
+ src={url}
68
+ alt={fileName}
69
+ className="h-48 w-full rounded-t-xl object-cover"
70
+ loading="lazy"
71
+ />
72
+ )
73
+ case 'video':
74
+ return (
75
+ <video
76
+ src={url}
77
+ aria-label={`${fileName} preview`}
78
+ className="h-48 w-full rounded-t-xl bg-black"
79
+ preload="metadata"
80
+ muted
81
+ />
82
+ )
83
+ case 'audio':
84
+ return (
85
+ <div className="flex h-48 w-full flex-col items-center justify-center gap-2 rounded-t-xl bg-muted/20 px-4">
86
+ <svg
87
+ className="h-8 w-8 text-muted-foreground/50"
88
+ fill="none"
89
+ viewBox="0 0 24 24"
90
+ stroke="currentColor"
91
+ aria-hidden="true"
92
+ >
93
+ <path
94
+ strokeLinecap="round"
95
+ strokeLinejoin="round"
96
+ strokeWidth={1.5}
97
+ d={ICON_PATHS.audio}
98
+ />
99
+ </svg>
100
+ <audio
101
+ src={url}
102
+ aria-label={fileName}
103
+ controls
104
+ className="w-full"
105
+ preload="metadata"
106
+ onClick={(event) => event.stopPropagation()}
107
+ />
108
+ </div>
109
+ )
110
+ default:
111
+ return <FileIcon category={category} />
112
+ }
113
+ }
114
+
115
+ function TabularPreview({
116
+ headers,
117
+ rows,
118
+ truncated,
119
+ }: {
120
+ headers: string[]
121
+ rows: string[][]
122
+ truncated: boolean
123
+ }) {
124
+ return (
125
+ <div className="w-full overflow-auto rounded-lg border border-border">
126
+ <table className="w-full border-collapse font-mono text-sm">
127
+ <thead>
128
+ <tr>
129
+ <th className="sticky top-0 border-b-2 border-border bg-muted/50 px-3 py-2 text-right text-xs text-muted-foreground">
130
+ #
131
+ </th>
132
+ {headers.map((header, index) => (
133
+ <th
134
+ key={`${index}-${header}`}
135
+ className="sticky top-0 whitespace-nowrap border-b-2 border-border bg-muted/50 px-3 py-2 text-left font-semibold text-foreground"
136
+ >
137
+ {header}
138
+ </th>
139
+ ))}
140
+ </tr>
141
+ </thead>
142
+ <tbody>
143
+ {rows.map((row, rowIndex) => (
144
+ <tr key={rowIndex} className={rowIndex % 2 === 0 ? '' : 'bg-muted/20'}>
145
+ <td className="border-b border-border/50 px-3 py-1.5 text-right text-xs text-muted-foreground">
146
+ {rowIndex + 1}
147
+ </td>
148
+ {headers.map((_, columnIndex) => (
149
+ <td
150
+ key={columnIndex}
151
+ className="max-w-[300px] truncate whitespace-nowrap border-b border-border/50 px-3 py-1.5 text-foreground"
152
+ title={row[columnIndex] ?? ''}
153
+ >
154
+ {row[columnIndex] ?? ''}
155
+ </td>
156
+ ))}
157
+ </tr>
158
+ ))}
159
+ </tbody>
160
+ </table>
161
+ {truncated && (
162
+ <div className="border-t border-border bg-muted/30 px-4 py-2 text-center text-xs text-muted-foreground">
163
+ Showing first 500 rows. Download the file to see all data.
164
+ </div>
165
+ )}
166
+ </div>
167
+ )
168
+ }
169
+
170
+ function DocxPreview({
171
+ data,
172
+ module: docxModule,
173
+ }: {
174
+ data: ArrayBuffer
175
+ module: typeof import('docx-preview')
176
+ }) {
177
+ const styleRef = useRef<HTMLDivElement>(null)
178
+ const contentRef = useRef<HTMLDivElement>(null)
179
+ const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading')
180
+
181
+ useEffect(() => {
182
+ const content = contentRef.current
183
+ const styles = styleRef.current
184
+ if (!content || !styles) return
185
+
186
+ let active = true
187
+ setStatus('loading')
188
+ content.replaceChildren()
189
+ styles.replaceChildren()
190
+
191
+ void docxModule
192
+ .renderAsync(data, content, styles, {
193
+ className: 'docx-preview-page',
194
+ inWrapper: true,
195
+ ignoreWidth: true,
196
+ ignoreHeight: false,
197
+ ignoreFonts: false,
198
+ breakPages: true,
199
+ renderHeaders: true,
200
+ renderFooters: true,
201
+ renderFootnotes: true,
202
+ renderEndnotes: true,
203
+ useBase64URL: true,
204
+ })
205
+ .then(
206
+ () => {
207
+ if (active) setStatus('ready')
208
+ },
209
+ () => {
210
+ if (active) setStatus('error')
211
+ },
212
+ )
213
+
214
+ return () => {
215
+ active = false
216
+ content.replaceChildren()
217
+ styles.replaceChildren()
218
+ }
219
+ }, [data, docxModule])
220
+
221
+ return (
222
+ <div
223
+ className="min-h-[400px] w-full overflow-auto rounded-lg"
224
+ style={{ background: '#e8e8e8' }}
225
+ >
226
+ {status === 'loading' && (
227
+ <div className="p-8 text-center text-muted-foreground">Loading document...</div>
228
+ )}
229
+ {status === 'error' && (
230
+ <div className="p-8 text-center text-muted-foreground">Failed to render document.</div>
231
+ )}
232
+ <div ref={styleRef} />
233
+ <div ref={contentRef} />
234
+ <style>{`
235
+ .docx-preview-page-wrapper {
236
+ background: #e8e8e8;
237
+ padding: 16px 0;
238
+ display: flex;
239
+ flex-direction: column;
240
+ align-items: center;
241
+ gap: 16px;
242
+ }
243
+ .docx-preview-page-wrapper > section.docx-preview-page {
244
+ background: #ffffff !important;
245
+ color: #1a1a1a !important;
246
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
247
+ border-radius: 4px;
248
+ overflow: hidden;
249
+ width: 100% !important;
250
+ padding: 24px !important;
251
+ box-sizing: border-box !important;
252
+ }
253
+ .docx-preview-page-wrapper > section.docx-preview-page * {
254
+ color: inherit;
255
+ }
256
+ `}</style>
257
+ </div>
258
+ )
259
+ }
260
+
261
+ function XlsxPreview({ data }: { data: XlsxData }) {
262
+ const [activeSheet, setActiveSheet] = useState(0)
263
+
264
+ useEffect(() => {
265
+ setActiveSheet(0)
266
+ }, [data])
267
+
268
+ const sheet = data.sheets[activeSheet] ?? data.sheets[0]
269
+ if (!sheet) {
270
+ return <div className="p-8 text-center text-muted-foreground">This workbook has no sheets.</div>
271
+ }
272
+
273
+ return (
274
+ <div className="w-full">
275
+ {data.sheets.length > 1 && (
276
+ <div className="flex gap-1 overflow-x-auto border-b border-border bg-muted/30 px-2 py-1">
277
+ {data.sheets.map((candidate, index) => (
278
+ <button
279
+ key={candidate.name}
280
+ type="button"
281
+ onClick={() => setActiveSheet(index)}
282
+ className={`whitespace-nowrap rounded px-3 py-1 text-xs font-medium transition-colors ${
283
+ index === activeSheet
284
+ ? 'bg-primary text-primary-foreground'
285
+ : 'text-muted-foreground hover:bg-muted'
286
+ }`}
287
+ >
288
+ {candidate.name}
289
+ </button>
290
+ ))}
291
+ </div>
292
+ )}
293
+ <TabularPreview headers={sheet.headers} rows={sheet.rows} truncated={sheet.truncated} />
294
+ </div>
295
+ )
296
+ }
297
+
298
+ function PdfPage({ canvas }: { canvas: HTMLCanvasElement }) {
299
+ const containerRef = useRef<HTMLDivElement>(null)
300
+
301
+ useEffect(() => {
302
+ const container = containerRef.current
303
+ if (!container) return
304
+ canvas.style.width = '100%'
305
+ canvas.style.height = 'auto'
306
+ canvas.style.display = 'block'
307
+ container.replaceChildren(canvas)
308
+ return () => {
309
+ if (canvas.parentNode === container) canvas.remove()
310
+ }
311
+ }, [canvas])
312
+
313
+ return (
314
+ <div
315
+ ref={containerRef}
316
+ className="w-full flex-shrink-0 overflow-hidden rounded-lg border border-border bg-white"
317
+ style={{ aspectRatio: `${canvas.width / canvas.height}` }}
318
+ />
319
+ )
320
+ }
321
+
322
+ export function AttachmentPreview({
323
+ url,
324
+ fileName,
325
+ mimeType,
326
+ }: {
327
+ url: string
328
+ fileName: string
329
+ mimeType: string
330
+ }) {
331
+ const preview = useFilePreview({ url, fileName, mimeType })
332
+
333
+ if (preview.isLoading) {
334
+ return (
335
+ <div className="flex items-center justify-center py-16">
336
+ <div className="h-8 w-8 animate-spin rounded-full border-2 border-primary/20 border-t-primary" />
337
+ </div>
338
+ )
339
+ }
340
+
341
+ if (preview.error) {
342
+ return (
343
+ <div className="p-8 text-center text-muted-foreground">
344
+ Failed to load preview: {preview.error}
345
+ </div>
346
+ )
347
+ }
348
+
349
+ if (preview.category === 'image') {
350
+ return <img src={url} alt={fileName} className="w-full rounded-lg" />
351
+ }
352
+
353
+ if (preview.category === 'video') {
354
+ return <video src={url} aria-label={fileName} controls className="w-full rounded-lg bg-black" />
355
+ }
356
+
357
+ if (preview.category === 'audio') {
358
+ return (
359
+ <div className="flex flex-col items-center gap-4 p-8">
360
+ <FileIcon category="audio" />
361
+ <audio src={url} aria-label={fileName} controls className="w-full" />
362
+ </div>
363
+ )
364
+ }
365
+
366
+ if (preview.category === 'pdf' && preview.pdfPages) {
367
+ return (
368
+ <div className="flex max-h-[80vh] flex-col gap-4 overflow-auto">
369
+ {preview.pdfPages.map((canvas, index) => (
370
+ <PdfPage key={index} canvas={canvas} />
371
+ ))}
372
+ </div>
373
+ )
374
+ }
375
+
376
+ if (
377
+ (preview.category === 'code' || preview.category === 'text') &&
378
+ preview.textContent !== null
379
+ ) {
380
+ if (preview.codeEditorKit) {
381
+ const { Component, langExtensions, darkTheme } = preview.codeEditorKit
382
+ return (
383
+ <div className="w-full overflow-hidden rounded-lg border border-border">
384
+ <Component
385
+ value={preview.textContent}
386
+ extensions={[...langExtensions, darkTheme]}
387
+ readOnly
388
+ editable={false}
389
+ theme="dark"
390
+ height="400px"
391
+ basicSetup={{ lineNumbers: true, foldGutter: true, highlightActiveLine: false }}
392
+ style={{ fontSize: '13px' }}
393
+ />
394
+ </div>
395
+ )
396
+ }
397
+
398
+ return (
399
+ <pre className="max-h-[400px] w-full overflow-auto whitespace-pre-wrap rounded-lg border border-border bg-muted/20 p-4 text-sm text-foreground">
400
+ {preview.textContent}
401
+ </pre>
402
+ )
403
+ }
404
+
405
+ if (preview.csvData) {
406
+ return <TabularPreview {...preview.csvData} />
407
+ }
408
+
409
+ if (preview.category === 'document' && preview.docxData && preview.docxModule) {
410
+ return <DocxPreview data={preview.docxData} module={preview.docxModule} />
411
+ }
412
+
413
+ if (preview.xlsxData) {
414
+ return <XlsxPreview data={preview.xlsxData} />
415
+ }
416
+
417
+ return (
418
+ <div className="flex flex-col items-center gap-4 p-12 text-center">
419
+ <FileIcon category={preview.category} />
420
+ <div className="space-y-1">
421
+ <p className="text-sm font-medium text-foreground">No preview available</p>
422
+ <p className="text-xs text-muted-foreground">
423
+ Preview is not supported for this file type. You can download the file to view it.
424
+ </p>
425
+ </div>
426
+ </div>
427
+ )
428
+ }