@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.
Files changed (112) hide show
  1. package/README.md +63 -0
  2. package/components/content/info-panel-primitives.tsx +297 -0
  3. package/components/diagrams/diagram-utils.tsx +908 -0
  4. package/components/hooks/use-click-outside.ts +27 -0
  5. package/components/hooks/use-dropdown-max-height.ts +20 -0
  6. package/components/hooks/use-navigation-history.ts +94 -0
  7. package/components/lib/ai-tools.tsx +44 -0
  8. package/components/lib/cn.ts +6 -0
  9. package/components/lib/form-colors.ts +32 -0
  10. package/components/lib/theme-engine.ts +97 -0
  11. package/components/lib/toolr-brand.tsx +31 -0
  12. package/components/sections/ai-tools-paths/index.ts +37 -0
  13. package/components/sections/ai-tools-paths/tools-paths-panel.tsx +212 -0
  14. package/components/sections/ai-tools-paths/types.ts +111 -0
  15. package/components/sections/ai-tools-paths/use-tools-paths.ts +159 -0
  16. package/components/sections/captured-issues/captured-issues-panel.tsx +214 -0
  17. package/components/sections/captured-issues/index.ts +38 -0
  18. package/components/sections/captured-issues/types.ts +113 -0
  19. package/components/sections/captured-issues/use-captured-issues.ts +111 -0
  20. package/components/sections/golden-snapshots/file-diff-viewer.tsx +420 -0
  21. package/components/sections/golden-snapshots/golden-sync-panel.tsx +223 -0
  22. package/components/sections/golden-snapshots/index.ts +145 -0
  23. package/components/sections/golden-snapshots/snapshot-manager.tsx +200 -0
  24. package/components/sections/golden-snapshots/status-overview.tsx +305 -0
  25. package/components/sections/golden-snapshots/types.ts +288 -0
  26. package/components/sections/golden-snapshots/use-golden-sync.ts +477 -0
  27. package/components/sections/golden-snapshots/version-manager.tsx +186 -0
  28. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +210 -0
  29. package/components/sections/prompt-editor/index.ts +121 -0
  30. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +276 -0
  31. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +514 -0
  32. package/components/sections/prompt-editor/types.ts +101 -0
  33. package/components/sections/prompt-editor/use-prompt-editor.ts +131 -0
  34. package/components/sections/report-bug/error-logger.ts +392 -0
  35. package/components/sections/report-bug/index.ts +59 -0
  36. package/components/sections/report-bug/issue-reporter-api.ts +83 -0
  37. package/components/sections/report-bug/report-bug-form.tsx +282 -0
  38. package/components/sections/report-bug/screenshot-uploader.tsx +228 -0
  39. package/components/sections/report-bug/use-report-bug.ts +170 -0
  40. package/components/sections/snapshot-browser/index.ts +53 -0
  41. package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +147 -0
  42. package/components/sections/snapshot-browser/snapshot-tree.tsx +451 -0
  43. package/components/sections/snapshot-browser/types.ts +106 -0
  44. package/components/sections/snapshot-browser/use-snapshot-browser.ts +125 -0
  45. package/components/sections/snippets-editor/index.ts +31 -0
  46. package/components/sections/snippets-editor/snippets-editor.tsx +381 -0
  47. package/components/sections/snippets-editor/types.ts +48 -0
  48. package/components/sections/snippets-editor/use-snippets-editor.ts +217 -0
  49. package/components/ui/action-dialog.tsx +309 -0
  50. package/components/ui/ai-action-button.tsx +137 -0
  51. package/components/ui/ai-execution-action-buttons.tsx +106 -0
  52. package/components/ui/badge.tsx +67 -0
  53. package/components/ui/bottom-panel-header.tsx +240 -0
  54. package/components/ui/breadcrumb.tsx +168 -0
  55. package/components/ui/checkbox.tsx +102 -0
  56. package/components/ui/collapsible-section.tsx +100 -0
  57. package/components/ui/confirm-badge.tsx +71 -0
  58. package/components/ui/detail-section.tsx +67 -0
  59. package/components/ui/detail-view-wrapper.tsx +55 -0
  60. package/components/ui/editor-placeholder-card.tsx +197 -0
  61. package/components/ui/editor-toolbar.tsx +123 -0
  62. package/components/ui/execution-details-panel.tsx +93 -0
  63. package/components/ui/extension-list-card.tsx +105 -0
  64. package/components/ui/file-structure-section.tsx +373 -0
  65. package/components/ui/file-tree.tsx +171 -0
  66. package/components/ui/files-panel.tsx +251 -0
  67. package/components/ui/filter-dropdown.tsx +173 -0
  68. package/components/ui/form-actions.tsx +127 -0
  69. package/components/ui/frontmatter-form-header.tsx +80 -0
  70. package/components/ui/icon-button.tsx +388 -0
  71. package/components/ui/input.tsx +211 -0
  72. package/components/ui/label.tsx +159 -0
  73. package/components/ui/layout-tab-bar.tsx +289 -0
  74. package/components/ui/modal.tsx +194 -0
  75. package/components/ui/nav-card.tsx +81 -0
  76. package/components/ui/navigation-bar.tsx +285 -0
  77. package/components/ui/number-input.tsx +165 -0
  78. package/components/ui/registry-browser.tsx +261 -0
  79. package/components/ui/registry-card.tsx +710 -0
  80. package/components/ui/registry-detail.tsx +224 -0
  81. package/components/ui/resizable-textarea.tsx +290 -0
  82. package/components/ui/scope-badge.tsx +67 -0
  83. package/components/ui/segmented-toggle.tsx +133 -0
  84. package/components/ui/select.tsx +172 -0
  85. package/components/ui/selection-grid.tsx +313 -0
  86. package/components/ui/setting-row.tsx +97 -0
  87. package/components/ui/snapshot-card.tsx +107 -0
  88. package/components/ui/snippets-panel.tsx +161 -0
  89. package/components/ui/sort-dropdown.tsx +109 -0
  90. package/components/ui/status-card.tsx +96 -0
  91. package/components/ui/tab-bar.tsx +340 -0
  92. package/components/ui/toggle.tsx +142 -0
  93. package/components/ui/tooltip.tsx +326 -0
  94. package/dist/content.d.ts +110 -0
  95. package/dist/content.js +195 -0
  96. package/dist/diagrams.d.ts +371 -0
  97. package/dist/diagrams.js +702 -0
  98. package/dist/index.d.ts +2714 -0
  99. package/dist/index.js +11220 -0
  100. package/dist/preset.d.ts +24 -0
  101. package/dist/preset.js +17 -0
  102. package/dist/tokens/tokens/primitives.css +45 -0
  103. package/dist/tokens/tokens/semantic.css +46 -0
  104. package/dist/tokens/tokens/theme.css +11 -0
  105. package/dist/tokens/tokens/tokens.json +65 -0
  106. package/index.ts +123 -0
  107. package/package.json +63 -0
  108. package/tailwind-preset.ts +22 -0
  109. package/tokens/primitives.css +45 -0
  110. package/tokens/semantic.css +46 -0
  111. package/tokens/theme.css +11 -0
  112. 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
+ }