@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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Tools Paths — Shared type definitions
|
|
3
|
+
*
|
|
4
|
+
* Part of: Sections > AI Tools Paths
|
|
5
|
+
*
|
|
6
|
+
* These types define the CONTRACT between the UI layer (this package) and
|
|
7
|
+
* the Rust/Tauri backend that each app must implement. Every type here has
|
|
8
|
+
* a 1:1 mapping to a Rust struct returned by Tauri commands.
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT FOR AI AGENTS:
|
|
11
|
+
* When adding a new field here, the corresponding Rust struct must also be
|
|
12
|
+
* updated (and vice versa). The serde rename attributes in Rust use camelCase
|
|
13
|
+
* to match these TypeScript interfaces.
|
|
14
|
+
*
|
|
15
|
+
* Generic design:
|
|
16
|
+
* - Tools are keyed by string ID (not hardcoded to specific tools)
|
|
17
|
+
* - Configr uses: "claude", "gemini", "copilot", "codex", "opencode"
|
|
18
|
+
* - Other apps can define their own tool sets
|
|
19
|
+
* - The UI renders whatever tools the consumer provides
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Tool configuration
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** Per-tool configuration state */
|
|
27
|
+
export interface AiToolConfig {
|
|
28
|
+
/** Unique identifier for the tool (e.g. "claude", "gemini") */
|
|
29
|
+
id: string
|
|
30
|
+
/** Display name (e.g. "Claude Code", "Gemini CLI") */
|
|
31
|
+
name: string
|
|
32
|
+
/** Whether the tool is enabled for use */
|
|
33
|
+
enabled: boolean
|
|
34
|
+
/** User-configured or auto-detected binary path */
|
|
35
|
+
binaryPath: string
|
|
36
|
+
/** Whether the tool was found during detection */
|
|
37
|
+
detected: boolean
|
|
38
|
+
/** Path found by auto-detection (may differ from binaryPath if user overrode it) */
|
|
39
|
+
detectedPath?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Detection results
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/** Result of detecting a single tool's binary */
|
|
47
|
+
export interface ToolDetectionResult {
|
|
48
|
+
/** Whether the binary was found */
|
|
49
|
+
detected: boolean
|
|
50
|
+
/** Resolved path to the binary, if found */
|
|
51
|
+
path?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// API adapter interface
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* ToolsPathsApi — The callback interface that each app must implement.
|
|
60
|
+
*
|
|
61
|
+
* This is the bridge between the shared UI and the app-specific backend.
|
|
62
|
+
* Each function maps to a Tauri command (or any other backend).
|
|
63
|
+
*
|
|
64
|
+
* ┌─────────────────────────────────────────────────────────────────────┐
|
|
65
|
+
* │ RUST BACKEND IMPLEMENTATION GUIDE │
|
|
66
|
+
* │ │
|
|
67
|
+
* │ Each method below corresponds to a Tauri #[tauri::command]. │
|
|
68
|
+
* │ The Rust reference implementation lives in: │
|
|
69
|
+
* │ configr/main/src-tauri/src/commands/tools/ │
|
|
70
|
+
* │ ├── mod.rs — Types + helpers │
|
|
71
|
+
* │ └── detection.rs — Binary detection logic │
|
|
72
|
+
* │ │
|
|
73
|
+
* │ REQUIRED Tauri commands for full functionality: │
|
|
74
|
+
* │ │
|
|
75
|
+
* │ DETECTION: │
|
|
76
|
+
* │ detect_all_tools() → Record<string, ToolDetectionResult> │
|
|
77
|
+
* │ Scans PATH and known install locations for all tool binaries. │
|
|
78
|
+
* │ Returns a map of tool ID → detection result. │
|
|
79
|
+
* │ │
|
|
80
|
+
* │ detect_tool(toolId) → ToolDetectionResult │
|
|
81
|
+
* │ Scans for a single tool binary. Used for per-tool refresh. │
|
|
82
|
+
* │ │
|
|
83
|
+
* │ VALIDATION: │
|
|
84
|
+
* │ validate_binary_path(path) → bool │
|
|
85
|
+
* │ Checks if a given path points to a valid executable. │
|
|
86
|
+
* │ Typically: fs::metadata(path).is_ok() && is_executable(path) │
|
|
87
|
+
* │ │
|
|
88
|
+
* │ DETECTION STRATEGY (Rust backend should implement): │
|
|
89
|
+
* │ 1. Check PATH environment variable (which <tool-binary>) │
|
|
90
|
+
* │ 2. Check common install locations: │
|
|
91
|
+
* │ - /usr/local/bin/<binary> │
|
|
92
|
+
* │ - /opt/homebrew/bin/<binary> │
|
|
93
|
+
* │ - ~/.local/bin/<binary> │
|
|
94
|
+
* │ - ~/.cargo/bin/<binary> (for Rust-based tools) │
|
|
95
|
+
* │ - ~/.npm/bin/<binary> (for npm-based tools) │
|
|
96
|
+
* │ 3. Check tool-specific locations: │
|
|
97
|
+
* │ - Claude: ~/.claude/bin/claude │
|
|
98
|
+
* │ - Copilot: gh extension path │
|
|
99
|
+
* │ 4. Return first valid path found, or detected: false │
|
|
100
|
+
* └─────────────────────────────────────────────────────────────────────┘
|
|
101
|
+
*/
|
|
102
|
+
export interface ToolsPathsApi {
|
|
103
|
+
/** Scan all tools and return detection results keyed by tool ID */
|
|
104
|
+
detectAll: () => Promise<Record<string, ToolDetectionResult>>
|
|
105
|
+
|
|
106
|
+
/** Scan a single tool and return its detection result */
|
|
107
|
+
detectTool: (toolId: string) => Promise<ToolDetectionResult>
|
|
108
|
+
|
|
109
|
+
/** Check if a given binary path is valid (exists and is executable) */
|
|
110
|
+
validatePath: (path: string) => Promise<boolean>
|
|
111
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useToolsPaths — Tool detection and configuration state hook
|
|
3
|
+
*
|
|
4
|
+
* Part of: Sections > AI Tools Paths
|
|
5
|
+
*
|
|
6
|
+
* Manages tool detection state, per-tool refresh tracking, and path validation.
|
|
7
|
+
* Used internally by ToolsPathsPanel but can also be used standalone for
|
|
8
|
+
* custom UIs.
|
|
9
|
+
*
|
|
10
|
+
* AI agent notes:
|
|
11
|
+
* - This hook does NOT store tool configs — the consumer owns that state.
|
|
12
|
+
* It only manages detection/scanning ephemeral state.
|
|
13
|
+
* - The hook tracks which tools have been scanned, which are currently
|
|
14
|
+
* refreshing, and whether a full scan has completed.
|
|
15
|
+
* - Path validation is debounced by the consumer (typically on blur or
|
|
16
|
+
* after typing stops).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
20
|
+
import type { AiToolConfig, ToolsPathsApi, ToolDetectionResult } from './types.ts'
|
|
21
|
+
|
|
22
|
+
export interface UseToolsPathsOptions {
|
|
23
|
+
api: ToolsPathsApi
|
|
24
|
+
tools: AiToolConfig[]
|
|
25
|
+
onToolConfigChange: (toolId: string, config: Partial<AiToolConfig>) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UseToolsPathsReturn {
|
|
29
|
+
/** Whether a full scan is in progress */
|
|
30
|
+
isDetecting: boolean
|
|
31
|
+
/** Whether a full scan has completed at least once */
|
|
32
|
+
hasScanned: boolean
|
|
33
|
+
/** Set of tool IDs currently being individually refreshed */
|
|
34
|
+
refreshingTools: Set<string>
|
|
35
|
+
/** Set of tool IDs that were found during the last scan */
|
|
36
|
+
scannedTools: Set<string>
|
|
37
|
+
/** Run detection for all tools */
|
|
38
|
+
detectAll: () => Promise<void>
|
|
39
|
+
/** Run detection for a single tool */
|
|
40
|
+
detectTool: (toolId: string) => Promise<void>
|
|
41
|
+
/** Validate and update a tool's binary path */
|
|
42
|
+
validateAndUpdatePath: (toolId: string, path: string) => Promise<void>
|
|
43
|
+
/** Toggle a tool's enabled state */
|
|
44
|
+
toggleEnabled: (toolId: string) => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useToolsPaths({ api, tools, onToolConfigChange }: UseToolsPathsOptions): UseToolsPathsReturn {
|
|
48
|
+
const [isDetecting, setIsDetecting] = useState(false)
|
|
49
|
+
const [hasScanned, setHasScanned] = useState(false)
|
|
50
|
+
const [refreshingTools, setRefreshingTools] = useState<Set<string>>(new Set())
|
|
51
|
+
const [scannedTools, setScannedTools] = useState<Set<string>>(new Set())
|
|
52
|
+
const wasDetectingRef = useRef(false)
|
|
53
|
+
|
|
54
|
+
// Track when full detection completes
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (wasDetectingRef.current && !isDetecting) {
|
|
57
|
+
setHasScanned(true)
|
|
58
|
+
}
|
|
59
|
+
wasDetectingRef.current = isDetecting
|
|
60
|
+
}, [isDetecting])
|
|
61
|
+
|
|
62
|
+
const detectAll = useCallback(async () => {
|
|
63
|
+
setIsDetecting(true)
|
|
64
|
+
try {
|
|
65
|
+
const results = await api.detectAll()
|
|
66
|
+
const detected = new Set<string>()
|
|
67
|
+
|
|
68
|
+
for (const tool of tools) {
|
|
69
|
+
const result = results[tool.id]
|
|
70
|
+
if (result) {
|
|
71
|
+
onToolConfigChange(tool.id, {
|
|
72
|
+
detected: result.detected,
|
|
73
|
+
detectedPath: result.path,
|
|
74
|
+
binaryPath: result.path || tool.binaryPath,
|
|
75
|
+
})
|
|
76
|
+
if (result.detected) {
|
|
77
|
+
detected.add(tool.id)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setScannedTools(detected)
|
|
83
|
+
} finally {
|
|
84
|
+
setIsDetecting(false)
|
|
85
|
+
}
|
|
86
|
+
}, [api, tools, onToolConfigChange])
|
|
87
|
+
|
|
88
|
+
const detectTool = useCallback(async (toolId: string) => {
|
|
89
|
+
setRefreshingTools((prev) => new Set(prev).add(toolId))
|
|
90
|
+
try {
|
|
91
|
+
const result: ToolDetectionResult = await api.detectTool(toolId)
|
|
92
|
+
onToolConfigChange(toolId, {
|
|
93
|
+
detected: result.detected,
|
|
94
|
+
detectedPath: result.path,
|
|
95
|
+
binaryPath: result.path || '',
|
|
96
|
+
})
|
|
97
|
+
setScannedTools((prev) => {
|
|
98
|
+
const next = new Set(prev)
|
|
99
|
+
if (result.detected) {
|
|
100
|
+
next.add(toolId)
|
|
101
|
+
} else {
|
|
102
|
+
next.delete(toolId)
|
|
103
|
+
}
|
|
104
|
+
return next
|
|
105
|
+
})
|
|
106
|
+
} finally {
|
|
107
|
+
setRefreshingTools((prev) => {
|
|
108
|
+
const next = new Set(prev)
|
|
109
|
+
next.delete(toolId)
|
|
110
|
+
return next
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}, [api, onToolConfigChange])
|
|
114
|
+
|
|
115
|
+
const validateAndUpdatePath = useCallback(async (toolId: string, path: string) => {
|
|
116
|
+
onToolConfigChange(toolId, { binaryPath: path })
|
|
117
|
+
setScannedTools((prev) => {
|
|
118
|
+
const next = new Set(prev)
|
|
119
|
+
next.delete(toolId)
|
|
120
|
+
return next
|
|
121
|
+
})
|
|
122
|
+
setHasScanned(false)
|
|
123
|
+
|
|
124
|
+
if (path.trim()) {
|
|
125
|
+
try {
|
|
126
|
+
const isValid = await api.validatePath(path.trim())
|
|
127
|
+
onToolConfigChange(toolId, {
|
|
128
|
+
binaryPath: path.trim(),
|
|
129
|
+
detected: isValid,
|
|
130
|
+
detectedPath: isValid ? path.trim() : undefined,
|
|
131
|
+
})
|
|
132
|
+
} catch {
|
|
133
|
+
onToolConfigChange(toolId, {
|
|
134
|
+
binaryPath: path.trim(),
|
|
135
|
+
detected: false,
|
|
136
|
+
detectedPath: undefined,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}, [api, onToolConfigChange])
|
|
141
|
+
|
|
142
|
+
const toggleEnabled = useCallback((toolId: string) => {
|
|
143
|
+
const tool = tools.find((t) => t.id === toolId)
|
|
144
|
+
if (tool) {
|
|
145
|
+
onToolConfigChange(toolId, { enabled: !tool.enabled })
|
|
146
|
+
}
|
|
147
|
+
}, [tools, onToolConfigChange])
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
isDetecting,
|
|
151
|
+
hasScanned,
|
|
152
|
+
refreshingTools,
|
|
153
|
+
scannedTools,
|
|
154
|
+
detectAll,
|
|
155
|
+
detectTool,
|
|
156
|
+
validateAndUpdatePath,
|
|
157
|
+
toggleEnabled,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CapturedIssuesPanel — Self-contained captured issues review and reporting
|
|
3
|
+
*
|
|
4
|
+
* Part of: Sections > Captured Issues
|
|
5
|
+
*
|
|
6
|
+
* Replicates the configr "Settings > Support > Captured Issues" page as a
|
|
7
|
+
* reusable component. Shows captured errors/warnings, lets the user add
|
|
8
|
+
* context and submit a report, or dismiss. Also shows previously reported
|
|
9
|
+
* errors in a collapsible section.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* <CapturedIssuesPanel
|
|
13
|
+
* api={capturedIssuesApi}
|
|
14
|
+
* errors={currentErrors}
|
|
15
|
+
* onDismiss={() => logger.clearLogs()}
|
|
16
|
+
* onSubmitSuccess={(id) => toast(`Reported ${id}`)}
|
|
17
|
+
* />
|
|
18
|
+
*
|
|
19
|
+
* AI agent notes:
|
|
20
|
+
* - All backend calls go through the api prop (CapturedIssuesApi interface)
|
|
21
|
+
* - Errors are passed in as props — the component does not capture them itself
|
|
22
|
+
* - Uses ui-design components (Input, ResizableTextarea, IconButton) for consistency
|
|
23
|
+
* - Dark theme with Catppuccin-like colors matching configr
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { AlertTriangle, Check, X, Send, ChevronDown, Shield, Loader2 } from 'lucide-react'
|
|
27
|
+
import { cn } from '../../lib/cn.ts'
|
|
28
|
+
import { Input } from '../../ui/input.tsx'
|
|
29
|
+
import { ResizableTextarea } from '../../ui/resizable-textarea.tsx'
|
|
30
|
+
import { IconButton } from '../../ui/icon-button.tsx'
|
|
31
|
+
import { useCapturedIssues } from './use-captured-issues.ts'
|
|
32
|
+
import type { CapturedError, CapturedIssuesApi } from './types.ts'
|
|
33
|
+
|
|
34
|
+
export interface CapturedIssuesPanelProps {
|
|
35
|
+
/** Backend API implementation */
|
|
36
|
+
api: CapturedIssuesApi
|
|
37
|
+
/** Current captured errors to display */
|
|
38
|
+
errors: CapturedError[]
|
|
39
|
+
/** Called after errors are dismissed */
|
|
40
|
+
onDismiss?: () => void
|
|
41
|
+
/** Called after successful submission */
|
|
42
|
+
onSubmitSuccess?: (issueId?: string) => void
|
|
43
|
+
className?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function CapturedIssuesPanel({
|
|
47
|
+
api,
|
|
48
|
+
errors,
|
|
49
|
+
onDismiss,
|
|
50
|
+
onSubmitSuccess,
|
|
51
|
+
className,
|
|
52
|
+
}: CapturedIssuesPanelProps) {
|
|
53
|
+
const {
|
|
54
|
+
errorCount,
|
|
55
|
+
warnCount,
|
|
56
|
+
title,
|
|
57
|
+
setTitle,
|
|
58
|
+
description,
|
|
59
|
+
setDescription,
|
|
60
|
+
email,
|
|
61
|
+
setEmail,
|
|
62
|
+
isSubmitting,
|
|
63
|
+
submittedErrors,
|
|
64
|
+
handleSubmit,
|
|
65
|
+
handleDismiss,
|
|
66
|
+
} = useCapturedIssues({ api, errors, onDismiss, onSubmitSuccess })
|
|
67
|
+
|
|
68
|
+
const hasErrors = errorCount > 0 || warnCount > 0
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className={cn('space-y-6', className)}>
|
|
72
|
+
{hasErrors ? (
|
|
73
|
+
<div className="bg-[#181825] border border-[#313244] rounded-lg p-4 space-y-4">
|
|
74
|
+
{/* Error Summary */}
|
|
75
|
+
<div className="flex items-center justify-between">
|
|
76
|
+
<div className="flex items-center gap-2">
|
|
77
|
+
<AlertTriangle className="w-4 h-4 text-red-400" />
|
|
78
|
+
<span className="text-sm text-[#cdd6f4]">
|
|
79
|
+
{errorCount > 0 && (
|
|
80
|
+
<span className="text-red-400">
|
|
81
|
+
{errorCount} error{errorCount !== 1 ? 's' : ''}
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
{errorCount > 0 && warnCount > 0 && <span className="text-[#6c7086]">, </span>}
|
|
85
|
+
{warnCount > 0 && (
|
|
86
|
+
<span className="text-yellow-400">
|
|
87
|
+
{warnCount} warning{warnCount !== 1 ? 's' : ''}
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
90
|
+
</span>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Error List */}
|
|
95
|
+
<div className="space-y-2">
|
|
96
|
+
<p className="text-sm text-[#a6adc8]">Captured Errors</p>
|
|
97
|
+
<div className="max-h-48 overflow-y-auto bg-[#11111b] border border-[#313244] rounded-lg p-3 space-y-1">
|
|
98
|
+
{errors.map((error) => (
|
|
99
|
+
<div key={error.fingerprint} className="text-xs font-mono break-words">
|
|
100
|
+
<span className={cn('mr-2 shrink-0', error.level === 'warning' ? 'text-yellow-400' : 'text-red-400')}>
|
|
101
|
+
{error.count > 1 ? `\u00d7${error.count}` : '\u2022'}
|
|
102
|
+
</span>
|
|
103
|
+
<span className="text-[#a6adc8] break-all">{error.message}</span>
|
|
104
|
+
</div>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Optional Details */}
|
|
110
|
+
<div className="space-y-3">
|
|
111
|
+
<p className="text-sm text-[#a6adc8]">Add context (optional)</p>
|
|
112
|
+
<Input
|
|
113
|
+
value={title}
|
|
114
|
+
onChange={setTitle}
|
|
115
|
+
placeholder="Brief summary of what you were doing"
|
|
116
|
+
/>
|
|
117
|
+
<ResizableTextarea
|
|
118
|
+
value={description}
|
|
119
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
120
|
+
placeholder="What were you doing when this happened?"
|
|
121
|
+
rows={3}
|
|
122
|
+
className="w-full px-3 py-1.5 bg-[#313244] border border-[#45475a] rounded-lg text-[#cdd6f4] placeholder-[#6c7086] focus:outline-none focus:border-blue-500 transition-colors resize-none text-sm"
|
|
123
|
+
/>
|
|
124
|
+
<Input
|
|
125
|
+
type="text"
|
|
126
|
+
value={email}
|
|
127
|
+
onChange={setEmail}
|
|
128
|
+
placeholder="Email for follow-up"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Actions */}
|
|
133
|
+
<div className="flex items-center justify-end gap-2 pt-2">
|
|
134
|
+
<IconButton
|
|
135
|
+
icon={<X className="w-3.5 h-3.5" />}
|
|
136
|
+
onClick={handleDismiss}
|
|
137
|
+
disabled={isSubmitting}
|
|
138
|
+
size="sm"
|
|
139
|
+
tooltip={{ title: 'Dismiss', description: 'Ignore these errors' }}
|
|
140
|
+
tooltipPosition="top"
|
|
141
|
+
/>
|
|
142
|
+
<IconButton
|
|
143
|
+
icon={
|
|
144
|
+
isSubmitting ? (
|
|
145
|
+
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
146
|
+
) : (
|
|
147
|
+
<Send className="w-3.5 h-3.5" />
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
onClick={handleSubmit}
|
|
151
|
+
disabled={isSubmitting}
|
|
152
|
+
size="sm"
|
|
153
|
+
color="red"
|
|
154
|
+
tooltip={{ title: 'Send Report', description: 'Submit error report to developers' }}
|
|
155
|
+
tooltipPosition="top"
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
) : (
|
|
160
|
+
<div className="bg-[#181825] border border-[#313244] rounded-lg p-6">
|
|
161
|
+
<div className="flex items-center gap-4">
|
|
162
|
+
<div className="w-10 h-10 bg-green-500/10 rounded-lg flex items-center justify-center">
|
|
163
|
+
<Check className="w-5 h-5 text-green-400" />
|
|
164
|
+
</div>
|
|
165
|
+
<div>
|
|
166
|
+
<h3 className="text-[#cdd6f4] font-medium">No Issues Captured</h3>
|
|
167
|
+
<p className="text-sm text-[#6c7086]">Everything is running smoothly.</p>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
{/* Previously Reported */}
|
|
174
|
+
{submittedErrors.length > 0 && (
|
|
175
|
+
<div className="bg-[#181825] border border-[#313244] rounded-lg overflow-hidden">
|
|
176
|
+
<details className="group">
|
|
177
|
+
<summary className="flex items-center justify-between p-4 cursor-pointer hover:bg-[#1e1e2e] transition-colors">
|
|
178
|
+
<div className="flex items-center gap-2">
|
|
179
|
+
<Check className="w-4 h-4 text-green-400" />
|
|
180
|
+
<span className="text-sm text-[#cdd6f4]">Previously Reported</span>
|
|
181
|
+
<span className="text-xs text-[#6c7086]">({submittedErrors.length})</span>
|
|
182
|
+
</div>
|
|
183
|
+
<ChevronDown className="w-4 h-4 text-[#6c7086] transition-transform group-open:rotate-180" />
|
|
184
|
+
</summary>
|
|
185
|
+
<div className="border-t border-[#313244] p-4 space-y-2">
|
|
186
|
+
{submittedErrors.map((error) => (
|
|
187
|
+
<div
|
|
188
|
+
key={error.fingerprint}
|
|
189
|
+
className="flex items-start gap-3 p-3 bg-[#11111b] border border-[#313244] rounded-lg"
|
|
190
|
+
>
|
|
191
|
+
<Check className="w-4 h-4 text-green-400 mt-0.5 shrink-0" />
|
|
192
|
+
<div className="flex-1 min-w-0">
|
|
193
|
+
<p
|
|
194
|
+
className="text-sm text-[#cdd6f4] font-mono truncate"
|
|
195
|
+
title={error.message}
|
|
196
|
+
>
|
|
197
|
+
{error.message}
|
|
198
|
+
</p>
|
|
199
|
+
<div className="flex items-center gap-3 mt-1 text-xs text-[#6c7086]">
|
|
200
|
+
{error.count > 0 && (
|
|
201
|
+
<span className="text-yellow-400/80">+{error.count} since</span>
|
|
202
|
+
)}
|
|
203
|
+
<span>{new Date(error.submittedAt).toLocaleDateString()}</span>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
))}
|
|
208
|
+
</div>
|
|
209
|
+
</details>
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Captured Issues — Section barrel export
|
|
3
|
+
*
|
|
4
|
+
* This section provides a complete, reusable captured issues review and
|
|
5
|
+
* reporting feature. It shows captured errors/warnings, lets the user add
|
|
6
|
+
* context and submit a report, and tracks previously reported errors.
|
|
7
|
+
*
|
|
8
|
+
* File structure:
|
|
9
|
+
* - captured-issues-panel.tsx — Main panel component (drop-in usage)
|
|
10
|
+
* - use-captured-issues.ts — State management hook (used by panel, also standalone)
|
|
11
|
+
* - types.ts — Types and API interface contract
|
|
12
|
+
*
|
|
13
|
+
* Quick start for consuming apps:
|
|
14
|
+
* import { CapturedIssuesPanel, type CapturedIssuesApi } from '@toolr/ui-design'
|
|
15
|
+
*
|
|
16
|
+
* const api: CapturedIssuesApi = {
|
|
17
|
+
* getSystemInfo: () => invoke('get_system_info'),
|
|
18
|
+
* submitIssue: (data) => invoke('submit_issue', data),
|
|
19
|
+
* dismissErrors: () => invoke('dismiss_errors'),
|
|
20
|
+
* getSubmittedErrors: () => invoke('get_submitted_errors'),
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* <CapturedIssuesPanel
|
|
24
|
+
* api={api}
|
|
25
|
+
* errors={currentErrors}
|
|
26
|
+
* onDismiss={() => logger.clearLogs()}
|
|
27
|
+
* onSubmitSuccess={(id) => toast(`Reported ${id}`)}
|
|
28
|
+
* />
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// Main panel component
|
|
32
|
+
export { CapturedIssuesPanel, type CapturedIssuesPanelProps } from './captured-issues-panel.tsx'
|
|
33
|
+
|
|
34
|
+
// Hook for custom UIs
|
|
35
|
+
export { useCapturedIssues, type UseCapturedIssuesOptions, type UseCapturedIssuesReturn } from './use-captured-issues.ts'
|
|
36
|
+
|
|
37
|
+
// Types and API interface
|
|
38
|
+
export type { CapturedError, SubmittedError, CapturedIssuesApi } from './types.ts'
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Captured Issues — Shared type definitions
|
|
3
|
+
*
|
|
4
|
+
* Part of: Sections > Captured Issues
|
|
5
|
+
*
|
|
6
|
+
* These types define the CONTRACT between the UI layer (this package) and
|
|
7
|
+
* the backend that each app must implement. Each type maps to a backend
|
|
8
|
+
* data structure (Rust struct, API response, etc.).
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT FOR AI AGENTS:
|
|
11
|
+
* When adding a new field here, the corresponding backend type must also be
|
|
12
|
+
* updated. Rust backends use serde rename with camelCase to match these
|
|
13
|
+
* TypeScript interfaces.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Error entries
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/** A captured error or warning entry from the error logger */
|
|
21
|
+
export interface CapturedError {
|
|
22
|
+
/** Unique hash identifying this error (djb2 of message + stack) */
|
|
23
|
+
fingerprint: string
|
|
24
|
+
/** First occurrence message text */
|
|
25
|
+
message: string
|
|
26
|
+
/** Number of times this error has been seen */
|
|
27
|
+
count: number
|
|
28
|
+
/** Severity level */
|
|
29
|
+
level: 'error' | 'warning'
|
|
30
|
+
/** ISO-8601 timestamp of first occurrence */
|
|
31
|
+
firstSeen: string
|
|
32
|
+
/** ISO-8601 timestamp of most recent occurrence */
|
|
33
|
+
lastSeen: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A previously reported error that was submitted with an issue */
|
|
37
|
+
export interface SubmittedError {
|
|
38
|
+
/** Unique hash identifying this error */
|
|
39
|
+
fingerprint: string
|
|
40
|
+
/** Error message text */
|
|
41
|
+
message: string
|
|
42
|
+
/** Number of new occurrences since submission */
|
|
43
|
+
count: number
|
|
44
|
+
/** ISO-8601 timestamp of when this error was submitted */
|
|
45
|
+
submittedAt: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// API adapter interface
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* CapturedIssuesApi — The callback interface that each app must implement.
|
|
54
|
+
*
|
|
55
|
+
* This is the bridge between the shared UI and the app-specific backend.
|
|
56
|
+
* Each function maps to a backend command (Tauri command, API endpoint, etc.).
|
|
57
|
+
*
|
|
58
|
+
* ┌─────────────────────────────────────────────────────────────────────┐
|
|
59
|
+
* │ RUST BACKEND IMPLEMENTATION GUIDE │
|
|
60
|
+
* │ │
|
|
61
|
+
* │ Each method below corresponds to a Tauri #[tauri::command]. │
|
|
62
|
+
* │ The Rust reference implementation lives in: │
|
|
63
|
+
* │ configr/main/src-tauri/src/commands/support.rs │
|
|
64
|
+
* │ │
|
|
65
|
+
* │ REQUIRED Tauri commands: │
|
|
66
|
+
* │ │
|
|
67
|
+
* │ get_system_info() → SystemInfo │
|
|
68
|
+
* │ Returns { os, osVersion, appVersion, arch } │
|
|
69
|
+
* │ Used to include environment details in issue reports. │
|
|
70
|
+
* │ │
|
|
71
|
+
* │ submit_issue(title, description, email, errors) → SubmitResult │
|
|
72
|
+
* │ Sends the issue to your issue tracker (Linear, GitHub, etc.) │
|
|
73
|
+
* │ Returns { success: bool, issue_id: Option<String> } │
|
|
74
|
+
* │ │
|
|
75
|
+
* │ dismiss_errors() → () │
|
|
76
|
+
* │ Clears the current error log without submitting. │
|
|
77
|
+
* │ Typically calls error_logger.clear_logs(). │
|
|
78
|
+
* │ │
|
|
79
|
+
* │ get_submitted_errors() → Vec<SubmittedError> │
|
|
80
|
+
* │ Returns previously reported errors from local storage/DB. │
|
|
81
|
+
* │ Used to show the "Previously Reported" section. │
|
|
82
|
+
* │ │
|
|
83
|
+
* │ Tauri adapter example: │
|
|
84
|
+
* │ │
|
|
85
|
+
* │ import { invoke } from '@tauri-apps/api/core' │
|
|
86
|
+
* │ import type { CapturedIssuesApi } from '@toolr/ui-design' │
|
|
87
|
+
* │ │
|
|
88
|
+
* │ const api: CapturedIssuesApi = { │
|
|
89
|
+
* │ getSystemInfo: () => invoke('get_system_info'), │
|
|
90
|
+
* │ submitIssue: (data) => invoke('submit_issue', data), │
|
|
91
|
+
* │ dismissErrors: () => invoke('dismiss_errors'), │
|
|
92
|
+
* │ getSubmittedErrors: () => invoke('get_submitted_errors'), │
|
|
93
|
+
* │ } │
|
|
94
|
+
* └─────────────────────────────────────────────────────────────────────┘
|
|
95
|
+
*/
|
|
96
|
+
export interface CapturedIssuesApi {
|
|
97
|
+
/** Get system environment info to include in issue reports */
|
|
98
|
+
getSystemInfo: () => Promise<{ os: string; osVersion: string; appVersion: string; arch: string }>
|
|
99
|
+
|
|
100
|
+
/** Submit an issue with captured errors to the issue tracker */
|
|
101
|
+
submitIssue: (data: {
|
|
102
|
+
title: string
|
|
103
|
+
description: string
|
|
104
|
+
email: string
|
|
105
|
+
errors: CapturedError[]
|
|
106
|
+
}) => Promise<{ success: boolean; issueId?: string }>
|
|
107
|
+
|
|
108
|
+
/** Dismiss/clear current errors without submitting */
|
|
109
|
+
dismissErrors: () => Promise<void>
|
|
110
|
+
|
|
111
|
+
/** Get list of previously submitted errors */
|
|
112
|
+
getSubmittedErrors: () => Promise<SubmittedError[]>
|
|
113
|
+
}
|