@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,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
+ }