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