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,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Items Page
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates:
|
|
5
|
+
* - useQuery for fetching records
|
|
6
|
+
* - useMutations for CRUD operations
|
|
7
|
+
* - useUser for current user
|
|
8
|
+
* - Role-based UI elements
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState, useMemo } from 'react'
|
|
12
|
+
import { useUser } from 'deepspace'
|
|
13
|
+
import { useQuery } from 'deepspace'
|
|
14
|
+
import { useMutations } from 'deepspace'
|
|
15
|
+
import { Button, Modal, EmptyState, Badge } from 'deepspace'
|
|
16
|
+
import { ROLES, type Role } from 'deepspace'
|
|
17
|
+
import { ITEM_STATUS } from '../components/items/items-constants'
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Types
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
interface Item {
|
|
24
|
+
title: string
|
|
25
|
+
description?: string
|
|
26
|
+
status: string
|
|
27
|
+
ownerId: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ItemsPageProps {
|
|
31
|
+
className?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Create Item Modal
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
interface CreateItemModalProps {
|
|
39
|
+
isOpen: boolean
|
|
40
|
+
onClose: () => void
|
|
41
|
+
onCreate: (title: string, description: string) => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function CreateItemModal({ isOpen, onClose, onCreate }: CreateItemModalProps) {
|
|
45
|
+
const [title, setTitle] = useState('')
|
|
46
|
+
const [description, setDescription] = useState('')
|
|
47
|
+
|
|
48
|
+
const handleSubmit = () => {
|
|
49
|
+
if (title.trim()) {
|
|
50
|
+
onCreate(title.trim(), description.trim())
|
|
51
|
+
setTitle('')
|
|
52
|
+
setDescription('')
|
|
53
|
+
onClose()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Modal open={isOpen} onClose={onClose} size="sm">
|
|
59
|
+
<Modal.Header onClose={onClose}>
|
|
60
|
+
<Modal.Title>Create Item</Modal.Title>
|
|
61
|
+
</Modal.Header>
|
|
62
|
+
<Modal.Body>
|
|
63
|
+
<div className="space-y-4">
|
|
64
|
+
<div>
|
|
65
|
+
<label className="block text-sm font-medium text-muted-foreground mb-2">Title</label>
|
|
66
|
+
<input
|
|
67
|
+
type="text"
|
|
68
|
+
value={title}
|
|
69
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
70
|
+
placeholder="Enter item title"
|
|
71
|
+
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"
|
|
72
|
+
autoFocus
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
<div>
|
|
76
|
+
<label className="block text-sm font-medium text-muted-foreground mb-2">Description</label>
|
|
77
|
+
<textarea
|
|
78
|
+
value={description}
|
|
79
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
80
|
+
placeholder="Enter description (optional)"
|
|
81
|
+
rows={3}
|
|
82
|
+
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 resize-none"
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</Modal.Body>
|
|
87
|
+
<Modal.Footer>
|
|
88
|
+
<Button variant="secondary" onClick={onClose}>Cancel</Button>
|
|
89
|
+
<Button onClick={handleSubmit} disabled={!title.trim()}>Create</Button>
|
|
90
|
+
</Modal.Footer>
|
|
91
|
+
</Modal>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Item Card Component
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
interface ItemCardProps {
|
|
100
|
+
item: { recordId: string; data: Item; createdAt: string }
|
|
101
|
+
isOwner: boolean
|
|
102
|
+
onToggleStatus: () => void
|
|
103
|
+
onDelete: () => void
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ItemCard({ item, isOwner, onToggleStatus, onDelete }: ItemCardProps) {
|
|
107
|
+
const isArchived = item.data.status === ITEM_STATUS.ARCHIVED
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className={`p-4 bg-card/60 rounded-xl border ${isArchived ? 'border-border/30 opacity-60' : 'border-border'}`}>
|
|
111
|
+
<div className="flex items-start justify-between gap-3">
|
|
112
|
+
<div className="flex-1 min-w-0">
|
|
113
|
+
<div className="flex items-center gap-2 mb-1">
|
|
114
|
+
<h3 className={`font-medium ${isArchived ? 'text-muted-foreground line-through' : 'text-foreground'}`}>
|
|
115
|
+
{item.data.title}
|
|
116
|
+
</h3>
|
|
117
|
+
<Badge
|
|
118
|
+
variant={isArchived ? 'secondary' : 'success'}
|
|
119
|
+
size="sm"
|
|
120
|
+
>
|
|
121
|
+
{isArchived ? 'Archived' : 'Active'}
|
|
122
|
+
</Badge>
|
|
123
|
+
</div>
|
|
124
|
+
{item.data.description && (
|
|
125
|
+
<p className="text-sm text-muted-foreground line-clamp-2">{item.data.description}</p>
|
|
126
|
+
)}
|
|
127
|
+
<p className="text-xs text-muted-foreground mt-2">
|
|
128
|
+
{new Date(item.createdAt).toLocaleDateString()}
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{isOwner && (
|
|
133
|
+
<div className="flex items-center gap-1">
|
|
134
|
+
<button
|
|
135
|
+
onClick={onToggleStatus}
|
|
136
|
+
className="p-2 text-muted-foreground hover:text-foreground hover:bg-muted/60 rounded-lg transition-colors"
|
|
137
|
+
title={isArchived ? 'Restore' : 'Archive'}
|
|
138
|
+
>
|
|
139
|
+
{isArchived ? (
|
|
140
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
141
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
142
|
+
</svg>
|
|
143
|
+
) : (
|
|
144
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
145
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
|
146
|
+
</svg>
|
|
147
|
+
)}
|
|
148
|
+
</button>
|
|
149
|
+
<button
|
|
150
|
+
onClick={onDelete}
|
|
151
|
+
className="p-2 text-muted-foreground hover:text-destructive hover:bg-destructive/20 rounded-lg transition-colors"
|
|
152
|
+
title="Delete"
|
|
153
|
+
>
|
|
154
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
155
|
+
<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" />
|
|
156
|
+
</svg>
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// Main Page
|
|
167
|
+
// ============================================================================
|
|
168
|
+
|
|
169
|
+
export default function ItemsPage({ className }: ItemsPageProps) {
|
|
170
|
+
const { user } = useUser()
|
|
171
|
+
const userRole = (user?.role ?? ROLES.VIEWER) as Role
|
|
172
|
+
const canCreate = userRole === ROLES.MEMBER || userRole === ROLES.ADMIN
|
|
173
|
+
|
|
174
|
+
const [showCreateModal, setShowCreateModal] = useState(false)
|
|
175
|
+
|
|
176
|
+
// Query all items
|
|
177
|
+
const { records: items, status } = useQuery<Item>('items', {
|
|
178
|
+
orderBy: 'createdAt',
|
|
179
|
+
orderDir: 'desc',
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// Mutations for items
|
|
183
|
+
const { create, put, remove } = useMutations<Item>('items')
|
|
184
|
+
|
|
185
|
+
// Separate own items and others
|
|
186
|
+
const { myItems, otherItems } = useMemo(() => {
|
|
187
|
+
const my: typeof items = []
|
|
188
|
+
const other: typeof items = []
|
|
189
|
+
|
|
190
|
+
items.forEach(item => {
|
|
191
|
+
if (item.data.ownerId === user?.id) {
|
|
192
|
+
my.push(item)
|
|
193
|
+
} else {
|
|
194
|
+
other.push(item)
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
return { myItems: my, otherItems: other }
|
|
199
|
+
}, [items, user?.id])
|
|
200
|
+
|
|
201
|
+
const handleCreate = async (title: string, description: string) => {
|
|
202
|
+
await create({
|
|
203
|
+
title,
|
|
204
|
+
description,
|
|
205
|
+
status: ITEM_STATUS.ACTIVE,
|
|
206
|
+
ownerId: user!.id,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const handleToggleStatus = async (itemId: string, currentStatus: string) => {
|
|
211
|
+
const newStatus = currentStatus === ITEM_STATUS.ACTIVE
|
|
212
|
+
? ITEM_STATUS.ARCHIVED
|
|
213
|
+
: ITEM_STATUS.ACTIVE
|
|
214
|
+
|
|
215
|
+
const item = items.find(i => i.recordId === itemId)
|
|
216
|
+
if (item) {
|
|
217
|
+
await put(itemId, { ...item.data, status: newStatus })
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const handleDelete = async (itemId: string) => {
|
|
222
|
+
if (confirm('Are you sure you want to delete this item?')) {
|
|
223
|
+
await remove(itemId)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const isLoading = status === 'loading'
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div className={`h-full bg-background overflow-y-auto ${className ?? ''}`}>
|
|
231
|
+
{/* Header */}
|
|
232
|
+
<div className="bg-card/60 backdrop-blur-md border-b border-border sticky top-0 z-10">
|
|
233
|
+
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
234
|
+
<div className="flex items-center justify-between">
|
|
235
|
+
<div>
|
|
236
|
+
<h1 className="text-2xl font-bold text-foreground">Welcome, {user?.name ?? 'Guest'}</h1>
|
|
237
|
+
<p className="text-muted-foreground mt-1">
|
|
238
|
+
{canCreate
|
|
239
|
+
? 'Create and manage your items below'
|
|
240
|
+
: 'You can view items but need member access to create'
|
|
241
|
+
}
|
|
242
|
+
</p>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
{canCreate && (
|
|
246
|
+
<Button onClick={() => setShowCreateModal(true)}>
|
|
247
|
+
<svg className="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
248
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
249
|
+
</svg>
|
|
250
|
+
New Item
|
|
251
|
+
</Button>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{/* Content */}
|
|
258
|
+
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
259
|
+
{isLoading ? (
|
|
260
|
+
<div className="flex items-center justify-center py-16">
|
|
261
|
+
<div className="w-8 h-8 border-2 border-primary/20 border-t-primary rounded-full animate-spin" />
|
|
262
|
+
</div>
|
|
263
|
+
) : items.length === 0 ? (
|
|
264
|
+
<EmptyState
|
|
265
|
+
title="No items yet"
|
|
266
|
+
description={canCreate
|
|
267
|
+
? "Create your first item to get started"
|
|
268
|
+
: "No items have been created yet"
|
|
269
|
+
}
|
|
270
|
+
icon={
|
|
271
|
+
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
272
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
|
273
|
+
</svg>
|
|
274
|
+
}
|
|
275
|
+
/>
|
|
276
|
+
) : (
|
|
277
|
+
<div className="space-y-8">
|
|
278
|
+
{/* My Items */}
|
|
279
|
+
{myItems.length > 0 && (
|
|
280
|
+
<section>
|
|
281
|
+
<h2 className="text-lg font-semibold text-foreground mb-4">My Items</h2>
|
|
282
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
283
|
+
{myItems.map(item => (
|
|
284
|
+
<ItemCard
|
|
285
|
+
key={item.recordId}
|
|
286
|
+
item={item}
|
|
287
|
+
isOwner={true}
|
|
288
|
+
onToggleStatus={() => handleToggleStatus(item.recordId, item.data.status)}
|
|
289
|
+
onDelete={() => handleDelete(item.recordId)}
|
|
290
|
+
/>
|
|
291
|
+
))}
|
|
292
|
+
</div>
|
|
293
|
+
</section>
|
|
294
|
+
)}
|
|
295
|
+
|
|
296
|
+
{/* Other Items */}
|
|
297
|
+
{otherItems.length > 0 && (
|
|
298
|
+
<section>
|
|
299
|
+
<h2 className="text-lg font-semibold text-foreground mb-4">All Items</h2>
|
|
300
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
301
|
+
{otherItems.map(item => (
|
|
302
|
+
<ItemCard
|
|
303
|
+
key={item.recordId}
|
|
304
|
+
item={item}
|
|
305
|
+
isOwner={false}
|
|
306
|
+
onToggleStatus={() => {}}
|
|
307
|
+
onDelete={() => {}}
|
|
308
|
+
/>
|
|
309
|
+
))}
|
|
310
|
+
</div>
|
|
311
|
+
</section>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{/* Create Modal */}
|
|
318
|
+
<CreateItemModal
|
|
319
|
+
isOpen={showCreateModal}
|
|
320
|
+
onClose={() => setShowCreateModal(false)}
|
|
321
|
+
onCreate={handleCreate}
|
|
322
|
+
/>
|
|
323
|
+
</div>
|
|
324
|
+
)
|
|
325
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Items Feature - Schema
|
|
3
|
+
*
|
|
4
|
+
* A simple collection with ownership demonstrating:
|
|
5
|
+
* - ownerField for 'own' permission checks
|
|
6
|
+
* - userBound fields that auto-populate with current user ID
|
|
7
|
+
* - immutable fields that cannot change after creation
|
|
8
|
+
* - Basic CRUD with role-based permissions
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { CollectionSchema } from 'deepspace/worker'
|
|
12
|
+
|
|
13
|
+
export const itemsSchema: CollectionSchema = {
|
|
14
|
+
name: 'items',
|
|
15
|
+
columns: [
|
|
16
|
+
{ name: 'title', storage: 'text', interpretation: 'plain', required: true },
|
|
17
|
+
{ name: 'description', storage: 'text', interpretation: 'plain' },
|
|
18
|
+
{ name: 'status', storage: 'text', interpretation: { kind: 'select', options: ['active', 'archived'] }, default: 'active' },
|
|
19
|
+
{ name: 'ownerId', storage: 'text', interpretation: 'plain', required: true, userBound: true, immutable: true },
|
|
20
|
+
],
|
|
21
|
+
ownerField: 'ownerId',
|
|
22
|
+
permissions: {
|
|
23
|
+
viewer: { read: true, create: false, update: false, delete: false },
|
|
24
|
+
member: { read: true, create: true, update: 'own', delete: 'own' },
|
|
25
|
+
admin: { read: true, create: true, update: true, delete: true },
|
|
26
|
+
},
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "kanban",
|
|
3
|
+
"name": "Kanban Board",
|
|
4
|
+
"category": "display",
|
|
5
|
+
"description": "Drag-and-drop kanban board component with columns and cards",
|
|
6
|
+
"details": "Generic KanbanBoard component with drag-and-drop between columns, customizable card rendering, and a useKanban hook for state management. No schema needed -- it is a pure UI component that works with any data source.",
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"src": "src/Kanban.tsx",
|
|
10
|
+
"dest": "src/components/kanban/Kanban.tsx"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"requires": [],
|
|
14
|
+
"instructions": [
|
|
15
|
+
"Import KanbanBoard and useKanban from 'src/components/kanban/Kanban' (adjust the relative path to match where you use it)"
|
|
16
|
+
],
|
|
17
|
+
"patterns": []
|
|
18
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import React, { useState, useCallback, ReactNode, DragEvent } from 'react'
|
|
2
|
+
import { MoreHorizontal, Plus, GripVertical } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Types
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
export interface KanbanItem {
|
|
9
|
+
id: string
|
|
10
|
+
title: string
|
|
11
|
+
description?: string
|
|
12
|
+
columnId: string
|
|
13
|
+
[key: string]: unknown
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface KanbanColumn {
|
|
17
|
+
id: string
|
|
18
|
+
title: string
|
|
19
|
+
color?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// KanbanBoard - Main container
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
interface KanbanBoardProps<T extends KanbanItem> {
|
|
27
|
+
columns: KanbanColumn[]
|
|
28
|
+
items: T[]
|
|
29
|
+
onMoveItem: (itemId: string, toColumnId: string) => void
|
|
30
|
+
onAddItem?: (columnId: string) => void
|
|
31
|
+
renderItem?: (item: T) => ReactNode
|
|
32
|
+
className?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function KanbanBoard<T extends KanbanItem>({
|
|
36
|
+
columns,
|
|
37
|
+
items,
|
|
38
|
+
onMoveItem,
|
|
39
|
+
onAddItem,
|
|
40
|
+
renderItem,
|
|
41
|
+
className = '',
|
|
42
|
+
}: KanbanBoardProps<T>) {
|
|
43
|
+
const [draggedItem, setDraggedItem] = useState<string | null>(null)
|
|
44
|
+
const [dragOverColumn, setDragOverColumn] = useState<string | null>(null)
|
|
45
|
+
|
|
46
|
+
const handleDragStart = useCallback((itemId: string) => {
|
|
47
|
+
setDraggedItem(itemId)
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
const handleDragEnd = useCallback(() => {
|
|
51
|
+
setDraggedItem(null)
|
|
52
|
+
setDragOverColumn(null)
|
|
53
|
+
}, [])
|
|
54
|
+
|
|
55
|
+
const handleDragOver = useCallback((e: DragEvent, columnId: string) => {
|
|
56
|
+
e.preventDefault()
|
|
57
|
+
setDragOverColumn(columnId)
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
const handleDrop = useCallback((e: DragEvent, columnId: string) => {
|
|
61
|
+
e.preventDefault()
|
|
62
|
+
if (draggedItem) {
|
|
63
|
+
onMoveItem(draggedItem, columnId)
|
|
64
|
+
}
|
|
65
|
+
setDraggedItem(null)
|
|
66
|
+
setDragOverColumn(null)
|
|
67
|
+
}, [draggedItem, onMoveItem])
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className={`flex gap-4 overflow-x-auto pb-4 ${className}`}>
|
|
71
|
+
{columns.map((column) => {
|
|
72
|
+
const columnItems = items.filter((item) => item.columnId === column.id)
|
|
73
|
+
const isDragOver = dragOverColumn === column.id
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<KanbanColumnComponent
|
|
77
|
+
key={column.id}
|
|
78
|
+
column={column}
|
|
79
|
+
items={columnItems}
|
|
80
|
+
isDragOver={isDragOver}
|
|
81
|
+
onDragOver={(e) => handleDragOver(e, column.id)}
|
|
82
|
+
onDrop={(e) => handleDrop(e, column.id)}
|
|
83
|
+
onDragLeave={() => setDragOverColumn(null)}
|
|
84
|
+
onAddItem={onAddItem ? () => onAddItem(column.id) : undefined}
|
|
85
|
+
>
|
|
86
|
+
{columnItems.map((item) => (
|
|
87
|
+
<KanbanCard
|
|
88
|
+
key={item.id}
|
|
89
|
+
item={item}
|
|
90
|
+
isDragging={draggedItem === item.id}
|
|
91
|
+
onDragStart={() => handleDragStart(item.id)}
|
|
92
|
+
onDragEnd={handleDragEnd}
|
|
93
|
+
renderContent={renderItem}
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
</KanbanColumnComponent>
|
|
97
|
+
)
|
|
98
|
+
})}
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// KanbanColumn
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
interface KanbanColumnComponentProps<T extends KanbanItem> {
|
|
108
|
+
column: KanbanColumn
|
|
109
|
+
items: T[]
|
|
110
|
+
children: ReactNode
|
|
111
|
+
isDragOver: boolean
|
|
112
|
+
onDragOver: (e: DragEvent) => void
|
|
113
|
+
onDrop: (e: DragEvent) => void
|
|
114
|
+
onDragLeave: () => void
|
|
115
|
+
onAddItem?: () => void
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function KanbanColumnComponent<T extends KanbanItem>({
|
|
119
|
+
column,
|
|
120
|
+
items,
|
|
121
|
+
children,
|
|
122
|
+
isDragOver,
|
|
123
|
+
onDragOver,
|
|
124
|
+
onDrop,
|
|
125
|
+
onDragLeave,
|
|
126
|
+
onAddItem,
|
|
127
|
+
}: KanbanColumnComponentProps<T>) {
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
className={`
|
|
131
|
+
flex flex-col w-72 min-w-72 bg-muted/50 rounded-lg
|
|
132
|
+
transition-colors duration-200
|
|
133
|
+
${isDragOver ? 'bg-primary/10 ring-2 ring-primary/20' : ''}
|
|
134
|
+
`}
|
|
135
|
+
onDragOver={onDragOver}
|
|
136
|
+
onDrop={onDrop}
|
|
137
|
+
onDragLeave={onDragLeave}
|
|
138
|
+
>
|
|
139
|
+
{/* Header */}
|
|
140
|
+
<div className="flex items-center justify-between p-3 border-b border-border">
|
|
141
|
+
<div className="flex items-center gap-2">
|
|
142
|
+
{column.color && (
|
|
143
|
+
<span
|
|
144
|
+
className="w-3 h-3 rounded-full"
|
|
145
|
+
style={{ backgroundColor: column.color }}
|
|
146
|
+
/>
|
|
147
|
+
)}
|
|
148
|
+
<h3 className="font-medium text-foreground">{column.title}</h3>
|
|
149
|
+
<span className="px-2 py-0.5 text-xs bg-muted rounded-full text-muted-foreground">
|
|
150
|
+
{items.length}
|
|
151
|
+
</span>
|
|
152
|
+
</div>
|
|
153
|
+
<button className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground">
|
|
154
|
+
<MoreHorizontal className="w-4 h-4" />
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Items */}
|
|
159
|
+
<div className="flex-1 p-2 space-y-2 min-h-[200px] overflow-y-auto">
|
|
160
|
+
{children}
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Add button */}
|
|
164
|
+
{onAddItem && (
|
|
165
|
+
<button
|
|
166
|
+
onClick={onAddItem}
|
|
167
|
+
className="flex items-center gap-2 p-3 text-sm text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
|
|
168
|
+
>
|
|
169
|
+
<Plus className="w-4 h-4" />
|
|
170
|
+
Add item
|
|
171
|
+
</button>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// KanbanCard
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
interface KanbanCardProps<T extends KanbanItem> {
|
|
182
|
+
item: T
|
|
183
|
+
isDragging: boolean
|
|
184
|
+
onDragStart: () => void
|
|
185
|
+
onDragEnd: () => void
|
|
186
|
+
renderContent?: (item: T) => ReactNode
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function KanbanCard<T extends KanbanItem>({
|
|
190
|
+
item,
|
|
191
|
+
isDragging,
|
|
192
|
+
onDragStart,
|
|
193
|
+
onDragEnd,
|
|
194
|
+
renderContent,
|
|
195
|
+
}: KanbanCardProps<T>) {
|
|
196
|
+
return (
|
|
197
|
+
<div
|
|
198
|
+
draggable
|
|
199
|
+
onDragStart={onDragStart}
|
|
200
|
+
onDragEnd={onDragEnd}
|
|
201
|
+
className={`
|
|
202
|
+
group bg-card border border-border rounded-lg p-3 cursor-grab
|
|
203
|
+
hover:border-border/80 hover:shadow-sm transition-all
|
|
204
|
+
${isDragging ? 'opacity-50 rotate-2 shadow-lg' : ''}
|
|
205
|
+
`}
|
|
206
|
+
>
|
|
207
|
+
{renderContent ? (
|
|
208
|
+
renderContent(item)
|
|
209
|
+
) : (
|
|
210
|
+
<>
|
|
211
|
+
<div className="flex items-start gap-2">
|
|
212
|
+
<GripVertical className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity shrink-0 mt-0.5" />
|
|
213
|
+
<div className="flex-1 min-w-0">
|
|
214
|
+
<p className="font-medium text-foreground text-sm">{item.title}</p>
|
|
215
|
+
{item.description && (
|
|
216
|
+
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
|
217
|
+
{item.description}
|
|
218
|
+
</p>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// useKanban - Hook for managing kanban state
|
|
230
|
+
// ============================================================================
|
|
231
|
+
|
|
232
|
+
interface UseKanbanOptions<T extends KanbanItem> {
|
|
233
|
+
initialItems: T[]
|
|
234
|
+
onItemMoved?: (item: T, fromColumn: string, toColumn: string) => void
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface UseKanbanReturn<T extends KanbanItem> {
|
|
238
|
+
items: T[]
|
|
239
|
+
moveItem: (itemId: string, toColumnId: string) => void
|
|
240
|
+
addItem: (item: T) => void
|
|
241
|
+
updateItem: (itemId: string, updates: Partial<T>) => void
|
|
242
|
+
removeItem: (itemId: string) => void
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function useKanban<T extends KanbanItem>({
|
|
246
|
+
initialItems,
|
|
247
|
+
onItemMoved,
|
|
248
|
+
}: UseKanbanOptions<T>): UseKanbanReturn<T> {
|
|
249
|
+
const [items, setItems] = useState<T[]>(initialItems)
|
|
250
|
+
|
|
251
|
+
const moveItem = useCallback((itemId: string, toColumnId: string) => {
|
|
252
|
+
setItems((prev) => {
|
|
253
|
+
const item = prev.find((i) => i.id === itemId)
|
|
254
|
+
if (!item || item.columnId === toColumnId) return prev
|
|
255
|
+
|
|
256
|
+
const fromColumn = item.columnId
|
|
257
|
+
const updated = prev.map((i) =>
|
|
258
|
+
i.id === itemId ? { ...i, columnId: toColumnId } : i
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
onItemMoved?.({ ...item, columnId: toColumnId } as T, fromColumn, toColumnId)
|
|
262
|
+
return updated
|
|
263
|
+
})
|
|
264
|
+
}, [onItemMoved])
|
|
265
|
+
|
|
266
|
+
const addItem = useCallback((item: T) => {
|
|
267
|
+
setItems((prev) => [...prev, item])
|
|
268
|
+
}, [])
|
|
269
|
+
|
|
270
|
+
const updateItem = useCallback((itemId: string, updates: Partial<T>) => {
|
|
271
|
+
setItems((prev) =>
|
|
272
|
+
prev.map((i) => (i.id === itemId ? { ...i, ...updates } : i))
|
|
273
|
+
)
|
|
274
|
+
}, [])
|
|
275
|
+
|
|
276
|
+
const removeItem = useCallback((itemId: string) => {
|
|
277
|
+
setItems((prev) => prev.filter((i) => i.id !== itemId))
|
|
278
|
+
}, [])
|
|
279
|
+
|
|
280
|
+
return { items, moveItem, addItem, updateItem, removeItem }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export default KanbanBoard
|