@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,477 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
2
|
+
import type { GoldenSnapshotsApi, GoldenStatus, GoldenLiveDiff, FileDiffInfo, FileDiffStatus, SnapshotsManifest } from './types.ts'
|
|
3
|
+
|
|
4
|
+
export interface DiffTreeNode {
|
|
5
|
+
files: FileDiffInfo[]
|
|
6
|
+
children: Record<string, DiffTreeNode>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UseGoldenSyncOptions {
|
|
10
|
+
api: GoldenSnapshotsApi
|
|
11
|
+
devtools: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function insertIntoTree(node: DiffTreeNode, pathParts: string[], file: FileDiffInfo) {
|
|
15
|
+
if (pathParts.length === 1) {
|
|
16
|
+
node.files.push(file)
|
|
17
|
+
} else {
|
|
18
|
+
const folder = pathParts[0]
|
|
19
|
+
if (!node.children[folder]) {
|
|
20
|
+
node.children[folder] = { files: [], children: {} }
|
|
21
|
+
}
|
|
22
|
+
insertIntoTree(node.children[folder], pathParts.slice(1), file)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sortTreeFiles(node: DiffTreeNode) {
|
|
27
|
+
node.files.sort((a, b) => {
|
|
28
|
+
const statusOrder = { modified: 0, added: 1, removed: 2, unchanged: 3 }
|
|
29
|
+
return statusOrder[a.status] - statusOrder[b.status] || a.path.localeCompare(b.path)
|
|
30
|
+
})
|
|
31
|
+
const sortedEntries = Object.entries(node.children).sort(([a], [b]) => a.localeCompare(b))
|
|
32
|
+
node.children = Object.fromEntries(sortedEntries)
|
|
33
|
+
for (const child of Object.values(node.children)) {
|
|
34
|
+
sortTreeFiles(child)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function collectCategoryKeys(node: DiffTreeNode, categoryKey: string): string[] {
|
|
39
|
+
const keys: string[] = []
|
|
40
|
+
const hasContent = node.files.length > 0 || Object.keys(node.children).length > 0
|
|
41
|
+
if (hasContent) {
|
|
42
|
+
keys.push(categoryKey)
|
|
43
|
+
for (const [childName, childNode] of Object.entries(node.children)) {
|
|
44
|
+
keys.push(...collectCategoryKeys(childNode, `${categoryKey}-${childName}`))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return keys
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function useGoldenSync({ api, devtools }: UseGoldenSyncOptions) {
|
|
51
|
+
// Status
|
|
52
|
+
const [status, setStatus] = useState<GoldenStatus | null>(null)
|
|
53
|
+
const [loading, setLoading] = useState(true)
|
|
54
|
+
const [error, setError] = useState<string | null>(null)
|
|
55
|
+
const [lastAction, setLastAction] = useState<string | null>(null)
|
|
56
|
+
|
|
57
|
+
// Diff state
|
|
58
|
+
const [diff, setDiff] = useState<GoldenLiveDiff | null>(null)
|
|
59
|
+
const [diffLoading, setDiffLoading] = useState(false)
|
|
60
|
+
const [selectedDiffFile, setSelectedDiffFile] = useState<FileDiffInfo | null>(null)
|
|
61
|
+
const [goldenContent, setGoldenContent] = useState<string | null>(null)
|
|
62
|
+
const [liveContent, setLiveContent] = useState<string | null>(null)
|
|
63
|
+
const [editedLiveContent, setEditedLiveContent] = useState<string | null>(null)
|
|
64
|
+
const [diffFilter, setDiffFilter] = useState<FileDiffStatus | 'all'>('all')
|
|
65
|
+
const [diffSideBySide, setDiffSideBySide] = useState(true)
|
|
66
|
+
const [saving, setSaving] = useState(false)
|
|
67
|
+
const [collapsedCategories, setCollapsedCategories] = useState<Set<string>>(new Set())
|
|
68
|
+
|
|
69
|
+
// Reset state
|
|
70
|
+
const [resettingFile, setResettingFile] = useState<string | null>(null)
|
|
71
|
+
const [resettingComponent, setResettingComponent] = useState<string | null>(null)
|
|
72
|
+
const [showResetAllDialog, setShowResetAllDialog] = useState(false)
|
|
73
|
+
const [resettingAll, setResettingAll] = useState(false)
|
|
74
|
+
|
|
75
|
+
// Snapshot state
|
|
76
|
+
const [manifest, setManifest] = useState<SnapshotsManifest | null>(null)
|
|
77
|
+
const [manifestLoading, setManifestLoading] = useState(false)
|
|
78
|
+
const [snapshotBusy, setSnapshotBusy] = useState(false)
|
|
79
|
+
|
|
80
|
+
const hasUnsavedChanges = editedLiveContent !== null && editedLiveContent !== liveContent
|
|
81
|
+
|
|
82
|
+
// --- Data loading ---
|
|
83
|
+
|
|
84
|
+
const loadStatus = useCallback(async () => {
|
|
85
|
+
try {
|
|
86
|
+
setError(null)
|
|
87
|
+
const statusData = await api.getStatus()
|
|
88
|
+
setStatus(statusData)
|
|
89
|
+
} catch (err) {
|
|
90
|
+
setError(err instanceof Error ? err.message : 'Failed to load status')
|
|
91
|
+
} finally {
|
|
92
|
+
setLoading(false)
|
|
93
|
+
}
|
|
94
|
+
}, [api])
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
loadStatus()
|
|
98
|
+
}, [loadStatus])
|
|
99
|
+
|
|
100
|
+
// Auto-clear success message after 3 seconds
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!lastAction) return
|
|
103
|
+
const timer = setTimeout(() => setLastAction(null), 3000)
|
|
104
|
+
return () => clearTimeout(timer)
|
|
105
|
+
}, [lastAction])
|
|
106
|
+
|
|
107
|
+
const loadDiff = useCallback(async () => {
|
|
108
|
+
setDiffLoading(true)
|
|
109
|
+
setError(null)
|
|
110
|
+
try {
|
|
111
|
+
const diffData = await api.getDiff()
|
|
112
|
+
setDiff(diffData)
|
|
113
|
+
} catch (err) {
|
|
114
|
+
setError(err instanceof Error ? err.message : 'Failed to load diff')
|
|
115
|
+
} finally {
|
|
116
|
+
setDiffLoading(false)
|
|
117
|
+
}
|
|
118
|
+
}, [api])
|
|
119
|
+
|
|
120
|
+
const refresh = useCallback(async () => {
|
|
121
|
+
await Promise.all([loadStatus(), diff ? loadDiff() : Promise.resolve()])
|
|
122
|
+
}, [loadStatus, loadDiff, diff])
|
|
123
|
+
|
|
124
|
+
// --- File selection ---
|
|
125
|
+
|
|
126
|
+
const handleSelectDiffFile = async (file: FileDiffInfo) => {
|
|
127
|
+
if (hasUnsavedChanges) {
|
|
128
|
+
const confirm = window.confirm('You have unsaved changes. Discard them?')
|
|
129
|
+
if (!confirm) return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
setSelectedDiffFile(file)
|
|
133
|
+
setGoldenContent(null)
|
|
134
|
+
setLiveContent(null)
|
|
135
|
+
setEditedLiveContent(null)
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const [golden, live] = await Promise.all([
|
|
139
|
+
api.getFileContent(file.component, file.path, 'golden'),
|
|
140
|
+
api.getFileContent(file.component, file.path, 'live'),
|
|
141
|
+
])
|
|
142
|
+
setGoldenContent(golden)
|
|
143
|
+
setLiveContent(live)
|
|
144
|
+
} catch (err) {
|
|
145
|
+
setError(err instanceof Error ? err.message : 'Failed to load file content')
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- Save (devtools only) ---
|
|
150
|
+
|
|
151
|
+
const handleSaveLiveFile = async () => {
|
|
152
|
+
if (!devtools || !selectedDiffFile || editedLiveContent === null) return
|
|
153
|
+
|
|
154
|
+
setSaving(true)
|
|
155
|
+
setError(null)
|
|
156
|
+
try {
|
|
157
|
+
await api.saveFile(selectedDiffFile.component, selectedDiffFile.path, editedLiveContent)
|
|
158
|
+
setLiveContent(editedLiveContent)
|
|
159
|
+
setEditedLiveContent(null)
|
|
160
|
+
setLastAction(`Saved ${selectedDiffFile.path}`)
|
|
161
|
+
await loadDiff()
|
|
162
|
+
} catch (err) {
|
|
163
|
+
setError(err instanceof Error ? err.message : 'Failed to save file')
|
|
164
|
+
} finally {
|
|
165
|
+
setSaving(false)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- Reset operations (golden → live) ---
|
|
170
|
+
|
|
171
|
+
const handleResetFile = async (file: FileDiffInfo) => {
|
|
172
|
+
const key = `${file.component}:${file.path}`
|
|
173
|
+
setResettingFile(key)
|
|
174
|
+
setError(null)
|
|
175
|
+
try {
|
|
176
|
+
await api.resetFile(file.component, file.path)
|
|
177
|
+
setLastAction(`Reset ${file.path} to golden`)
|
|
178
|
+
if (selectedDiffFile?.path === file.path && selectedDiffFile?.component === file.component) {
|
|
179
|
+
const [golden, live] = await Promise.all([
|
|
180
|
+
api.getFileContent(file.component, file.path, 'golden'),
|
|
181
|
+
api.getFileContent(file.component, file.path, 'live'),
|
|
182
|
+
])
|
|
183
|
+
setGoldenContent(golden)
|
|
184
|
+
setLiveContent(live)
|
|
185
|
+
setEditedLiveContent(null)
|
|
186
|
+
}
|
|
187
|
+
await Promise.all([loadDiff(), loadStatus()])
|
|
188
|
+
} catch (err) {
|
|
189
|
+
setError(err instanceof Error ? err.message : 'Failed to reset file')
|
|
190
|
+
} finally {
|
|
191
|
+
setResettingFile(null)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const handleResetComponent = async (component: string) => {
|
|
196
|
+
setResettingComponent(component)
|
|
197
|
+
setError(null)
|
|
198
|
+
try {
|
|
199
|
+
const result = await api.resetComponent(component)
|
|
200
|
+
setLastAction(`Reset ${component} to golden (${result.filesReset} files)`)
|
|
201
|
+
setSelectedDiffFile(null)
|
|
202
|
+
setGoldenContent(null)
|
|
203
|
+
setLiveContent(null)
|
|
204
|
+
setEditedLiveContent(null)
|
|
205
|
+
await Promise.all([loadDiff(), loadStatus()])
|
|
206
|
+
} catch (err) {
|
|
207
|
+
setError(err instanceof Error ? err.message : `Failed to reset ${component}`)
|
|
208
|
+
} finally {
|
|
209
|
+
setResettingComponent(null)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const handleResetAll = async () => {
|
|
214
|
+
setResettingAll(true)
|
|
215
|
+
setError(null)
|
|
216
|
+
try {
|
|
217
|
+
const result = await api.resetComponent('all')
|
|
218
|
+
setLastAction(`Reset all components to golden (${result.filesReset} files)`)
|
|
219
|
+
setSelectedDiffFile(null)
|
|
220
|
+
setGoldenContent(null)
|
|
221
|
+
setLiveContent(null)
|
|
222
|
+
setEditedLiveContent(null)
|
|
223
|
+
setShowResetAllDialog(false)
|
|
224
|
+
await Promise.all([loadDiff(), loadStatus()])
|
|
225
|
+
} catch (err) {
|
|
226
|
+
setError(err instanceof Error ? err.message : 'Failed to reset all components')
|
|
227
|
+
} finally {
|
|
228
|
+
setResettingAll(false)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// --- Snapshot management (devtools only) ---
|
|
233
|
+
|
|
234
|
+
const loadManifest = useCallback(async () => {
|
|
235
|
+
setManifestLoading(true)
|
|
236
|
+
try {
|
|
237
|
+
const data = await api.listSnapshots()
|
|
238
|
+
setManifest(data)
|
|
239
|
+
} catch (err) {
|
|
240
|
+
setError(err instanceof Error ? err.message : 'Failed to load snapshots')
|
|
241
|
+
} finally {
|
|
242
|
+
setManifestLoading(false)
|
|
243
|
+
}
|
|
244
|
+
}, [api])
|
|
245
|
+
|
|
246
|
+
const handleCreateSnapshot = async (description?: string) => {
|
|
247
|
+
setSnapshotBusy(true)
|
|
248
|
+
setError(null)
|
|
249
|
+
try {
|
|
250
|
+
const result = await api.createSnapshot(description)
|
|
251
|
+
setLastAction(`Created snapshot v${result.version}`)
|
|
252
|
+
await Promise.all([loadManifest(), loadStatus()])
|
|
253
|
+
} catch (err) {
|
|
254
|
+
setError(err instanceof Error ? err.message : 'Failed to create snapshot')
|
|
255
|
+
} finally {
|
|
256
|
+
setSnapshotBusy(false)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const handleRestoreSnapshot = async (version: number, resetLive: boolean) => {
|
|
261
|
+
setSnapshotBusy(true)
|
|
262
|
+
setError(null)
|
|
263
|
+
try {
|
|
264
|
+
await api.restoreSnapshot(version, resetLive)
|
|
265
|
+
setLastAction(`Restored snapshot v${version}${resetLive ? ' (live reset)' : ''}`)
|
|
266
|
+
await Promise.all([loadStatus(), loadManifest()])
|
|
267
|
+
} catch (err) {
|
|
268
|
+
setError(err instanceof Error ? err.message : 'Failed to restore snapshot')
|
|
269
|
+
} finally {
|
|
270
|
+
setSnapshotBusy(false)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const handlePinSnapshot = async (version: number, pinned: boolean) => {
|
|
275
|
+
try {
|
|
276
|
+
await api.pinSnapshot(version, pinned)
|
|
277
|
+
setLastAction(`${pinned ? 'Pinned' : 'Unpinned'} snapshot v${version}`)
|
|
278
|
+
await loadManifest()
|
|
279
|
+
} catch (err) {
|
|
280
|
+
setError(err instanceof Error ? err.message : 'Failed to pin snapshot')
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const handleDeleteSnapshot = async (version: number) => {
|
|
285
|
+
setSnapshotBusy(true)
|
|
286
|
+
setError(null)
|
|
287
|
+
try {
|
|
288
|
+
await api.deleteSnapshot(version)
|
|
289
|
+
setLastAction(`Deleted snapshot v${version}`)
|
|
290
|
+
await loadManifest()
|
|
291
|
+
} catch (err) {
|
|
292
|
+
setError(err instanceof Error ? err.message : 'Failed to delete snapshot')
|
|
293
|
+
} finally {
|
|
294
|
+
setSnapshotBusy(false)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// --- Version management (devtools only) ---
|
|
299
|
+
|
|
300
|
+
const handleUpdateComponentVersion = async (component: string, version: string, description?: string) => {
|
|
301
|
+
setError(null)
|
|
302
|
+
try {
|
|
303
|
+
await api.updateComponentVersion(component, version, description)
|
|
304
|
+
setLastAction(`Updated ${component} to v${version}`)
|
|
305
|
+
await loadStatus()
|
|
306
|
+
} catch (err) {
|
|
307
|
+
setError(err instanceof Error ? err.message : 'Failed to update component version')
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const handleBumpVersion = async (newVersion: string, description?: string) => {
|
|
312
|
+
setError(null)
|
|
313
|
+
try {
|
|
314
|
+
await api.bumpVersion(newVersion, description)
|
|
315
|
+
setLastAction(`Bumped golden version to ${newVersion}`)
|
|
316
|
+
await loadStatus()
|
|
317
|
+
} catch (err) {
|
|
318
|
+
setError(err instanceof Error ? err.message : 'Failed to bump version')
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// --- Tree helpers ---
|
|
323
|
+
|
|
324
|
+
const toggleCategory = (category: string) => {
|
|
325
|
+
setCollapsedCategories((prev) => {
|
|
326
|
+
const next = new Set(prev)
|
|
327
|
+
if (next.has(category)) {
|
|
328
|
+
next.delete(category)
|
|
329
|
+
} else {
|
|
330
|
+
next.add(category)
|
|
331
|
+
}
|
|
332
|
+
return next
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const groupedFiles = useMemo(() => {
|
|
337
|
+
if (!diff) return new Map<string, DiffTreeNode>()
|
|
338
|
+
|
|
339
|
+
const filtered = diff.files.filter((f) => diffFilter === 'all' || f.status === diffFilter)
|
|
340
|
+
const groups = new Map<string, DiffTreeNode>()
|
|
341
|
+
|
|
342
|
+
for (const file of filtered) {
|
|
343
|
+
if (!groups.has(file.component)) {
|
|
344
|
+
groups.set(file.component, { files: [], children: {} })
|
|
345
|
+
}
|
|
346
|
+
const pathParts = file.path.split('/')
|
|
347
|
+
insertIntoTree(groups.get(file.component)!, pathParts, file)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const node of groups.values()) {
|
|
351
|
+
sortTreeFiles(node)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return groups
|
|
355
|
+
}, [diff, diffFilter])
|
|
356
|
+
|
|
357
|
+
const countFiles = (node: DiffTreeNode): number => {
|
|
358
|
+
let count = node.files.length
|
|
359
|
+
for (const child of Object.values(node.children)) {
|
|
360
|
+
count += countFiles(child)
|
|
361
|
+
}
|
|
362
|
+
return count
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const allCategoryKeys = useMemo(() => {
|
|
366
|
+
const keys: string[] = []
|
|
367
|
+
for (const [component, node] of groupedFiles) {
|
|
368
|
+
keys.push(...collectCategoryKeys(node, component))
|
|
369
|
+
}
|
|
370
|
+
return keys
|
|
371
|
+
}, [groupedFiles])
|
|
372
|
+
|
|
373
|
+
const allExpanded = allCategoryKeys.length > 0 && !allCategoryKeys.some((key) => collapsedCategories.has(key))
|
|
374
|
+
|
|
375
|
+
const handleExpandAll = () => {
|
|
376
|
+
setCollapsedCategories(new Set())
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const handleCollapseAll = () => {
|
|
380
|
+
setCollapsedCategories(new Set(allCategoryKeys))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
// Status
|
|
385
|
+
status,
|
|
386
|
+
loading,
|
|
387
|
+
error,
|
|
388
|
+
lastAction,
|
|
389
|
+
loadStatus,
|
|
390
|
+
|
|
391
|
+
// Diff
|
|
392
|
+
diff,
|
|
393
|
+
diffLoading,
|
|
394
|
+
loadDiff,
|
|
395
|
+
selectedDiffFile,
|
|
396
|
+
goldenContent,
|
|
397
|
+
liveContent,
|
|
398
|
+
editedLiveContent,
|
|
399
|
+
setEditedLiveContent,
|
|
400
|
+
diffFilter,
|
|
401
|
+
setDiffFilter,
|
|
402
|
+
diffSideBySide,
|
|
403
|
+
setDiffSideBySide,
|
|
404
|
+
saving,
|
|
405
|
+
hasUnsavedChanges,
|
|
406
|
+
|
|
407
|
+
// File operations
|
|
408
|
+
handleSelectDiffFile,
|
|
409
|
+
handleSaveLiveFile,
|
|
410
|
+
|
|
411
|
+
// Reset operations
|
|
412
|
+
resettingFile,
|
|
413
|
+
resettingComponent,
|
|
414
|
+
resettingAll,
|
|
415
|
+
showResetAllDialog,
|
|
416
|
+
setShowResetAllDialog,
|
|
417
|
+
handleResetFile,
|
|
418
|
+
handleResetComponent,
|
|
419
|
+
handleResetAll,
|
|
420
|
+
|
|
421
|
+
// Tree
|
|
422
|
+
groupedFiles,
|
|
423
|
+
collapsedCategories,
|
|
424
|
+
toggleCategory,
|
|
425
|
+
countFiles,
|
|
426
|
+
allCategoryKeys,
|
|
427
|
+
allExpanded,
|
|
428
|
+
handleExpandAll,
|
|
429
|
+
handleCollapseAll,
|
|
430
|
+
|
|
431
|
+
// Snapshots
|
|
432
|
+
manifest,
|
|
433
|
+
manifestLoading,
|
|
434
|
+
snapshotBusy,
|
|
435
|
+
loadManifest,
|
|
436
|
+
handleCreateSnapshot,
|
|
437
|
+
handleRestoreSnapshot,
|
|
438
|
+
handlePinSnapshot,
|
|
439
|
+
handleDeleteSnapshot,
|
|
440
|
+
|
|
441
|
+
// Versions
|
|
442
|
+
handleUpdateComponentVersion,
|
|
443
|
+
handleBumpVersion,
|
|
444
|
+
|
|
445
|
+
// General
|
|
446
|
+
refresh,
|
|
447
|
+
devtools,
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export type UseGoldenSyncReturn = ReturnType<typeof useGoldenSync>
|
|
452
|
+
|
|
453
|
+
export function formatDate(isoString: string) {
|
|
454
|
+
const date = new Date(isoString)
|
|
455
|
+
return date.toLocaleString()
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function formatBytes(bytes: number) {
|
|
459
|
+
if (bytes < 1024) return `${bytes} B`
|
|
460
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
461
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function getLanguage(path: string): string {
|
|
465
|
+
const ext = path.split('.').pop()?.toLowerCase()
|
|
466
|
+
switch (ext) {
|
|
467
|
+
case 'json': return 'json'
|
|
468
|
+
case 'md': return 'markdown'
|
|
469
|
+
case 'ts':
|
|
470
|
+
case 'tsx': return 'typescript'
|
|
471
|
+
case 'js':
|
|
472
|
+
case 'jsx': return 'javascript'
|
|
473
|
+
case 'yaml':
|
|
474
|
+
case 'yml': return 'yaml'
|
|
475
|
+
default: return 'plaintext'
|
|
476
|
+
}
|
|
477
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Tag, ArrowUp, Loader2 } from 'lucide-react'
|
|
3
|
+
import { IconButton } from '../../ui/icon-button.tsx'
|
|
4
|
+
import { Input } from '../../ui/input.tsx'
|
|
5
|
+
import type { UseGoldenSyncReturn } from './use-golden-sync.ts'
|
|
6
|
+
|
|
7
|
+
export interface VersionManagerProps {
|
|
8
|
+
sync: UseGoldenSyncReturn
|
|
9
|
+
components: string[]
|
|
10
|
+
componentLabels?: Record<string, string>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function VersionManager({ sync, components, componentLabels }: VersionManagerProps) {
|
|
14
|
+
const { status, handleUpdateComponentVersion, handleBumpVersion } = sync
|
|
15
|
+
|
|
16
|
+
const [globalVersion, setGlobalVersion] = useState('')
|
|
17
|
+
const [globalDescription, setGlobalDescription] = useState('')
|
|
18
|
+
const [bumpingGlobal, setBumpingGlobal] = useState(false)
|
|
19
|
+
|
|
20
|
+
const [editingComponent, setEditingComponent] = useState<string | null>(null)
|
|
21
|
+
const [componentVersion, setComponentVersion] = useState('')
|
|
22
|
+
const [componentDescription, setComponentDescription] = useState('')
|
|
23
|
+
const [bumpingComponent, setBumpingComponent] = useState(false)
|
|
24
|
+
|
|
25
|
+
const getLabel = (comp: string) => componentLabels?.[comp] ?? comp.charAt(0).toUpperCase() + comp.slice(1)
|
|
26
|
+
|
|
27
|
+
const onBumpGlobal = async () => {
|
|
28
|
+
if (!globalVersion.trim()) return
|
|
29
|
+
setBumpingGlobal(true)
|
|
30
|
+
await handleBumpVersion(globalVersion.trim(), globalDescription.trim() || undefined)
|
|
31
|
+
setBumpingGlobal(false)
|
|
32
|
+
setGlobalVersion('')
|
|
33
|
+
setGlobalDescription('')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const onBumpComponent = async () => {
|
|
37
|
+
if (!editingComponent || !componentVersion.trim()) return
|
|
38
|
+
setBumpingComponent(true)
|
|
39
|
+
await handleUpdateComponentVersion(editingComponent, componentVersion.trim(), componentDescription.trim() || undefined)
|
|
40
|
+
setBumpingComponent(false)
|
|
41
|
+
setEditingComponent(null)
|
|
42
|
+
setComponentVersion('')
|
|
43
|
+
setComponentDescription('')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const startEditComponent = (comp: string) => {
|
|
47
|
+
const current = status?.goldenMeta?.components[comp]?.version ?? ''
|
|
48
|
+
setEditingComponent(comp)
|
|
49
|
+
setComponentVersion(current)
|
|
50
|
+
setComponentDescription('')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-4">
|
|
55
|
+
{/* Global Version Bump */}
|
|
56
|
+
<div className="bg-[#181825] rounded-lg p-4 border border-teal-500/30">
|
|
57
|
+
<div className="flex items-center gap-3 mb-3">
|
|
58
|
+
<Tag className="w-5 h-5 text-teal-400" />
|
|
59
|
+
<h4 className="text-[#cdd6f4] font-medium">Golden Version</h4>
|
|
60
|
+
{status?.goldenMeta && (
|
|
61
|
+
<span className="px-2 py-0.5 bg-teal-500/20 text-teal-400 text-xs rounded font-mono">
|
|
62
|
+
{status.goldenMeta.version}
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
<p className="text-xs text-[#585b70] mb-3">
|
|
67
|
+
Bump the overall golden version number. This updates the top-level version in meta.json.
|
|
68
|
+
</p>
|
|
69
|
+
<div className="flex gap-2">
|
|
70
|
+
<div className="w-32">
|
|
71
|
+
<Input
|
|
72
|
+
value={globalVersion}
|
|
73
|
+
onChange={setGlobalVersion}
|
|
74
|
+
placeholder="e.g. 1.3.0"
|
|
75
|
+
size="sm"
|
|
76
|
+
variant="outline"
|
|
77
|
+
mono
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="flex-1">
|
|
81
|
+
<Input
|
|
82
|
+
value={globalDescription}
|
|
83
|
+
onChange={setGlobalDescription}
|
|
84
|
+
placeholder="Optional description..."
|
|
85
|
+
size="sm"
|
|
86
|
+
variant="outline"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
<IconButton
|
|
90
|
+
icon={bumpingGlobal ? <Loader2 className="animate-spin" /> : <ArrowUp />}
|
|
91
|
+
onClick={onBumpGlobal}
|
|
92
|
+
disabled={bumpingGlobal || !globalVersion.trim()}
|
|
93
|
+
size="sm"
|
|
94
|
+
color="cyan"
|
|
95
|
+
tooltip={{ title: 'Bump version', description: 'Update the golden version' }}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Component Versions */}
|
|
101
|
+
<div className="bg-[#181825] rounded-lg border border-[#313244] overflow-hidden">
|
|
102
|
+
<div className="px-4 py-3 border-b border-[#313244]">
|
|
103
|
+
<h4 className="text-[#cdd6f4] font-medium">Component Versions</h4>
|
|
104
|
+
<p className="text-xs text-[#585b70] mt-1">
|
|
105
|
+
Update individual component versions. Click a component to edit.
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div className="divide-y divide-[#313244]">
|
|
110
|
+
{components.map((comp) => {
|
|
111
|
+
const currentVersion = status?.goldenMeta?.components[comp]?.version
|
|
112
|
+
const liveVersion = status?.liveMeta?.components[comp]?.version
|
|
113
|
+
const isEditing = editingComponent === comp
|
|
114
|
+
const mismatch = currentVersion && liveVersion && currentVersion !== liveVersion
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div key={comp} className="px-4 py-3">
|
|
118
|
+
<div className="flex items-center justify-between">
|
|
119
|
+
<div className="flex items-center gap-2">
|
|
120
|
+
<span className="text-sm text-[#cdd6f4]">{getLabel(comp)}</span>
|
|
121
|
+
{currentVersion && (
|
|
122
|
+
<span className="text-xs font-mono text-[#6c7086]">v{currentVersion}</span>
|
|
123
|
+
)}
|
|
124
|
+
{mismatch && (
|
|
125
|
+
<span className="text-[10px] text-yellow-400">
|
|
126
|
+
(live: v{liveVersion})
|
|
127
|
+
</span>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
{!isEditing && (
|
|
131
|
+
<IconButton
|
|
132
|
+
icon={<Tag />}
|
|
133
|
+
onClick={() => startEditComponent(comp)}
|
|
134
|
+
size="xs"
|
|
135
|
+
color="neutral"
|
|
136
|
+
tooltip={{ title: 'Edit version', description: `Change ${getLabel(comp)} version` }}
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{isEditing && (
|
|
142
|
+
<div className="flex gap-2 mt-2">
|
|
143
|
+
<div className="w-28">
|
|
144
|
+
<Input
|
|
145
|
+
value={componentVersion}
|
|
146
|
+
onChange={setComponentVersion}
|
|
147
|
+
placeholder="Version"
|
|
148
|
+
size="sm"
|
|
149
|
+
variant="outline"
|
|
150
|
+
mono
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
<div className="flex-1">
|
|
154
|
+
<Input
|
|
155
|
+
value={componentDescription}
|
|
156
|
+
onChange={setComponentDescription}
|
|
157
|
+
placeholder="Description..."
|
|
158
|
+
size="sm"
|
|
159
|
+
variant="outline"
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
<IconButton
|
|
163
|
+
icon={bumpingComponent ? <Loader2 className="animate-spin" /> : <ArrowUp />}
|
|
164
|
+
onClick={onBumpComponent}
|
|
165
|
+
disabled={bumpingComponent || !componentVersion.trim()}
|
|
166
|
+
size="sm"
|
|
167
|
+
color="green"
|
|
168
|
+
tooltip={{ title: 'Update', description: 'Save new version' }}
|
|
169
|
+
/>
|
|
170
|
+
<IconButton
|
|
171
|
+
icon="close"
|
|
172
|
+
onClick={() => setEditingComponent(null)}
|
|
173
|
+
size="sm"
|
|
174
|
+
color="neutral"
|
|
175
|
+
tooltip={{ title: 'Cancel', description: 'Cancel editing' }}
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
})}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|