@toolr/ui-design 0.1.8 → 0.1.10

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 (101) hide show
  1. package/ai-manifest.json +35 -20
  2. package/components/composites/dashboard-list-item.tsx +172 -0
  3. package/components/composites/dashboard-panel.tsx +218 -0
  4. package/components/content/info-panel-primitives.tsx +9 -8
  5. package/components/diagrams/diagram-utils.tsx +2 -1
  6. package/components/hooks/use-dropdown-portal.ts +39 -0
  7. package/components/lib/accent-context.ts +10 -0
  8. package/components/lib/{ai-tools.tsx → coding-agents.tsx} +23 -8
  9. package/components/lib/custom-icons.tsx +37 -0
  10. package/components/lib/form-colors.ts +16 -16
  11. package/components/lib/git-providers.tsx +39 -0
  12. package/components/lib/theme-engine.ts +59 -10
  13. package/components/lib/toolr-brand.tsx +23 -9
  14. package/components/sections/captured-issues/captured-issues-panel.tsx +17 -8
  15. package/components/sections/{ai-tools-paths/tools-paths-panel.tsx → coding-agent-paths/agent-paths-panel.tsx} +70 -62
  16. package/components/sections/coding-agent-paths/index.ts +37 -0
  17. package/components/sections/{ai-tools-paths → coding-agent-paths}/types.ts +28 -28
  18. package/components/sections/coding-agent-paths/use-agent-paths.ts +159 -0
  19. package/components/sections/golden-snapshots/file-diff-viewer.tsx +10 -9
  20. package/components/sections/golden-snapshots/golden-sync-panel.tsx +12 -3
  21. package/components/sections/golden-snapshots/snapshot-manager.tsx +9 -7
  22. package/components/sections/golden-snapshots/status-overview.tsx +8 -8
  23. package/components/sections/golden-snapshots/version-manager.tsx +6 -6
  24. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +3 -3
  25. package/components/sections/prompt-editor/index.ts +1 -1
  26. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +13 -5
  27. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +18 -10
  28. package/components/sections/prompt-editor/types.ts +2 -2
  29. package/components/sections/report-bug/report-bug-form.tsx +12 -4
  30. package/components/sections/report-bug/screenshot-uploader.tsx +11 -3
  31. package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +12 -4
  32. package/components/sections/snapshot-browser/snapshot-tree.tsx +5 -4
  33. package/components/sections/snapshot-browser/types.ts +1 -1
  34. package/components/sections/snippets-editor/snippets-editor.tsx +16 -9
  35. package/components/settings/SettingsHeader.tsx +2 -2
  36. package/components/settings/SettingsPanel.tsx +11 -3
  37. package/components/settings/SettingsTreeNav.tsx +15 -9
  38. package/components/ui/action-dialog.tsx +24 -30
  39. package/components/ui/ai-action-button.tsx +10 -7
  40. package/components/ui/ai-execution-action-buttons.tsx +13 -5
  41. package/components/ui/badge.tsx +7 -4
  42. package/components/ui/bottom-panel-header.tsx +9 -5
  43. package/components/ui/breadcrumb.tsx +13 -5
  44. package/components/ui/{extension-list-card.tsx → capability-list-card.tsx} +13 -5
  45. package/components/ui/checkbox.tsx +6 -3
  46. package/components/ui/collapsible-section.tsx +38 -29
  47. package/components/ui/confirm-badge.tsx +7 -4
  48. package/components/ui/cookie-consent.tsx +13 -7
  49. package/components/ui/detail-section.tsx +24 -16
  50. package/components/ui/detail-view-wrapper.tsx +30 -22
  51. package/components/ui/editor-placeholder-card.tsx +28 -24
  52. package/components/ui/editor-toolbar.tsx +7 -4
  53. package/components/ui/execution-details-panel.tsx +10 -5
  54. package/components/ui/file-structure-section.tsx +7 -4
  55. package/components/ui/file-tree.tsx +3 -1
  56. package/components/ui/files-panel.tsx +147 -27
  57. package/components/ui/filter-dropdown.tsx +84 -74
  58. package/components/ui/form-actions.tsx +14 -6
  59. package/components/ui/frontmatter-form-header.tsx +10 -2
  60. package/components/ui/icon-button.tsx +22 -9
  61. package/components/ui/input.tsx +7 -4
  62. package/components/ui/label.tsx +5 -5
  63. package/components/ui/layout-tab-bar.tsx +7 -5
  64. package/components/ui/modal.tsx +18 -4
  65. package/components/ui/nav-card.tsx +6 -3
  66. package/components/ui/navigation-bar.tsx +164 -82
  67. package/components/ui/number-input.tsx +8 -4
  68. package/components/ui/project-explorer.tsx +666 -0
  69. package/components/ui/registry-browser.tsx +12 -1
  70. package/components/ui/registry-card.tsx +49 -42
  71. package/components/ui/registry-detail.tsx +40 -17
  72. package/components/ui/resizable-textarea.tsx +18 -11
  73. package/components/ui/scope-badge.tsx +18 -11
  74. package/components/ui/segmented-toggle.tsx +5 -2
  75. package/components/ui/select.tsx +12 -9
  76. package/components/ui/selection-grid.tsx +36 -37
  77. package/components/ui/setting-row.tsx +2 -2
  78. package/components/ui/settings-card.tsx +10 -3
  79. package/components/ui/settings-info-box.tsx +9 -5
  80. package/components/ui/settings-section-title.tsx +14 -2
  81. package/components/ui/snapshot-card.tsx +10 -2
  82. package/components/ui/snippets-panel.tsx +4 -2
  83. package/components/ui/sort-dropdown.tsx +39 -29
  84. package/components/ui/status-card.tsx +9 -1
  85. package/components/ui/tab-bar.tsx +12 -9
  86. package/components/ui/toggle.tsx +13 -7
  87. package/components/ui/tooltip.tsx +9 -1
  88. package/dist/content.js +8 -8
  89. package/dist/diagrams.d.ts +0 -1
  90. package/dist/index.d.ts +427 -184
  91. package/dist/index.js +3098 -1761
  92. package/dist/tokens/primitives.css +28 -6
  93. package/dist/tokens/semantic.css +15 -15
  94. package/dist/tokens/theme.css +23 -0
  95. package/index.ts +25 -11
  96. package/package.json +1 -1
  97. package/tokens/primitives.css +28 -6
  98. package/tokens/semantic.css +15 -15
  99. package/tokens/theme.css +23 -0
  100. package/components/sections/ai-tools-paths/index.ts +0 -37
  101. 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?: 'purple' | 'blue' | 'neutral' | 'sky'
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
- purple: {
32
- name: 'text-purple-400',
33
- nameBg: 'bg-purple-500/10',
34
- },
35
- blue: {
36
- name: 'text-blue-400',
37
- nameBg: 'bg-blue-500/10',
38
- },
39
- neutral: {
40
- name: 'text-neutral-400',
41
- nameBg: 'bg-neutral-500/10',
42
- },
43
- sky: {
44
- name: 'text-sky-400',
45
- nameBg: 'bg-sky-500/10',
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 = 'purple',
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[accentColor]
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
- color={isPlaceholderCopied ? 'green' : 'neutral'}
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
- color={isValueCopied ? 'green' : 'neutral'}
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
- color="neutral"
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-800/50 rounded text-sm text-neutral-400 font-mono ${
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-900 border-b border-neutral-800">
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
- color="yellow"
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
- color="orange"
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
- color={isDirty && !hasError ? 'amber' : 'neutral'}
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, Info } from 'lucide-react'
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="flex items-center gap-2">
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
  }
@@ -1,6 +1,7 @@
1
1
  import { useState, useEffect, useCallback, useRef, useMemo, type ReactNode } from 'react'
2
2
  import { FileCode, FolderTree, Loader2, AlertCircle, AlignLeft, Code2, Type } from 'lucide-react'
3
3
  import { type AccentColor, ACCENT_ICON } from '../lib/form-colors.ts'
4
+ import { useAccentColor } from '../lib/accent-context.ts'
4
5
  import { CollapseButton } from './icon-button.tsx'
5
6
  import { SegmentedToggle } from './segmented-toggle.tsx'
6
7
  import { FileTree, collectDirPaths, type FileTreeNode } from './file-tree.tsx'
@@ -124,10 +125,12 @@ export function FileStructureSection({
124
125
  format,
125
126
  language,
126
127
  default: defaultMode,
127
- accentColor = 'blue',
128
+ accentColor: accentColorProp,
128
129
  renderPreview,
129
130
  initialHeight,
130
131
  }: FileStructureSectionProps) {
132
+ const contextAccent = useAccentColor()
133
+ const accentColor = accentColorProp ?? contextAccent ?? 'blue'
131
134
  const [selectedFilePath, setSelectedFilePath] = useState<string | null>(null)
132
135
  const [fileContent, setFileContent] = useState<string | null>(null)
133
136
  const [fetchedFilePath, setFetchedFilePath] = useState<string | null>(null)
@@ -328,14 +331,14 @@ export function FileStructureSection({
328
331
  }
329
332
 
330
333
  const treePanel = (
331
- <div className={`flex flex-col bg-neutral-900 border ${ACCENT_BORDER[accentColor]} rounded-lg overflow-hidden ${variant === 'split' && effectiveFilePath ? 'w-1/3 shrink-0' : 'flex-1'}`}>
334
+ <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
335
  <div className={`flex items-center px-3 py-2 border-b ${ACCENT_BORDER[accentColor]} shrink-0 gap-2 min-w-0`}>
333
336
  <FolderTree className={`w-3.5 h-3.5 shrink-0 ${ACCENT_ICON[accentColor]}`} />
334
337
  <span className="text-sm text-neutral-200 truncate flex-1">Files</span>
335
338
  <CollapseButton
336
339
  collapsed={allCollapsed}
337
340
  onToggle={() => setExpandedPaths(allCollapsed ? new Set(allDirPaths) : new Set())}
338
- color={accentColor}
341
+ accentColor="neutral"
339
342
  />
340
343
  </div>
341
344
  <div className={`${variant === 'split' ? 'flex-1 overflow-y-auto' : ''} p-3`}>
@@ -353,7 +356,7 @@ export function FileStructureSection({
353
356
  )
354
357
 
355
358
  const previewPanel = effectiveFilePath ? (
356
- <div className={`flex-1 flex flex-col bg-neutral-900 border ${ACCENT_BORDER[accentColor]} rounded-lg overflow-hidden`}>
359
+ <div className={`flex-1 flex flex-col bg-neutral-980 border ${ACCENT_BORDER[accentColor]} rounded-lg overflow-hidden`}>
357
360
  <div className={`flex items-center px-3 py-2 border-b ${ACCENT_BORDER[accentColor]} shrink-0 gap-2 min-w-0`}>
358
361
  <FileCode className={`w-3.5 h-3.5 shrink-0 ${ACCENT_ICON[accentColor]}`} />
359
362
  <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 = 'blue' }: FileTreeProps) {
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({ entry, depth, selectedPath, expandedPaths, onToggleExpand, onSelect, onAction, accentColor }: FileNodeProps) {
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 isSelected = !isFolder && selectedPath === entry.path
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
- <button
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 rounded text-sm transition-colors cursor-pointer',
135
- isSelected
136
- ? ACCENT_SELECTED[accentColor] ?? ACCENT_SELECTED.blue
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
- <ChevronRight
143
- className={cn('w-3 h-3 shrink-0 transition-transform', isExpanded && 'rotate-90')}
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="w-3.5 h-3.5 shrink-0"
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
- {onAction && (
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
- </button>
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 = 'blue',
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 [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => collectAllFolderPaths(files))
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
- setExpandedPaths((prev: Set<string>) => {
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-800 rounded-lg overflow-hidden', className)}>
221
- <div className="flex items-center justify-between px-3 py-2 border-b border-neutral-700">
222
- <span className="text-xs font-semibold uppercase tracking-wider text-neutral-500">Files</span>
223
- <span className="text-xs text-neutral-500">{fileCount} files</span>
224
- </div>
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
  }