@toolr/ui-design 0.1.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/README.md +63 -0
- package/components/content/info-panel-primitives.tsx +297 -0
- package/components/diagrams/diagram-utils.tsx +908 -0
- package/components/hooks/use-click-outside.ts +27 -0
- package/components/hooks/use-dropdown-max-height.ts +20 -0
- package/components/hooks/use-navigation-history.ts +94 -0
- package/components/lib/ai-tools.tsx +44 -0
- package/components/lib/cn.ts +6 -0
- package/components/lib/form-colors.ts +32 -0
- package/components/lib/theme-engine.ts +97 -0
- package/components/lib/toolr-brand.tsx +31 -0
- package/components/sections/ai-tools-paths/index.ts +37 -0
- package/components/sections/ai-tools-paths/tools-paths-panel.tsx +212 -0
- package/components/sections/ai-tools-paths/types.ts +111 -0
- package/components/sections/ai-tools-paths/use-tools-paths.ts +159 -0
- package/components/sections/captured-issues/captured-issues-panel.tsx +214 -0
- package/components/sections/captured-issues/index.ts +38 -0
- package/components/sections/captured-issues/types.ts +113 -0
- package/components/sections/captured-issues/use-captured-issues.ts +111 -0
- package/components/sections/golden-snapshots/file-diff-viewer.tsx +420 -0
- package/components/sections/golden-snapshots/golden-sync-panel.tsx +223 -0
- package/components/sections/golden-snapshots/index.ts +145 -0
- package/components/sections/golden-snapshots/snapshot-manager.tsx +200 -0
- package/components/sections/golden-snapshots/status-overview.tsx +305 -0
- package/components/sections/golden-snapshots/types.ts +288 -0
- package/components/sections/golden-snapshots/use-golden-sync.ts +477 -0
- package/components/sections/golden-snapshots/version-manager.tsx +186 -0
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +210 -0
- package/components/sections/prompt-editor/index.ts +121 -0
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +276 -0
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +514 -0
- package/components/sections/prompt-editor/types.ts +101 -0
- package/components/sections/prompt-editor/use-prompt-editor.ts +131 -0
- package/components/sections/report-bug/error-logger.ts +392 -0
- package/components/sections/report-bug/index.ts +59 -0
- package/components/sections/report-bug/issue-reporter-api.ts +83 -0
- package/components/sections/report-bug/report-bug-form.tsx +282 -0
- package/components/sections/report-bug/screenshot-uploader.tsx +228 -0
- package/components/sections/report-bug/use-report-bug.ts +170 -0
- package/components/sections/snapshot-browser/index.ts +53 -0
- package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +147 -0
- package/components/sections/snapshot-browser/snapshot-tree.tsx +451 -0
- package/components/sections/snapshot-browser/types.ts +106 -0
- package/components/sections/snapshot-browser/use-snapshot-browser.ts +125 -0
- package/components/sections/snippets-editor/index.ts +31 -0
- package/components/sections/snippets-editor/snippets-editor.tsx +381 -0
- package/components/sections/snippets-editor/types.ts +48 -0
- package/components/sections/snippets-editor/use-snippets-editor.ts +217 -0
- package/components/ui/action-dialog.tsx +309 -0
- package/components/ui/ai-action-button.tsx +137 -0
- package/components/ui/ai-execution-action-buttons.tsx +106 -0
- package/components/ui/badge.tsx +67 -0
- package/components/ui/bottom-panel-header.tsx +240 -0
- package/components/ui/breadcrumb.tsx +168 -0
- package/components/ui/checkbox.tsx +102 -0
- package/components/ui/collapsible-section.tsx +100 -0
- package/components/ui/confirm-badge.tsx +71 -0
- package/components/ui/detail-section.tsx +67 -0
- package/components/ui/detail-view-wrapper.tsx +55 -0
- package/components/ui/editor-placeholder-card.tsx +197 -0
- package/components/ui/editor-toolbar.tsx +123 -0
- package/components/ui/execution-details-panel.tsx +93 -0
- package/components/ui/extension-list-card.tsx +105 -0
- package/components/ui/file-structure-section.tsx +373 -0
- package/components/ui/file-tree.tsx +171 -0
- package/components/ui/files-panel.tsx +251 -0
- package/components/ui/filter-dropdown.tsx +173 -0
- package/components/ui/form-actions.tsx +127 -0
- package/components/ui/frontmatter-form-header.tsx +80 -0
- package/components/ui/icon-button.tsx +388 -0
- package/components/ui/input.tsx +211 -0
- package/components/ui/label.tsx +159 -0
- package/components/ui/layout-tab-bar.tsx +289 -0
- package/components/ui/modal.tsx +194 -0
- package/components/ui/nav-card.tsx +81 -0
- package/components/ui/navigation-bar.tsx +285 -0
- package/components/ui/number-input.tsx +165 -0
- package/components/ui/registry-browser.tsx +261 -0
- package/components/ui/registry-card.tsx +710 -0
- package/components/ui/registry-detail.tsx +224 -0
- package/components/ui/resizable-textarea.tsx +290 -0
- package/components/ui/scope-badge.tsx +67 -0
- package/components/ui/segmented-toggle.tsx +133 -0
- package/components/ui/select.tsx +172 -0
- package/components/ui/selection-grid.tsx +313 -0
- package/components/ui/setting-row.tsx +97 -0
- package/components/ui/snapshot-card.tsx +107 -0
- package/components/ui/snippets-panel.tsx +161 -0
- package/components/ui/sort-dropdown.tsx +109 -0
- package/components/ui/status-card.tsx +96 -0
- package/components/ui/tab-bar.tsx +340 -0
- package/components/ui/toggle.tsx +142 -0
- package/components/ui/tooltip.tsx +326 -0
- package/dist/content.d.ts +110 -0
- package/dist/content.js +195 -0
- package/dist/diagrams.d.ts +371 -0
- package/dist/diagrams.js +702 -0
- package/dist/index.d.ts +2714 -0
- package/dist/index.js +11220 -0
- package/dist/preset.d.ts +24 -0
- package/dist/preset.js +17 -0
- package/dist/tokens/tokens/primitives.css +45 -0
- package/dist/tokens/tokens/semantic.css +46 -0
- package/dist/tokens/tokens/theme.css +11 -0
- package/dist/tokens/tokens/tokens.json +65 -0
- package/index.ts +123 -0
- package/package.json +63 -0
- package/tailwind-preset.ts +22 -0
- package/tokens/primitives.css +45 -0
- package/tokens/semantic.css +46 -0
- package/tokens/theme.css +11 -0
- package/tokens/tokens.json +65 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCapturedIssues — State management hook for captured issues panel
|
|
3
|
+
*
|
|
4
|
+
* Part of: Sections > Captured Issues
|
|
5
|
+
*
|
|
6
|
+
* Manages form state (title, description, email), submission flow,
|
|
7
|
+
* dismiss functionality, and previously reported errors loading.
|
|
8
|
+
*
|
|
9
|
+
* AI agent notes:
|
|
10
|
+
* - This hook is used internally by CapturedIssuesPanel but can also
|
|
11
|
+
* be used standalone for custom UIs
|
|
12
|
+
* - All backend calls go through the CapturedIssuesApi interface
|
|
13
|
+
* - The hook resets form fields on successful submission
|
|
14
|
+
* - Previously reported errors are loaded once on mount
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
18
|
+
import type { CapturedError, CapturedIssuesApi, SubmittedError } from './types.ts'
|
|
19
|
+
|
|
20
|
+
export interface UseCapturedIssuesOptions {
|
|
21
|
+
api: CapturedIssuesApi
|
|
22
|
+
errors: CapturedError[]
|
|
23
|
+
onDismiss?: () => void
|
|
24
|
+
onSubmitSuccess?: (issueId?: string) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UseCapturedIssuesReturn {
|
|
28
|
+
errorCount: number
|
|
29
|
+
warnCount: number
|
|
30
|
+
title: string
|
|
31
|
+
setTitle: (title: string) => void
|
|
32
|
+
description: string
|
|
33
|
+
setDescription: (desc: string) => void
|
|
34
|
+
email: string
|
|
35
|
+
setEmail: (email: string) => void
|
|
36
|
+
isSubmitting: boolean
|
|
37
|
+
submitError: string | null
|
|
38
|
+
submittedErrors: SubmittedError[]
|
|
39
|
+
handleSubmit: () => Promise<void>
|
|
40
|
+
handleDismiss: () => Promise<void>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useCapturedIssues(options: UseCapturedIssuesOptions): UseCapturedIssuesReturn {
|
|
44
|
+
const { api, errors, onDismiss, onSubmitSuccess } = options
|
|
45
|
+
|
|
46
|
+
const [title, setTitle] = useState('')
|
|
47
|
+
const [description, setDescription] = useState('')
|
|
48
|
+
const [email, setEmail] = useState('')
|
|
49
|
+
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
50
|
+
const [submitError, setSubmitError] = useState<string | null>(null)
|
|
51
|
+
const [submittedErrors, setSubmittedErrors] = useState<SubmittedError[]>([])
|
|
52
|
+
|
|
53
|
+
const errorCount = errors.filter((e) => e.level === 'error').length
|
|
54
|
+
const warnCount = errors.filter((e) => e.level === 'warning').length
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
api.getSubmittedErrors().then(setSubmittedErrors).catch(() => {})
|
|
58
|
+
}, [api])
|
|
59
|
+
|
|
60
|
+
const handleSubmit = useCallback(async () => {
|
|
61
|
+
setIsSubmitting(true)
|
|
62
|
+
setSubmitError(null)
|
|
63
|
+
try {
|
|
64
|
+
const result = await api.submitIssue({
|
|
65
|
+
title: title.trim(),
|
|
66
|
+
description: description.trim(),
|
|
67
|
+
email: email.trim(),
|
|
68
|
+
errors,
|
|
69
|
+
})
|
|
70
|
+
if (result.success) {
|
|
71
|
+
setTitle('')
|
|
72
|
+
setDescription('')
|
|
73
|
+
setEmail('')
|
|
74
|
+
onSubmitSuccess?.(result.issueId)
|
|
75
|
+
// Refresh submitted errors list
|
|
76
|
+
api.getSubmittedErrors().then(setSubmittedErrors).catch(() => {})
|
|
77
|
+
} else {
|
|
78
|
+
setSubmitError('Failed to submit issue')
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
setSubmitError(`An error occurred: ${err}`)
|
|
82
|
+
} finally {
|
|
83
|
+
setIsSubmitting(false)
|
|
84
|
+
}
|
|
85
|
+
}, [api, title, description, email, errors, onSubmitSuccess])
|
|
86
|
+
|
|
87
|
+
const handleDismiss = useCallback(async () => {
|
|
88
|
+
try {
|
|
89
|
+
await api.dismissErrors()
|
|
90
|
+
onDismiss?.()
|
|
91
|
+
} catch (err) {
|
|
92
|
+
setSubmitError(`Failed to dismiss: ${err}`)
|
|
93
|
+
}
|
|
94
|
+
}, [api, onDismiss])
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
errorCount,
|
|
98
|
+
warnCount,
|
|
99
|
+
title,
|
|
100
|
+
setTitle,
|
|
101
|
+
description,
|
|
102
|
+
setDescription,
|
|
103
|
+
email,
|
|
104
|
+
setEmail,
|
|
105
|
+
isSubmitting,
|
|
106
|
+
submitError,
|
|
107
|
+
submittedErrors,
|
|
108
|
+
handleSubmit,
|
|
109
|
+
handleDismiss,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
ChevronRight,
|
|
4
|
+
ChevronDown,
|
|
5
|
+
Folder,
|
|
6
|
+
Columns2,
|
|
7
|
+
Rows2,
|
|
8
|
+
ChevronsUpDown,
|
|
9
|
+
ChevronsDownUp,
|
|
10
|
+
Save,
|
|
11
|
+
RotateCcw,
|
|
12
|
+
FileCode,
|
|
13
|
+
} from 'lucide-react'
|
|
14
|
+
import { DiffEditor } from '@monaco-editor/react'
|
|
15
|
+
import { IconButton } from '../../ui/icon-button.tsx'
|
|
16
|
+
import type { FileDiffInfo } from './types.ts'
|
|
17
|
+
import type { DiffTreeNode } from './use-golden-sync.ts'
|
|
18
|
+
import { getLanguage, type UseGoldenSyncReturn } from './use-golden-sync.ts'
|
|
19
|
+
|
|
20
|
+
// --- Color palette for component tree nodes ---
|
|
21
|
+
|
|
22
|
+
interface ComponentColorSet {
|
|
23
|
+
text: string
|
|
24
|
+
pillBg: string
|
|
25
|
+
bg: string
|
|
26
|
+
hoverBg: string
|
|
27
|
+
border: string
|
|
28
|
+
line: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const COMPONENT_COLORS: ComponentColorSet[] = [
|
|
32
|
+
{ text: 'text-blue-400', pillBg: 'bg-blue-500/20', bg: 'bg-blue-500/5', hoverBg: 'hover:bg-blue-500/10', border: 'border-blue-500/40', line: 'border-blue-500/20' },
|
|
33
|
+
{ text: 'text-purple-400', pillBg: 'bg-purple-500/20', bg: 'bg-purple-500/5', hoverBg: 'hover:bg-purple-500/10', border: 'border-purple-500/40', line: 'border-purple-500/20' },
|
|
34
|
+
{ text: 'text-teal-400', pillBg: 'bg-teal-500/20', bg: 'bg-teal-500/5', hoverBg: 'hover:bg-teal-500/10', border: 'border-teal-500/40', line: 'border-teal-500/20' },
|
|
35
|
+
{ text: 'text-red-400', pillBg: 'bg-red-500/20', bg: 'bg-red-500/5', hoverBg: 'hover:bg-red-500/10', border: 'border-red-500/40', line: 'border-red-500/20' },
|
|
36
|
+
{ text: 'text-amber-400', pillBg: 'bg-amber-500/20', bg: 'bg-amber-500/5', hoverBg: 'hover:bg-amber-500/10', border: 'border-amber-500/40', line: 'border-amber-500/20' },
|
|
37
|
+
{ text: 'text-green-400', pillBg: 'bg-green-500/20', bg: 'bg-green-500/5', hoverBg: 'hover:bg-green-500/10', border: 'border-green-500/40', line: 'border-green-500/20' },
|
|
38
|
+
{ text: 'text-pink-400', pillBg: 'bg-pink-500/20', bg: 'bg-pink-500/5', hoverBg: 'hover:bg-pink-500/10', border: 'border-pink-500/40', line: 'border-pink-500/20' },
|
|
39
|
+
{ text: 'text-cyan-400', pillBg: 'bg-cyan-500/20', bg: 'bg-cyan-500/5', hoverBg: 'hover:bg-cyan-500/10', border: 'border-cyan-500/40', line: 'border-cyan-500/20' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
function getComponentColor(index: number): ComponentColorSet {
|
|
43
|
+
return COMPONENT_COLORS[index % COMPONENT_COLORS.length]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- File status helpers ---
|
|
47
|
+
|
|
48
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
49
|
+
modified: 'text-yellow-400',
|
|
50
|
+
added: 'text-green-400',
|
|
51
|
+
removed: 'text-red-400',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const STATUS_LABELS: Record<string, string> = {
|
|
55
|
+
modified: 'M',
|
|
56
|
+
added: 'A',
|
|
57
|
+
removed: 'D',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- File tree item component ---
|
|
61
|
+
|
|
62
|
+
interface DiffFileItemProps {
|
|
63
|
+
file: FileDiffInfo
|
|
64
|
+
isSelected: boolean
|
|
65
|
+
onSelect: (file: FileDiffInfo) => void
|
|
66
|
+
onReset: (file: FileDiffInfo) => void
|
|
67
|
+
resettingFile: string | null
|
|
68
|
+
anyResetting: boolean
|
|
69
|
+
renderFileIcon?: (filename: string, component: string) => ReactNode
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function DiffFileItem({ file, isSelected, onSelect, onReset, resettingFile, anyResetting, renderFileIcon }: DiffFileItemProps) {
|
|
73
|
+
const filename = file.path.split('/').pop() ?? file.path
|
|
74
|
+
const statusColor = STATUS_COLORS[file.status] ?? ''
|
|
75
|
+
const statusLabel = STATUS_LABELS[file.status] ?? ''
|
|
76
|
+
|
|
77
|
+
const icon = renderFileIcon?.(filename, file.component) ?? (
|
|
78
|
+
<FileCode className="w-3 h-3 flex-shrink-0 text-blue-400/50" />
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
className={`flex items-center transition-colors ${
|
|
84
|
+
isSelected ? 'bg-[#313244]' : 'hover:bg-[#1e1e2e]'
|
|
85
|
+
}`}
|
|
86
|
+
>
|
|
87
|
+
<button
|
|
88
|
+
onClick={() => onSelect(file)}
|
|
89
|
+
className={`flex-1 flex items-center gap-2 px-3 py-1 text-xs ${
|
|
90
|
+
isSelected ? 'text-[#cdd6f4]' : 'text-[#a6adc8]'
|
|
91
|
+
}`}
|
|
92
|
+
>
|
|
93
|
+
{icon}
|
|
94
|
+
<span className="truncate">{filename}</span>
|
|
95
|
+
{statusLabel && (
|
|
96
|
+
<span className={`text-[10px] font-medium ml-auto flex-shrink-0 ${statusColor}`}>
|
|
97
|
+
{statusLabel}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
</button>
|
|
101
|
+
{file.status === 'modified' && (
|
|
102
|
+
<div className="pr-2 flex-shrink-0">
|
|
103
|
+
<IconButton
|
|
104
|
+
icon={<RotateCcw className={resettingFile === `${file.component}:${file.path}` ? 'animate-spin' : ''} />}
|
|
105
|
+
onClick={() => onReset(file)}
|
|
106
|
+
disabled={anyResetting}
|
|
107
|
+
size="xs"
|
|
108
|
+
color="orange"
|
|
109
|
+
tooltip={{ title: 'Reset file', description: 'Reset this file to golden' }}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- File Tree Panel ---
|
|
118
|
+
|
|
119
|
+
interface DiffFileTreePanelProps {
|
|
120
|
+
sync: UseGoldenSyncReturn
|
|
121
|
+
componentLabels?: Record<string, string>
|
|
122
|
+
renderFileIcon?: (filename: string, component: string) => ReactNode
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function DiffFileTreePanel({ sync, componentLabels, renderFileIcon }: DiffFileTreePanelProps) {
|
|
126
|
+
const {
|
|
127
|
+
diff,
|
|
128
|
+
selectedDiffFile,
|
|
129
|
+
diffFilter,
|
|
130
|
+
setDiffFilter,
|
|
131
|
+
handleSelectDiffFile,
|
|
132
|
+
handleResetFile,
|
|
133
|
+
resettingFile,
|
|
134
|
+
groupedFiles,
|
|
135
|
+
collapsedCategories,
|
|
136
|
+
toggleCategory,
|
|
137
|
+
countFiles,
|
|
138
|
+
allCategoryKeys,
|
|
139
|
+
allExpanded,
|
|
140
|
+
handleExpandAll,
|
|
141
|
+
handleCollapseAll,
|
|
142
|
+
} = sync
|
|
143
|
+
|
|
144
|
+
const anyResetting = resettingFile !== null
|
|
145
|
+
|
|
146
|
+
// Build component index for color assignment
|
|
147
|
+
const componentOrder: string[] = Array.from(groupedFiles.keys())
|
|
148
|
+
|
|
149
|
+
const renderTreeNode = (
|
|
150
|
+
node: DiffTreeNode,
|
|
151
|
+
categoryKey: string,
|
|
152
|
+
label: string,
|
|
153
|
+
lineClass: string,
|
|
154
|
+
isRoot: boolean,
|
|
155
|
+
colorIndex: number,
|
|
156
|
+
): ReactNode => {
|
|
157
|
+
const hasContent = node.files.length > 0 || Object.keys(node.children).length > 0
|
|
158
|
+
if (!hasContent) return null
|
|
159
|
+
|
|
160
|
+
const isCollapsed = collapsedCategories.has(categoryKey)
|
|
161
|
+
const fileCount = countFiles(node)
|
|
162
|
+
const color = getComponentColor(colorIndex)
|
|
163
|
+
|
|
164
|
+
const displayLabel = isRoot ? (componentLabels?.[label] ?? label.charAt(0).toUpperCase() + label.slice(1)) : label
|
|
165
|
+
|
|
166
|
+
// Root-level nodes get colored styling, nested folders get muted styling
|
|
167
|
+
const headerClasses = isRoot
|
|
168
|
+
? `${color.text} ${color.hoverBg} border-l-2 ${color.border} ${color.bg} px-3 py-2 text-sm font-medium`
|
|
169
|
+
: 'text-[#6c7086] hover:bg-[#1e1e2e] px-3 py-1.5'
|
|
170
|
+
|
|
171
|
+
const chevronSize = isRoot ? 'w-4 h-4' : 'w-3 h-3'
|
|
172
|
+
|
|
173
|
+
const countElement = isRoot ? (
|
|
174
|
+
<span className={`ml-auto px-1.5 py-0.5 rounded-full text-[10px] font-medium ${color.pillBg} ${color.text}`}>
|
|
175
|
+
{fileCount}
|
|
176
|
+
</span>
|
|
177
|
+
) : (
|
|
178
|
+
<span className="text-[#585b70] ml-auto">{fileCount}</span>
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
const containerLineClass = isRoot ? color.line : lineClass
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div key={categoryKey}>
|
|
185
|
+
<button
|
|
186
|
+
onClick={() => toggleCategory(categoryKey)}
|
|
187
|
+
className={`w-full flex items-center gap-2 ${headerClasses} transition-colors cursor-pointer`}
|
|
188
|
+
>
|
|
189
|
+
{isCollapsed ? (
|
|
190
|
+
<ChevronRight className={chevronSize} />
|
|
191
|
+
) : (
|
|
192
|
+
<ChevronDown className={chevronSize} />
|
|
193
|
+
)}
|
|
194
|
+
{isRoot ? null : <Folder className="w-3 h-3 text-[#585b70]" />}
|
|
195
|
+
{displayLabel}
|
|
196
|
+
{countElement}
|
|
197
|
+
</button>
|
|
198
|
+
{!isCollapsed && (
|
|
199
|
+
<div className={`${isRoot ? 'ml-2' : 'ml-4'} border-l ${containerLineClass}`}>
|
|
200
|
+
{node.files.map((file) => (
|
|
201
|
+
<DiffFileItem
|
|
202
|
+
key={`${file.component}-${file.path}`}
|
|
203
|
+
file={file}
|
|
204
|
+
isSelected={selectedDiffFile?.path === file.path && selectedDiffFile?.component === file.component}
|
|
205
|
+
onSelect={handleSelectDiffFile}
|
|
206
|
+
onReset={handleResetFile}
|
|
207
|
+
resettingFile={resettingFile}
|
|
208
|
+
anyResetting={anyResetting}
|
|
209
|
+
renderFileIcon={renderFileIcon}
|
|
210
|
+
/>
|
|
211
|
+
))}
|
|
212
|
+
{Object.entries(node.children).map(([childName, childNode]) =>
|
|
213
|
+
renderTreeNode(childNode, `${categoryKey}-${childName}`, childName, containerLineClass, false, colorIndex)
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div className="w-72 flex-shrink-0 bg-[#181825] rounded-lg border border-[#313244] overflow-hidden flex flex-col">
|
|
223
|
+
{/* Summary Header */}
|
|
224
|
+
<div className="p-3 border-b border-[#313244] flex-shrink-0">
|
|
225
|
+
<div className="flex gap-3 text-xs">
|
|
226
|
+
<span className="text-green-400">+{diff!.summary.added}</span>
|
|
227
|
+
<span className="text-red-400">-{diff!.summary.removed}</span>
|
|
228
|
+
<span className="text-yellow-400">~{diff!.summary.modified}</span>
|
|
229
|
+
<span className="text-neutral-400">={diff!.summary.unchanged}</span>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{/* Filter Tabs */}
|
|
234
|
+
<div className="flex items-center justify-between p-2 border-b border-[#313244] flex-shrink-0">
|
|
235
|
+
<div className="flex items-center gap-1">
|
|
236
|
+
{(['all', 'modified', 'added', 'removed'] as const).map((filter) => (
|
|
237
|
+
<button
|
|
238
|
+
key={filter}
|
|
239
|
+
onClick={() => setDiffFilter(filter)}
|
|
240
|
+
className={`px-2 py-1 rounded text-xs transition-colors cursor-pointer ${
|
|
241
|
+
diffFilter === filter
|
|
242
|
+
? 'bg-[#313244] text-[#cdd6f4]'
|
|
243
|
+
: 'text-[#6c7086] hover:text-[#a6adc8]'
|
|
244
|
+
}`}
|
|
245
|
+
>
|
|
246
|
+
{filter === 'all' ? 'All' : filter.charAt(0).toUpperCase() + filter.slice(1)}
|
|
247
|
+
</button>
|
|
248
|
+
))}
|
|
249
|
+
</div>
|
|
250
|
+
{allCategoryKeys.length > 0 && (
|
|
251
|
+
<IconButton
|
|
252
|
+
icon={allExpanded ? <ChevronsDownUp className="w-3.5 h-3.5" /> : <ChevronsUpDown className="w-3.5 h-3.5" />}
|
|
253
|
+
onClick={allExpanded ? handleCollapseAll : handleExpandAll}
|
|
254
|
+
size="sm"
|
|
255
|
+
color="neutral"
|
|
256
|
+
tooltip={{
|
|
257
|
+
title: allExpanded ? 'Collapse All' : 'Expand All',
|
|
258
|
+
description: allExpanded ? 'Collapse all folders' : 'Expand all folders',
|
|
259
|
+
}}
|
|
260
|
+
/>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
{/* File Tree */}
|
|
265
|
+
<div className="flex-1 overflow-y-auto py-1">
|
|
266
|
+
{componentOrder.map((component, index) => {
|
|
267
|
+
const node = groupedFiles.get(component)!
|
|
268
|
+
return renderTreeNode(node, component, component, '', true, index)
|
|
269
|
+
})}
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// --- Diff Editor Panel ---
|
|
276
|
+
|
|
277
|
+
interface DiffEditorPanelProps {
|
|
278
|
+
sync: UseGoldenSyncReturn
|
|
279
|
+
componentLabels?: Record<string, string>
|
|
280
|
+
monacoTheme?: string
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function DiffEditorPanel({ sync, componentLabels, monacoTheme }: DiffEditorPanelProps) {
|
|
284
|
+
const {
|
|
285
|
+
selectedDiffFile,
|
|
286
|
+
goldenContent,
|
|
287
|
+
liveContent,
|
|
288
|
+
editedLiveContent,
|
|
289
|
+
setEditedLiveContent,
|
|
290
|
+
diffSideBySide,
|
|
291
|
+
hasUnsavedChanges,
|
|
292
|
+
devtools,
|
|
293
|
+
} = sync
|
|
294
|
+
|
|
295
|
+
if (!selectedDiffFile) {
|
|
296
|
+
return (
|
|
297
|
+
<div className="flex-1 min-w-0 bg-[#181825] rounded-lg border border-[#313244] overflow-hidden flex flex-col">
|
|
298
|
+
<div className="flex items-center justify-center h-full text-sm text-[#6c7086]">
|
|
299
|
+
Select a file from the tree to view differences
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const compLabel = componentLabels?.[selectedDiffFile.component] ?? selectedDiffFile.component
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<div className="flex-1 min-w-0 bg-[#181825] rounded-lg border border-[#313244] overflow-hidden flex flex-col">
|
|
309
|
+
{/* File Header */}
|
|
310
|
+
<div className="flex items-center justify-between px-4 py-2 border-b border-[#313244] flex-shrink-0">
|
|
311
|
+
<div className="flex items-center gap-2">
|
|
312
|
+
<FileCode className="w-4 h-4 flex-shrink-0 text-blue-400/50" />
|
|
313
|
+
<span className="text-sm text-[#cdd6f4] font-medium">
|
|
314
|
+
{selectedDiffFile.component}/{selectedDiffFile.path}
|
|
315
|
+
</span>
|
|
316
|
+
{devtools && hasUnsavedChanges && (
|
|
317
|
+
<span className="px-2 py-0.5 bg-amber-500/20 text-amber-400 text-xs rounded">
|
|
318
|
+
Modified
|
|
319
|
+
</span>
|
|
320
|
+
)}
|
|
321
|
+
</div>
|
|
322
|
+
<span className="text-xs text-[#6c7086]">{compLabel}</span>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
{/* Diff Editor */}
|
|
326
|
+
<div className="flex-1 min-h-0">
|
|
327
|
+
{goldenContent !== null || liveContent !== null ? (
|
|
328
|
+
<DiffEditor
|
|
329
|
+
key={`diff-${selectedDiffFile.path}-${diffSideBySide}`}
|
|
330
|
+
height="100%"
|
|
331
|
+
language={getLanguage(selectedDiffFile.path)}
|
|
332
|
+
theme={monacoTheme ?? 'vs-dark'}
|
|
333
|
+
original={goldenContent ?? ''}
|
|
334
|
+
modified={editedLiveContent ?? liveContent ?? ''}
|
|
335
|
+
keepCurrentOriginalModel={true}
|
|
336
|
+
keepCurrentModifiedModel={true}
|
|
337
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
338
|
+
onMount={devtools ? (diffEditor: any) => {
|
|
339
|
+
const modifiedEditor = diffEditor.getModifiedEditor()
|
|
340
|
+
modifiedEditor.onDidChangeModelContent(() => {
|
|
341
|
+
const newContent = modifiedEditor.getValue()
|
|
342
|
+
setEditedLiveContent(newContent)
|
|
343
|
+
})
|
|
344
|
+
} : undefined}
|
|
345
|
+
options={{
|
|
346
|
+
readOnly: !devtools,
|
|
347
|
+
renderSideBySide: diffSideBySide,
|
|
348
|
+
originalEditable: false,
|
|
349
|
+
minimap: { enabled: false },
|
|
350
|
+
scrollBeyondLastLine: false,
|
|
351
|
+
fontSize: 12,
|
|
352
|
+
lineNumbers: 'on',
|
|
353
|
+
wordWrap: 'on',
|
|
354
|
+
automaticLayout: true,
|
|
355
|
+
}}
|
|
356
|
+
/>
|
|
357
|
+
) : (
|
|
358
|
+
<div className="flex items-center justify-center h-full text-sm text-[#6c7086]">
|
|
359
|
+
Loading file content...
|
|
360
|
+
</div>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// --- Main Component ---
|
|
368
|
+
|
|
369
|
+
export interface FileDiffViewerProps {
|
|
370
|
+
sync: UseGoldenSyncReturn
|
|
371
|
+
componentLabels?: Record<string, string>
|
|
372
|
+
monacoTheme?: string
|
|
373
|
+
renderFileIcon?: (filename: string, component: string) => ReactNode
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function FileDiffViewer({ sync, componentLabels, monacoTheme, renderFileIcon }: FileDiffViewerProps) {
|
|
377
|
+
const { diff, diffLoading, selectedDiffFile, diffSideBySide, setDiffSideBySide, saving, hasUnsavedChanges, handleSaveLiveFile, devtools } = sync
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<div className="flex flex-col h-full">
|
|
381
|
+
{/* Controls */}
|
|
382
|
+
<div className="flex items-center justify-end mb-4 flex-shrink-0">
|
|
383
|
+
<div className="flex items-center gap-2">
|
|
384
|
+
{selectedDiffFile && (
|
|
385
|
+
<>
|
|
386
|
+
<IconButton
|
|
387
|
+
icon={diffSideBySide ? <Rows2 /> : <Columns2 />}
|
|
388
|
+
onClick={() => setDiffSideBySide((prev: boolean) => !prev)}
|
|
389
|
+
color="blue"
|
|
390
|
+
tooltip={{ title: diffSideBySide ? 'Inline view' : 'Side by side', description: 'Toggle diff display mode' }}
|
|
391
|
+
/>
|
|
392
|
+
{devtools && hasUnsavedChanges && (
|
|
393
|
+
<IconButton
|
|
394
|
+
icon={<Save className={saving ? 'animate-pulse' : ''} />}
|
|
395
|
+
onClick={handleSaveLiveFile}
|
|
396
|
+
disabled={saving}
|
|
397
|
+
color="amber"
|
|
398
|
+
tooltip={{ title: 'Save changes', description: 'Save edited content to live file' }}
|
|
399
|
+
/>
|
|
400
|
+
)}
|
|
401
|
+
</>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
{diffLoading ? (
|
|
407
|
+
<div className="text-sm text-[#6c7086] text-center py-8">Loading diff...</div>
|
|
408
|
+
) : diff ? (
|
|
409
|
+
<div className="flex gap-4 flex-1 min-h-[400px]">
|
|
410
|
+
<DiffFileTreePanel sync={sync} componentLabels={componentLabels} renderFileIcon={renderFileIcon} />
|
|
411
|
+
<DiffEditorPanel sync={sync} componentLabels={componentLabels} monacoTheme={monacoTheme} />
|
|
412
|
+
</div>
|
|
413
|
+
) : (
|
|
414
|
+
<div className="text-sm text-[#6c7086] text-center py-8">
|
|
415
|
+
No diff data available. Click refresh to load.
|
|
416
|
+
</div>
|
|
417
|
+
)}
|
|
418
|
+
</div>
|
|
419
|
+
)
|
|
420
|
+
}
|