@deck-ui/workspace 0.1.2 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ja-818
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deck-ui/workspace",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -18,4 +18,4 @@
18
18
  "scripts": {
19
19
  "typecheck": "tsc --noEmit"
20
20
  }
21
- }
21
+ }
@@ -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 } from "@deck-ui/core"
8
+ import { FolderPlus } 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,10 @@ 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
36
28
  /** Title for empty state */
37
29
  emptyTitle?: string
38
30
  /** Description for empty state */
@@ -45,247 +37,169 @@ export function FilesBrowser({
45
37
  onOpen,
46
38
  onReveal,
47
39
  onDelete,
40
+ onFilesDropped,
41
+ onCreateFolder,
48
42
  emptyTitle = "Your work shows up here",
49
43
  emptyDescription = "When agents create files, they'll appear here for you to open and review.",
50
44
  }: 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
- }
45
+ const { isDragging, dragHandlers } = useDropZone(onFilesDropped)
46
+ const [creatingFolder, setCreatingFolder] = useState(false)
47
+ const [folderDropTarget, setFolderDropTarget] = useState<string | null>(null)
48
+ const isEmpty = !loading && files.length === 0
49
+ const grouped = isEmpty ? {} : groupByFolder(files)
50
+ const folders = isEmpty ? [] : Object.keys(grouped).sort()
63
51
 
64
- const grouped = groupByFolder(files)
65
- const folders = Object.keys(grouped).sort()
52
+ const isRootTarget = isDragging && folderDropTarget === null
66
53
 
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
- }
110
-
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
- }
120
-
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)
54
+ const onDragActive = useCallback((folder: string | null) => {
55
+ setFolderDropTarget(folder)
56
+ }, [])
135
57
 
136
58
  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",
59
+ <div
60
+ className="relative flex-1 flex flex-col h-full min-h-0 overflow-hidden"
61
+ {...(onFilesDropped ? dragHandlers : {})}
62
+ >
63
+ {isEmpty ? (
64
+ <div className="flex-1 flex flex-col items-center pt-[20vh] gap-4 px-8">
65
+ <div className="space-y-2 text-center max-w-md">
66
+ <h1 className="text-2xl font-semibold text-foreground tracking-tight">
67
+ {emptyTitle}
68
+ </h1>
69
+ <p className="text-sm text-muted-foreground">
70
+ {emptyDescription}
71
+ </p>
72
+ </div>
73
+ {onCreateFolder && (
74
+ <button
75
+ onClick={() => setCreatingFolder(true)}
76
+ className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
77
+ >
78
+ <FolderPlus className="size-4" />
79
+ New folder
80
+ </button>
146
81
  )}
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}
82
+ {creatingFolder && (
83
+ <NewFolderInput
84
+ onConfirm={(name) => {
85
+ onCreateFolder?.(name)
86
+ setCreatingFolder(false)
87
+ }}
88
+ onCancel={() => setCreatingFolder(false)}
165
89
  />
166
- ))}
90
+ )}
167
91
  </div>
92
+ ) : (
93
+ <>
94
+ <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">
95
+ <span className="flex-1 pl-8">Name</span>
96
+ <span className="w-20 text-right">Size</span>
97
+ <div className="w-10 flex justify-end">
98
+ {onCreateFolder && (
99
+ <button
100
+ onClick={() => setCreatingFolder(true)}
101
+ 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"
102
+ >
103
+ <FolderPlus className="size-3.5" />
104
+ </button>
105
+ )}
106
+ </div>
107
+ </div>
108
+
109
+ <div
110
+ className="flex-1 overflow-y-auto"
111
+ style={{
112
+ backgroundColor: isRootTarget ? "rgba(0,0,0,0.04)" : undefined,
113
+ transition: "background-color 150ms",
114
+ }}
115
+ >
116
+ {loading ? (
117
+ <div className="flex items-center justify-center py-16">
118
+ <p className="text-sm text-muted-foreground/50">Loading...</p>
119
+ </div>
120
+ ) : (
121
+ <div>
122
+ {creatingFolder && (
123
+ <NewFolderInput
124
+ onConfirm={(name) => {
125
+ onCreateFolder?.(name)
126
+ setCreatingFolder(false)
127
+ }}
128
+ onCancel={() => setCreatingFolder(false)}
129
+ />
130
+ )}
131
+ {folders.map((folder) =>
132
+ folder ? (
133
+ <FolderSection
134
+ key={folder}
135
+ name={folder}
136
+ files={grouped[folder]}
137
+ onOpen={onOpen}
138
+ onReveal={onReveal}
139
+ onDelete={onDelete}
140
+ onFilesDropped={onFilesDropped}
141
+ onDragActive={onDragActive}
142
+ />
143
+ ) : (
144
+ grouped[folder].map((f) => (
145
+ <FileRow
146
+ key={f.path}
147
+ file={f}
148
+ onOpen={onOpen}
149
+ onReveal={onReveal}
150
+ onDelete={onDelete}
151
+ />
152
+ ))
153
+ ),
154
+ )}
155
+ </div>
156
+ )}
157
+ </div>
158
+ </>
168
159
  )}
169
160
  </div>
170
161
  )
171
162
  }
172
163
 
173
- function FileRow({
174
- file,
175
- indent,
176
- onOpen,
177
- onReveal,
178
- onDelete,
164
+ function NewFolderInput({
165
+ onConfirm,
166
+ onCancel,
179
167
  }: {
180
- file: FileEntry
181
- indent?: boolean
182
- onOpen?: (file: FileEntry) => void
183
- onReveal?: (file: FileEntry) => void
184
- onDelete?: (file: FileEntry) => void
168
+ onConfirm: (name: string) => void
169
+ onCancel: () => void
185
170
  }) {
186
- const ext = file.extension
187
- const hasActions = onOpen || onReveal || onDelete
171
+ const [value, setValue] = useState("")
172
+ const inputRef = useRef<HTMLInputElement>(null)
188
173
 
189
174
  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>
175
+ <div className={cn("flex items-center h-9 px-6 bg-secondary/50")}>
176
+ <FolderPlus className="size-4 shrink-0 text-muted-foreground/50 mr-2.5" />
177
+ <input
178
+ ref={inputRef}
179
+ autoFocus
180
+ value={value}
181
+ onChange={(e) => setValue(e.target.value)}
182
+ onKeyDown={(e) => {
183
+ if (e.key === "Enter" && value.trim()) onConfirm(value.trim())
184
+ if (e.key === "Escape") onCancel()
185
+ }}
186
+ onBlur={() => {
187
+ if (value.trim()) onConfirm(value.trim())
188
+ else onCancel()
189
+ }}
190
+ placeholder="Folder name"
191
+ className="flex-1 text-[13px] bg-transparent outline-none placeholder:text-muted-foreground/40"
192
+ />
193
+ </div>
259
194
  )
260
195
  }
261
196
 
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
- )
197
+ function groupByFolder(files: FileEntry[]): Record<string, FileEntry[]> {
198
+ const grouped: Record<string, FileEntry[]> = {}
199
+ for (const f of files) {
200
+ const parts = f.path.split("/")
201
+ const folder = parts.length > 1 ? parts.slice(0, -1).join("/") : ""
202
+ ;(grouped[folder] ??= []).push(f)
283
203
  }
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`
204
+ return grouped
291
205
  }
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"