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,607 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree Sidebar - Hierarchical Navigation + Drag & Drop
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Main navigation views (All, Today, Upcoming, Logbook, Trash)
|
|
6
|
+
* - Hierarchical project tree with drag-and-drop reordering
|
|
7
|
+
* - User list with task assignment via drag-and-drop
|
|
8
|
+
* - Collapsible sections
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { useState, useEffect, useCallback, type ReactNode } from 'react'
|
|
12
|
+
import { VIEWS, VIEW_CONFIG, type CurrentView, type ViewType } from './tree-constants'
|
|
13
|
+
import { useTreeDragDrop } from './tree-hooks'
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
interface ProjectNode {
|
|
20
|
+
id: string
|
|
21
|
+
title: string
|
|
22
|
+
color?: string
|
|
23
|
+
parentId?: string | null
|
|
24
|
+
children?: ProjectNode[]
|
|
25
|
+
totalTaskCount?: number
|
|
26
|
+
totalCompleted?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface User {
|
|
30
|
+
id: string
|
|
31
|
+
name?: string
|
|
32
|
+
email?: string
|
|
33
|
+
color?: string
|
|
34
|
+
imageUrl?: string
|
|
35
|
+
isCustom?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface TaskCounts {
|
|
39
|
+
[key: string]: { total: number; completed: number; uncompleted: number } | number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface TreeSidebarProps {
|
|
43
|
+
currentView: CurrentView
|
|
44
|
+
onViewChange: (view: CurrentView) => void
|
|
45
|
+
taskCounts?: TaskCounts
|
|
46
|
+
projectTree?: ProjectNode[]
|
|
47
|
+
onAddProject?: (parentId: string | null) => void
|
|
48
|
+
onEditProject?: (project: ProjectNode) => void
|
|
49
|
+
onReorderProject?: (draggedId: string, targetId: string | null, position: 'before' | 'after' | 'inside') => void
|
|
50
|
+
onItemDrop?: (itemId: string, projectId: string) => void
|
|
51
|
+
onItemDropOnUser?: (itemId: string, userId: string | null) => void
|
|
52
|
+
onItemDragEnd?: () => void
|
|
53
|
+
allUsers?: User[]
|
|
54
|
+
currentUser?: User
|
|
55
|
+
onManageUsers?: () => void
|
|
56
|
+
width?: number
|
|
57
|
+
isReadOnly?: boolean
|
|
58
|
+
getDisplayName?: (user: User) => string
|
|
59
|
+
header?: ReactNode
|
|
60
|
+
footer?: ReactNode
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Icon Component
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
interface IconProps {
|
|
68
|
+
name: string
|
|
69
|
+
size?: number
|
|
70
|
+
className?: string
|
|
71
|
+
style?: React.CSSProperties
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function Icon({ name, size = 16, className = '', style }: IconProps) {
|
|
75
|
+
const iconMap: Record<string, ReactNode> = {
|
|
76
|
+
list: (
|
|
77
|
+
<svg className={className} style={style} width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
78
|
+
<line x1="8" y1="6" x2="21" y2="6" /><line x1="8" y1="12" x2="21" y2="12" /><line x1="8" y1="18" x2="21" y2="18" />
|
|
79
|
+
<line x1="3" y1="6" x2="3.01" y2="6" /><line x1="3" y1="12" x2="3.01" y2="12" /><line x1="3" y1="18" x2="3.01" y2="18" />
|
|
80
|
+
</svg>
|
|
81
|
+
),
|
|
82
|
+
star: (
|
|
83
|
+
<svg className={className} style={style} width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
84
|
+
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
|
85
|
+
</svg>
|
|
86
|
+
),
|
|
87
|
+
calendar: (
|
|
88
|
+
<svg className={className} style={style} width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
89
|
+
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" /><line x1="16" y1="2" x2="16" y2="6" /><line x1="8" y1="2" x2="8" y2="6" /><line x1="3" y1="10" x2="21" y2="10" />
|
|
90
|
+
</svg>
|
|
91
|
+
),
|
|
92
|
+
'book-open': (
|
|
93
|
+
<svg className={className} style={style} width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
94
|
+
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" /><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
|
|
95
|
+
</svg>
|
|
96
|
+
),
|
|
97
|
+
'trash-2': (
|
|
98
|
+
<svg className={className} style={style} width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
99
|
+
<polyline points="3 6 5 6 21 6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
100
|
+
</svg>
|
|
101
|
+
),
|
|
102
|
+
'chevron-right': (
|
|
103
|
+
<svg className={className} style={style} width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
104
|
+
<polyline points="9 18 15 12 9 6" />
|
|
105
|
+
</svg>
|
|
106
|
+
),
|
|
107
|
+
'chevron-down': (
|
|
108
|
+
<svg className={className} style={style} width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
109
|
+
<polyline points="6 9 12 15 18 9" />
|
|
110
|
+
</svg>
|
|
111
|
+
),
|
|
112
|
+
plus: (
|
|
113
|
+
<svg className={className} style={style} width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
114
|
+
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
|
|
115
|
+
</svg>
|
|
116
|
+
),
|
|
117
|
+
pencil: (
|
|
118
|
+
<svg className={className} style={style} width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
119
|
+
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" />
|
|
120
|
+
</svg>
|
|
121
|
+
),
|
|
122
|
+
users: (
|
|
123
|
+
<svg className={className} style={style} width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
124
|
+
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M23 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
|
125
|
+
</svg>
|
|
126
|
+
),
|
|
127
|
+
'user-plus': (
|
|
128
|
+
<svg className={className} style={style} width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
129
|
+
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /><circle cx="8.5" cy="7" r="4" /><line x1="20" y1="8" x2="20" y2="14" /><line x1="23" y1="11" x2="17" y2="11" />
|
|
130
|
+
</svg>
|
|
131
|
+
),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return <>{iconMap[name] || <span style={{ width: size, height: size }} />}</>
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// Project Tree Node
|
|
139
|
+
// ============================================================================
|
|
140
|
+
|
|
141
|
+
interface ProjectTreeNodeProps {
|
|
142
|
+
node: ProjectNode
|
|
143
|
+
depth: number
|
|
144
|
+
currentView: CurrentView
|
|
145
|
+
onViewChange: (view: CurrentView) => void
|
|
146
|
+
expandedProjects: Record<string, boolean>
|
|
147
|
+
toggleExpand: (id: string) => void
|
|
148
|
+
onAddProject?: (parentId: string | null) => void
|
|
149
|
+
onEditProject?: (project: ProjectNode) => void
|
|
150
|
+
dragState: ReturnType<typeof useTreeDragDrop>['dragState']
|
|
151
|
+
dragHandlers: ReturnType<typeof useTreeDragDrop>['handlers']
|
|
152
|
+
onItemDrop?: (itemId: string, projectId: string) => void
|
|
153
|
+
onItemDragEnd?: () => void
|
|
154
|
+
isReadOnly?: boolean
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function ProjectTreeNode({
|
|
158
|
+
node, depth, currentView, onViewChange, expandedProjects, toggleExpand,
|
|
159
|
+
onAddProject, onEditProject, dragState, dragHandlers, onItemDrop, onItemDragEnd, isReadOnly,
|
|
160
|
+
}: ProjectTreeNodeProps) {
|
|
161
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
162
|
+
const [isItemDragOver, setIsItemDragOver] = useState(false)
|
|
163
|
+
|
|
164
|
+
const isActive = currentView.type === VIEWS.PROJECT && currentView.id === node.id
|
|
165
|
+
const isExpanded = expandedProjects[node.id]
|
|
166
|
+
const hasChildren = node.children && node.children.length > 0
|
|
167
|
+
const isDragging = dragState.draggingId === node.id
|
|
168
|
+
const isDropTarget = dragState.dropTargetId === node.id
|
|
169
|
+
const dropPosition = isDropTarget ? dragState.dropPosition : null
|
|
170
|
+
|
|
171
|
+
const handleDragOver = (e: React.DragEvent) => {
|
|
172
|
+
if (isReadOnly) return
|
|
173
|
+
const isItemDrag = e.dataTransfer.types.includes('application/x-item')
|
|
174
|
+
if (isItemDrag) {
|
|
175
|
+
e.preventDefault()
|
|
176
|
+
e.stopPropagation()
|
|
177
|
+
e.dataTransfer.dropEffect = 'move'
|
|
178
|
+
if (!isItemDragOver) setIsItemDragOver(true)
|
|
179
|
+
} else {
|
|
180
|
+
dragHandlers.onDragOver(e, node.id)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const handleDragLeave = (e: React.DragEvent) => {
|
|
185
|
+
if (isReadOnly) return
|
|
186
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
|
187
|
+
setIsItemDragOver(false)
|
|
188
|
+
dragHandlers.onDragLeave(e)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const handleDrop = (e: React.DragEvent) => {
|
|
193
|
+
if (isReadOnly) return
|
|
194
|
+
const itemId = e.dataTransfer.getData('application/x-item')
|
|
195
|
+
if (itemId && onItemDrop) {
|
|
196
|
+
e.preventDefault()
|
|
197
|
+
e.stopPropagation()
|
|
198
|
+
onItemDrop(itemId, node.id)
|
|
199
|
+
setIsItemDragOver(false)
|
|
200
|
+
if (onItemDragEnd) onItemDragEnd()
|
|
201
|
+
} else {
|
|
202
|
+
dragHandlers.onDrop(e, node.id)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div style={{ opacity: isDragging ? 0.5 : 1 }}>
|
|
208
|
+
<div
|
|
209
|
+
draggable={!isReadOnly}
|
|
210
|
+
onDragStart={(e) => !isReadOnly && dragHandlers.onDragStart(e, node)}
|
|
211
|
+
onDragOver={handleDragOver}
|
|
212
|
+
onDragLeave={handleDragLeave}
|
|
213
|
+
onDrop={handleDrop}
|
|
214
|
+
onDragEnd={dragHandlers.onDragEnd}
|
|
215
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
216
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
217
|
+
className={`
|
|
218
|
+
flex items-center gap-1 py-1.5 px-2 rounded-md transition-colors cursor-grab relative
|
|
219
|
+
${isActive ? 'bg-primary/10' : ''}
|
|
220
|
+
${isItemDragOver ? 'bg-primary/10' : ''}
|
|
221
|
+
`}
|
|
222
|
+
style={{ paddingLeft: 8 + depth * 16 }}
|
|
223
|
+
>
|
|
224
|
+
{dropPosition === 'before' && (
|
|
225
|
+
<div className="absolute left-2 right-2 top-0 h-0.5 bg-primary rounded" />
|
|
226
|
+
)}
|
|
227
|
+
{dropPosition === 'after' && (
|
|
228
|
+
<div className="absolute left-2 right-2 bottom-0 h-0.5 bg-primary rounded" />
|
|
229
|
+
)}
|
|
230
|
+
{dropPosition === 'inside' && (
|
|
231
|
+
<div className="absolute inset-1 border-2 border-dashed border-primary rounded pointer-events-none" />
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
<button
|
|
235
|
+
onClick={(e) => { e.stopPropagation(); toggleExpand(node.id) }}
|
|
236
|
+
className="w-4 h-4 flex items-center justify-center flex-shrink-0"
|
|
237
|
+
>
|
|
238
|
+
{hasChildren ? (
|
|
239
|
+
<Icon name={isExpanded ? 'chevron-down' : 'chevron-right'} size={12} className="text-muted-foreground" />
|
|
240
|
+
) : (
|
|
241
|
+
<span className="w-3" />
|
|
242
|
+
)}
|
|
243
|
+
</button>
|
|
244
|
+
|
|
245
|
+
<span
|
|
246
|
+
className="w-2.5 h-2.5 rounded-full flex-shrink-0"
|
|
247
|
+
style={{ backgroundColor: node.color || 'var(--color-primary)' }}
|
|
248
|
+
/>
|
|
249
|
+
|
|
250
|
+
<button
|
|
251
|
+
onClick={() => onViewChange({ type: VIEWS.PROJECT, id: node.id })}
|
|
252
|
+
className="flex-1 text-left min-w-0"
|
|
253
|
+
>
|
|
254
|
+
<span className="text-sm text-foreground truncate block">{node.title}</span>
|
|
255
|
+
</button>
|
|
256
|
+
|
|
257
|
+
{(node.totalTaskCount ?? 0) > 0 && (
|
|
258
|
+
<span className="text-xs text-muted-foreground flex-shrink-0">
|
|
259
|
+
{(node.totalTaskCount ?? 0) - (node.totalCompleted ?? 0)}/{node.totalTaskCount}
|
|
260
|
+
</span>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{!isReadOnly && (
|
|
264
|
+
<div className={`flex items-center gap-0.5 ml-auto transition-opacity ${isHovered ? 'opacity-100' : 'opacity-0'}`}>
|
|
265
|
+
{onAddProject && (
|
|
266
|
+
<button
|
|
267
|
+
onClick={(e) => { e.stopPropagation(); onAddProject(node.id) }}
|
|
268
|
+
className="w-5 h-5 flex items-center justify-center rounded hover:bg-muted"
|
|
269
|
+
title="Add sub-project"
|
|
270
|
+
>
|
|
271
|
+
<Icon name="plus" size={12} className="text-muted-foreground" />
|
|
272
|
+
</button>
|
|
273
|
+
)}
|
|
274
|
+
{onEditProject && (
|
|
275
|
+
<button
|
|
276
|
+
onClick={(e) => { e.stopPropagation(); onEditProject(node) }}
|
|
277
|
+
className="w-5 h-5 flex items-center justify-center rounded hover:bg-muted"
|
|
278
|
+
title="Edit project"
|
|
279
|
+
>
|
|
280
|
+
<Icon name="pencil" size={12} className="text-muted-foreground" />
|
|
281
|
+
</button>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{isExpanded && hasChildren && (
|
|
288
|
+
<div>
|
|
289
|
+
{node.children!.map(child => (
|
|
290
|
+
<ProjectTreeNode
|
|
291
|
+
key={child.id}
|
|
292
|
+
node={child}
|
|
293
|
+
depth={depth + 1}
|
|
294
|
+
currentView={currentView}
|
|
295
|
+
onViewChange={onViewChange}
|
|
296
|
+
expandedProjects={expandedProjects}
|
|
297
|
+
toggleExpand={toggleExpand}
|
|
298
|
+
onAddProject={onAddProject}
|
|
299
|
+
onEditProject={onEditProject}
|
|
300
|
+
dragState={dragState}
|
|
301
|
+
dragHandlers={dragHandlers}
|
|
302
|
+
onItemDrop={onItemDrop}
|
|
303
|
+
onItemDragEnd={onItemDragEnd}
|
|
304
|
+
isReadOnly={isReadOnly}
|
|
305
|
+
/>
|
|
306
|
+
))}
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ============================================================================
|
|
314
|
+
// User List Item
|
|
315
|
+
// ============================================================================
|
|
316
|
+
|
|
317
|
+
interface UserListItemProps {
|
|
318
|
+
user: User
|
|
319
|
+
isActive: boolean
|
|
320
|
+
isYou: boolean
|
|
321
|
+
displayName: string
|
|
322
|
+
taskCount?: { total: number; completed: number }
|
|
323
|
+
onViewChange: (view: CurrentView) => void
|
|
324
|
+
onItemDropOnUser?: (itemId: string, userId: string | null) => void
|
|
325
|
+
onItemDragEnd?: () => void
|
|
326
|
+
isReadOnly?: boolean
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function UserListItem({
|
|
330
|
+
user, isActive, isYou, displayName, taskCount, onViewChange,
|
|
331
|
+
onItemDropOnUser, onItemDragEnd, isReadOnly,
|
|
332
|
+
}: UserListItemProps) {
|
|
333
|
+
const [isItemDragOver, setIsItemDragOver] = useState(false)
|
|
334
|
+
|
|
335
|
+
const handleDragOver = (e: React.DragEvent) => {
|
|
336
|
+
if (isReadOnly) return
|
|
337
|
+
const isItemDrag = e.dataTransfer.types.includes('application/x-item')
|
|
338
|
+
if (isItemDrag) {
|
|
339
|
+
e.preventDefault()
|
|
340
|
+
e.stopPropagation()
|
|
341
|
+
e.dataTransfer.dropEffect = 'move'
|
|
342
|
+
if (!isItemDragOver) setIsItemDragOver(true)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const handleDragLeave = (e: React.DragEvent) => {
|
|
347
|
+
if (isReadOnly) return
|
|
348
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
|
349
|
+
setIsItemDragOver(false)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const handleDrop = (e: React.DragEvent) => {
|
|
354
|
+
if (isReadOnly) return
|
|
355
|
+
const itemId = e.dataTransfer.getData('application/x-item')
|
|
356
|
+
if (itemId && onItemDropOnUser) {
|
|
357
|
+
e.preventDefault()
|
|
358
|
+
e.stopPropagation()
|
|
359
|
+
onItemDropOnUser(itemId, user.id)
|
|
360
|
+
setIsItemDragOver(false)
|
|
361
|
+
if (onItemDragEnd) onItemDragEnd()
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const total = taskCount?.total ?? 0
|
|
366
|
+
const completed = taskCount?.completed ?? 0
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<button
|
|
370
|
+
onClick={() => onViewChange({ type: VIEWS.USER, id: user.id })}
|
|
371
|
+
onDragOver={handleDragOver}
|
|
372
|
+
onDragLeave={handleDragLeave}
|
|
373
|
+
onDrop={handleDrop}
|
|
374
|
+
className={`
|
|
375
|
+
w-full flex items-center gap-2.5 py-1.5 px-2.5 rounded-md transition-colors text-left
|
|
376
|
+
${isActive ? 'bg-black/5' : 'hover:bg-black/[0.02]'}
|
|
377
|
+
${isItemDragOver ? 'bg-primary/10 ring-2 ring-primary/30' : ''}
|
|
378
|
+
`}
|
|
379
|
+
>
|
|
380
|
+
<span
|
|
381
|
+
className="w-5 h-5 rounded-full flex items-center justify-center text-[11px] font-semibold text-white flex-shrink-0"
|
|
382
|
+
style={{ backgroundColor: user.color || 'var(--color-primary)' }}
|
|
383
|
+
>
|
|
384
|
+
{displayName?.charAt(0).toUpperCase() || '?'}
|
|
385
|
+
</span>
|
|
386
|
+
<span className="flex-1 text-sm text-foreground truncate">{displayName}</span>
|
|
387
|
+
{isYou && (
|
|
388
|
+
<span className="text-[10px] font-medium px-2 py-0.5 rounded-full bg-primary/10 text-primary">
|
|
389
|
+
You
|
|
390
|
+
</span>
|
|
391
|
+
)}
|
|
392
|
+
{total > 0 && (
|
|
393
|
+
<span className="text-xs text-muted-foreground">{total - completed}/{total}</span>
|
|
394
|
+
)}
|
|
395
|
+
</button>
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ============================================================================
|
|
400
|
+
// Main Sidebar Component
|
|
401
|
+
// ============================================================================
|
|
402
|
+
|
|
403
|
+
export default function TreeSidebar({
|
|
404
|
+
currentView, onViewChange, taskCounts = {}, projectTree = [],
|
|
405
|
+
onAddProject, onEditProject, onReorderProject, onItemDrop, onItemDropOnUser,
|
|
406
|
+
onItemDragEnd, allUsers = [], currentUser, onManageUsers, width = 260,
|
|
407
|
+
isReadOnly = false, getDisplayName, header, footer,
|
|
408
|
+
}: TreeSidebarProps) {
|
|
409
|
+
const [expandedSections, setExpandedSections] = useState({ projects: true, users: true })
|
|
410
|
+
const [expandedProjects, setExpandedProjects] = useState<Record<string, boolean>>({})
|
|
411
|
+
|
|
412
|
+
const { dragState, handlers: dragHandlers } = useTreeDragDrop(onReorderProject)
|
|
413
|
+
|
|
414
|
+
// Auto-expand parents of active project
|
|
415
|
+
useEffect(() => {
|
|
416
|
+
if (currentView.type === VIEWS.PROJECT && currentView.id) {
|
|
417
|
+
const findPath = (nodes: ProjectNode[], targetId: string, path: string[] = []): string[] | null => {
|
|
418
|
+
for (const node of nodes) {
|
|
419
|
+
if (node.id === targetId) return path
|
|
420
|
+
if (node.children) {
|
|
421
|
+
const found = findPath(node.children, targetId, [...path, node.id])
|
|
422
|
+
if (found) return found
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return null
|
|
426
|
+
}
|
|
427
|
+
const path = findPath(projectTree, currentView.id)
|
|
428
|
+
if (path) {
|
|
429
|
+
setExpandedProjects(prev => {
|
|
430
|
+
const next = { ...prev }
|
|
431
|
+
path.forEach(id => { next[id] = true })
|
|
432
|
+
return next
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}, [currentView, projectTree])
|
|
437
|
+
|
|
438
|
+
const toggleSection = (section: 'projects' | 'users') => {
|
|
439
|
+
setExpandedSections(prev => ({ ...prev, [section]: !prev[section] }))
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const toggleExpand = useCallback((projectId: string) => {
|
|
443
|
+
setExpandedProjects(prev => ({ ...prev, [projectId]: !prev[projectId] }))
|
|
444
|
+
}, [])
|
|
445
|
+
|
|
446
|
+
const mainViews: Array<{ type: ViewType; icon: string; color: string }> = [
|
|
447
|
+
{ type: VIEWS.ALL, icon: 'list', color: '#6366f1' },
|
|
448
|
+
{ type: VIEWS.TODAY, icon: 'star', color: '#f59e0b' },
|
|
449
|
+
{ type: VIEWS.UPCOMING, icon: 'calendar', color: '#ef4444' },
|
|
450
|
+
{ type: VIEWS.LOGBOOK, icon: 'book-open', color: '#9ca3af' },
|
|
451
|
+
{ type: VIEWS.TRASH, icon: 'trash-2', color: '#9ca3af' },
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<div
|
|
456
|
+
className="h-full bg-card border-r border-border flex flex-col overflow-auto flex-shrink-0"
|
|
457
|
+
style={{ width }}
|
|
458
|
+
>
|
|
459
|
+
{header}
|
|
460
|
+
|
|
461
|
+
<nav className="p-3 flex flex-col gap-0.5">
|
|
462
|
+
{mainViews.map(({ type, icon, color }) => {
|
|
463
|
+
const config = VIEW_CONFIG[type]
|
|
464
|
+
const isActive = currentView.type === type
|
|
465
|
+
const countData = taskCounts[type]
|
|
466
|
+
const count = typeof countData === 'number' ? countData : (countData?.total ?? 0)
|
|
467
|
+
const completed = typeof countData === 'number' ? 0 : (countData?.completed ?? 0)
|
|
468
|
+
const uncompleted = count - completed
|
|
469
|
+
const countDisplay = count > 0 ? `${uncompleted}/${count}` : null
|
|
470
|
+
|
|
471
|
+
return (
|
|
472
|
+
<button
|
|
473
|
+
key={type}
|
|
474
|
+
onClick={() => onViewChange({ type })}
|
|
475
|
+
className={`
|
|
476
|
+
flex items-center gap-2.5 px-2.5 py-2 rounded-lg w-full text-left transition-colors
|
|
477
|
+
${isActive ? 'bg-black/[0.06]' : 'hover:bg-black/[0.02]'}
|
|
478
|
+
`}
|
|
479
|
+
>
|
|
480
|
+
<span className="w-6 h-6 rounded-md flex items-center justify-center">
|
|
481
|
+
<Icon name={icon} size={16} className={isActive ? 'text-primary' : ''} style={{ color }} />
|
|
482
|
+
</span>
|
|
483
|
+
<span className={`flex-1 text-sm ${isActive ? 'font-semibold text-foreground' : 'text-muted-foreground'}`}>
|
|
484
|
+
{config?.title ?? type}
|
|
485
|
+
</span>
|
|
486
|
+
{countDisplay && (
|
|
487
|
+
<span className="text-xs text-muted-foreground">
|
|
488
|
+
{countDisplay}
|
|
489
|
+
</span>
|
|
490
|
+
)}
|
|
491
|
+
</button>
|
|
492
|
+
)
|
|
493
|
+
})}
|
|
494
|
+
</nav>
|
|
495
|
+
|
|
496
|
+
<div className="h-px bg-border mx-4 my-2" />
|
|
497
|
+
|
|
498
|
+
{/* Projects Section */}
|
|
499
|
+
<div className="flex flex-col">
|
|
500
|
+
<button onClick={() => toggleSection('projects')} className="flex items-center justify-between px-3 py-2">
|
|
501
|
+
<div className="flex items-center gap-1.5">
|
|
502
|
+
<Icon name={expandedSections.projects ? 'chevron-down' : 'chevron-right'} size={14} className="text-muted-foreground" />
|
|
503
|
+
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Projects</span>
|
|
504
|
+
</div>
|
|
505
|
+
{!isReadOnly && onAddProject && (
|
|
506
|
+
<button
|
|
507
|
+
onClick={(e) => { e.stopPropagation(); onAddProject(null) }}
|
|
508
|
+
className="w-5 h-5 rounded flex items-center justify-center hover:bg-muted"
|
|
509
|
+
title="Add Project"
|
|
510
|
+
>
|
|
511
|
+
<Icon name="plus" size={14} className="text-muted-foreground" />
|
|
512
|
+
</button>
|
|
513
|
+
)}
|
|
514
|
+
</button>
|
|
515
|
+
|
|
516
|
+
{expandedSections.projects && (
|
|
517
|
+
<div
|
|
518
|
+
className="px-1 pb-2 min-h-[40px]"
|
|
519
|
+
onDragOver={(e) => e.preventDefault()}
|
|
520
|
+
onDrop={dragHandlers.onRootDrop}
|
|
521
|
+
>
|
|
522
|
+
{projectTree.length > 0 ? (
|
|
523
|
+
projectTree.map(node => (
|
|
524
|
+
<ProjectTreeNode
|
|
525
|
+
key={node.id}
|
|
526
|
+
node={node}
|
|
527
|
+
depth={0}
|
|
528
|
+
currentView={currentView}
|
|
529
|
+
onViewChange={onViewChange}
|
|
530
|
+
expandedProjects={expandedProjects}
|
|
531
|
+
toggleExpand={toggleExpand}
|
|
532
|
+
onAddProject={onAddProject}
|
|
533
|
+
onEditProject={onEditProject}
|
|
534
|
+
dragState={dragState}
|
|
535
|
+
dragHandlers={dragHandlers}
|
|
536
|
+
onItemDrop={onItemDrop}
|
|
537
|
+
onItemDragEnd={onItemDragEnd}
|
|
538
|
+
isReadOnly={isReadOnly}
|
|
539
|
+
/>
|
|
540
|
+
))
|
|
541
|
+
) : (
|
|
542
|
+
<div className="text-xs text-muted-foreground px-2.5 py-2 italic">No projects yet</div>
|
|
543
|
+
)}
|
|
544
|
+
</div>
|
|
545
|
+
)}
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
<div className="h-px bg-border mx-4 my-2" />
|
|
549
|
+
|
|
550
|
+
{/* Users Section */}
|
|
551
|
+
<div className="flex flex-col">
|
|
552
|
+
<button onClick={() => toggleSection('users')} className="flex items-center justify-between px-3 py-2">
|
|
553
|
+
<div className="flex items-center gap-1.5">
|
|
554
|
+
<Icon name={expandedSections.users ? 'chevron-down' : 'chevron-right'} size={14} className="text-muted-foreground" />
|
|
555
|
+
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">By User</span>
|
|
556
|
+
</div>
|
|
557
|
+
{!isReadOnly && onManageUsers && (
|
|
558
|
+
<button
|
|
559
|
+
onClick={(e) => { e.stopPropagation(); onManageUsers() }}
|
|
560
|
+
className="w-5 h-5 rounded flex items-center justify-center hover:bg-muted"
|
|
561
|
+
title="Manage Users"
|
|
562
|
+
>
|
|
563
|
+
<Icon name="users" size={14} className="text-muted-foreground" />
|
|
564
|
+
</button>
|
|
565
|
+
)}
|
|
566
|
+
</button>
|
|
567
|
+
|
|
568
|
+
{expandedSections.users && (
|
|
569
|
+
<div className="px-1 pb-2">
|
|
570
|
+
{allUsers.map(user => {
|
|
571
|
+
const isYou = currentUser?.id === user.id
|
|
572
|
+
const displayName = getDisplayName ? getDisplayName(user) : (user.name || user.email || 'Unknown')
|
|
573
|
+
|
|
574
|
+
return (
|
|
575
|
+
<UserListItem
|
|
576
|
+
key={user.id}
|
|
577
|
+
user={user}
|
|
578
|
+
isActive={currentView.type === VIEWS.USER && currentView.id === user.id}
|
|
579
|
+
isYou={isYou}
|
|
580
|
+
displayName={displayName}
|
|
581
|
+
onViewChange={onViewChange}
|
|
582
|
+
onItemDropOnUser={onItemDropOnUser}
|
|
583
|
+
onItemDragEnd={onItemDragEnd}
|
|
584
|
+
isReadOnly={isReadOnly}
|
|
585
|
+
/>
|
|
586
|
+
)
|
|
587
|
+
})}
|
|
588
|
+
|
|
589
|
+
{!isReadOnly && allUsers.length === 0 && onManageUsers && (
|
|
590
|
+
<button
|
|
591
|
+
onClick={onManageUsers}
|
|
592
|
+
className="w-full flex items-center justify-center gap-1.5 p-2.5 border border-dashed border-border rounded-lg text-primary text-sm hover:bg-primary/5"
|
|
593
|
+
>
|
|
594
|
+
<Icon name="user-plus" size={14} className="text-primary" />
|
|
595
|
+
<span>Add users</span>
|
|
596
|
+
</button>
|
|
597
|
+
)}
|
|
598
|
+
</div>
|
|
599
|
+
)}
|
|
600
|
+
</div>
|
|
601
|
+
|
|
602
|
+
<div className="flex-1" />
|
|
603
|
+
|
|
604
|
+
{footer}
|
|
605
|
+
</div>
|
|
606
|
+
)
|
|
607
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree Sidebar - Constants
|
|
3
|
+
*
|
|
4
|
+
* View types and configurations for tree sidebar navigation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const VIEWS = {
|
|
8
|
+
ALL: 'all',
|
|
9
|
+
TODAY: 'today',
|
|
10
|
+
UPCOMING: 'upcoming',
|
|
11
|
+
LOGBOOK: 'logbook',
|
|
12
|
+
TRASH: 'trash',
|
|
13
|
+
PROJECT: 'project',
|
|
14
|
+
USER: 'user',
|
|
15
|
+
} as const
|
|
16
|
+
|
|
17
|
+
export type ViewType = typeof VIEWS[keyof typeof VIEWS]
|
|
18
|
+
|
|
19
|
+
export interface ViewConfig {
|
|
20
|
+
title: string
|
|
21
|
+
icon: string
|
|
22
|
+
color: string
|
|
23
|
+
description: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const VIEW_CONFIG: Record<string, ViewConfig> = {
|
|
27
|
+
[VIEWS.ALL]: {
|
|
28
|
+
title: 'All Tasks',
|
|
29
|
+
icon: 'list',
|
|
30
|
+
color: '#6366f1',
|
|
31
|
+
description: 'All your tasks',
|
|
32
|
+
},
|
|
33
|
+
[VIEWS.TODAY]: {
|
|
34
|
+
title: 'Today',
|
|
35
|
+
icon: 'star',
|
|
36
|
+
color: '#f59e0b',
|
|
37
|
+
description: 'Focus on what matters today',
|
|
38
|
+
},
|
|
39
|
+
[VIEWS.UPCOMING]: {
|
|
40
|
+
title: 'calendar',
|
|
41
|
+
icon: 'calendar',
|
|
42
|
+
color: '#ef4444',
|
|
43
|
+
description: 'Plan ahead',
|
|
44
|
+
},
|
|
45
|
+
[VIEWS.LOGBOOK]: {
|
|
46
|
+
title: 'Logbook',
|
|
47
|
+
icon: 'book-open',
|
|
48
|
+
color: '#9ca3af',
|
|
49
|
+
description: 'Completed tasks',
|
|
50
|
+
},
|
|
51
|
+
[VIEWS.TRASH]: {
|
|
52
|
+
title: 'Trash',
|
|
53
|
+
icon: 'trash-2',
|
|
54
|
+
color: '#9ca3af',
|
|
55
|
+
description: 'Deleted tasks',
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CurrentView {
|
|
60
|
+
type: ViewType
|
|
61
|
+
id?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface DragState {
|
|
65
|
+
draggingId: string | null
|
|
66
|
+
dropTargetId: string | null
|
|
67
|
+
dropPosition: 'before' | 'after' | 'inside' | null
|
|
68
|
+
}
|