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.
Files changed (120) hide show
  1. package/dist/cli.js +331 -115
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.d.ts +199 -16
  4. package/dist/index.js +354 -166
  5. package/dist/index.js.map +1 -1
  6. package/dist/server.d.ts +242 -73
  7. package/dist/server.js +985 -374
  8. package/dist/server.js.map +1 -1
  9. package/dist/worker.d.ts +322 -73
  10. package/dist/worker.js +961 -343
  11. package/dist/worker.js.map +1 -1
  12. package/features/admin-page/feature.json +29 -0
  13. package/features/admin-page/src/AdminPage.tsx +292 -0
  14. package/features/admin-page/src/admin-schema.ts +20 -0
  15. package/features/ai-chat/feature.json +41 -0
  16. package/features/ai-chat/src/AiChatPage.tsx +692 -0
  17. package/features/ai-chat/src/ChatPanel.tsx +1098 -0
  18. package/features/ai-chat/src/ai-chat-schema.ts +11 -0
  19. package/features/canvas/feature.json +45 -0
  20. package/features/canvas/src/CanvasEditorPage.tsx +87 -0
  21. package/features/canvas/src/CanvasListPage.tsx +202 -0
  22. package/features/canvas/src/canvas-schema.ts +33 -0
  23. package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
  24. package/features/canvas/src/components/CanvasView.tsx +369 -0
  25. package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
  26. package/features/cron/feature.json +33 -0
  27. package/features/cron/src/CronLogPage.tsx +100 -0
  28. package/features/cron/src/cron.ts +25 -0
  29. package/features/docs/feature.json +96 -0
  30. package/features/docs/src/DocsEditorPage.tsx +1012 -0
  31. package/features/docs/src/DocsListPage.tsx +951 -0
  32. package/features/docs/src/DocsOutlinePanel.tsx +68 -0
  33. package/features/docs/src/DocsPresence.tsx +138 -0
  34. package/features/docs/src/InviteDialog.tsx +336 -0
  35. package/features/docs/src/LibrarySidebar.tsx +464 -0
  36. package/features/docs/src/docs-favorites.ts +18 -0
  37. package/features/docs/src/docs-library-types.ts +15 -0
  38. package/features/docs/src/docs-schema.ts +80 -0
  39. package/features/docs/src/docs-ui.css +309 -0
  40. package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
  41. package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
  42. package/features/docs/src/editor/useDocEditor.ts +206 -0
  43. package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
  44. package/features/file-attachments/feature.json +42 -0
  45. package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
  46. package/features/file-attachments/src/attachments-constants.ts +9 -0
  47. package/features/file-attachments/src/attachments-schema.ts +45 -0
  48. package/features/file-attachments/src/useFilePreview.ts +471 -0
  49. package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
  50. package/features/file-manager/feature.json +24 -0
  51. package/features/file-manager/src/FileManagerPage.tsx +330 -0
  52. package/features/integration-test/feature.json +18 -0
  53. package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
  54. package/features/integration-test/src/integration.spec.ts +42 -0
  55. package/features/items/feature.json +33 -0
  56. package/features/items/src/ItemsPage.tsx +325 -0
  57. package/features/items/src/items-constants.ts +10 -0
  58. package/features/items/src/items-schema.ts +27 -0
  59. package/features/kanban/feature.json +18 -0
  60. package/features/kanban/src/Kanban.tsx +283 -0
  61. package/features/landing/feature.json +62 -0
  62. package/features/landing/src/LandingPage.tsx +627 -0
  63. package/features/landing/src/primitives.tsx +389 -0
  64. package/features/landing/src/sections/FAQSection.tsx +110 -0
  65. package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
  66. package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
  67. package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
  68. package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
  69. package/features/landing/src/sections/StatsSection.tsx +31 -0
  70. package/features/landing/src/sections/TeamSection.tsx +86 -0
  71. package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
  72. package/features/landing/src/sections/VideoSection.tsx +71 -0
  73. package/features/leaderboard/feature.json +33 -0
  74. package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
  75. package/features/leaderboard/src/leaderboard-constants.ts +21 -0
  76. package/features/leaderboard/src/leaderboard-schema.ts +36 -0
  77. package/features/messaging/feature.json +111 -0
  78. package/features/messaging/src/ChatMultiPage.tsx +300 -0
  79. package/features/messaging/src/ChatPage.tsx +75 -0
  80. package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
  81. package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
  82. package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
  83. package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
  84. package/features/messaging/src/components/chat/MessageList.tsx +248 -0
  85. package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
  86. package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
  87. package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
  88. package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
  89. package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
  90. package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
  91. package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
  92. package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
  93. package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
  94. package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
  95. package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
  96. package/features/messaging/src/hooks/useChatChannel.ts +64 -0
  97. package/features/messaging/src/hooks/useLongPress.ts +74 -0
  98. package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
  99. package/features/messaging/src/messaging-schema.ts +25 -0
  100. package/features/presence-test/feature.json +21 -0
  101. package/features/presence-test/src/PresenceTestPage.tsx +120 -0
  102. package/features/sidebar/feature.json +19 -0
  103. package/features/sidebar/src/AppSidebar.tsx +558 -0
  104. package/features/tasks/feature.json +33 -0
  105. package/features/tasks/src/TasksPage.tsx +598 -0
  106. package/features/tasks/src/tasks-constants.ts +43 -0
  107. package/features/tasks/src/tasks-schema.ts +56 -0
  108. package/features/testing/feature.json +49 -0
  109. package/features/testing/src/TestPage.tsx +145 -0
  110. package/features/testing/src/cpu-burn-action.ts +51 -0
  111. package/features/testing/src/subrequest-burn-action.ts +53 -0
  112. package/features/testing/src/test-schema.ts +23 -0
  113. package/features/topbar/feature.json +18 -0
  114. package/features/topbar/src/Topbar.tsx +353 -0
  115. package/features/tree/feature.json +26 -0
  116. package/features/tree/src/TreeSidebar.tsx +607 -0
  117. package/features/tree/src/tree-constants.ts +68 -0
  118. package/features/tree/src/tree-hooks.ts +258 -0
  119. package/package.json +4 -2
  120. 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
+ }