deepspace 0.3.10 → 0.4.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/dist/cli.js +331 -115
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +199 -16
- package/dist/index.js +354 -166
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +242 -73
- package/dist/server.js +985 -374
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +322 -73
- package/dist/worker.js +961 -343
- package/dist/worker.js.map +1 -1
- package/features/admin-page/feature.json +29 -0
- package/features/admin-page/src/AdminPage.tsx +292 -0
- package/features/admin-page/src/admin-schema.ts +20 -0
- package/features/ai-chat/feature.json +41 -0
- package/features/ai-chat/src/AiChatPage.tsx +692 -0
- package/features/ai-chat/src/ChatPanel.tsx +1098 -0
- package/features/ai-chat/src/ai-chat-schema.ts +11 -0
- package/features/canvas/feature.json +45 -0
- package/features/canvas/src/CanvasEditorPage.tsx +87 -0
- package/features/canvas/src/CanvasListPage.tsx +202 -0
- package/features/canvas/src/canvas-schema.ts +33 -0
- package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
- package/features/canvas/src/components/CanvasView.tsx +369 -0
- package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
- package/features/cron/feature.json +33 -0
- package/features/cron/src/CronLogPage.tsx +100 -0
- package/features/cron/src/cron.ts +25 -0
- package/features/docs/feature.json +96 -0
- package/features/docs/src/DocsEditorPage.tsx +1012 -0
- package/features/docs/src/DocsListPage.tsx +951 -0
- package/features/docs/src/DocsOutlinePanel.tsx +68 -0
- package/features/docs/src/DocsPresence.tsx +138 -0
- package/features/docs/src/InviteDialog.tsx +336 -0
- package/features/docs/src/LibrarySidebar.tsx +464 -0
- package/features/docs/src/docs-favorites.ts +18 -0
- package/features/docs/src/docs-library-types.ts +15 -0
- package/features/docs/src/docs-schema.ts +80 -0
- package/features/docs/src/docs-ui.css +309 -0
- package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
- package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
- package/features/docs/src/editor/useDocEditor.ts +206 -0
- package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
- package/features/file-attachments/feature.json +42 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
- package/features/file-attachments/src/attachments-constants.ts +9 -0
- package/features/file-attachments/src/attachments-schema.ts +45 -0
- package/features/file-attachments/src/useFilePreview.ts +471 -0
- package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
- package/features/file-manager/feature.json +24 -0
- package/features/file-manager/src/FileManagerPage.tsx +330 -0
- package/features/integration-test/feature.json +18 -0
- package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
- package/features/integration-test/src/integration.spec.ts +42 -0
- package/features/items/feature.json +33 -0
- package/features/items/src/ItemsPage.tsx +325 -0
- package/features/items/src/items-constants.ts +10 -0
- package/features/items/src/items-schema.ts +27 -0
- package/features/kanban/feature.json +18 -0
- package/features/kanban/src/Kanban.tsx +283 -0
- package/features/landing/feature.json +62 -0
- package/features/landing/src/LandingPage.tsx +627 -0
- package/features/landing/src/primitives.tsx +389 -0
- package/features/landing/src/sections/FAQSection.tsx +110 -0
- package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
- package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
- package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
- package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
- package/features/landing/src/sections/StatsSection.tsx +31 -0
- package/features/landing/src/sections/TeamSection.tsx +86 -0
- package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
- package/features/landing/src/sections/VideoSection.tsx +71 -0
- package/features/leaderboard/feature.json +33 -0
- package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
- package/features/leaderboard/src/leaderboard-constants.ts +21 -0
- package/features/leaderboard/src/leaderboard-schema.ts +36 -0
- package/features/messaging/feature.json +111 -0
- package/features/messaging/src/ChatMultiPage.tsx +300 -0
- package/features/messaging/src/ChatPage.tsx +75 -0
- package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
- package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
- package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
- package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
- package/features/messaging/src/components/chat/MessageList.tsx +248 -0
- package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
- package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
- package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
- package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
- package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
- package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
- package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
- package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
- package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
- package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
- package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
- package/features/messaging/src/hooks/useChatChannel.ts +64 -0
- package/features/messaging/src/hooks/useLongPress.ts +74 -0
- package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
- package/features/messaging/src/messaging-schema.ts +25 -0
- package/features/presence-test/feature.json +21 -0
- package/features/presence-test/src/PresenceTestPage.tsx +120 -0
- package/features/sidebar/feature.json +19 -0
- package/features/sidebar/src/AppSidebar.tsx +558 -0
- package/features/tasks/feature.json +33 -0
- package/features/tasks/src/TasksPage.tsx +598 -0
- package/features/tasks/src/tasks-constants.ts +43 -0
- package/features/tasks/src/tasks-schema.ts +56 -0
- package/features/testing/feature.json +49 -0
- package/features/testing/src/TestPage.tsx +145 -0
- package/features/testing/src/cpu-burn-action.ts +51 -0
- package/features/testing/src/subrequest-burn-action.ts +53 -0
- package/features/testing/src/test-schema.ts +23 -0
- package/features/topbar/feature.json +18 -0
- package/features/topbar/src/Topbar.tsx +353 -0
- package/features/tree/feature.json +26 -0
- package/features/tree/src/TreeSidebar.tsx +607 -0
- package/features/tree/src/tree-constants.ts +68 -0
- package/features/tree/src/tree-hooks.ts +258 -0
- package/package.json +4 -2
- package/scripts/add-feature.cjs +654 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Chat Feature — Schema
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the SDK's pre-built AI chat collection schemas.
|
|
5
|
+
* Spread into the app's schemas array: ...aiChatSchemas
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CollectionSchema } from 'deepspace/worker'
|
|
9
|
+
import { AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA } from 'deepspace/worker'
|
|
10
|
+
|
|
11
|
+
export const aiChatSchemas: CollectionSchema[] = [AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "canvas",
|
|
3
|
+
"name": "Canvas",
|
|
4
|
+
"category": "display",
|
|
5
|
+
"description": "Collaborative spatial canvas with shapes, zoom/pan, and multi-user cursors",
|
|
6
|
+
"details": "SVG-based canvas with shape tools (rect, ellipse, line, text), color picker, zoom/pan, keyboard shortcuts, and real-time multi-user cursor display. Document metadata stored in RecordRoom, shape data via useCanvas hook.",
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"src": "src/CanvasListPage.tsx",
|
|
10
|
+
"dest": "src/pages/canvas/index.tsx"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"src": "src/CanvasEditorPage.tsx",
|
|
14
|
+
"dest": "src/pages/canvas/[docId].tsx"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"src": "src/canvas-schema.ts",
|
|
18
|
+
"dest": "src/schemas/canvas-schema.ts"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"src": "src/components/CanvasView.tsx",
|
|
22
|
+
"dest": "src/components/canvas/CanvasView.tsx"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"src": "src/components/CanvasToolbar.tsx",
|
|
26
|
+
"dest": "src/components/canvas/CanvasToolbar.tsx"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"src": "src/components/ShapeRenderer.tsx",
|
|
30
|
+
"dest": "src/components/canvas/ShapeRenderer.tsx"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"schema": {
|
|
34
|
+
"exportName": "canvasSchema",
|
|
35
|
+
"importPath": "./schemas/canvas-schema",
|
|
36
|
+
"spreadOperator": false
|
|
37
|
+
},
|
|
38
|
+
"route": {
|
|
39
|
+
"path": "/canvas",
|
|
40
|
+
"protected": true
|
|
41
|
+
},
|
|
42
|
+
"requires": [],
|
|
43
|
+
"instructions": [],
|
|
44
|
+
"patterns": []
|
|
45
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CanvasEditorPage — Canvas editor for a single document.
|
|
3
|
+
*
|
|
4
|
+
* Gets docId from URL params and renders the CanvasView editor
|
|
5
|
+
* connected to the CanvasRoom DO via useCanvas.
|
|
6
|
+
*
|
|
7
|
+
* Installed at: src/pages/canvas/[docId].tsx
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useMemo } from 'react'
|
|
11
|
+
import { useParams, useNavigate } from 'react-router-dom'
|
|
12
|
+
import { useUser } from 'deepspace'
|
|
13
|
+
import { useQuery } from 'deepspace'
|
|
14
|
+
import { CanvasView } from '../../components/canvas/CanvasView'
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
interface CanvasDocument {
|
|
21
|
+
title: string
|
|
22
|
+
ownerId: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Main Page
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
export default function CanvasEditorPage() {
|
|
30
|
+
const { docId } = useParams<{ docId: string }>()
|
|
31
|
+
const navigate = useNavigate()
|
|
32
|
+
const { user } = useUser()
|
|
33
|
+
|
|
34
|
+
const { records: canvases, status } = useQuery<CanvasDocument>('canvases', {
|
|
35
|
+
orderBy: 'createdAt',
|
|
36
|
+
orderDir: 'desc',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const selectedCanvas = useMemo(
|
|
40
|
+
() => docId ? canvases.find((c) => c.recordId === docId) : null,
|
|
41
|
+
[canvases, docId],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const isLoading = status === 'loading'
|
|
45
|
+
|
|
46
|
+
if (isLoading) {
|
|
47
|
+
return (
|
|
48
|
+
<div data-testid="canvas-page" className="h-full flex items-center justify-center bg-background">
|
|
49
|
+
<div className="w-8 h-8 border-2 border-primary/20 border-t-primary rounded-full animate-spin" />
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!selectedCanvas) {
|
|
55
|
+
return (
|
|
56
|
+
<div data-testid="canvas-page" className="h-full flex flex-col items-center justify-center bg-background gap-4">
|
|
57
|
+
<p className="text-muted-foreground">Canvas not found</p>
|
|
58
|
+
<button
|
|
59
|
+
onClick={() => navigate('/canvas')}
|
|
60
|
+
className="text-primary hover:underline"
|
|
61
|
+
>
|
|
62
|
+
Back to canvas list
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div data-testid="canvas-page" className="h-full flex flex-col bg-background">
|
|
70
|
+
{/* Back header */}
|
|
71
|
+
<div className="px-4 py-3 border-b border-border flex items-center gap-4 bg-card/60 backdrop-blur-md">
|
|
72
|
+
<button
|
|
73
|
+
data-testid="canvas-back"
|
|
74
|
+
onClick={() => navigate('/canvas')}
|
|
75
|
+
className="p-2 hover:bg-muted/60 rounded-lg transition-colors"
|
|
76
|
+
>
|
|
77
|
+
<svg className="w-5 h-5 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
78
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
79
|
+
</svg>
|
|
80
|
+
</button>
|
|
81
|
+
<h2 className="text-lg font-semibold text-foreground">{selectedCanvas.data.title}</h2>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<CanvasView key={selectedCanvas.recordId} docId={selectedCanvas.recordId} className="flex-1" />
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CanvasListPage — List of canvas documents.
|
|
3
|
+
*
|
|
4
|
+
* Shows all canvas documents (stored in RecordRoom) with
|
|
5
|
+
* create and delete functionality.
|
|
6
|
+
*
|
|
7
|
+
* Navigates to /canvas/:docId when a canvas is selected.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState } from 'react'
|
|
11
|
+
import { useNavigate } from 'react-router-dom'
|
|
12
|
+
import { useUser } from 'deepspace'
|
|
13
|
+
import { useQuery } from 'deepspace'
|
|
14
|
+
import { useMutations } from 'deepspace'
|
|
15
|
+
import { Button, Modal, EmptyState } from 'deepspace'
|
|
16
|
+
import { ROLES, type Role } from 'deepspace'
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Types
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
interface CanvasDocument {
|
|
23
|
+
title: string
|
|
24
|
+
ownerId: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Create Canvas Modal
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
interface CreateCanvasModalProps {
|
|
32
|
+
isOpen: boolean
|
|
33
|
+
onClose: () => void
|
|
34
|
+
onCreate: (title: string) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function CreateCanvasModal({ isOpen, onClose, onCreate }: CreateCanvasModalProps) {
|
|
38
|
+
const [title, setTitle] = useState('')
|
|
39
|
+
|
|
40
|
+
const handleSubmit = () => {
|
|
41
|
+
if (title.trim()) {
|
|
42
|
+
onCreate(title.trim())
|
|
43
|
+
setTitle('')
|
|
44
|
+
onClose()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Modal open={isOpen} onClose={onClose} size="sm">
|
|
50
|
+
<Modal.Header onClose={onClose}>
|
|
51
|
+
<Modal.Title>New Canvas</Modal.Title>
|
|
52
|
+
</Modal.Header>
|
|
53
|
+
<Modal.Body>
|
|
54
|
+
<div>
|
|
55
|
+
<label className="block text-sm font-medium text-muted-foreground mb-2">Title</label>
|
|
56
|
+
<input
|
|
57
|
+
data-testid="canvas-title-input"
|
|
58
|
+
type="text"
|
|
59
|
+
value={title}
|
|
60
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
61
|
+
placeholder="e.g., Wireframe Draft"
|
|
62
|
+
className="w-full px-3 py-2 bg-transparent border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-ring"
|
|
63
|
+
autoFocus
|
|
64
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
</Modal.Body>
|
|
68
|
+
<Modal.Footer>
|
|
69
|
+
<Button variant="secondary" onClick={onClose}>Cancel</Button>
|
|
70
|
+
<Button data-testid="canvas-create-submit" onClick={handleSubmit} disabled={!title.trim()}>Create</Button>
|
|
71
|
+
</Modal.Footer>
|
|
72
|
+
</Modal>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Main Page
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
export default function CanvasListPage() {
|
|
81
|
+
const { user } = useUser()
|
|
82
|
+
const navigate = useNavigate()
|
|
83
|
+
const userRole = (user?.role ?? ROLES.VIEWER) as Role
|
|
84
|
+
const canCreate = userRole === ROLES.MEMBER || userRole === ROLES.ADMIN
|
|
85
|
+
|
|
86
|
+
const [showCreateModal, setShowCreateModal] = useState(false)
|
|
87
|
+
|
|
88
|
+
const { records: canvases, status } = useQuery<CanvasDocument>('canvases', {
|
|
89
|
+
orderBy: 'createdAt',
|
|
90
|
+
orderDir: 'desc',
|
|
91
|
+
})
|
|
92
|
+
const { create, remove } = useMutations<CanvasDocument>('canvases')
|
|
93
|
+
|
|
94
|
+
const handleCreate = async (title: string) => {
|
|
95
|
+
await create({ title, ownerId: user!.id })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const handleDelete = async (canvasId: string) => {
|
|
99
|
+
if (confirm('Delete this canvas?')) {
|
|
100
|
+
await remove(canvasId)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const isLoading = status === 'loading'
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div data-testid="canvas-page" className="h-full bg-background overflow-y-auto">
|
|
108
|
+
{/* Header */}
|
|
109
|
+
<div className="bg-card/60 backdrop-blur-md border-b border-border sticky top-0 z-10">
|
|
110
|
+
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
111
|
+
<div className="flex items-center justify-between">
|
|
112
|
+
<div>
|
|
113
|
+
<h1 className="text-2xl font-bold text-foreground">Canvas</h1>
|
|
114
|
+
<p className="text-muted-foreground mt-1">Collaborative spatial editing</p>
|
|
115
|
+
</div>
|
|
116
|
+
{canCreate && (
|
|
117
|
+
<Button data-testid="canvas-create-btn" onClick={() => setShowCreateModal(true)}>
|
|
118
|
+
<svg className="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
119
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
120
|
+
</svg>
|
|
121
|
+
New Canvas
|
|
122
|
+
</Button>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Canvas list */}
|
|
129
|
+
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
130
|
+
{isLoading ? (
|
|
131
|
+
<div className="flex items-center justify-center py-16">
|
|
132
|
+
<div className="w-8 h-8 border-2 border-primary/20 border-t-primary rounded-full animate-spin" />
|
|
133
|
+
</div>
|
|
134
|
+
) : canvases.length === 0 ? (
|
|
135
|
+
<EmptyState
|
|
136
|
+
title="No canvases yet"
|
|
137
|
+
description={canCreate ? 'Create your first canvas to start collaborating' : 'No canvases have been created yet'}
|
|
138
|
+
icon={
|
|
139
|
+
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
140
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="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" />
|
|
141
|
+
</svg>
|
|
142
|
+
}
|
|
143
|
+
/>
|
|
144
|
+
) : (
|
|
145
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
146
|
+
{canvases.map((canvas) => {
|
|
147
|
+
const isOwner = canvas.data.ownerId === user?.id
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
key={canvas.recordId}
|
|
151
|
+
data-testid={`canvas-card-${canvas.recordId}`}
|
|
152
|
+
className="group p-4 bg-card/60 rounded-xl border border-border hover:border-primary/30 hover:bg-muted/40 transition-all cursor-pointer"
|
|
153
|
+
onClick={() => navigate(`/canvas/${canvas.recordId}`)}
|
|
154
|
+
>
|
|
155
|
+
<div className="flex items-start justify-between gap-3">
|
|
156
|
+
<div className="flex-1 min-w-0">
|
|
157
|
+
<div className="flex items-center gap-2 mb-1">
|
|
158
|
+
<svg className="w-4 h-4 text-primary shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
159
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="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" />
|
|
160
|
+
</svg>
|
|
161
|
+
<h3 className="font-medium text-foreground truncate">{canvas.data.title}</h3>
|
|
162
|
+
</div>
|
|
163
|
+
<p className="text-xs text-muted-foreground">
|
|
164
|
+
{new Date(canvas.createdAt).toLocaleDateString()}
|
|
165
|
+
{isOwner && ' · You'}
|
|
166
|
+
</p>
|
|
167
|
+
</div>
|
|
168
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
169
|
+
{isOwner && (
|
|
170
|
+
<button
|
|
171
|
+
onClick={(e) => {
|
|
172
|
+
e.stopPropagation()
|
|
173
|
+
handleDelete(canvas.recordId)
|
|
174
|
+
}}
|
|
175
|
+
className="p-2 text-muted-foreground hover:text-destructive hover:bg-destructive/20 rounded-lg transition-colors opacity-0 group-hover:opacity-100"
|
|
176
|
+
title="Delete"
|
|
177
|
+
>
|
|
178
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
179
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
180
|
+
</svg>
|
|
181
|
+
</button>
|
|
182
|
+
)}
|
|
183
|
+
<svg className="w-5 h-5 text-muted-foreground group-hover:text-primary transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
184
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
185
|
+
</svg>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
)
|
|
190
|
+
})}
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<CreateCanvasModal
|
|
196
|
+
isOpen={showCreateModal}
|
|
197
|
+
onClose={() => setShowCreateModal(false)}
|
|
198
|
+
onCreate={handleCreate}
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Feature - Schema
|
|
3
|
+
*
|
|
4
|
+
* A canvas document collection for listing/managing canvas documents.
|
|
5
|
+
* The actual shape data lives in the CanvasRoom DO (Yjs-backed),
|
|
6
|
+
* not in RecordRoom. This schema is just for document metadata.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CollectionSchema } from 'deepspace/worker'
|
|
10
|
+
|
|
11
|
+
export const canvasSchema: CollectionSchema = {
|
|
12
|
+
name: 'canvases',
|
|
13
|
+
columns: [
|
|
14
|
+
{ name: 'title', storage: 'text', interpretation: 'plain', required: true },
|
|
15
|
+
{ name: 'ownerId', storage: 'text', interpretation: 'plain', required: true, userBound: true, immutable: true },
|
|
16
|
+
],
|
|
17
|
+
ownerField: 'ownerId',
|
|
18
|
+
permissions: {
|
|
19
|
+
viewer: {
|
|
20
|
+
read: true,
|
|
21
|
+
create: false,
|
|
22
|
+
update: false,
|
|
23
|
+
delete: false,
|
|
24
|
+
},
|
|
25
|
+
member: {
|
|
26
|
+
read: true,
|
|
27
|
+
create: true,
|
|
28
|
+
update: 'own',
|
|
29
|
+
delete: 'own',
|
|
30
|
+
},
|
|
31
|
+
admin: { read: true, create: true, update: true, delete: true },
|
|
32
|
+
},
|
|
33
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CanvasToolbar — Shape creation tools, selection, delete, color picker.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type CanvasTool = 'select' | 'rect' | 'ellipse' | 'line' | 'text'
|
|
6
|
+
|
|
7
|
+
const PRESET_COLORS = [
|
|
8
|
+
'#6366f1', // indigo
|
|
9
|
+
'#ef4444', // red
|
|
10
|
+
'#22c55e', // green
|
|
11
|
+
'#3b82f6', // blue
|
|
12
|
+
'#f59e0b', // amber
|
|
13
|
+
'#8b5cf6', // violet
|
|
14
|
+
'#ec4899', // pink
|
|
15
|
+
'#14b8a6', // teal
|
|
16
|
+
'#64748b', // slate
|
|
17
|
+
'#000000', // black
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
interface CanvasToolbarProps {
|
|
21
|
+
activeTool: CanvasTool
|
|
22
|
+
activeColor: string
|
|
23
|
+
hasSelection: boolean
|
|
24
|
+
onToolChange: (tool: CanvasTool) => void
|
|
25
|
+
onColorChange: (color: string) => void
|
|
26
|
+
onDelete: () => void
|
|
27
|
+
onUndo: () => void
|
|
28
|
+
onRedo: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const tools: { id: CanvasTool; label: string; icon: React.ReactNode }[] = [
|
|
32
|
+
{
|
|
33
|
+
id: 'select',
|
|
34
|
+
label: 'Select',
|
|
35
|
+
icon: (
|
|
36
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
37
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122" />
|
|
38
|
+
</svg>
|
|
39
|
+
),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'rect',
|
|
43
|
+
label: 'Rectangle',
|
|
44
|
+
icon: (
|
|
45
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
46
|
+
<rect x="3" y="3" width="18" height="18" rx="2" strokeWidth={2} />
|
|
47
|
+
</svg>
|
|
48
|
+
),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'ellipse',
|
|
52
|
+
label: 'Ellipse',
|
|
53
|
+
icon: (
|
|
54
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
55
|
+
<ellipse cx="12" cy="12" rx="9" ry="7" strokeWidth={2} />
|
|
56
|
+
</svg>
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'line',
|
|
61
|
+
label: 'Line',
|
|
62
|
+
icon: (
|
|
63
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
64
|
+
<line x1="5" y1="19" x2="19" y2="5" strokeWidth={2} strokeLinecap="round" />
|
|
65
|
+
</svg>
|
|
66
|
+
),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'text',
|
|
70
|
+
label: 'Text',
|
|
71
|
+
icon: (
|
|
72
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
73
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M8 6v12m0 0h8" />
|
|
74
|
+
</svg>
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
export function CanvasToolbar({
|
|
80
|
+
activeTool,
|
|
81
|
+
activeColor,
|
|
82
|
+
hasSelection,
|
|
83
|
+
onToolChange,
|
|
84
|
+
onColorChange,
|
|
85
|
+
onDelete,
|
|
86
|
+
onUndo,
|
|
87
|
+
onRedo,
|
|
88
|
+
}: CanvasToolbarProps) {
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
data-testid="canvas-toolbar"
|
|
92
|
+
className="flex items-center gap-3 px-4 py-2 bg-card/80 backdrop-blur-md border-b border-border"
|
|
93
|
+
>
|
|
94
|
+
{/* Shape tools */}
|
|
95
|
+
<div className="flex items-center gap-1">
|
|
96
|
+
{tools.map((tool) => (
|
|
97
|
+
<button
|
|
98
|
+
key={tool.id}
|
|
99
|
+
data-testid={`tool-${tool.id}`}
|
|
100
|
+
onClick={() => onToolChange(tool.id)}
|
|
101
|
+
title={tool.label}
|
|
102
|
+
className={`p-2 rounded-lg transition-colors ${
|
|
103
|
+
activeTool === tool.id
|
|
104
|
+
? 'bg-primary text-primary-foreground'
|
|
105
|
+
: 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
|
|
106
|
+
}`}
|
|
107
|
+
>
|
|
108
|
+
{tool.icon}
|
|
109
|
+
</button>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div className="w-px h-6 bg-border" />
|
|
114
|
+
|
|
115
|
+
{/* Color picker */}
|
|
116
|
+
<div className="flex items-center gap-1">
|
|
117
|
+
{PRESET_COLORS.map((color) => (
|
|
118
|
+
<button
|
|
119
|
+
key={color}
|
|
120
|
+
data-testid={`color-${color}`}
|
|
121
|
+
onClick={() => onColorChange(color)}
|
|
122
|
+
title={color}
|
|
123
|
+
className={`w-5 h-5 rounded-full border-2 transition-transform ${
|
|
124
|
+
activeColor === color ? 'border-foreground scale-125' : 'border-transparent hover:scale-110'
|
|
125
|
+
}`}
|
|
126
|
+
style={{ backgroundColor: color }}
|
|
127
|
+
/>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div className="w-px h-6 bg-border" />
|
|
132
|
+
|
|
133
|
+
{/* Actions */}
|
|
134
|
+
<div className="flex items-center gap-1">
|
|
135
|
+
<button
|
|
136
|
+
data-testid="canvas-undo"
|
|
137
|
+
onClick={onUndo}
|
|
138
|
+
title="Undo"
|
|
139
|
+
className="p-2 rounded-lg text-muted-foreground hover:bg-muted/60 hover:text-foreground transition-colors"
|
|
140
|
+
>
|
|
141
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
142
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a5 5 0 015 5v2M3 10l4-4m-4 4l4 4" />
|
|
143
|
+
</svg>
|
|
144
|
+
</button>
|
|
145
|
+
<button
|
|
146
|
+
data-testid="canvas-redo"
|
|
147
|
+
onClick={onRedo}
|
|
148
|
+
title="Redo"
|
|
149
|
+
className="p-2 rounded-lg text-muted-foreground hover:bg-muted/60 hover:text-foreground transition-colors"
|
|
150
|
+
>
|
|
151
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
152
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 10H11a5 5 0 00-5 5v2m15-7l-4-4m4 4l-4 4" />
|
|
153
|
+
</svg>
|
|
154
|
+
</button>
|
|
155
|
+
|
|
156
|
+
<button
|
|
157
|
+
data-testid="canvas-delete"
|
|
158
|
+
onClick={onDelete}
|
|
159
|
+
disabled={!hasSelection}
|
|
160
|
+
title="Delete selected"
|
|
161
|
+
className="p-2 rounded-lg text-muted-foreground hover:bg-destructive/20 hover:text-destructive transition-colors disabled:opacity-30 disabled:pointer-events-none"
|
|
162
|
+
>
|
|
163
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
164
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
165
|
+
</svg>
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
)
|
|
170
|
+
}
|