deepspace 0.4.4 → 0.5.0
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/README.md +90 -0
- package/dist/cli.js +2912 -1984
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +31 -4
- package/dist/index.js +511 -366
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +29 -108
- package/dist/server.js +126 -589
- package/dist/server.js.map +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +29 -108
- package/dist/worker.js +126 -589
- package/dist/worker.js.map +1 -1
- package/features/admin-page/src/AdminPage.tsx +3 -5
- package/features/canvas/src/CanvasEditorPage.tsx +0 -2
- package/features/canvas/src/components/CanvasView.tsx +1 -1
- package/features/docs/src/LibrarySidebar.tsx +7 -1
- package/features/docs/src/use-yjs-room-with-awareness.ts +4 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +7 -1
- package/features/file-attachments/src/useFilePreview.ts +7 -5
- package/features/file-manager/src/FileManagerPage.tsx +2 -3
- package/features/integration-test/feature.json +10 -3
- package/features/integration-test/src/IntegrationTestPage.tsx +15 -12
- package/features/landing/src/LandingPage.tsx +1 -1
- package/features/landing/src/sections/FeaturesGridSection.tsx +0 -92
- package/features/messaging/src/components/chat/MessageItem.tsx +5 -1
- package/features/messaging/src/hooks/useChatChannel.ts +5 -1
- package/features/presence-test/feature.json +2 -1
- package/features/sidebar/src/AppSidebar.tsx +1 -1
- package/features/tasks/src/TasksPage.tsx +1 -1
- package/features/testing/feature.json +2 -1
- package/features/testing/src/TestPage.tsx +8 -8
- package/package.json +4 -4
- package/scripts/add-feature.cjs +277 -272
|
@@ -13,7 +13,7 @@ import { useUser } from 'deepspace'
|
|
|
13
13
|
import { useUsers } from 'deepspace'
|
|
14
14
|
import { useQuery } from 'deepspace'
|
|
15
15
|
import { useMutations } from 'deepspace'
|
|
16
|
-
import { Button, Badge, Avatar, AvatarFallback, useToast } from '@/components/ui'
|
|
16
|
+
import { Button, Badge, Avatar, AvatarFallback, useToast, type BadgeProps } from '@/components/ui'
|
|
17
17
|
import { ROLES, ROLE_CONFIG, type Role } from 'deepspace'
|
|
18
18
|
|
|
19
19
|
// ============================================================================
|
|
@@ -129,8 +129,6 @@ interface UsersPanelProps {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
function UsersPanel({ users, currentUserId, onSetRole }: UsersPanelProps) {
|
|
132
|
-
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
|
|
133
|
-
|
|
134
132
|
// Sort users by last seen
|
|
135
133
|
const sortedUsers = useMemo(() => {
|
|
136
134
|
return [...users].sort((a, b) =>
|
|
@@ -169,7 +167,7 @@ function UsersPanel({ users, currentUserId, onSetRole }: UsersPanelProps) {
|
|
|
169
167
|
</p>
|
|
170
168
|
|
|
171
169
|
<div className="flex items-center gap-2">
|
|
172
|
-
<Badge variant={roleConfig.badgeVariant as
|
|
170
|
+
<Badge variant={roleConfig.badgeVariant as BadgeProps['variant']}>{roleConfig.title}</Badge>
|
|
173
171
|
<select
|
|
174
172
|
value={user.role}
|
|
175
173
|
onChange={(e) => onSetRole(user.id, e.target.value)}
|
|
@@ -211,7 +209,7 @@ function SettingsPanel({ toast }: SettingsPanelProps) {
|
|
|
211
209
|
|
|
212
210
|
// Query settings (admin-only collection)
|
|
213
211
|
const { records: settings, status } = useQuery<Setting>('settings')
|
|
214
|
-
const { create,
|
|
212
|
+
const { create, remove } = useMutations<Setting>('settings')
|
|
215
213
|
|
|
216
214
|
const handleCreate = async () => {
|
|
217
215
|
if (!newKey.trim() || !newValue.trim()) return
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
import { useMemo } from 'react'
|
|
11
11
|
import { useParams, useNavigate } from 'react-router-dom'
|
|
12
|
-
import { useUser } from 'deepspace'
|
|
13
12
|
import { useQuery } from 'deepspace'
|
|
14
13
|
import { CanvasView } from '../../components/canvas/CanvasView'
|
|
15
14
|
|
|
@@ -29,7 +28,6 @@ interface CanvasDocument {
|
|
|
29
28
|
export default function CanvasEditorPage() {
|
|
30
29
|
const { docId } = useParams<{ docId: string }>()
|
|
31
30
|
const navigate = useNavigate()
|
|
32
|
-
const { user } = useUser()
|
|
33
31
|
|
|
34
32
|
const { records: canvases, status } = useQuery<CanvasDocument>('canvases', {
|
|
35
33
|
orderBy: 'createdAt',
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useState, useRef, useCallback, useEffect } from 'react'
|
|
6
|
-
import { useCanvas
|
|
6
|
+
import { useCanvas } from 'deepspace'
|
|
7
7
|
import { CanvasToolbar, type CanvasTool } from './CanvasToolbar'
|
|
8
8
|
import { ShapeRenderer } from './ShapeRenderer'
|
|
9
9
|
import { Badge } from '@/components/ui'
|
|
@@ -126,7 +126,9 @@ function SidebarFolderRenameField({
|
|
|
126
126
|
}
|
|
127
127
|
})
|
|
128
128
|
return () => cancelAnimationFrame(raf)
|
|
129
|
-
|
|
129
|
+
// sessionStartRef is a stable ref (only written here); listing it is a no-op
|
|
130
|
+
// but satisfies the rule without changing re-run behavior.
|
|
131
|
+
}, [folderId, sessionStartRef])
|
|
130
132
|
|
|
131
133
|
return (
|
|
132
134
|
<input
|
|
@@ -216,6 +218,10 @@ export function LibrarySidebar({
|
|
|
216
218
|
|
|
217
219
|
useEffect(() => {
|
|
218
220
|
return () => {
|
|
221
|
+
// Intentionally read the ref's LATEST value at unmount to clear whatever
|
|
222
|
+
// blur timer is pending; capturing it at effect-setup time (null) would be
|
|
223
|
+
// wrong here since this is a timer ref, not a DOM node.
|
|
224
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- must clear the latest pending timer at unmount, not the setup-time value
|
|
219
225
|
if (folderRenameBlurTimerRef.current) clearTimeout(folderRenameBlurTimerRef.current)
|
|
220
226
|
}
|
|
221
227
|
}, [])
|
|
@@ -53,6 +53,10 @@ export function useYjsRoomWithAwareness(
|
|
|
53
53
|
const { doc, awareness } = useMemo(() => {
|
|
54
54
|
const nextDoc = new Y.Doc()
|
|
55
55
|
return { doc: nextDoc, awareness: new Awareness(nextDoc) }
|
|
56
|
+
// docId is a deliberate semantic dep (not textually used): switching rooms
|
|
57
|
+
// MUST mint a fresh Y.Doc/Awareness so state from the old room never leaks
|
|
58
|
+
// into the new one. Removing it would reuse a stale doc across docIds.
|
|
59
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- docId forces a fresh Y.Doc per room; removing it reuses a stale doc across rooms
|
|
56
60
|
}, [docId])
|
|
57
61
|
|
|
58
62
|
const wsRef = useRef<WebSocket | null>(null)
|
|
@@ -69,7 +69,7 @@ function FileIcon({ category }: { category: FileCategory }) {
|
|
|
69
69
|
// Thumbnail Preview (card grid view)
|
|
70
70
|
// ============================================================================
|
|
71
71
|
|
|
72
|
-
function ThumbnailPreview({ url, fileName, mimeType, category }: {
|
|
72
|
+
function ThumbnailPreview({ url, fileName, mimeType: _mimeType, category }: {
|
|
73
73
|
url: string | null
|
|
74
74
|
fileName: string
|
|
75
75
|
mimeType: string
|
|
@@ -616,9 +616,15 @@ export default function AttachmentsPage() {
|
|
|
616
616
|
// Free all blob URLs we created on unmount.
|
|
617
617
|
useEffect(() => {
|
|
618
618
|
return () => {
|
|
619
|
+
// Read the refs' LATEST contents at unmount so we revoke every URL and
|
|
620
|
+
// clear every key accumulated over the component's life. Capturing at
|
|
621
|
+
// setup time (empty) would leak URLs; these are collection refs, not DOM
|
|
622
|
+
// nodes.
|
|
623
|
+
/* eslint-disable react-hooks/exhaustive-deps -- must revoke/clear the latest ref contents at unmount, not the empty setup-time values */
|
|
619
624
|
for (const url of objectUrlsRef.current) URL.revokeObjectURL(url)
|
|
620
625
|
objectUrlsRef.current.clear()
|
|
621
626
|
inflightKeysRef.current.clear()
|
|
627
|
+
/* eslint-enable react-hooks/exhaustive-deps */
|
|
622
628
|
}
|
|
623
629
|
}, [])
|
|
624
630
|
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { useState, useEffect
|
|
25
|
+
import { useState, useEffect } from 'react'
|
|
26
26
|
import { getFileCategory, getExtension, type FileCategory } from './useMimeTypeDetection'
|
|
27
27
|
|
|
28
28
|
// ============================================================================
|
|
@@ -64,9 +64,11 @@ export interface FilePreviewResult {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export interface CodeEditorKit {
|
|
67
|
-
Component: React.ComponentType<
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
Component: React.ComponentType<Record<string, unknown>>
|
|
68
|
+
/** Opaque CodeMirror Extension objects (dynamically imported, no stable public type here). */
|
|
69
|
+
langExtensions: unknown[]
|
|
70
|
+
/** Opaque CodeMirror theme Extension. */
|
|
71
|
+
darkTheme: unknown
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export interface XlsxSheet {
|
|
@@ -245,7 +247,7 @@ async function loadCodePreview(
|
|
|
245
247
|
const [cmModule, jsModule, themeModule] = editorModules
|
|
246
248
|
if (cmModule && jsModule && themeModule) {
|
|
247
249
|
const mode = getLanguageMode(fileName)
|
|
248
|
-
const langExtensions:
|
|
250
|
+
const langExtensions: unknown[] = []
|
|
249
251
|
|
|
250
252
|
if (mode === 'javascript') {
|
|
251
253
|
langExtensions.push(jsModule.javascript({ jsx: true }))
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { useR2Files } from 'deepspace'
|
|
13
|
+
import { EmptyState, Badge, Modal } from '@/components/ui'
|
|
14
14
|
import type { R2FileInfo } from 'deepspace'
|
|
15
15
|
|
|
16
16
|
// ============================================================================
|
|
@@ -226,7 +226,6 @@ function PreviewModal({ file, imageUrl, onClose }: PreviewModalProps) {
|
|
|
226
226
|
// ============================================================================
|
|
227
227
|
|
|
228
228
|
export default function FileManagerPage({ className }: { className?: string }) {
|
|
229
|
-
const { user } = useUser()
|
|
230
229
|
const { upload, downloadFile, deleteFile, list, getUrl, isUploading } = useR2Files()
|
|
231
230
|
const [files, setFiles] = useState<R2FileInfo[]>([])
|
|
232
231
|
const [loading, setLoading] = useState(true)
|
|
@@ -5,8 +5,14 @@
|
|
|
5
5
|
"description": "Test page for exercising integration API endpoints",
|
|
6
6
|
"details": "Lets you call any integration endpoint (OpenAI, weather, search, etc.) and see the result. Used by Playwright tests to verify the full integration proxy flow.",
|
|
7
7
|
"files": [
|
|
8
|
-
{
|
|
9
|
-
|
|
8
|
+
{
|
|
9
|
+
"src": "src/IntegrationTestPage.tsx",
|
|
10
|
+
"dest": "src/pages/integration-test.tsx"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"src": "src/integration.spec.ts",
|
|
14
|
+
"dest": "tests/integration.spec.ts"
|
|
15
|
+
}
|
|
10
16
|
],
|
|
11
17
|
"route": {
|
|
12
18
|
"path": "/integration-test",
|
|
@@ -14,5 +20,6 @@
|
|
|
14
20
|
},
|
|
15
21
|
"requires": [],
|
|
16
22
|
"instructions": [],
|
|
17
|
-
"patterns": []
|
|
23
|
+
"patterns": [],
|
|
24
|
+
"internal": true
|
|
18
25
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { useState, useEffect, useMemo } from 'react'
|
|
13
13
|
import { integration } from 'deepspace'
|
|
14
|
+
import type { IntegrationResponse } from 'deepspace'
|
|
14
15
|
import { useAuth } from 'deepspace'
|
|
15
16
|
|
|
16
17
|
// ============================================================================
|
|
@@ -41,6 +42,7 @@ interface JsonSchemaProperty {
|
|
|
41
42
|
maxLength?: number
|
|
42
43
|
items?: JsonSchemaProperty
|
|
43
44
|
properties?: Record<string, JsonSchemaProperty>
|
|
45
|
+
anyOf?: JsonSchemaProperty[]
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
type Catalog = Record<string, CatalogEntry[]>
|
|
@@ -68,19 +70,19 @@ function buildFullExample(schema?: JsonSchema | null, catalogExample?: Record<st
|
|
|
68
70
|
return example
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
function buildExampleItem(items:
|
|
73
|
+
function buildExampleItem(items: JsonSchemaProperty): unknown {
|
|
72
74
|
if (items.type === 'string') return 'example'
|
|
73
75
|
if (items.type === 'number' || items.type === 'integer') return 0
|
|
74
76
|
if (items.type === 'object' && items.properties) {
|
|
75
77
|
const obj: Record<string, unknown> = {}
|
|
76
|
-
for (const [k, v] of Object.entries(items.properties
|
|
78
|
+
for (const [k, v] of Object.entries(items.properties)) {
|
|
77
79
|
if (v.default !== undefined) obj[k] = v.default
|
|
78
80
|
else if (v.enum) obj[k] = v.enum[0]
|
|
79
81
|
else if (v.type === 'string') obj[k] = k
|
|
80
82
|
else if (v.type === 'number' || v.type === 'integer') obj[k] = v.minimum ?? 0
|
|
81
83
|
else if (v.type === 'boolean') obj[k] = false
|
|
82
84
|
else if (v.anyOf) {
|
|
83
|
-
const str = v.anyOf.find((a
|
|
85
|
+
const str = v.anyOf.find((a) => a.type === 'string')
|
|
84
86
|
obj[k] = str ? k : null
|
|
85
87
|
}
|
|
86
88
|
}
|
|
@@ -186,7 +188,7 @@ function FieldInput({
|
|
|
186
188
|
if (prop.type === 'array' && Array.isArray(defaultValue) && defaultValue.length === 0 && prop.items?.properties) {
|
|
187
189
|
// Build an example object from the items schema
|
|
188
190
|
const exampleItem: Record<string, unknown> = {}
|
|
189
|
-
for (const [k, v] of Object.entries(prop.items.properties
|
|
191
|
+
for (const [k, v] of Object.entries(prop.items.properties)) {
|
|
190
192
|
if (v.default !== undefined) exampleItem[k] = v.default
|
|
191
193
|
else if (v.enum) exampleItem[k] = v.enum[0]
|
|
192
194
|
else if (v.type === 'string') exampleItem[k] = k
|
|
@@ -194,7 +196,7 @@ function FieldInput({
|
|
|
194
196
|
else if (v.type === 'boolean') exampleItem[k] = false
|
|
195
197
|
else if (v.anyOf) {
|
|
196
198
|
// Pick the simplest type from anyOf
|
|
197
|
-
const stringType = v.anyOf.find((a
|
|
199
|
+
const stringType = v.anyOf.find((a) => a.type === 'string')
|
|
198
200
|
if (stringType) exampleItem[k] = k
|
|
199
201
|
else exampleItem[k] = null
|
|
200
202
|
}
|
|
@@ -237,15 +239,16 @@ export default function IntegrationTestPage() {
|
|
|
237
239
|
const [formValues, setFormValues] = useState<Record<string, unknown>>({})
|
|
238
240
|
const [rawMode, setRawMode] = useState(false)
|
|
239
241
|
const [rawBody, setRawBody] = useState('{}')
|
|
240
|
-
const [result, setResult] = useState<
|
|
242
|
+
const [result, setResult] = useState<IntegrationResponse | null>(null)
|
|
241
243
|
const [loading, setLoading] = useState(false)
|
|
242
244
|
const [error, setError] = useState<string | null>(null)
|
|
243
245
|
|
|
244
246
|
// Fetch catalog on mount
|
|
245
247
|
useEffect(() => {
|
|
246
|
-
integration.get<{ integrations
|
|
248
|
+
integration.get<{ integrations?: Catalog } & Catalog>('').then((res) => {
|
|
247
249
|
if (res.success && res.data) {
|
|
248
|
-
|
|
250
|
+
const { integrations, ...rest } = res.data
|
|
251
|
+
setCatalog(integrations ?? (rest as Catalog))
|
|
249
252
|
}
|
|
250
253
|
})
|
|
251
254
|
}, [])
|
|
@@ -276,8 +279,8 @@ export default function IntegrationTestPage() {
|
|
|
276
279
|
const body = rawMode ? JSON.parse(rawBody) : formValues
|
|
277
280
|
const res = await integration.post(endpoint, body)
|
|
278
281
|
setResult(res)
|
|
279
|
-
} catch (err:
|
|
280
|
-
setError(err.message)
|
|
282
|
+
} catch (err: unknown) {
|
|
283
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
281
284
|
} finally {
|
|
282
285
|
setLoading(false)
|
|
283
286
|
}
|
|
@@ -339,7 +342,7 @@ export default function IntegrationTestPage() {
|
|
|
339
342
|
onClick={() => {
|
|
340
343
|
setRawMode(!rawMode)
|
|
341
344
|
if (!rawMode) setRawBody(JSON.stringify(formValues, null, 2))
|
|
342
|
-
else try { setFormValues(JSON.parse(rawBody)) } catch {}
|
|
345
|
+
else try { setFormValues(JSON.parse(rawBody)) } catch { /* invalid JSON: keep current form values */ }
|
|
343
346
|
}}
|
|
344
347
|
className="ml-3 mt-5 rounded-lg border border-border px-3 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-secondary transition-colors"
|
|
345
348
|
>
|
|
@@ -396,7 +399,7 @@ export default function IntegrationTestPage() {
|
|
|
396
399
|
{result && !result.success && result.issues && (
|
|
397
400
|
<div className="rounded-lg bg-destructive/10 border border-destructive/20 px-4 py-3 text-sm space-y-1">
|
|
398
401
|
<div className="font-medium text-destructive">{result.error}</div>
|
|
399
|
-
{result.issues.map((issue
|
|
402
|
+
{result.issues.map((issue, i) => (
|
|
400
403
|
<div key={i} className="text-destructive/80">
|
|
401
404
|
<span className="font-mono">{issue.path?.join('.')}</span>: {issue.message}
|
|
402
405
|
</div>
|
|
@@ -121,7 +121,7 @@ export function hasSeenLanding(): boolean {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
export function markLandingSeen(): void {
|
|
124
|
-
try { localStorage.setItem(LANDING_SEEN_KEY, 'true') } catch {}
|
|
124
|
+
try { localStorage.setItem(LANDING_SEEN_KEY, 'true') } catch { /* storage unavailable (private mode/quota): non-critical */ }
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// ============================================================================
|
|
@@ -8,7 +8,6 @@ import { type ReactNode } from 'react'
|
|
|
8
8
|
import { motion } from 'framer-motion'
|
|
9
9
|
import { Zap, Shield, BarChart3, Sparkles, Check } from 'lucide-react'
|
|
10
10
|
import {
|
|
11
|
-
ScrollReveal,
|
|
12
11
|
StaggerContainer,
|
|
13
12
|
staggerChild,
|
|
14
13
|
GlassCard,
|
|
@@ -77,26 +76,6 @@ const FEATURES: Array<{
|
|
|
77
76
|
// Decorative Visuals
|
|
78
77
|
// ============================================================================
|
|
79
78
|
|
|
80
|
-
function SpeedPulseVisual() {
|
|
81
|
-
return (
|
|
82
|
-
<div className="relative h-24 w-full overflow-hidden rounded-lg bg-foreground/[0.02]">
|
|
83
|
-
<div className="absolute inset-0 flex items-center justify-center gap-[3px]">
|
|
84
|
-
{[0.3, 0.7, 0.5, 1, 0.6, 0.85, 0.45, 0.9, 0.35, 0.75, 0.55, 0.95, 0.4, 0.8, 0.65].map((h, i) => (
|
|
85
|
-
<motion.div
|
|
86
|
-
key={i}
|
|
87
|
-
className="w-[4px] rounded-full bg-gradient-to-t from-amber-500/60 to-orange-400/30"
|
|
88
|
-
initial={{ height: '10%' }}
|
|
89
|
-
whileInView={{ height: `${h * 80}%` }}
|
|
90
|
-
viewport={{ once: true }}
|
|
91
|
-
transition={{ delay: i * 0.04, duration: 0.6, ease: 'easeOut' }}
|
|
92
|
-
/>
|
|
93
|
-
))}
|
|
94
|
-
</div>
|
|
95
|
-
<div className="absolute bottom-0 inset-x-0 h-8 bg-gradient-to-t from-background/90 to-transparent" />
|
|
96
|
-
</div>
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
79
|
function ShieldVisual() {
|
|
101
80
|
const checkItems = ['Encryption', 'RBAC', 'Audit Log']
|
|
102
81
|
return (
|
|
@@ -154,77 +133,6 @@ function BarChartVisual() {
|
|
|
154
133
|
)
|
|
155
134
|
}
|
|
156
135
|
|
|
157
|
-
function SparkleVisual() {
|
|
158
|
-
const nodes = [
|
|
159
|
-
{ x: 20, y: 30, delay: 0 },
|
|
160
|
-
{ x: 50, y: 15, delay: 0.1 },
|
|
161
|
-
{ x: 80, y: 35, delay: 0.2 },
|
|
162
|
-
{ x: 35, y: 65, delay: 0.15 },
|
|
163
|
-
{ x: 65, y: 55, delay: 0.25 },
|
|
164
|
-
{ x: 50, y: 80, delay: 0.3 },
|
|
165
|
-
]
|
|
166
|
-
const connections = [
|
|
167
|
-
[0, 1], [1, 2], [0, 3], [1, 4], [2, 4], [3, 5], [4, 5],
|
|
168
|
-
]
|
|
169
|
-
return (
|
|
170
|
-
<div className="relative h-24 w-full overflow-hidden rounded-lg bg-foreground/[0.02]">
|
|
171
|
-
<svg className="absolute inset-0 w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet">
|
|
172
|
-
{connections.map(([a, b], i) => (
|
|
173
|
-
<motion.line
|
|
174
|
-
key={i}
|
|
175
|
-
x1={nodes[a].x}
|
|
176
|
-
y1={nodes[a].y}
|
|
177
|
-
x2={nodes[b].x}
|
|
178
|
-
y2={nodes[b].y}
|
|
179
|
-
stroke="url(#sparkleGrad)"
|
|
180
|
-
strokeWidth="0.5"
|
|
181
|
-
initial={{ opacity: 0 }}
|
|
182
|
-
whileInView={{ opacity: 0.4 }}
|
|
183
|
-
viewport={{ once: true }}
|
|
184
|
-
transition={{ delay: 0.2 + i * 0.05, duration: 0.5 }}
|
|
185
|
-
/>
|
|
186
|
-
))}
|
|
187
|
-
<defs>
|
|
188
|
-
<linearGradient id="sparkleGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
189
|
-
<stop offset="0%" stopColor="rgb(139,92,246)" stopOpacity="0.6" />
|
|
190
|
-
<stop offset="100%" stopColor="rgb(168,85,247)" stopOpacity="0.3" />
|
|
191
|
-
</linearGradient>
|
|
192
|
-
</defs>
|
|
193
|
-
{nodes.map((node, i) => (
|
|
194
|
-
<motion.circle
|
|
195
|
-
key={i}
|
|
196
|
-
cx={node.x}
|
|
197
|
-
cy={node.y}
|
|
198
|
-
r="3"
|
|
199
|
-
fill="rgba(139,92,246,0.4)"
|
|
200
|
-
stroke="rgba(139,92,246,0.6)"
|
|
201
|
-
strokeWidth="0.5"
|
|
202
|
-
initial={{ scale: 0, opacity: 0 }}
|
|
203
|
-
whileInView={{ scale: 1, opacity: 1 }}
|
|
204
|
-
viewport={{ once: true }}
|
|
205
|
-
transition={{ delay: node.delay + 0.1, duration: 0.4, ease: 'easeOut' }}
|
|
206
|
-
/>
|
|
207
|
-
))}
|
|
208
|
-
{nodes.map((node, i) => (
|
|
209
|
-
<motion.circle
|
|
210
|
-
key={`glow-${i}`}
|
|
211
|
-
cx={node.x}
|
|
212
|
-
cy={node.y}
|
|
213
|
-
r="5"
|
|
214
|
-
fill="none"
|
|
215
|
-
stroke="rgba(139,92,246,0.15)"
|
|
216
|
-
strokeWidth="1"
|
|
217
|
-
initial={{ scale: 0 }}
|
|
218
|
-
whileInView={{ scale: [1, 1.8, 1] }}
|
|
219
|
-
viewport={{ once: true }}
|
|
220
|
-
transition={{ delay: node.delay + 0.5, duration: 2, repeat: Infinity, ease: 'easeInOut' }}
|
|
221
|
-
/>
|
|
222
|
-
))}
|
|
223
|
-
</svg>
|
|
224
|
-
</div>
|
|
225
|
-
)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
136
|
// ============================================================================
|
|
229
137
|
// Feature Card
|
|
230
138
|
// ============================================================================
|
|
@@ -86,7 +86,11 @@ export function MessageItem({
|
|
|
86
86
|
setIsEditing(true)
|
|
87
87
|
setEditContent(message.data.content)
|
|
88
88
|
}
|
|
89
|
-
|
|
89
|
+
// Deps intentionally limited to [forceEdit]: this reacts only to the
|
|
90
|
+
// forceEdit trigger. Including isEditing/message.data.content would re-fire
|
|
91
|
+
// on every keystroke and clobber the in-progress edit buffer.
|
|
92
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one-shot on forceEdit; adding isEditing/message.data.content would re-fire on keystrokes and clobber the edit buffer
|
|
93
|
+
}, [forceEdit])
|
|
90
94
|
|
|
91
95
|
const timestamp = new Date(message.createdAt)
|
|
92
96
|
const timeStr = timestamp.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
|
@@ -47,7 +47,11 @@ export function useChatChannel(channelName: string = DEFAULT_CHANNEL_NAME) {
|
|
|
47
47
|
description: 'Default chat channel',
|
|
48
48
|
})
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
// Deps intentionally limited to [user, status]: this is a one-shot init
|
|
51
|
+
// guarded by hasInitialized. Including defaultChannel/create/channelName
|
|
52
|
+
// would re-run and defeat the create-once semantics.
|
|
53
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one-shot init guarded by hasInitialized; adding defaultChannel/create/channelName would defeat create-once
|
|
54
|
+
}, [user, status])
|
|
51
55
|
|
|
52
56
|
// Wait for the members query to be ready before auto-joining.
|
|
53
57
|
useEffect(() => {
|
|
@@ -379,7 +379,7 @@ function readCollapsed(): boolean {
|
|
|
379
379
|
function writeCollapsed(value: boolean): void {
|
|
380
380
|
try {
|
|
381
381
|
localStorage.setItem(STORAGE_KEY, String(value))
|
|
382
|
-
} catch {}
|
|
382
|
+
} catch { /* storage unavailable (private mode/quota): non-critical */ }
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
// ============================================================================
|
|
@@ -13,7 +13,7 @@ import { useUser } from 'deepspace'
|
|
|
13
13
|
import { useUsers } from 'deepspace'
|
|
14
14
|
import { useQuery } from 'deepspace'
|
|
15
15
|
import { useMutations } from 'deepspace'
|
|
16
|
-
import { Button, Modal, Badge, EmptyState
|
|
16
|
+
import { Button, Modal, Badge, EmptyState } from '@/components/ui'
|
|
17
17
|
import { ROLES, type Role } from 'deepspace'
|
|
18
18
|
import { DIFFICULTY, DIFFICULTY_CONFIG, GRADE, GRADE_CONFIG, type Difficulty, type Grade, type BadgeVariant } from '../components/tasks/tasks-constants'
|
|
19
19
|
|
|
@@ -37,8 +37,8 @@ export default function TestPage() {
|
|
|
37
37
|
try {
|
|
38
38
|
const id = await createConfirmed({ title: 'Test Item', description: 'Created by test', status: 'draft', createdBy: user?.id ?? 'anonymous' })
|
|
39
39
|
setLastResult(`created:${id}`)
|
|
40
|
-
} catch (e:
|
|
41
|
-
setLastError(e.message
|
|
40
|
+
} catch (e: unknown) {
|
|
41
|
+
setLastError(e instanceof Error ? e.message : String(e))
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -47,8 +47,8 @@ export default function TestPage() {
|
|
|
47
47
|
try {
|
|
48
48
|
const id = await createConfirmed({ title: 'Public Item', description: 'Visible to anonymous', status: 'published', createdBy: user?.id ?? 'anonymous' })
|
|
49
49
|
setLastResult(`created:${id}`)
|
|
50
|
-
} catch (e:
|
|
51
|
-
setLastError(e.message
|
|
50
|
+
} catch (e: unknown) {
|
|
51
|
+
setLastError(e instanceof Error ? e.message : String(e))
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -57,8 +57,8 @@ export default function TestPage() {
|
|
|
57
57
|
try {
|
|
58
58
|
await putConfirmed(recordId, { title: 'Updated Title', description: 'Updated by test', status: 'draft', createdBy: user?.id ?? 'anonymous' })
|
|
59
59
|
setLastResult(`updated:${recordId}`)
|
|
60
|
-
} catch (e:
|
|
61
|
-
setLastError(e.message
|
|
60
|
+
} catch (e: unknown) {
|
|
61
|
+
setLastError(e instanceof Error ? e.message : String(e))
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -67,8 +67,8 @@ export default function TestPage() {
|
|
|
67
67
|
try {
|
|
68
68
|
await removeConfirmed(recordId)
|
|
69
69
|
setLastResult(`deleted:${recordId}`)
|
|
70
|
-
} catch (e:
|
|
71
|
-
setLastError(e.message
|
|
70
|
+
} catch (e: unknown) {
|
|
71
|
+
setLastError(e instanceof Error ? e.message : String(e))
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "DeepSpace SDK — build real-time collaborative apps on Cloudflare Workers",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"main": "./dist/index.js",
|
|
7
8
|
"types": "./dist/index.d.ts",
|
|
8
9
|
"bin": {
|
|
@@ -47,8 +48,7 @@
|
|
|
47
48
|
"hono": "^4.6.0",
|
|
48
49
|
"jose": "^6.1.0",
|
|
49
50
|
"smol-toml": "^1.6.1",
|
|
50
|
-
"yjs": "^13.6.0"
|
|
51
|
-
"zustand": "^5.0.0"
|
|
51
|
+
"yjs": "^13.6.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@ai-sdk/anthropic": "^2.0.0",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"tailwind-merge": "^3.5.0",
|
|
113
113
|
"tsup": "^8.3.0",
|
|
114
114
|
"typescript": "^5.7.0",
|
|
115
|
-
"vitest": "^
|
|
115
|
+
"vitest": "^4.1.0"
|
|
116
116
|
},
|
|
117
117
|
"keywords": [
|
|
118
118
|
"deepspace",
|