@toolr/ui-design 0.1.8 → 0.1.9
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/ai-manifest.json +35 -20
- package/components/composites/dashboard-list-item.tsx +172 -0
- package/components/composites/dashboard-panel.tsx +218 -0
- package/components/content/info-panel-primitives.tsx +9 -8
- package/components/diagrams/diagram-utils.tsx +2 -1
- package/components/hooks/use-dropdown-portal.ts +39 -0
- package/components/lib/accent-context.ts +10 -0
- package/components/lib/{ai-tools.tsx → coding-agents.tsx} +23 -8
- package/components/lib/custom-icons.tsx +37 -0
- package/components/lib/git-providers.tsx +39 -0
- package/components/lib/theme-engine.ts +59 -10
- package/components/lib/toolr-brand.tsx +23 -9
- package/components/sections/captured-issues/captured-issues-panel.tsx +17 -8
- package/components/sections/{ai-tools-paths/tools-paths-panel.tsx → coding-agent-paths/agent-paths-panel.tsx} +70 -62
- package/components/sections/coding-agent-paths/index.ts +37 -0
- package/components/sections/{ai-tools-paths → coding-agent-paths}/types.ts +28 -28
- package/components/sections/coding-agent-paths/use-agent-paths.ts +159 -0
- package/components/sections/golden-snapshots/file-diff-viewer.tsx +10 -9
- package/components/sections/golden-snapshots/golden-sync-panel.tsx +12 -3
- package/components/sections/golden-snapshots/snapshot-manager.tsx +9 -7
- package/components/sections/golden-snapshots/status-overview.tsx +8 -8
- package/components/sections/golden-snapshots/version-manager.tsx +6 -6
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +3 -3
- package/components/sections/prompt-editor/index.ts +1 -1
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +13 -5
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +18 -10
- package/components/sections/prompt-editor/types.ts +2 -2
- package/components/sections/report-bug/report-bug-form.tsx +12 -4
- package/components/sections/report-bug/screenshot-uploader.tsx +11 -3
- package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +12 -4
- package/components/sections/snapshot-browser/snapshot-tree.tsx +5 -4
- package/components/sections/snapshot-browser/types.ts +1 -1
- package/components/sections/snippets-editor/snippets-editor.tsx +16 -9
- package/components/settings/SettingsHeader.tsx +2 -2
- package/components/settings/SettingsPanel.tsx +11 -3
- package/components/settings/SettingsTreeNav.tsx +15 -9
- package/components/ui/action-dialog.tsx +24 -30
- package/components/ui/ai-action-button.tsx +10 -7
- package/components/ui/ai-execution-action-buttons.tsx +13 -5
- package/components/ui/badge.tsx +7 -4
- package/components/ui/bottom-panel-header.tsx +9 -5
- package/components/ui/breadcrumb.tsx +9 -1
- package/components/ui/{extension-list-card.tsx → capability-list-card.tsx} +13 -5
- package/components/ui/checkbox.tsx +6 -3
- package/components/ui/collapsible-section.tsx +38 -29
- package/components/ui/confirm-badge.tsx +7 -4
- package/components/ui/cookie-consent.tsx +13 -7
- package/components/ui/detail-section.tsx +24 -16
- package/components/ui/detail-view-wrapper.tsx +30 -22
- package/components/ui/editor-placeholder-card.tsx +28 -24
- package/components/ui/editor-toolbar.tsx +7 -4
- package/components/ui/execution-details-panel.tsx +10 -5
- package/components/ui/file-structure-section.tsx +3 -3
- package/components/ui/file-tree.tsx +3 -1
- package/components/ui/files-panel.tsx +147 -27
- package/components/ui/filter-dropdown.tsx +84 -74
- package/components/ui/form-actions.tsx +14 -6
- package/components/ui/frontmatter-form-header.tsx +10 -2
- package/components/ui/icon-button.tsx +22 -9
- package/components/ui/input.tsx +7 -4
- package/components/ui/label.tsx +5 -5
- package/components/ui/layout-tab-bar.tsx +7 -5
- package/components/ui/modal.tsx +18 -4
- package/components/ui/nav-card.tsx +6 -3
- package/components/ui/navigation-bar.tsx +37 -9
- package/components/ui/number-input.tsx +8 -4
- package/components/ui/project-explorer.tsx +666 -0
- package/components/ui/registry-browser.tsx +12 -1
- package/components/ui/registry-card.tsx +49 -42
- package/components/ui/registry-detail.tsx +34 -11
- package/components/ui/resizable-textarea.tsx +18 -11
- package/components/ui/scope-badge.tsx +18 -11
- package/components/ui/segmented-toggle.tsx +5 -2
- package/components/ui/select.tsx +12 -9
- package/components/ui/selection-grid.tsx +36 -37
- package/components/ui/setting-row.tsx +2 -2
- package/components/ui/settings-card.tsx +10 -3
- package/components/ui/settings-info-box.tsx +9 -5
- package/components/ui/settings-section-title.tsx +14 -2
- package/components/ui/snapshot-card.tsx +10 -2
- package/components/ui/snippets-panel.tsx +4 -2
- package/components/ui/sort-dropdown.tsx +39 -29
- package/components/ui/status-card.tsx +9 -1
- package/components/ui/tab-bar.tsx +12 -9
- package/components/ui/toggle.tsx +13 -7
- package/components/ui/tooltip.tsx +9 -1
- package/dist/content.js +8 -8
- package/dist/diagrams.d.ts +0 -1
- package/dist/index.d.ts +421 -182
- package/dist/index.js +2984 -1691
- package/dist/tokens/primitives.css +28 -6
- package/dist/tokens/semantic.css +15 -15
- package/dist/tokens/theme.css +23 -0
- package/index.ts +25 -11
- package/package.json +1 -1
- package/tokens/primitives.css +28 -6
- package/tokens/semantic.css +15 -15
- package/tokens/theme.css +23 -0
- package/components/sections/ai-tools-paths/index.ts +0 -37
- package/components/sections/ai-tools-paths/use-tools-paths.ts +0 -159
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useState, useRef, useLayoutEffect } from 'react'
|
|
2
2
|
import { IconButton, type ActionItem } from './icon-button.tsx'
|
|
3
3
|
import { Input } from './input.tsx'
|
|
4
|
+
import { useAccentColor } from '../lib/accent-context.ts'
|
|
5
|
+
import type { FormColor } from '../lib/form-colors.ts'
|
|
4
6
|
|
|
5
7
|
export interface EditorPlaceholderCardProps {
|
|
6
8
|
/** Placeholder name (without braces) */
|
|
@@ -14,7 +16,7 @@ export interface EditorPlaceholderCardProps {
|
|
|
14
16
|
/** Label for the value section (default: "Value:") */
|
|
15
17
|
valueLabel?: string
|
|
16
18
|
/** Color scheme */
|
|
17
|
-
accentColor?:
|
|
19
|
+
accentColor?: FormColor
|
|
18
20
|
/** Action buttons to show on the right (e.g., edit/delete for settings) */
|
|
19
21
|
actions?: ActionItem[]
|
|
20
22
|
/** Show copy button that copies the {{PLACEHOLDER}} syntax */
|
|
@@ -27,23 +29,22 @@ export interface EditorPlaceholderCardProps {
|
|
|
27
29
|
className?: string
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
const COLORS = {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
},
|
|
43
|
-
sky: {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
},
|
|
32
|
+
const COLORS: Record<FormColor, { name: string; nameBg: string }> = {
|
|
33
|
+
blue: { name: 'text-blue-400', nameBg: 'bg-blue-500/10' },
|
|
34
|
+
green: { name: 'text-green-400', nameBg: 'bg-green-500/10' },
|
|
35
|
+
red: { name: 'text-red-400', nameBg: 'bg-red-500/10' },
|
|
36
|
+
orange: { name: 'text-orange-400', nameBg: 'bg-orange-500/10' },
|
|
37
|
+
cyan: { name: 'text-cyan-400', nameBg: 'bg-cyan-500/10' },
|
|
38
|
+
yellow: { name: 'text-yellow-400', nameBg: 'bg-yellow-500/10' },
|
|
39
|
+
purple: { name: 'text-purple-400', nameBg: 'bg-purple-500/10' },
|
|
40
|
+
indigo: { name: 'text-indigo-400', nameBg: 'bg-indigo-500/10' },
|
|
41
|
+
emerald: { name: 'text-emerald-400', nameBg: 'bg-emerald-500/10' },
|
|
42
|
+
amber: { name: 'text-amber-400', nameBg: 'bg-amber-500/10' },
|
|
43
|
+
violet: { name: 'text-violet-400', nameBg: 'bg-violet-500/10' },
|
|
44
|
+
neutral: { name: 'text-neutral-400', nameBg: 'bg-neutral-500/10' },
|
|
45
|
+
sky: { name: 'text-sky-400', nameBg: 'bg-sky-500/10' },
|
|
46
|
+
pink: { name: 'text-pink-400', nameBg: 'bg-pink-500/10' },
|
|
47
|
+
teal: { name: 'text-teal-400', nameBg: 'bg-teal-500/10' },
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
export function EditorPlaceholderCard({
|
|
@@ -52,20 +53,22 @@ export function EditorPlaceholderCard({
|
|
|
52
53
|
value,
|
|
53
54
|
required = false,
|
|
54
55
|
valueLabel = 'Value:',
|
|
55
|
-
accentColor
|
|
56
|
+
accentColor,
|
|
56
57
|
actions,
|
|
57
58
|
showCopyPlaceholder = false,
|
|
58
59
|
showCopyValue = false,
|
|
59
60
|
hideValue = false,
|
|
60
61
|
className = '',
|
|
61
62
|
}: EditorPlaceholderCardProps) {
|
|
63
|
+
const contextAccent = useAccentColor()
|
|
64
|
+
const effectiveColor = accentColor ?? contextAccent ?? 'purple'
|
|
62
65
|
const [isExpanded, setIsExpanded] = useState(false)
|
|
63
66
|
const [isPlaceholderCopied, setIsPlaceholderCopied] = useState(false)
|
|
64
67
|
const [isValueCopied, setIsValueCopied] = useState(false)
|
|
65
68
|
const [isOverflowing, setIsOverflowing] = useState(false)
|
|
66
69
|
const valueRef = useRef<HTMLDivElement>(null)
|
|
67
70
|
|
|
68
|
-
const colors = COLORS[
|
|
71
|
+
const colors = COLORS[effectiveColor]
|
|
69
72
|
const hasValue = !!value
|
|
70
73
|
|
|
71
74
|
// Check if content overflows (truncated) - sync state with DOM measurement
|
|
@@ -123,7 +126,7 @@ export function EditorPlaceholderCard({
|
|
|
123
126
|
icon={isPlaceholderCopied ? 'check' : 'copy'}
|
|
124
127
|
onClick={handleCopyPlaceholder}
|
|
125
128
|
size="sm"
|
|
126
|
-
|
|
129
|
+
accentColor={isPlaceholderCopied ? 'green' : 'neutral'}
|
|
127
130
|
tooltip={{ description: `Copy {{${name}}}` }}
|
|
128
131
|
tooltipPosition="left"
|
|
129
132
|
/>
|
|
@@ -133,7 +136,7 @@ export function EditorPlaceholderCard({
|
|
|
133
136
|
icon={isValueCopied ? 'check' : 'copy'}
|
|
134
137
|
onClick={handleCopyValue}
|
|
135
138
|
size="sm"
|
|
136
|
-
|
|
139
|
+
accentColor={isValueCopied ? 'green' : 'neutral'}
|
|
137
140
|
tooltip={{ description: 'Copy value to clipboard' }}
|
|
138
141
|
tooltipPosition="left"
|
|
139
142
|
/>
|
|
@@ -156,7 +159,7 @@ export function EditorPlaceholderCard({
|
|
|
156
159
|
readOnly
|
|
157
160
|
size="xs"
|
|
158
161
|
variant="filled"
|
|
159
|
-
|
|
162
|
+
accentColor="neutral"
|
|
160
163
|
|
|
161
164
|
/>
|
|
162
165
|
</div>
|
|
@@ -170,13 +173,14 @@ export function EditorPlaceholderCard({
|
|
|
170
173
|
icon={isExpanded ? 'chevron-up' : 'chevron-down'}
|
|
171
174
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
172
175
|
size="xss"
|
|
176
|
+
accentColor="neutral"
|
|
173
177
|
tooltip={{ description: isExpanded ? 'Show less' : 'Show more' }}
|
|
174
178
|
/>
|
|
175
179
|
)}
|
|
176
180
|
</div>
|
|
177
181
|
<div
|
|
178
182
|
ref={valueRef}
|
|
179
|
-
className={`mt-1.5 px-2 py-1.5 bg-neutral-
|
|
183
|
+
className={`mt-1.5 px-2 py-1.5 bg-neutral-960/50 rounded text-sm text-neutral-400 font-mono ${
|
|
180
184
|
isExpanded
|
|
181
185
|
? 'whitespace-pre-wrap break-all max-h-[190px] overflow-y-auto'
|
|
182
186
|
: 'truncate'
|
|
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
|
|
2
2
|
import { IconButton, type ActionItem } from './icon-button.tsx'
|
|
3
3
|
import { Label } from './label.tsx'
|
|
4
4
|
import { ConfirmModal } from './modal.tsx'
|
|
5
|
+
import type { FormColor } from '../lib/form-colors.ts'
|
|
5
6
|
|
|
6
7
|
export interface EditorToolbarProps {
|
|
7
8
|
/** Optional title displayed in the toolbar */
|
|
@@ -31,6 +32,7 @@ export interface EditorToolbarProps {
|
|
|
31
32
|
leftActions?: ActionItem[]
|
|
32
33
|
/** Optional: Additional action buttons on the right side (before save) */
|
|
33
34
|
rightActions?: ActionItem[]
|
|
35
|
+
accentColor?: FormColor
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export function EditorToolbar({
|
|
@@ -49,6 +51,7 @@ export function EditorToolbar({
|
|
|
49
51
|
hasError = false,
|
|
50
52
|
leftActions,
|
|
51
53
|
rightActions,
|
|
54
|
+
accentColor: _accentColor,
|
|
52
55
|
}: EditorToolbarProps) {
|
|
53
56
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
|
54
57
|
|
|
@@ -69,7 +72,7 @@ export function EditorToolbar({
|
|
|
69
72
|
|
|
70
73
|
return (
|
|
71
74
|
<>
|
|
72
|
-
<div className="flex items-center justify-between px-4 py-1.5 bg-neutral-
|
|
75
|
+
<div className="flex items-center justify-between px-4 py-1.5 bg-neutral-980 border-b border-neutral-960">
|
|
73
76
|
{/* Left side */}
|
|
74
77
|
<div className="flex items-center gap-2">
|
|
75
78
|
{(title || description) && (
|
|
@@ -85,7 +88,7 @@ export function EditorToolbar({
|
|
|
85
88
|
{isDirty && (
|
|
86
89
|
<Label
|
|
87
90
|
text="modified"
|
|
88
|
-
|
|
91
|
+
accentColor="yellow"
|
|
89
92
|
icon="pencil"
|
|
90
93
|
size="xs"
|
|
91
94
|
tooltip={{ description: 'File has unsaved changes' }}
|
|
@@ -101,7 +104,7 @@ export function EditorToolbar({
|
|
|
101
104
|
icon="rotate"
|
|
102
105
|
onClick={handleResetClick}
|
|
103
106
|
size="sm"
|
|
104
|
-
|
|
107
|
+
accentColor="orange"
|
|
105
108
|
tooltip={resetTooltip}
|
|
106
109
|
/>
|
|
107
110
|
)}
|
|
@@ -112,7 +115,7 @@ export function EditorToolbar({
|
|
|
112
115
|
onClick={onSave}
|
|
113
116
|
disabled={!isDirty || isSaving || hasError}
|
|
114
117
|
size="sm"
|
|
115
|
-
|
|
118
|
+
accentColor={isDirty && !hasError ? 'green' : 'neutral'}
|
|
116
119
|
status={isSaving ? 'loading' : undefined}
|
|
117
120
|
tooltip={{ description: 'Save changes to file' }}
|
|
118
121
|
/>
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* Used inside ActionDialog as the mandatory execution details section.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { AlertTriangle
|
|
9
|
+
import { AlertTriangle } from 'lucide-react'
|
|
10
10
|
import { Checkbox } from './checkbox.tsx'
|
|
11
11
|
import { cn } from '../lib/cn.ts'
|
|
12
12
|
import type { DetailRow } from './detail-section.tsx'
|
|
13
|
+
import type { FormColor } from '../lib/form-colors.ts'
|
|
14
|
+
import { useAccentColor, AccentColorProvider } from '../lib/accent-context.ts'
|
|
13
15
|
|
|
14
16
|
export interface ExecutionDetailsPanelProps {
|
|
15
17
|
details: DetailRow[]
|
|
@@ -19,6 +21,7 @@ export interface ExecutionDetailsPanelProps {
|
|
|
19
21
|
/** Warning message shown below the toggle */
|
|
20
22
|
warningMessage?: string
|
|
21
23
|
className?: string
|
|
24
|
+
accentColor?: FormColor
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
export function ExecutionDetailsPanel({
|
|
@@ -27,16 +30,17 @@ export function ExecutionDetailsPanel({
|
|
|
27
30
|
onAllowDirectEditsChange,
|
|
28
31
|
warningMessage,
|
|
29
32
|
className,
|
|
33
|
+
accentColor: accentColorProp,
|
|
30
34
|
}: ExecutionDetailsPanelProps) {
|
|
35
|
+
const contextAccent = useAccentColor()
|
|
36
|
+
const effectiveColor = accentColorProp ?? contextAccent ?? 'blue'
|
|
31
37
|
const showToggle = onAllowDirectEditsChange !== undefined
|
|
32
38
|
|
|
33
39
|
return (
|
|
40
|
+
<AccentColorProvider value={effectiveColor}>
|
|
34
41
|
<div className={cn('space-y-3', className)}>
|
|
35
42
|
{/* Header */}
|
|
36
|
-
<div className="
|
|
37
|
-
<Info className="w-4 h-4 text-neutral-500" />
|
|
38
|
-
<span className="font-medium text-neutral-400 text-md">Execution Details</span>
|
|
39
|
-
</div>
|
|
43
|
+
<div className="font-medium text-neutral-400 text-md">Execution Details:</div>
|
|
40
44
|
|
|
41
45
|
{/* Direct edits toggle */}
|
|
42
46
|
{showToggle && (
|
|
@@ -84,5 +88,6 @@ export function ExecutionDetailsPanel({
|
|
|
84
88
|
</div>
|
|
85
89
|
)}
|
|
86
90
|
</div>
|
|
91
|
+
</AccentColorProvider>
|
|
87
92
|
)
|
|
88
93
|
}
|
|
@@ -328,14 +328,14 @@ export function FileStructureSection({
|
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
const treePanel = (
|
|
331
|
-
<div className={`flex flex-col bg-neutral-
|
|
331
|
+
<div className={`flex flex-col bg-neutral-980 border ${ACCENT_BORDER[accentColor]} rounded-lg overflow-hidden ${variant === 'split' && effectiveFilePath ? 'w-1/3 shrink-0' : 'flex-1'}`}>
|
|
332
332
|
<div className={`flex items-center px-3 py-2 border-b ${ACCENT_BORDER[accentColor]} shrink-0 gap-2 min-w-0`}>
|
|
333
333
|
<FolderTree className={`w-3.5 h-3.5 shrink-0 ${ACCENT_ICON[accentColor]}`} />
|
|
334
334
|
<span className="text-sm text-neutral-200 truncate flex-1">Files</span>
|
|
335
335
|
<CollapseButton
|
|
336
336
|
collapsed={allCollapsed}
|
|
337
337
|
onToggle={() => setExpandedPaths(allCollapsed ? new Set(allDirPaths) : new Set())}
|
|
338
|
-
|
|
338
|
+
accentColor={accentColor}
|
|
339
339
|
/>
|
|
340
340
|
</div>
|
|
341
341
|
<div className={`${variant === 'split' ? 'flex-1 overflow-y-auto' : ''} p-3`}>
|
|
@@ -353,7 +353,7 @@ export function FileStructureSection({
|
|
|
353
353
|
)
|
|
354
354
|
|
|
355
355
|
const previewPanel = effectiveFilePath ? (
|
|
356
|
-
<div className={`flex-1 flex flex-col bg-neutral-
|
|
356
|
+
<div className={`flex-1 flex flex-col bg-neutral-980 border ${ACCENT_BORDER[accentColor]} rounded-lg overflow-hidden`}>
|
|
357
357
|
<div className={`flex items-center px-3 py-2 border-b ${ACCENT_BORDER[accentColor]} shrink-0 gap-2 min-w-0`}>
|
|
358
358
|
<FileCode className={`w-3.5 h-3.5 shrink-0 ${ACCENT_ICON[accentColor]}`} />
|
|
359
359
|
<span className="text-sm text-neutral-200 truncate flex-1">{selectedFileName}</span>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FileCode, Folder, FolderOpen, ChevronRight, ChevronDown } from 'lucide-react'
|
|
2
2
|
import { ACCENT_ICON, type AccentColor } from '../lib/form-colors.ts'
|
|
3
|
+
import { useAccentColor } from '../lib/accent-context.ts'
|
|
3
4
|
|
|
4
5
|
export interface FileTreeNode {
|
|
5
6
|
name: string
|
|
@@ -57,7 +58,8 @@ export function collectDirPaths(nodes: FileTreeNode[], rootName?: string, prefix
|
|
|
57
58
|
return paths
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
export function FileTree({ nodes, rootName, selectedPath, onSelectFile, prefix = '', expandedPaths, onTogglePath, accentColor
|
|
61
|
+
export function FileTree({ nodes, rootName, selectedPath, onSelectFile, prefix = '', expandedPaths, onTogglePath, accentColor: accentColorProp }: FileTreeProps) {
|
|
62
|
+
const accentColor = accentColorProp ?? useAccentColor() ?? 'blue'
|
|
61
63
|
if (rootName) {
|
|
62
64
|
const rootNode: FileTreeNode = { name: rootName, type: 'directory', children: nodes }
|
|
63
65
|
return (
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** File explorer panel with tree view, folder collapse/expand, search, and file selection. */
|
|
2
2
|
|
|
3
|
-
import { useState, useMemo } from 'react'
|
|
3
|
+
import { useState, useMemo, type ReactNode } from 'react'
|
|
4
4
|
import { Folder, FolderOpen, File, FileCode, FileText, FileJson, Image, ChevronRight, Search, MoreVertical } from 'lucide-react'
|
|
5
5
|
import type { LucideIcon } from 'lucide-react'
|
|
6
6
|
import { iconMap, type IconName } from './icon-button.tsx'
|
|
7
7
|
import { cn } from '../lib/cn.ts'
|
|
8
|
+
import { ACCENT_ICON, type AccentColor } from '../lib/form-colors.ts'
|
|
9
|
+
import { useAccentColor } from '../lib/accent-context.ts'
|
|
8
10
|
|
|
9
11
|
const ACCENT_SELECTED: Record<string, string> = {
|
|
10
12
|
blue: 'bg-blue-400/15 text-blue-400',
|
|
@@ -19,6 +21,19 @@ const ACCENT_SELECTED: Record<string, string> = {
|
|
|
19
21
|
violet: 'bg-violet-400/15 text-violet-400',
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
const ACCENT_SELECTED_BG: Record<string, string> = {
|
|
25
|
+
blue: 'bg-blue-500/20',
|
|
26
|
+
purple: 'bg-purple-500/20',
|
|
27
|
+
orange: 'bg-orange-500/20',
|
|
28
|
+
green: 'bg-green-500/20',
|
|
29
|
+
pink: 'bg-pink-500/20',
|
|
30
|
+
amber: 'bg-amber-500/20',
|
|
31
|
+
emerald: 'bg-emerald-500/20',
|
|
32
|
+
teal: 'bg-teal-500/20',
|
|
33
|
+
sky: 'bg-sky-500/20',
|
|
34
|
+
violet: 'bg-violet-500/20',
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
const EXTENSION_ICON_MAP: Record<string, LucideIcon> = {
|
|
23
38
|
ts: FileCode,
|
|
24
39
|
tsx: FileCode,
|
|
@@ -54,9 +69,29 @@ export interface FilesPanelProps {
|
|
|
54
69
|
showSearch?: boolean
|
|
55
70
|
className?: string
|
|
56
71
|
accentColor?: string
|
|
72
|
+
/** Right-click handler for tree nodes */
|
|
73
|
+
onContextMenu?: (e: React.MouseEvent, entry: FileEntry) => void
|
|
74
|
+
/** Path of the selected/highlighted folder (separate from file selection) */
|
|
75
|
+
selectedFolderPath?: string
|
|
76
|
+
/** Callback when a folder row is clicked (for folder selection). If set, chevron toggles expand while row selects. */
|
|
77
|
+
onSelectFolder?: (path: string) => void
|
|
78
|
+
/** Custom content rendered after the file/folder name (e.g. badges, labels) */
|
|
79
|
+
renderBadges?: (entry: FileEntry) => ReactNode
|
|
80
|
+
/** Custom hover actions rendered at the right edge of each row */
|
|
81
|
+
renderNodeActions?: (entry: FileEntry) => ReactNode
|
|
82
|
+
/** Set of paths that should render in a muted/dimmed style */
|
|
83
|
+
hiddenPaths?: Set<string>
|
|
84
|
+
/** Custom content above the tree (e.g. filter badges, creation inputs) */
|
|
85
|
+
header?: ReactNode
|
|
86
|
+
/** Custom footer content. When provided, replaces the default file count footer. */
|
|
87
|
+
footer?: ReactNode
|
|
88
|
+
/** Controlled expand state. When provided, FilesPanel uses this instead of internal state. */
|
|
89
|
+
expandedPaths?: Set<string>
|
|
90
|
+
/** Controlled toggle callback. Required when expandedPaths is provided. */
|
|
91
|
+
onToggleExpand?: (path: string) => void
|
|
57
92
|
}
|
|
58
93
|
|
|
59
|
-
function collectAllFolderPaths(entries: FileEntry[]): Set<string> {
|
|
94
|
+
export function collectAllFolderPaths(entries: FileEntry[]): Set<string> {
|
|
60
95
|
const paths = new Set<string>()
|
|
61
96
|
function walk(items: FileEntry[]) {
|
|
62
97
|
for (const entry of items) {
|
|
@@ -70,7 +105,7 @@ function collectAllFolderPaths(entries: FileEntry[]): Set<string> {
|
|
|
70
105
|
return paths
|
|
71
106
|
}
|
|
72
107
|
|
|
73
|
-
function countFiles(entries: FileEntry[]): number {
|
|
108
|
+
export function countFiles(entries: FileEntry[]): number {
|
|
74
109
|
let count = 0
|
|
75
110
|
for (const entry of entries) {
|
|
76
111
|
if (entry.type === 'file') count++
|
|
@@ -79,6 +114,17 @@ function countFiles(entries: FileEntry[]): number {
|
|
|
79
114
|
return count
|
|
80
115
|
}
|
|
81
116
|
|
|
117
|
+
export function countFolders(entries: FileEntry[]): number {
|
|
118
|
+
let count = 0
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (entry.type === 'folder') {
|
|
121
|
+
count++
|
|
122
|
+
if (entry.children) count += countFolders(entry.children)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return count
|
|
126
|
+
}
|
|
127
|
+
|
|
82
128
|
function getFileIcon(entry: FileEntry): LucideIcon {
|
|
83
129
|
if (entry.icon && iconMap[entry.icon]) return iconMap[entry.icon]!
|
|
84
130
|
if (entry.type === 'folder') return Folder
|
|
@@ -112,61 +158,96 @@ interface FileNodeProps {
|
|
|
112
158
|
entry: FileEntry
|
|
113
159
|
depth: number
|
|
114
160
|
selectedPath?: string
|
|
161
|
+
selectedFolderPath?: string
|
|
115
162
|
expandedPaths: Set<string>
|
|
163
|
+
hiddenPaths?: Set<string>
|
|
116
164
|
onToggleExpand: (path: string) => void
|
|
117
165
|
onSelect?: (path: string) => void
|
|
166
|
+
onSelectFolder?: (path: string) => void
|
|
118
167
|
onAction?: (action: string, path: string) => void
|
|
168
|
+
onContextMenu?: (e: React.MouseEvent, entry: FileEntry) => void
|
|
169
|
+
renderBadges?: (entry: FileEntry) => ReactNode
|
|
170
|
+
renderNodeActions?: (entry: FileEntry) => ReactNode
|
|
119
171
|
accentColor: string
|
|
120
172
|
}
|
|
121
173
|
|
|
122
|
-
function FileNode({
|
|
174
|
+
function FileNode({
|
|
175
|
+
entry, depth, selectedPath, selectedFolderPath, expandedPaths, hiddenPaths,
|
|
176
|
+
onToggleExpand, onSelect, onSelectFolder, onAction, onContextMenu,
|
|
177
|
+
renderBadges, renderNodeActions, accentColor,
|
|
178
|
+
}: FileNodeProps) {
|
|
123
179
|
const isFolder = entry.type === 'folder'
|
|
124
180
|
const isExpanded = isFolder && expandedPaths.has(entry.path)
|
|
125
|
-
const
|
|
181
|
+
const isFileSelected = !isFolder && selectedPath === entry.path
|
|
182
|
+
const isFolderSelected = isFolder && selectedFolderPath === entry.path
|
|
183
|
+
const isMuted = hiddenPaths?.has(entry.path)
|
|
126
184
|
const Icon = isFolder ? getFolderIcon(isExpanded, entry) : getFileIcon(entry)
|
|
127
185
|
|
|
186
|
+
const hasFolderSelection = !!onSelectFolder
|
|
187
|
+
const handleClick = isFolder
|
|
188
|
+
? (hasFolderSelection ? () => onSelectFolder!(entry.path) : () => onToggleExpand(entry.path))
|
|
189
|
+
: () => onSelect?.(entry.path)
|
|
190
|
+
|
|
191
|
+
const isHighlighted = isFileSelected || isFolderSelected
|
|
192
|
+
|
|
128
193
|
return (
|
|
129
194
|
<li>
|
|
130
|
-
<
|
|
131
|
-
type="button"
|
|
132
|
-
onClick={isFolder ? () => onToggleExpand(entry.path) : () => onSelect?.(entry.path)}
|
|
195
|
+
<div
|
|
133
196
|
className={cn(
|
|
134
|
-
'group flex items-center gap-1.5 w-full py-1 px-2
|
|
135
|
-
|
|
136
|
-
|
|
197
|
+
'group/node relative flex items-center gap-1.5 w-full py-1 px-2 text-sm transition-colors cursor-pointer select-none',
|
|
198
|
+
isMuted && 'opacity-50',
|
|
199
|
+
isHighlighted
|
|
200
|
+
? (isFolderSelected ? ACCENT_SELECTED_BG[accentColor] ?? ACCENT_SELECTED_BG.blue : ACCENT_SELECTED[accentColor] ?? ACCENT_SELECTED.blue)
|
|
137
201
|
: 'text-neutral-400 hover:bg-neutral-700/40 hover:text-neutral-200',
|
|
138
202
|
)}
|
|
139
203
|
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
|
204
|
+
onClick={handleClick}
|
|
205
|
+
onContextMenu={onContextMenu ? (e) => onContextMenu(e, entry) : undefined}
|
|
140
206
|
>
|
|
141
207
|
{isFolder ? (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
208
|
+
hasFolderSelection ? (
|
|
209
|
+
// Chevron is a separate click target when folders have selection behavior
|
|
210
|
+
<span
|
|
211
|
+
className="shrink-0 p-0.5 -m-0.5 rounded hover:bg-neutral-600/40 cursor-pointer"
|
|
212
|
+
onClick={(e) => { e.stopPropagation(); onToggleExpand(entry.path) }}
|
|
213
|
+
>
|
|
214
|
+
<ChevronRight className={cn('w-3 h-3 transition-transform', isExpanded && 'rotate-90')} />
|
|
215
|
+
</span>
|
|
216
|
+
) : (
|
|
217
|
+
<ChevronRight className={cn('w-3 h-3 shrink-0 transition-transform', isExpanded && 'rotate-90')} />
|
|
218
|
+
)
|
|
145
219
|
) : (
|
|
146
220
|
<span className="w-3 shrink-0" />
|
|
147
221
|
)}
|
|
148
222
|
<Icon
|
|
149
|
-
className=
|
|
223
|
+
className={cn('w-3.5 h-3.5 shrink-0', !entry.color && (ACCENT_ICON[accentColor as AccentColor] ?? ACCENT_ICON.blue))}
|
|
150
224
|
style={entry.color ? { color: entry.color } : undefined}
|
|
151
225
|
/>
|
|
152
226
|
<span className="truncate">{entry.name}</span>
|
|
153
|
-
{entry.badge && (
|
|
227
|
+
{renderBadges ? renderBadges(entry) : entry.badge && (
|
|
154
228
|
<span className="ml-auto shrink-0 px-1.5 py-0.5 text-xs rounded bg-neutral-700 text-neutral-500">
|
|
155
229
|
{entry.badge}
|
|
156
230
|
</span>
|
|
157
231
|
)}
|
|
158
|
-
{
|
|
232
|
+
{renderNodeActions ? (
|
|
233
|
+
<div
|
|
234
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 hidden group-hover/node:flex items-center gap-0.5 bg-neutral-960/90 rounded px-0.5"
|
|
235
|
+
onClick={(e) => e.stopPropagation()}
|
|
236
|
+
>
|
|
237
|
+
{renderNodeActions(entry)}
|
|
238
|
+
</div>
|
|
239
|
+
) : onAction && (
|
|
159
240
|
<span
|
|
160
241
|
role="button"
|
|
161
242
|
tabIndex={-1}
|
|
162
|
-
className="ml-auto shrink-0 opacity-0 group-hover:opacity-100 p-0.5 rounded hover:bg-neutral-700 text-neutral-500 hover:text-neutral-200 transition-all"
|
|
243
|
+
className="ml-auto shrink-0 opacity-0 group-hover/node:opacity-100 p-0.5 rounded hover:bg-neutral-700 text-neutral-500 hover:text-neutral-200 transition-all"
|
|
163
244
|
onClick={(e) => { e.stopPropagation(); onAction('menu', entry.path) }}
|
|
164
245
|
onKeyDown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); onAction('menu', entry.path) } }}
|
|
165
246
|
>
|
|
166
247
|
<MoreVertical className="w-3 h-3" />
|
|
167
248
|
</span>
|
|
168
249
|
)}
|
|
169
|
-
</
|
|
250
|
+
</div>
|
|
170
251
|
{isFolder && isExpanded && entry.children && (
|
|
171
252
|
<ul>
|
|
172
253
|
{entry.children.map((child) => (
|
|
@@ -175,10 +256,16 @@ function FileNode({ entry, depth, selectedPath, expandedPaths, onToggleExpand, o
|
|
|
175
256
|
entry={child}
|
|
176
257
|
depth={depth + 1}
|
|
177
258
|
selectedPath={selectedPath}
|
|
259
|
+
selectedFolderPath={selectedFolderPath}
|
|
178
260
|
expandedPaths={expandedPaths}
|
|
261
|
+
hiddenPaths={hiddenPaths}
|
|
179
262
|
onToggleExpand={onToggleExpand}
|
|
180
263
|
onSelect={onSelect}
|
|
264
|
+
onSelectFolder={onSelectFolder}
|
|
181
265
|
onAction={onAction}
|
|
266
|
+
onContextMenu={onContextMenu}
|
|
267
|
+
renderBadges={renderBadges}
|
|
268
|
+
renderNodeActions={renderNodeActions}
|
|
182
269
|
accentColor={accentColor}
|
|
183
270
|
/>
|
|
184
271
|
))}
|
|
@@ -195,11 +282,25 @@ export function FilesPanel({
|
|
|
195
282
|
onAction,
|
|
196
283
|
showSearch = false,
|
|
197
284
|
className,
|
|
198
|
-
accentColor
|
|
285
|
+
accentColor: accentColorProp,
|
|
286
|
+
onContextMenu,
|
|
287
|
+
selectedFolderPath,
|
|
288
|
+
onSelectFolder,
|
|
289
|
+
renderBadges,
|
|
290
|
+
renderNodeActions,
|
|
291
|
+
hiddenPaths,
|
|
292
|
+
header,
|
|
293
|
+
footer,
|
|
294
|
+
expandedPaths: controlledExpandedPaths,
|
|
295
|
+
onToggleExpand: controlledOnToggleExpand,
|
|
199
296
|
}: FilesPanelProps) {
|
|
200
|
-
const
|
|
297
|
+
const accentColor = accentColorProp ?? useAccentColor() ?? 'blue'
|
|
298
|
+
const [internalExpandedPaths, setInternalExpandedPaths] = useState<Set<string>>(() => collectAllFolderPaths(files))
|
|
201
299
|
const [searchQuery, setSearchQuery] = useState('')
|
|
202
300
|
|
|
301
|
+
const isControlled = controlledExpandedPaths !== undefined
|
|
302
|
+
const expandedPaths = isControlled ? controlledExpandedPaths : internalExpandedPaths
|
|
303
|
+
|
|
203
304
|
const fileCount = useMemo(() => countFiles(files), [files])
|
|
204
305
|
|
|
205
306
|
const displayedFiles = useMemo(
|
|
@@ -208,7 +309,11 @@ export function FilesPanel({
|
|
|
208
309
|
)
|
|
209
310
|
|
|
210
311
|
function handleToggleExpand(path: string) {
|
|
211
|
-
|
|
312
|
+
if (isControlled) {
|
|
313
|
+
controlledOnToggleExpand?.(path)
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
setInternalExpandedPaths((prev: Set<string>) => {
|
|
212
317
|
const next = new Set(prev)
|
|
213
318
|
if (next.has(path)) next.delete(path)
|
|
214
319
|
else next.add(path)
|
|
@@ -217,11 +322,14 @@ export function FilesPanel({
|
|
|
217
322
|
}
|
|
218
323
|
|
|
219
324
|
return (
|
|
220
|
-
<div className={cn('flex flex-col bg-neutral-
|
|
221
|
-
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
325
|
+
<div className={cn('flex flex-col bg-neutral-960 rounded-lg overflow-hidden', className)}>
|
|
326
|
+
{!header && (
|
|
327
|
+
<div className="flex items-center justify-between px-3 py-2 border-b border-neutral-700">
|
|
328
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-neutral-500">Files</span>
|
|
329
|
+
<span className="text-xs text-neutral-500">{fileCount} files</span>
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
{header}
|
|
225
333
|
{showSearch && (
|
|
226
334
|
<div className="px-2 py-2 border-b border-neutral-700">
|
|
227
335
|
<div className="flex items-center gap-1.5 px-2 py-1 bg-[var(--background)] border border-neutral-700 rounded text-sm">
|
|
@@ -244,10 +352,16 @@ export function FilesPanel({
|
|
|
244
352
|
entry={entry}
|
|
245
353
|
depth={0}
|
|
246
354
|
selectedPath={selectedPath}
|
|
355
|
+
selectedFolderPath={selectedFolderPath}
|
|
247
356
|
expandedPaths={expandedPaths}
|
|
357
|
+
hiddenPaths={hiddenPaths}
|
|
248
358
|
onToggleExpand={handleToggleExpand}
|
|
249
359
|
onSelect={onSelect}
|
|
360
|
+
onSelectFolder={onSelectFolder}
|
|
250
361
|
onAction={onAction}
|
|
362
|
+
onContextMenu={onContextMenu}
|
|
363
|
+
renderBadges={renderBadges}
|
|
364
|
+
renderNodeActions={renderNodeActions}
|
|
251
365
|
accentColor={accentColor}
|
|
252
366
|
/>
|
|
253
367
|
))}
|
|
@@ -256,6 +370,12 @@ export function FilesPanel({
|
|
|
256
370
|
<p className="text-xs text-neutral-500 text-center py-4">No files found</p>
|
|
257
371
|
)}
|
|
258
372
|
</div>
|
|
373
|
+
{footer}
|
|
374
|
+
{!footer && !header && (
|
|
375
|
+
<div className="flex items-center justify-between px-3 py-2 border-t border-neutral-700">
|
|
376
|
+
<span className="text-xs text-neutral-500">{fileCount} files</span>
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
259
379
|
</div>
|
|
260
380
|
)
|
|
261
381
|
}
|