@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,223 @@
1
+ import type { ReactNode } from 'react'
2
+ import { useState, useEffect } from 'react'
3
+ import { RefreshCw, AlertCircle, Check, Activity, GitCompareArrows, Archive, Tag } from 'lucide-react'
4
+ import { IconButton } from '../../ui/icon-button.tsx'
5
+ import { ConfirmModal } from '../../ui/modal.tsx'
6
+ import type { GoldenSnapshotsApi } from './types.ts'
7
+ import { useGoldenSync, type UseGoldenSyncReturn } from './use-golden-sync.ts'
8
+ import { StatusOverview } from './status-overview.tsx'
9
+ import { FileDiffViewer } from './file-diff-viewer.tsx'
10
+ import { SnapshotManager } from './snapshot-manager.tsx'
11
+ import { VersionManager } from './version-manager.tsx'
12
+
13
+ type TabId = 'status' | 'diff' | 'snapshots' | 'versions'
14
+
15
+ export interface GoldenSyncPanelProps {
16
+ api: GoldenSnapshotsApi
17
+ components: string[]
18
+ devtools?: boolean
19
+ componentLabels?: Record<string, string>
20
+ monacoTheme?: string
21
+ renderFileIcon?: (filename: string, component: string) => ReactNode
22
+ onTabChange?: (tab: TabId) => void
23
+ }
24
+
25
+ export function GoldenSyncPanel({
26
+ api,
27
+ components,
28
+ devtools = false,
29
+ componentLabels,
30
+ monacoTheme,
31
+ renderFileIcon,
32
+ onTabChange,
33
+ }: GoldenSyncPanelProps) {
34
+ const sync = useGoldenSync({ api, devtools })
35
+ const [activeTab, setActiveTab] = useState<TabId>('status')
36
+
37
+ const handleTabChange = (tab: TabId) => {
38
+ setActiveTab(tab)
39
+ onTabChange?.(tab)
40
+ }
41
+
42
+ const {
43
+ status,
44
+ loading,
45
+ error,
46
+ lastAction,
47
+ loadStatus,
48
+ diff,
49
+ diffLoading,
50
+ loadDiff,
51
+ manifest,
52
+ manifestLoading,
53
+ loadManifest,
54
+ resettingComponent,
55
+ resettingAll,
56
+ showResetAllDialog,
57
+ setShowResetAllDialog,
58
+ handleResetComponent,
59
+ handleResetAll,
60
+ } = sync
61
+
62
+ // Load diff when switching to diff tab
63
+ useEffect(() => {
64
+ if (activeTab === 'diff' && !diff && !diffLoading) {
65
+ loadDiff()
66
+ }
67
+ }, [activeTab, diff, diffLoading, loadDiff])
68
+
69
+ // Load manifest when switching to snapshots tab
70
+ useEffect(() => {
71
+ if (activeTab === 'snapshots' && !manifest && !manifestLoading) {
72
+ loadManifest()
73
+ }
74
+ }, [activeTab, manifest, manifestLoading, loadManifest])
75
+
76
+ const TAB_DESCRIPTIONS: Record<TabId, { title: string; description: string }> = {
77
+ status: {
78
+ title: 'Golden / Live Sync',
79
+ description: 'Version sync status between golden reference and your live working copy.',
80
+ },
81
+ diff: {
82
+ title: 'Golden vs Live Diff',
83
+ description: 'Compare files between golden (reference) and live (working copy)',
84
+ },
85
+ snapshots: {
86
+ title: 'Snapshot Management',
87
+ description: 'Create, restore, pin, and manage versioned snapshots of your live files.',
88
+ },
89
+ versions: {
90
+ title: 'Version Management',
91
+ description: 'Bump the overall golden version or update individual component versions.',
92
+ },
93
+ }
94
+
95
+ const handleRefresh = () => {
96
+ if (activeTab === 'diff') loadDiff()
97
+ else if (activeTab === 'snapshots') loadManifest()
98
+ else loadStatus()
99
+ }
100
+
101
+ const isLoading = diffLoading || loading || manifestLoading
102
+
103
+ const tabs: { id: TabId; label: string; icon: ReactNode; color: string; devtoolsOnly?: boolean }[] = [
104
+ { id: 'status', label: 'Status', icon: <Activity className="w-4 h-4" />, color: 'text-purple-400' },
105
+ { id: 'diff', label: 'File Diff', icon: <GitCompareArrows className="w-4 h-4" />, color: 'text-amber-400' },
106
+ { id: 'snapshots', label: 'Snapshots', icon: <Archive className="w-4 h-4" />, color: 'text-blue-400', devtoolsOnly: true },
107
+ { id: 'versions', label: 'Versions', icon: <Tag className="w-4 h-4" />, color: 'text-teal-400', devtoolsOnly: true },
108
+ ]
109
+
110
+ const visibleTabs = tabs.filter((t) => !t.devtoolsOnly || devtools)
111
+
112
+ return (
113
+ <div className={`flex-1 flex flex-col ${activeTab === 'diff' ? 'overflow-hidden' : 'overflow-y-auto'}`}>
114
+ {/* Header */}
115
+ <div className="flex-shrink-0">
116
+ <div className="flex items-start justify-between mb-4">
117
+ <div className="space-y-1">
118
+ <h3 className="text-base font-semibold text-[#cdd6f4]">
119
+ {TAB_DESCRIPTIONS[activeTab].title}
120
+ </h3>
121
+ <p className="text-sm text-[#a6adc8]">
122
+ {TAB_DESCRIPTIONS[activeTab].description}
123
+ </p>
124
+ </div>
125
+ <IconButton
126
+ icon={<RefreshCw className={isLoading ? 'animate-spin' : ''} />}
127
+ onClick={handleRefresh}
128
+ disabled={isLoading}
129
+ color="neutral"
130
+ size="sm"
131
+ tooltip={{ title: 'Reload', description: 'Refresh data' }}
132
+ />
133
+ </div>
134
+
135
+ {/* Tabs */}
136
+ <div className="flex">
137
+ {visibleTabs.map((tab) => (
138
+ <button
139
+ key={tab.id}
140
+ onClick={() => handleTabChange(tab.id)}
141
+ className={`h-[41px] flex items-center gap-2 px-4 text-sm border-b-2 transition-colors cursor-pointer ${
142
+ activeTab === tab.id
143
+ ? `${tab.color} border-current bg-neutral-800/50`
144
+ : 'border-transparent text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800/30'
145
+ }`}
146
+ >
147
+ {tab.icon}
148
+ {tab.label}
149
+ </button>
150
+ ))}
151
+ </div>
152
+
153
+ {/* Error Display */}
154
+ {error && (
155
+ <div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4 mt-4">
156
+ <div className="flex items-center gap-3">
157
+ <AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
158
+ <p className="text-red-400 text-sm">{error}</p>
159
+ </div>
160
+ </div>
161
+ )}
162
+
163
+ {/* Success Message */}
164
+ {lastAction && (
165
+ <div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4 mt-4">
166
+ <div className="flex items-center gap-3">
167
+ <Check className="w-5 h-5 text-green-400 flex-shrink-0" />
168
+ <p className="text-green-400 text-sm">{lastAction}</p>
169
+ </div>
170
+ </div>
171
+ )}
172
+ </div>
173
+
174
+ {/* Content */}
175
+ <div className={activeTab === 'diff' ? 'flex-1 min-h-0 mt-4' : 'space-y-6 mt-4'}>
176
+ {loading && activeTab === 'status' ? (
177
+ <div className="text-sm text-[#6c7086] text-center py-8">Loading...</div>
178
+ ) : activeTab === 'status' ? (
179
+ <StatusOverview
180
+ status={status}
181
+ components={components}
182
+ componentLabels={componentLabels}
183
+ devtools={devtools}
184
+ manifest={manifest}
185
+ resettingComponent={resettingComponent}
186
+ resettingAll={resettingAll}
187
+ onResetComponent={handleResetComponent}
188
+ onResetAll={() => setShowResetAllDialog(true)}
189
+ />
190
+ ) : activeTab === 'diff' ? (
191
+ <FileDiffViewer
192
+ sync={sync}
193
+ componentLabels={componentLabels}
194
+ monacoTheme={monacoTheme}
195
+ renderFileIcon={renderFileIcon}
196
+ />
197
+ ) : activeTab === 'snapshots' ? (
198
+ <SnapshotManager sync={sync} />
199
+ ) : (
200
+ <VersionManager
201
+ sync={sync}
202
+ components={components}
203
+ componentLabels={componentLabels}
204
+ />
205
+ )}
206
+ </div>
207
+
208
+ {/* Reset All Confirmation Dialog */}
209
+ <ConfirmModal
210
+ isOpen={showResetAllDialog}
211
+ onClose={() => setShowResetAllDialog(false)}
212
+ title="Reset All to Golden"
213
+ kind="warning"
214
+ message="This will overwrite all live files with the golden copies. Any live changes will be lost."
215
+ confirmColor="orange"
216
+ onConfirm={handleResetAll}
217
+ isLoading={resettingAll}
218
+ />
219
+ </div>
220
+ )
221
+ }
222
+
223
+ export type { UseGoldenSyncReturn }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Golden Snapshots — Section barrel export
3
+ *
4
+ * Part of: Sections > Golden Snapshots
5
+ *
6
+ * This section provides a complete golden/live file synchronization and
7
+ * snapshot management UI that can be reused across Tauri apps.
8
+ *
9
+ * ╔═══════════════════════════════════════════════════════════════════════╗
10
+ * ║ ARCHITECTURE OVERVIEW ║
11
+ * ╠═══════════════════════════════════════════════════════════════════════╣
12
+ * ║ ║
13
+ * ║ The system manages three tiers of configuration/prompt files: ║
14
+ * ║ ║
15
+ * ║ Seed (bundled in app) → Golden (reference) → Live (working copy) ║
16
+ * ║ ║
17
+ * ║ - Seed: Embedded in the app distribution (e.g. configr-seed.zip) ║
18
+ * ║ - Golden: Extracted on first launch; updated on app updates ║
19
+ * ║ - Live: Working copy that users edit; preserved across updates ║
20
+ * ║ ║
21
+ * ║ Each app defines its own "components" (named file groups): ║
22
+ * ║ - Configr: runtime, verifier, simulator, generator ║
23
+ * ║ - Reviewr: runtime, + others TBD ║
24
+ * ║ ║
25
+ * ║ The UI is generic — it renders whatever components the backend ║
26
+ * ║ returns. Component names are strings, not hardcoded enums. ║
27
+ * ║ ║
28
+ * ╠═══════════════════════════════════════════════════════════════════════╣
29
+ * ║ FILE STRUCTURE ║
30
+ * ╠═══════════════════════════════════════════════════════════════════════╣
31
+ * ║ ║
32
+ * ║ types.ts — Shared types (GoldenMeta, FileDiffInfo, etc) ║
33
+ * ║ Also contains GoldenSnapshotsApi interface ║
34
+ * ║ with full Rust backend implementation guide ║
35
+ * ║ ║
36
+ * ║ golden-sync-panel.tsx — Main tabbed panel (Status + File Diff) ║
37
+ * ║ status-overview.tsx — Component versions, sync badges, resets ║
38
+ * ║ file-diff-viewer.tsx — Monaco diff editor + file tree sidebar ║
39
+ * ║ use-golden-sync.ts — Hook orchestrating all operations ║
40
+ * ║ ║
41
+ * ╠═══════════════════════════════════════════════════════════════════════╣
42
+ * ║ INTEGRATION GUIDE FOR NEW APPS ║
43
+ * ╠═══════════════════════════════════════════════════════════════════════╣
44
+ * ║ ║
45
+ * ║ 1. RUST BACKEND: Implement Tauri commands matching GoldenSnapshotsApi║
46
+ * ║ Reference: configr/development/src-tauri/src/commands/snapshots/ ║
47
+ * ║ - mod.rs: Types + helpers (copy these structs) ║
48
+ * ║ - lifecycle.rs: All command logic (adapt paths & components) ║
49
+ * ║ - runtime_init.rs: Bootstrap flow (seed → golden → live) ║
50
+ * ║ - zip_ops.rs: Zip utilities ║
51
+ * ║ ║
52
+ * ║ 2. TS API ADAPTER: Create a file that wraps Tauri invokes: ║
53
+ * ║ import { invoke } from '@tauri-apps/api/core' ║
54
+ * ║ import type { GoldenSnapshotsApi } from '@toolr/ui-design' ║
55
+ * ║ ║
56
+ * ║ export const goldenApi: GoldenSnapshotsApi = { ║
57
+ * ║ getStatus: () => invoke('get_app_status'), ║
58
+ * ║ getDiff: () => invoke('get_golden_live_diff'), ║
59
+ * ║ getFileContent: (c, p, s) => ║
60
+ * ║ invoke('get_file_content_for_diff', ║
61
+ * ║ { component: c, path: p, source: s }), ║
62
+ * ║ // ... etc for each method ║
63
+ * ║ } ║
64
+ * ║ ║
65
+ * ║ 3. RENDER: Pass the adapter and component list to the panel: ║
66
+ * ║ <GoldenSyncPanel ║
67
+ * ║ api={goldenApi} ║
68
+ * ║ components={['runtime', 'verifier', 'simulator', 'generator']} ║
69
+ * ║ isDevtools={isDevMode} ║
70
+ * ║ /> ║
71
+ * ║ ║
72
+ * ║ 4. DIRECTORY LAYOUT the Rust backend must maintain: ║
73
+ * ║ {app_data_dir}/ ║
74
+ * ║ ├── golden/ — Reference (overwritten on updates) ║
75
+ * ║ │ ├── .meta/meta.json — GoldenMeta JSON ║
76
+ * ║ │ ├── runtime/ — Runtime component files ║
77
+ * ║ │ └── prompts/ — Prompt component files (if any) ║
78
+ * ║ ├── live/ — Working copy (user edits preserved) ║
79
+ * ║ │ ├── .meta/meta.json ║
80
+ * ║ │ ├── runtime/ ║
81
+ * ║ │ └── prompts/ ║
82
+ * ║ └── snapshots/ — Versioned zip archives ║
83
+ * ║ ├── manifest.json — SnapshotsManifest ║
84
+ * ║ └── snapshot_v001_YYYY-MM-DD.zip ║
85
+ * ║ ║
86
+ * ╠═══════════════════════════════════════════════════════════════════════╣
87
+ * ║ KEY RUST BEHAVIORS (do not skip these): ║
88
+ * ╠═══════════════════════════════════════════════════════════════════════╣
89
+ * ║ ║
90
+ * ║ ● Hash-based change detection: ║
91
+ * ║ Store SHA256 hashes in .meta/meta.json per component. ║
92
+ * ║ Compare hashes (not timestamps) to detect user edits. ║
93
+ * ║ ║
94
+ * ║ ● Semver auto-bump on snapshot creation: ║
95
+ * ║ When creating a snapshot, detect which components changed ║
96
+ * ║ (golden vs live hash mismatch). Auto-bump patch version for ║
97
+ * ║ each changed component. Copy live → golden after snapshot. ║
98
+ * ║ ║
99
+ * ║ ● Selective sync on app updates: ║
100
+ * ║ When seed is newer than golden, update golden from seed. ║
101
+ * ║ For live: only overwrite components where live hash matches ║
102
+ * ║ the OLD golden hash (i.e. user hasn't edited them). ║
103
+ * ║ If user has edited a component, skip it (preserve their work). ║
104
+ * ║ ║
105
+ * ║ ● Snapshot pruning: ║
106
+ * ║ Keep the N most recent snapshots (default 20) + all pinned ones. ║
107
+ * ║ Prune after each snapshot creation. ║
108
+ * ║ ║
109
+ * ║ ● Reset operations: ║
110
+ * ║ "Reset file" = copy golden/{path} → live/{path} ║
111
+ * ║ "Reset component" = copy golden/{component}/ → live/{component}/ ║
112
+ * ║ "Reset all" = copy entire golden/ → live/ ║
113
+ * ║ Always update live .meta/meta.json after reset. ║
114
+ * ║ ║
115
+ * ╚═══════════════════════════════════════════════════════════════════════╝
116
+ */
117
+
118
+ // Types (shared between UI and backend)
119
+ export type {
120
+ ComponentVersion,
121
+ GoldenMeta,
122
+ DirectoryStatus,
123
+ SeedInfo,
124
+ GoldenStatus,
125
+ FileDiffStatus,
126
+ FileDiffInfo,
127
+ DiffSummary,
128
+ ChangedComponents,
129
+ GoldenLiveDiff,
130
+ SnapshotInfo,
131
+ SnapshotsManifest,
132
+ CreateSnapshotResult,
133
+ ResetResult,
134
+ GoldenSnapshotsApi,
135
+ } from './types.ts'
136
+
137
+ // Hook
138
+ export { useGoldenSync, formatDate, formatBytes, getLanguage, type UseGoldenSyncOptions, type UseGoldenSyncReturn, type DiffTreeNode } from './use-golden-sync.ts'
139
+
140
+ // Components
141
+ export { GoldenSyncPanel, type GoldenSyncPanelProps } from './golden-sync-panel.tsx'
142
+ export { StatusOverview, type StatusOverviewProps } from './status-overview.tsx'
143
+ export { FileDiffViewer, type FileDiffViewerProps } from './file-diff-viewer.tsx'
144
+ export { SnapshotManager, type SnapshotManagerProps } from './snapshot-manager.tsx'
145
+ export { VersionManager, type VersionManagerProps } from './version-manager.tsx'
@@ -0,0 +1,200 @@
1
+ import { useState } from 'react'
2
+ import { Archive, Pin, PinOff, Trash2, RotateCcw, Plus, Loader2 } from 'lucide-react'
3
+ import { IconButton } from '../../ui/icon-button.tsx'
4
+ import { Input } from '../../ui/input.tsx'
5
+ import { ConfirmModal } from '../../ui/modal.tsx'
6
+ import type { SnapshotInfo } from './types.ts'
7
+ import { formatDate, type UseGoldenSyncReturn } from './use-golden-sync.ts'
8
+
9
+ export interface SnapshotManagerProps {
10
+ sync: UseGoldenSyncReturn
11
+ }
12
+
13
+ export function SnapshotManager({ sync }: SnapshotManagerProps) {
14
+ const {
15
+ manifest,
16
+ manifestLoading,
17
+ snapshotBusy,
18
+ loadManifest,
19
+ handleCreateSnapshot,
20
+ handleRestoreSnapshot,
21
+ handlePinSnapshot,
22
+ handleDeleteSnapshot,
23
+ } = sync
24
+
25
+ const [newDescription, setNewDescription] = useState('')
26
+ const [restoreTarget, setRestoreTarget] = useState<SnapshotInfo | null>(null)
27
+ const [restoreResetLive, setRestoreResetLive] = useState(true)
28
+ const [deleteTarget, setDeleteTarget] = useState<SnapshotInfo | null>(null)
29
+
30
+ const onCreate = async () => {
31
+ await handleCreateSnapshot(newDescription || undefined)
32
+ setNewDescription('')
33
+ }
34
+
35
+ const onRestore = async () => {
36
+ if (!restoreTarget) return
37
+ await handleRestoreSnapshot(restoreTarget.version, restoreResetLive)
38
+ setRestoreTarget(null)
39
+ }
40
+
41
+ const onDelete = async () => {
42
+ if (!deleteTarget) return
43
+ await handleDeleteSnapshot(deleteTarget.version)
44
+ setDeleteTarget(null)
45
+ }
46
+
47
+ if (manifestLoading && !manifest) {
48
+ return <div className="text-sm text-[#6c7086] text-center py-8">Loading snapshots...</div>
49
+ }
50
+
51
+ return (
52
+ <div className="space-y-4">
53
+ {/* Create Snapshot */}
54
+ <div className="bg-[#181825] rounded-lg p-4 border border-[#313244]">
55
+ <div className="flex items-center gap-3 mb-3">
56
+ <Plus className="w-5 h-5 text-green-400" />
57
+ <h4 className="text-[#cdd6f4] font-medium">Create Snapshot</h4>
58
+ </div>
59
+ <p className="text-xs text-[#585b70] mb-3">
60
+ Archives the current live state. If components differ from golden, their patch version is auto-bumped.
61
+ </p>
62
+ <div className="flex gap-2">
63
+ <div className="flex-1">
64
+ <Input
65
+ value={newDescription}
66
+ onChange={setNewDescription}
67
+ placeholder="Optional description..."
68
+ size="sm"
69
+ variant="outline"
70
+ />
71
+ </div>
72
+ <IconButton
73
+ icon={snapshotBusy ? <Loader2 className="animate-spin" /> : <Archive />}
74
+ onClick={onCreate}
75
+ disabled={snapshotBusy}
76
+ size="sm"
77
+ color="green"
78
+ tooltip={{ title: 'Create snapshot', description: 'Archive current live state' }}
79
+ />
80
+ </div>
81
+ </div>
82
+
83
+ {/* Snapshots List */}
84
+ <div className="bg-[#181825] rounded-lg border border-[#313244] overflow-hidden">
85
+ <div className="flex items-center justify-between px-4 py-3 border-b border-[#313244]">
86
+ <div className="flex items-center gap-2">
87
+ <Archive className="w-4 h-4 text-[#6c7086]" />
88
+ <h4 className="text-[#cdd6f4] font-medium">Snapshots</h4>
89
+ <span className="text-xs text-[#6c7086]">({manifest?.snapshots.length ?? 0})</span>
90
+ </div>
91
+ <IconButton
92
+ icon={manifestLoading ? <Loader2 className="animate-spin" /> : <RotateCcw />}
93
+ onClick={loadManifest}
94
+ disabled={manifestLoading}
95
+ size="xs"
96
+ color="neutral"
97
+ tooltip={{ title: 'Refresh', description: 'Reload snapshot list' }}
98
+ />
99
+ </div>
100
+
101
+ {!manifest || manifest.snapshots.length === 0 ? (
102
+ <div className="text-sm text-[#6c7086] text-center py-8">No snapshots yet</div>
103
+ ) : (
104
+ <div className="divide-y divide-[#313244]">
105
+ {[...manifest.snapshots].reverse().map((snap) => (
106
+ <div
107
+ key={snap.version}
108
+ className={`flex items-center gap-3 px-4 py-3 ${
109
+ snap.version === manifest.activeVersion ? 'bg-green-500/5' : ''
110
+ }`}
111
+ >
112
+ {/* Version + info */}
113
+ <div className="flex-1 min-w-0">
114
+ <div className="flex items-center gap-2">
115
+ <span className="text-sm font-mono text-[#cdd6f4]">v{snap.version}</span>
116
+ {snap.version === manifest.activeVersion && (
117
+ <span className="px-1.5 py-0.5 bg-green-500/20 text-green-400 text-[10px] rounded font-medium">
118
+ active
119
+ </span>
120
+ )}
121
+ {snap.pinned && (
122
+ <Pin className="w-3 h-3 text-amber-400" />
123
+ )}
124
+ {snap.metaVersion && (
125
+ <span className="text-xs text-[#585b70] font-mono">{snap.metaVersion}</span>
126
+ )}
127
+ </div>
128
+ <div className="flex items-center gap-2 mt-0.5">
129
+ <span className="text-xs text-[#585b70]">{formatDate(snap.createdAt)}</span>
130
+ {snap.description && (
131
+ <span className="text-xs text-[#6c7086] truncate">{snap.description}</span>
132
+ )}
133
+ </div>
134
+ </div>
135
+
136
+ {/* Actions */}
137
+ <div className="flex items-center gap-1 flex-shrink-0">
138
+ <IconButton
139
+ icon={snap.pinned ? <PinOff /> : <Pin />}
140
+ onClick={() => handlePinSnapshot(snap.version, !snap.pinned)}
141
+ disabled={snapshotBusy}
142
+ size="xs"
143
+ color={snap.pinned ? 'amber' : 'neutral'}
144
+ tooltip={{
145
+ title: snap.pinned ? 'Unpin' : 'Pin',
146
+ description: snap.pinned ? 'Allow auto-pruning' : 'Protect from auto-pruning',
147
+ }}
148
+ />
149
+ <IconButton
150
+ icon={<RotateCcw />}
151
+ onClick={() => { setRestoreTarget(snap); setRestoreResetLive(true) }}
152
+ disabled={snapshotBusy}
153
+ size="xs"
154
+ color="blue"
155
+ tooltip={{ title: 'Restore', description: 'Restore golden from this snapshot' }}
156
+ />
157
+ <IconButton
158
+ icon={<Trash2 />}
159
+ onClick={() => setDeleteTarget(snap)}
160
+ disabled={snapshotBusy || snap.pinned}
161
+ size="xs"
162
+ color="red"
163
+ tooltip={{
164
+ title: 'Delete',
165
+ description: snap.pinned ? 'Unpin first to delete' : 'Delete this snapshot',
166
+ }}
167
+ />
168
+ </div>
169
+ </div>
170
+ ))}
171
+ </div>
172
+ )}
173
+ </div>
174
+
175
+ {/* Restore Confirmation */}
176
+ <ConfirmModal
177
+ isOpen={!!restoreTarget}
178
+ onClose={() => setRestoreTarget(null)}
179
+ title={`Restore Snapshot v${restoreTarget?.version}`}
180
+ kind="warning"
181
+ message={`This will overwrite golden with snapshot v${restoreTarget?.version}. ${restoreResetLive ? 'Live files will also be reset to match.' : 'Live files will be kept as-is.'}`}
182
+ confirmColor="blue"
183
+ onConfirm={onRestore}
184
+ isLoading={snapshotBusy}
185
+ />
186
+
187
+ {/* Delete Confirmation */}
188
+ <ConfirmModal
189
+ isOpen={!!deleteTarget}
190
+ onClose={() => setDeleteTarget(null)}
191
+ title={`Delete Snapshot v${deleteTarget?.version}`}
192
+ kind="error"
193
+ message={`This will permanently delete snapshot v${deleteTarget?.version}. This action cannot be undone.`}
194
+ confirmColor="red"
195
+ onConfirm={onDelete}
196
+ isLoading={snapshotBusy}
197
+ />
198
+ </div>
199
+ )
200
+ }