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,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
+ }