@deck-ui/workspace 0.1.2 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deck-ui/workspace",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Drop zone hooks for drag-and-drop file imports.
3
+ * Container-level highlight + folder-level targeting (Finder-style).
4
+ */
5
+ import { useCallback, useRef, useState } from "react"
6
+
7
+ /** Tracks whether files are being dragged over the container. */
8
+ export function useDropZone(
9
+ onFilesDropped?: (files: File[], targetFolder?: string) => void,
10
+ ) {
11
+ const [isDragging, setIsDragging] = useState(false)
12
+ const counter = useRef(0)
13
+
14
+ const onDragEnter = useCallback((e: React.DragEvent) => {
15
+ e.preventDefault()
16
+ counter.current++
17
+ if (e.dataTransfer.types.includes("Files")) setIsDragging(true)
18
+ }, [])
19
+
20
+ const onDragLeave = useCallback((e: React.DragEvent) => {
21
+ e.preventDefault()
22
+ counter.current--
23
+ if (counter.current === 0) setIsDragging(false)
24
+ }, [])
25
+
26
+ const onDragOver = useCallback((e: React.DragEvent) => {
27
+ e.preventDefault()
28
+ }, [])
29
+
30
+ const onDrop = useCallback(
31
+ (e: React.DragEvent) => {
32
+ e.preventDefault()
33
+ counter.current = 0
34
+ setIsDragging(false)
35
+ if (!onFilesDropped) return
36
+ const files = Array.from(e.dataTransfer.files)
37
+ if (files.length > 0) onFilesDropped(files)
38
+ },
39
+ [onFilesDropped],
40
+ )
41
+
42
+ return {
43
+ isDragging,
44
+ dragHandlers: { onDragEnter, onDragLeave, onDragOver, onDrop },
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Tracks drag-over state for an entire folder section (header + children).
50
+ * Stops propagation on drop so the container handler doesn't also fire.
51
+ */
52
+ export function useFolderDropTarget(
53
+ folder: string,
54
+ onFilesDropped?: (files: File[], targetFolder?: string) => void,
55
+ ) {
56
+ const [isOver, setIsOver] = useState(false)
57
+ const counter = useRef(0)
58
+
59
+ const onDragEnter = useCallback((e: React.DragEvent) => {
60
+ e.preventDefault()
61
+ counter.current++
62
+ if (e.dataTransfer.types.includes("Files")) setIsOver(true)
63
+ }, [])
64
+
65
+ const onDragLeave = useCallback((e: React.DragEvent) => {
66
+ e.preventDefault()
67
+ counter.current--
68
+ if (counter.current === 0) setIsOver(false)
69
+ }, [])
70
+
71
+ const onDragOver = useCallback((e: React.DragEvent) => {
72
+ e.preventDefault()
73
+ }, [])
74
+
75
+ const onDrop = useCallback(
76
+ (e: React.DragEvent) => {
77
+ e.preventDefault()
78
+ e.stopPropagation() // only stop propagation on drop to prevent container double-handling
79
+ counter.current = 0
80
+ setIsOver(false)
81
+ if (!onFilesDropped) return
82
+ const files = Array.from(e.dataTransfer.files)
83
+ if (files.length > 0) onFilesDropped(files, folder)
84
+ },
85
+ [onFilesDropped, folder],
86
+ )
87
+
88
+ return {
89
+ isOver,
90
+ folderHandlers: { onDragEnter, onDragLeave, onDragOver, onDrop },
91
+ }
92
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * File row, folder section, and file icon sub-components.
3
+ * Extracted from FilesBrowser to keep files under 200 lines.
4
+ */
5
+ import { useEffect, useState } from "react"
6
+ import {
7
+ cn,
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuTrigger,
12
+ } from "@deck-ui/core"
13
+ import {
14
+ ChevronRight,
15
+ ExternalLink,
16
+ FileText,
17
+ FolderSearch,
18
+ Image as ImageIcon,
19
+ MoreVertical,
20
+ Trash2,
21
+ } from "lucide-react"
22
+ import type { FileEntry } from "./types"
23
+ import { useFolderDropTarget } from "./drop-zone"
24
+
25
+ export function FolderSection({
26
+ name,
27
+ files,
28
+ onOpen,
29
+ onReveal,
30
+ onDelete,
31
+ onFilesDropped,
32
+ onDragActive,
33
+ }: {
34
+ name: string
35
+ files: FileEntry[]
36
+ onOpen?: (file: FileEntry) => void
37
+ onReveal?: (file: FileEntry) => void
38
+ onDelete?: (file: FileEntry) => void
39
+ onFilesDropped?: (files: File[], targetFolder?: string) => void
40
+ onDragActive?: (folder: string | null) => void
41
+ }) {
42
+ const [open, setOpen] = useState(true)
43
+ const { isOver, folderHandlers } = useFolderDropTarget(name, onFilesDropped)
44
+
45
+ useEffect(() => {
46
+ onDragActive?.(isOver ? name : null)
47
+ }, [isOver, name, onDragActive])
48
+
49
+ return (
50
+ <div {...(onFilesDropped ? folderHandlers : {})}>
51
+ <button
52
+ onClick={() => setOpen(!open)}
53
+ className="w-full flex items-center h-8 px-6 transition-colors duration-150 select-none hover:bg-secondary"
54
+ style={isOver ? { backgroundColor: "rgba(0,0,0,0.06)" } : undefined}
55
+ >
56
+ <ChevronRight
57
+ className={cn(
58
+ "size-3.5 text-muted-foreground/40 transition-transform duration-200 mr-2",
59
+ open && "rotate-90",
60
+ )}
61
+ />
62
+ <span
63
+ className="text-xs font-medium flex-1 text-left text-muted-foreground/60"
64
+ style={isOver ? { color: "var(--color-foreground)" } : undefined}
65
+ >
66
+ {name}
67
+ </span>
68
+ <span className="text-[10px] text-muted-foreground/30">
69
+ {files.length}
70
+ </span>
71
+ </button>
72
+ {open && (
73
+ <div style={isOver ? { backgroundColor: "rgba(0,0,0,0.03)" } : undefined}>
74
+ {files.map((f) => (
75
+ <FileRow
76
+ key={f.path}
77
+ file={f}
78
+ indent
79
+ onOpen={onOpen}
80
+ onReveal={onReveal}
81
+ onDelete={onDelete}
82
+ />
83
+ ))}
84
+ </div>
85
+ )}
86
+ </div>
87
+ )
88
+ }
89
+
90
+ export function FileRow({
91
+ file,
92
+ indent,
93
+ onOpen,
94
+ onReveal,
95
+ onDelete,
96
+ }: {
97
+ file: FileEntry
98
+ indent?: boolean
99
+ onOpen?: (file: FileEntry) => void
100
+ onReveal?: (file: FileEntry) => void
101
+ onDelete?: (file: FileEntry) => void
102
+ }) {
103
+ const ext = file.extension
104
+ const hasActions = onOpen || onReveal || onDelete
105
+
106
+ return (
107
+ <button
108
+ onClick={() => onOpen?.(file)}
109
+ className={cn(
110
+ "w-full flex items-center h-9 px-6 hover:bg-secondary",
111
+ "active:bg-accent transition-colors duration-100 text-left group",
112
+ indent && "pl-11",
113
+ )}
114
+ >
115
+ <div className="flex items-center gap-2.5 flex-1 min-w-0">
116
+ <FileIcon extension={ext} />
117
+ <span className="text-[13px] text-foreground truncate">
118
+ {file.name}
119
+ </span>
120
+ </div>
121
+ <span className="w-20 text-right text-[11px] text-muted-foreground/30">
122
+ {formatSize(file.size)}
123
+ </span>
124
+ {hasActions && (
125
+ <div className="w-10 flex justify-end">
126
+ <DropdownMenu>
127
+ <DropdownMenuTrigger asChild>
128
+ <span
129
+ role="button"
130
+ onClick={(e) => e.stopPropagation()}
131
+ className="flex items-center justify-center w-6 h-6 rounded text-muted-foreground/20 hover:text-foreground hover:bg-black/[0.05] opacity-0 group-hover:opacity-100 focus:opacity-100 transition-all duration-150"
132
+ >
133
+ <MoreVertical className="size-3.5" />
134
+ </span>
135
+ </DropdownMenuTrigger>
136
+ <DropdownMenuContent align="end">
137
+ {onOpen && (
138
+ <DropdownMenuItem
139
+ onClick={(e) => {
140
+ e.stopPropagation()
141
+ onOpen(file)
142
+ }}
143
+ >
144
+ <ExternalLink className="size-4 mr-2" />
145
+ Open
146
+ </DropdownMenuItem>
147
+ )}
148
+ {onReveal && (
149
+ <DropdownMenuItem
150
+ onClick={(e) => {
151
+ e.stopPropagation()
152
+ onReveal(file)
153
+ }}
154
+ >
155
+ <FolderSearch className="size-4 mr-2" />
156
+ Show in Finder
157
+ </DropdownMenuItem>
158
+ )}
159
+ {onDelete && (
160
+ <DropdownMenuItem
161
+ onClick={(e) => {
162
+ e.stopPropagation()
163
+ onDelete(file)
164
+ }}
165
+ className="text-destructive focus:text-destructive"
166
+ >
167
+ <Trash2 className="size-4 mr-2" />
168
+ Delete
169
+ </DropdownMenuItem>
170
+ )}
171
+ </DropdownMenuContent>
172
+ </DropdownMenu>
173
+ </div>
174
+ )}
175
+ </button>
176
+ )
177
+ }
178
+
179
+ function FileIcon({ extension }: { extension: string }) {
180
+ if (["png", "jpg", "jpeg", "svg", "gif", "webp"].includes(extension)) {
181
+ return <ImageIcon className="size-4 shrink-0 text-muted-foreground/50" />
182
+ }
183
+ if (extension === "pdf") {
184
+ return (
185
+ <svg className="size-4 shrink-0" viewBox="0 0 16 16" fill="none">
186
+ <rect width="16" height="16" rx="3" fill="#E5252A" />
187
+ <text
188
+ x="8"
189
+ y="11"
190
+ textAnchor="middle"
191
+ fill="white"
192
+ fontSize="7"
193
+ fontWeight="700"
194
+ fontFamily="system-ui, sans-serif"
195
+ >
196
+ PDF
197
+ </text>
198
+ </svg>
199
+ )
200
+ }
201
+ return <FileText className="size-4 shrink-0 text-muted-foreground/50" />
202
+ }
203
+
204
+ export function formatSize(bytes: number): string {
205
+ if (bytes < 1024) return `${bytes} B`
206
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
207
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
208
+ }
@@ -1,26 +1,14 @@
1
1
  /**
2
2
  * FilesBrowser — file browser for an agent workspace.
3
3
  * Shows files grouped by folder with icons, sizes, and actions.
4
- * Extracted from Houston's FilesView, made props-driven.
4
+ * Supports drag-and-drop file import (with folder targeting) and folder creation.
5
5
  */
6
- import { useState } from "react"
7
- import {
8
- cn,
9
- DropdownMenu,
10
- DropdownMenuContent,
11
- DropdownMenuItem,
12
- DropdownMenuTrigger,
13
- } from "@deck-ui/core"
14
- import {
15
- ChevronRight,
16
- ExternalLink,
17
- FileText,
18
- FolderSearch,
19
- Image as ImageIcon,
20
- MoreVertical,
21
- Trash2,
22
- } from "lucide-react"
6
+ import { useCallback, useRef, useState } from "react"
7
+ import { cn, Button } from "@deck-ui/core"
8
+ import { FolderPlus, Upload, FolderOpen } from "lucide-react"
23
9
  import type { FileEntry } from "./types"
10
+ import { useDropZone } from "./drop-zone"
11
+ import { FileRow, FolderSection } from "./file-row"
24
12
 
25
13
  export interface FilesBrowserProps {
26
14
  /** Files to display */
@@ -33,6 +21,14 @@ export interface FilesBrowserProps {
33
21
  onReveal?: (file: FileEntry) => void
34
22
  /** Called when delete is selected */
35
23
  onDelete?: (file: FileEntry) => void
24
+ /** Called when files are dropped. targetFolder is set when dropped on a folder. */
25
+ onFilesDropped?: (files: File[], targetFolder?: string) => void
26
+ /** Called when the user creates a new folder via the inline input */
27
+ onCreateFolder?: (name: string) => void
28
+ /** Called when "Browse files" is clicked — wire to native file picker (e.g. tauri-plugin-dialog). */
29
+ onBrowse?: () => void
30
+ /** Called when "Open folder" is clicked — wire to reveal workspace dir (e.g. shell open). */
31
+ onRevealWorkspace?: () => void
36
32
  /** Title for empty state */
37
33
  emptyTitle?: string
38
34
  /** Description for empty state */
@@ -45,247 +41,187 @@ export function FilesBrowser({
45
41
  onOpen,
46
42
  onReveal,
47
43
  onDelete,
44
+ onFilesDropped,
45
+ onCreateFolder,
46
+ onBrowse,
47
+ onRevealWorkspace,
48
48
  emptyTitle = "Your work shows up here",
49
49
  emptyDescription = "When agents create files, they'll appear here for you to open and review.",
50
50
  }: FilesBrowserProps) {
51
- if (!loading && files.length === 0) {
52
- return (
53
- <div className="flex-1 flex flex-col items-center pt-[20vh] gap-4 px-8">
54
- <div className="space-y-2 text-center max-w-md">
55
- <h1 className="text-2xl font-semibold text-foreground tracking-tight">
56
- {emptyTitle}
57
- </h1>
58
- <p className="text-sm text-muted-foreground">{emptyDescription}</p>
59
- </div>
60
- </div>
61
- )
62
- }
63
-
64
- const grouped = groupByFolder(files)
65
- const folders = Object.keys(grouped).sort()
66
-
67
- return (
68
- <div className="flex-1 flex flex-col h-full min-h-0 overflow-hidden">
69
- <div className="flex items-center h-8 px-6 text-[11px] font-medium text-muted-foreground/40 border-b border-black/[0.06] shrink-0 select-none">
70
- <span className="flex-1 pl-8">Name</span>
71
- <span className="w-20 text-right">Size</span>
72
- <span className="w-10" />
73
- </div>
74
-
75
- <div className="flex-1 overflow-y-auto">
76
- {loading ? (
77
- <div className="flex items-center justify-center py-16">
78
- <p className="text-sm text-muted-foreground/50">Loading...</p>
79
- </div>
80
- ) : (
81
- <div>
82
- {folders.map((folder) =>
83
- folder ? (
84
- <FolderSection
85
- key={folder}
86
- name={folder}
87
- files={grouped[folder]}
88
- onOpen={onOpen}
89
- onReveal={onReveal}
90
- onDelete={onDelete}
91
- />
92
- ) : (
93
- grouped[folder].map((f) => (
94
- <FileRow
95
- key={f.path}
96
- file={f}
97
- onOpen={onOpen}
98
- onReveal={onReveal}
99
- onDelete={onDelete}
100
- />
101
- ))
102
- ),
103
- )}
104
- </div>
105
- )}
106
- </div>
107
- </div>
108
- )
109
- }
51
+ const { isDragging, dragHandlers } = useDropZone(onFilesDropped)
52
+ const [creatingFolder, setCreatingFolder] = useState(false)
53
+ const [folderDropTarget, setFolderDropTarget] = useState<string | null>(null)
54
+ const isEmpty = !loading && files.length === 0
55
+ const grouped = isEmpty ? {} : groupByFolder(files)
56
+ const folders = isEmpty ? [] : Object.keys(grouped).sort()
110
57
 
111
- function groupByFolder(files: FileEntry[]): Record<string, FileEntry[]> {
112
- const grouped: Record<string, FileEntry[]> = {}
113
- for (const f of files) {
114
- const parts = f.path.split("/")
115
- const folder = parts.length > 1 ? parts.slice(0, -1).join("/") : ""
116
- ;(grouped[folder] ??= []).push(f)
117
- }
118
- return grouped
119
- }
58
+ const isRootTarget = isDragging && folderDropTarget === null
120
59
 
121
- function FolderSection({
122
- name,
123
- files,
124
- onOpen,
125
- onReveal,
126
- onDelete,
127
- }: {
128
- name: string
129
- files: FileEntry[]
130
- onOpen?: (file: FileEntry) => void
131
- onReveal?: (file: FileEntry) => void
132
- onDelete?: (file: FileEntry) => void
133
- }) {
134
- const [open, setOpen] = useState(true)
60
+ const onDragActive = useCallback((folder: string | null) => {
61
+ setFolderDropTarget(folder)
62
+ }, [])
135
63
 
136
64
  return (
137
- <div>
138
- <button
139
- onClick={() => setOpen(!open)}
140
- className="w-full flex items-center h-8 px-6 hover:bg-secondary transition-colors duration-150 select-none"
141
- >
142
- <ChevronRight
143
- className={cn(
144
- "size-3.5 text-muted-foreground/40 transition-transform duration-200 mr-2",
145
- open && "rotate-90",
65
+ <div
66
+ className="relative flex-1 flex flex-col h-full min-h-0 overflow-hidden"
67
+ {...(onFilesDropped ? dragHandlers : {})}
68
+ >
69
+ {isEmpty ? (
70
+ <div className="flex-1 flex flex-col items-center pt-[20vh] gap-4 px-8">
71
+ <div className="space-y-2 text-center max-w-md">
72
+ <h1 className="text-2xl font-semibold text-foreground tracking-tight">
73
+ {emptyTitle}
74
+ </h1>
75
+ <p className="text-sm text-muted-foreground">
76
+ {emptyDescription}
77
+ </p>
78
+ </div>
79
+ {(onBrowse || onRevealWorkspace) && (
80
+ <div className="flex items-center gap-2">
81
+ {onBrowse && (
82
+ <Button variant="default" size="sm" onClick={onBrowse}>
83
+ <Upload className="size-4 mr-1.5" />
84
+ Browse files
85
+ </Button>
86
+ )}
87
+ {onRevealWorkspace && (
88
+ <Button variant="outline" size="sm" onClick={onRevealWorkspace}>
89
+ <FolderOpen className="size-4 mr-1.5" />
90
+ Open folder
91
+ </Button>
92
+ )}
93
+ </div>
146
94
  )}
147
- />
148
- <span className="text-xs font-medium text-muted-foreground/60 flex-1 text-left">
149
- {name}
150
- </span>
151
- <span className="text-[10px] text-muted-foreground/30">
152
- {files.length}
153
- </span>
154
- </button>
155
- {open && (
156
- <div>
157
- {files.map((f) => (
158
- <FileRow
159
- key={f.path}
160
- file={f}
161
- indent
162
- onOpen={onOpen}
163
- onReveal={onReveal}
164
- onDelete={onDelete}
95
+ {onCreateFolder && (
96
+ <button
97
+ onClick={() => setCreatingFolder(true)}
98
+ className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
99
+ >
100
+ <FolderPlus className="size-4" />
101
+ New folder
102
+ </button>
103
+ )}
104
+ {creatingFolder && (
105
+ <NewFolderInput
106
+ onConfirm={(name) => {
107
+ onCreateFolder?.(name)
108
+ setCreatingFolder(false)
109
+ }}
110
+ onCancel={() => setCreatingFolder(false)}
165
111
  />
166
- ))}
112
+ )}
167
113
  </div>
114
+ ) : (
115
+ <>
116
+ <div className="flex items-center h-8 px-6 text-[11px] font-medium text-muted-foreground/40 border-b border-black/[0.06] shrink-0 select-none">
117
+ <span className="flex-1 pl-8">Name</span>
118
+ <span className="w-20 text-right">Size</span>
119
+ <div className="w-10 flex justify-end">
120
+ {onCreateFolder && (
121
+ <button
122
+ onClick={() => setCreatingFolder(true)}
123
+ className="flex items-center justify-center w-6 h-6 rounded text-muted-foreground/30 hover:text-foreground hover:bg-black/[0.05] transition-all duration-150"
124
+ >
125
+ <FolderPlus className="size-3.5" />
126
+ </button>
127
+ )}
128
+ </div>
129
+ </div>
130
+
131
+ <div
132
+ className="flex-1 overflow-y-auto"
133
+ style={{
134
+ backgroundColor: isRootTarget ? "rgba(0,0,0,0.04)" : undefined,
135
+ transition: "background-color 150ms",
136
+ }}
137
+ >
138
+ {loading ? (
139
+ <div className="flex items-center justify-center py-16">
140
+ <p className="text-sm text-muted-foreground/50">Loading...</p>
141
+ </div>
142
+ ) : (
143
+ <div>
144
+ {creatingFolder && (
145
+ <NewFolderInput
146
+ onConfirm={(name) => {
147
+ onCreateFolder?.(name)
148
+ setCreatingFolder(false)
149
+ }}
150
+ onCancel={() => setCreatingFolder(false)}
151
+ />
152
+ )}
153
+ {folders.map((folder) =>
154
+ folder ? (
155
+ <FolderSection
156
+ key={folder}
157
+ name={folder}
158
+ files={grouped[folder]}
159
+ onOpen={onOpen}
160
+ onReveal={onReveal}
161
+ onDelete={onDelete}
162
+ onFilesDropped={onFilesDropped}
163
+ onDragActive={onDragActive}
164
+ />
165
+ ) : (
166
+ grouped[folder].map((f) => (
167
+ <FileRow
168
+ key={f.path}
169
+ file={f}
170
+ onOpen={onOpen}
171
+ onReveal={onReveal}
172
+ onDelete={onDelete}
173
+ />
174
+ ))
175
+ ),
176
+ )}
177
+ </div>
178
+ )}
179
+ </div>
180
+ </>
168
181
  )}
169
182
  </div>
170
183
  )
171
184
  }
172
185
 
173
- function FileRow({
174
- file,
175
- indent,
176
- onOpen,
177
- onReveal,
178
- onDelete,
186
+ function NewFolderInput({
187
+ onConfirm,
188
+ onCancel,
179
189
  }: {
180
- file: FileEntry
181
- indent?: boolean
182
- onOpen?: (file: FileEntry) => void
183
- onReveal?: (file: FileEntry) => void
184
- onDelete?: (file: FileEntry) => void
190
+ onConfirm: (name: string) => void
191
+ onCancel: () => void
185
192
  }) {
186
- const ext = file.extension
187
- const hasActions = onOpen || onReveal || onDelete
193
+ const [value, setValue] = useState("")
194
+ const inputRef = useRef<HTMLInputElement>(null)
188
195
 
189
196
  return (
190
- <button
191
- onClick={() => onOpen?.(file)}
192
- className={cn(
193
- "w-full flex items-center h-9 px-6 hover:bg-secondary",
194
- "active:bg-accent transition-colors duration-100 text-left group",
195
- indent && "pl-11",
196
- )}
197
- >
198
- <div className="flex items-center gap-2.5 flex-1 min-w-0">
199
- <FileIcon extension={ext} />
200
- <span className="text-[13px] text-foreground truncate">
201
- {file.name}
202
- </span>
203
- </div>
204
- <span className="w-20 text-right text-[11px] text-muted-foreground/30">
205
- {formatSize(file.size)}
206
- </span>
207
- {hasActions && (
208
- <div className="w-10 flex justify-end">
209
- <DropdownMenu>
210
- <DropdownMenuTrigger asChild>
211
- <span
212
- role="button"
213
- onClick={(e) => e.stopPropagation()}
214
- className="flex items-center justify-center w-6 h-6 rounded text-muted-foreground/20 hover:text-foreground hover:bg-black/[0.05] opacity-0 group-hover:opacity-100 focus:opacity-100 transition-all duration-150"
215
- >
216
- <MoreVertical className="size-3.5" />
217
- </span>
218
- </DropdownMenuTrigger>
219
- <DropdownMenuContent align="end">
220
- {onOpen && (
221
- <DropdownMenuItem
222
- onClick={(e) => {
223
- e.stopPropagation()
224
- onOpen(file)
225
- }}
226
- >
227
- <ExternalLink className="size-4 mr-2" />
228
- Open
229
- </DropdownMenuItem>
230
- )}
231
- {onReveal && (
232
- <DropdownMenuItem
233
- onClick={(e) => {
234
- e.stopPropagation()
235
- onReveal(file)
236
- }}
237
- >
238
- <FolderSearch className="size-4 mr-2" />
239
- Show in Finder
240
- </DropdownMenuItem>
241
- )}
242
- {onDelete && (
243
- <DropdownMenuItem
244
- onClick={(e) => {
245
- e.stopPropagation()
246
- onDelete(file)
247
- }}
248
- className="text-destructive focus:text-destructive"
249
- >
250
- <Trash2 className="size-4 mr-2" />
251
- Delete
252
- </DropdownMenuItem>
253
- )}
254
- </DropdownMenuContent>
255
- </DropdownMenu>
256
- </div>
257
- )}
258
- </button>
197
+ <div className={cn("flex items-center h-9 px-6 bg-secondary/50")}>
198
+ <FolderPlus className="size-4 shrink-0 text-muted-foreground/50 mr-2.5" />
199
+ <input
200
+ ref={inputRef}
201
+ autoFocus
202
+ value={value}
203
+ onChange={(e) => setValue(e.target.value)}
204
+ onKeyDown={(e) => {
205
+ if (e.key === "Enter" && value.trim()) onConfirm(value.trim())
206
+ if (e.key === "Escape") onCancel()
207
+ }}
208
+ onBlur={() => {
209
+ if (value.trim()) onConfirm(value.trim())
210
+ else onCancel()
211
+ }}
212
+ placeholder="Folder name"
213
+ className="flex-1 text-[13px] bg-transparent outline-none placeholder:text-muted-foreground/40"
214
+ />
215
+ </div>
259
216
  )
260
217
  }
261
218
 
262
- function FileIcon({ extension }: { extension: string }) {
263
- if (["png", "jpg", "jpeg", "svg", "gif", "webp"].includes(extension)) {
264
- return <ImageIcon className="size-4 shrink-0 text-muted-foreground/50" />
265
- }
266
- if (extension === "pdf") {
267
- return (
268
- <svg className="size-4 shrink-0" viewBox="0 0 16 16" fill="none">
269
- <rect width="16" height="16" rx="3" fill="#E5252A" />
270
- <text
271
- x="8"
272
- y="11"
273
- textAnchor="middle"
274
- fill="white"
275
- fontSize="7"
276
- fontWeight="700"
277
- fontFamily="system-ui, sans-serif"
278
- >
279
- PDF
280
- </text>
281
- </svg>
282
- )
219
+ function groupByFolder(files: FileEntry[]): Record<string, FileEntry[]> {
220
+ const grouped: Record<string, FileEntry[]> = {}
221
+ for (const f of files) {
222
+ const parts = f.path.split("/")
223
+ const folder = parts.length > 1 ? parts.slice(0, -1).join("/") : ""
224
+ ;(grouped[folder] ??= []).push(f)
283
225
  }
284
- return <FileText className="size-4 shrink-0 text-muted-foreground/50" />
285
- }
286
-
287
- function formatSize(bytes: number): string {
288
- if (bytes < 1024) return `${bytes} B`
289
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
290
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
226
+ return grouped
291
227
  }
package/src/index.ts CHANGED
@@ -7,3 +7,6 @@ export type { FilesBrowserProps } from "./files-browser"
7
7
 
8
8
  export { InstructionsPanel } from "./instructions-panel"
9
9
  export type { InstructionsPanelProps } from "./instructions-panel"
10
+
11
+ // Hooks
12
+ export { useDropZone, useFolderDropTarget } from "./drop-zone"