@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,27 @@
|
|
|
1
|
+
import { useEffect, type RefObject } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Close a menu/dropdown when clicking outside its ref element.
|
|
5
|
+
* If ref is null, closes on any mousedown event.
|
|
6
|
+
*/
|
|
7
|
+
export function useClickOutside(
|
|
8
|
+
ref: RefObject<HTMLElement | null> | null,
|
|
9
|
+
isOpen: boolean,
|
|
10
|
+
onClose: () => void
|
|
11
|
+
): void {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!isOpen) return
|
|
14
|
+
|
|
15
|
+
const handleClick = (event: MouseEvent) => {
|
|
16
|
+
if (!ref) {
|
|
17
|
+
onClose()
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
21
|
+
onClose()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
document.addEventListener('mousedown', handleClick)
|
|
25
|
+
return () => document.removeEventListener('mousedown', handleClick)
|
|
26
|
+
}, [ref, isOpen, onClose])
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Constrains a dropdown menu's height to fit within the viewport.
|
|
5
|
+
* Sets max-height = viewport bottom - element top - margin, and enables vertical scrolling.
|
|
6
|
+
*/
|
|
7
|
+
export function useDropdownMaxHeight<T extends HTMLElement>(isOpen: boolean, margin = 16) {
|
|
8
|
+
const ref = useRef<T>(null)
|
|
9
|
+
|
|
10
|
+
useLayoutEffect(() => {
|
|
11
|
+
if (isOpen && ref.current) {
|
|
12
|
+
const rect = ref.current.getBoundingClientRect()
|
|
13
|
+
ref.current.style.maxHeight = `${window.innerHeight - rect.top - margin}px`
|
|
14
|
+
ref.current.style.overflowY = 'auto'
|
|
15
|
+
ref.current.style.overscrollBehavior = 'contain'
|
|
16
|
+
}
|
|
17
|
+
}, [isOpen, margin])
|
|
18
|
+
|
|
19
|
+
return ref
|
|
20
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Hook for managing back/forward navigation history with a breadcrumb segment stack. */
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react'
|
|
4
|
+
import type { BreadcrumbSegment } from '../ui/breadcrumb.tsx'
|
|
5
|
+
|
|
6
|
+
interface NavigationState {
|
|
7
|
+
backStack: BreadcrumbSegment[][]
|
|
8
|
+
current: BreadcrumbSegment[] | null
|
|
9
|
+
forwardStack: BreadcrumbSegment[][]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UseNavigationHistoryReturn {
|
|
13
|
+
current: BreadcrumbSegment[] | null
|
|
14
|
+
canGoBack: boolean
|
|
15
|
+
canGoForward: boolean
|
|
16
|
+
push: (segments: BreadcrumbSegment[]) => void
|
|
17
|
+
goBack: () => void
|
|
18
|
+
goForward: () => void
|
|
19
|
+
goTo: (index: number) => void
|
|
20
|
+
history: BreadcrumbSegment[][]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useNavigationHistory(
|
|
24
|
+
initial?: BreadcrumbSegment[],
|
|
25
|
+
maxEntries = 50,
|
|
26
|
+
): UseNavigationHistoryReturn {
|
|
27
|
+
const [state, setState] = useState<NavigationState>({
|
|
28
|
+
backStack: [],
|
|
29
|
+
current: initial ?? null,
|
|
30
|
+
forwardStack: [],
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const push = useCallback((segments: BreadcrumbSegment[]) => {
|
|
34
|
+
setState(prev => ({
|
|
35
|
+
backStack: prev.current
|
|
36
|
+
? [...prev.backStack, prev.current].slice(-maxEntries)
|
|
37
|
+
: prev.backStack,
|
|
38
|
+
current: segments,
|
|
39
|
+
forwardStack: [],
|
|
40
|
+
}))
|
|
41
|
+
}, [maxEntries])
|
|
42
|
+
|
|
43
|
+
const goBack = useCallback(() => {
|
|
44
|
+
setState(prev => {
|
|
45
|
+
if (prev.backStack.length === 0) return prev
|
|
46
|
+
const backStack = prev.backStack.slice(0, -1)
|
|
47
|
+
const entry = prev.backStack[prev.backStack.length - 1]
|
|
48
|
+
return {
|
|
49
|
+
backStack,
|
|
50
|
+
current: entry,
|
|
51
|
+
forwardStack: prev.current
|
|
52
|
+
? [prev.current, ...prev.forwardStack]
|
|
53
|
+
: prev.forwardStack,
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}, [])
|
|
57
|
+
|
|
58
|
+
const goForward = useCallback(() => {
|
|
59
|
+
setState(prev => {
|
|
60
|
+
if (prev.forwardStack.length === 0) return prev
|
|
61
|
+
const [entry, ...rest] = prev.forwardStack
|
|
62
|
+
return {
|
|
63
|
+
backStack: prev.current
|
|
64
|
+
? [...prev.backStack, prev.current]
|
|
65
|
+
: prev.backStack,
|
|
66
|
+
current: entry,
|
|
67
|
+
forwardStack: rest,
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}, [])
|
|
71
|
+
|
|
72
|
+
const goTo = useCallback((index: number) => {
|
|
73
|
+
setState(prev => {
|
|
74
|
+
const fullHistory = [...prev.backStack, ...(prev.current ? [prev.current] : [])]
|
|
75
|
+
if (index < 0 || index >= fullHistory.length) return prev
|
|
76
|
+
return {
|
|
77
|
+
backStack: fullHistory.slice(0, index),
|
|
78
|
+
current: fullHistory[index],
|
|
79
|
+
forwardStack: [...fullHistory.slice(index + 1), ...prev.forwardStack],
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
}, [])
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
current: state.current,
|
|
86
|
+
canGoBack: state.backStack.length > 0,
|
|
87
|
+
canGoForward: state.forwardStack.length > 0,
|
|
88
|
+
push,
|
|
89
|
+
goBack,
|
|
90
|
+
goForward,
|
|
91
|
+
goTo,
|
|
92
|
+
history: [...state.backStack, ...(state.current ? [state.current] : [])],
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const AI_TOOL_LOGOS = {
|
|
2
|
+
claude: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAMAAABF0y+mAAAAJFBMVEVHcEzZd1fZd1fZd1fZd1fad1jZd1fZd1fZd1fZd1fZd1fZd1deZDooAAAADHRSTlMA//F3mhLfyjpWsiMDGU5mAAABH0lEQVQokXVSWXbEIAzDuw33v28FJCnpTP3BA6+yRGvbLK39a0F9RUvHZ5CIazZwkm+VpCi1nZP1GpJEnq2NdabzU594N0XpzInRhq/7jvm8wsOjFfVmcQG4guTWZKYL8HSiA1XFHDjgHMqCpKfpNPgIXiZVVvSJ9yazOJARxBiYf/ZMoIUxrYGjRHs/di2nbdzDZxIb1maPriold3QlyFJCAonMd08A7/WhkN19PWDolSeiXU7URWPm8S3ewMCuvGpB+kigVXvWZKlgIVycF0Hjl6DIoVRCGKz9pE8W0QKXkgkIEmjzUDuh11TSuVkn348DLHs1Y7/kzTi+ksX8GLn0wBRrdvCQS949C43fstj6FhfM8Unfqqwv3gf3+/kDMJgHC0kwnjEAAAAASUVORK5CYII=',
|
|
3
|
+
copilot: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjgiIGhlaWdodD0iMjgiIHZpZXdCb3g9IjAgMCAyOCAyOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjgiIGhlaWdodD0iMjgiIHJ4PSI2IiBmaWxsPSIjMWYxZjFmIi8+PHBhdGggZD0iTTE0IDVjLTQuOTcgMC05IDQuMDMtOSA5IDAgMy45NyAyLjU2IDcuMzQgNi4xMiA4LjUzLjQ1LjA4LjYxLS4xOS42MS0uNDN2LTEuNjdjLTIuNDkuNTQtMy4wMS0xLjA2LTMuMDEtMS4wNi0uNDEtMS4wNC0uOTktMS4zMS0uOTktMS4zMS0uODEtLjU2LjA2LS41NC4wNi0uNTQuOS4wNiAxLjM3LjkyIDEuMzcuOTIuOCAxLjM3IDIuMDkuOTggMi42Ljc0LjA4LS41OC4zMS0uOTguNTctMS4yLTEuOTktLjIzLTQuMDgtLjk5LTQuMDgtNC40MiAwLS45OC4zNS0xLjc4LjkyLTIuNDEtLjA5LS4yMy0uNC0xLjE0LjA5LTIuMzcgMCAwIC43NS0uMjQgMi40Ni45Mi43MS0uMiAxLjQ4LS4zIDIuMjQtLjNzMS41My4xIDIuMjQuMzFjMS43MS0xLjE2IDIuNDYtLjkyIDIuNDYtLjkyLjQ5IDEuMjMuMTggMi4xNC4wOSAyLjM3LjU3LjYzLjkyIDEuNDMuOTIgMi40MSAwIDMuNDQtMi4xIDQuMTktNC4wOSA0LjQxLjMyLjI4LjYxLjgyLjYxIDEuNjZ2Mi40NmMwIC4yNC4xNi41MS42MS40M0MxOS40NCAyMS4zNCAyMiAxOC4wMyAyMiAxNGMwLTQuOTctNC4wMy05LTktOVoiIGZpbGw9IiNmZmYiLz48L3N2Zz4=',
|
|
4
|
+
codex: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAP1BMVEVHcEwAgPgAgPgAgPcAgPcAgPcAgPcAgPcAgPcAgPcAd/dzsvq+2/2Kvvtfpvru+P/Z6/77/v83lvmnzfwbjPhuMd/TAAAACnRSTlMAF0adyun/9k+LLZxzywAAAXZJREFUeAFkUlmWwyAMC03qZITxAtz/rPNiaLpEvwhJCC1vpMe6PYn2bT3Sckf6e9KF55pux/SDvy9K2uiG7YNx7BQAgDdjP677r/PMXOhN2tOXPkSN2byUkis+XV75vBFA7mqugpE0BKY+M0DNuQIoXgbjlFinv1Zk0wxUAppeEin66ebe4Q2o7J6pq4zG0nLEfTPRDhUUNSlu3fKQeAwH0V5PQlPV1kHFfcZcl20GPAnuuWe1DIjx7HPZw+EkEOJ1cDcBtCBCLBGhGVVlimTQ3FxQbHgEgaoLuoVx6FiG+CQ8h4RWUFYVdHYhy8iDsEfI6IEjvTlrEJgxQq7DQ8W0kbgKgiAu85mPQYiiPZo+LdRnUcesGsZAzxVozp2MBa+q5293bx2gs2kgK2hgvb4b4mpsWgggbS9C+hgMcmtFrRT2ck37e9IAajHjjJ9hz9FO0n201+wn7rP/H76MQzjrIZQwI2deTqz5m4OZhR0oycLMhCQNACpMHlEZrAKTAAAAAElFTkSuQmCC',
|
|
5
|
+
gemini: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEMklEQVR4Ab2XA5AkSRSGv5ddXb095t2tbdu2bW/wbNsXONuBs23bttY2p2e7Kt/1VNQZ3cMv4o9U46+XrxJCSIvLduTv0ezjPTGzfaGNCk5KWAEVyMVSrKFQSsJ6UUolWAp9j5i3j2hK7sG9uMl9Ke0NtQ8nuS+ZKn+IeAceSLj2qtZ3X7sHQACaXLC3434n/rQvNLWiSNk21CZR0ZQIFEUprJDalIKSgoo6QTuo1/PKiHr7A7nJoCQalnG/jPriB2ZSfSv3+zKh+ZO3fCfBk/t5n1uhaeTn54m9dw1m70Zqg/r1sjiySSvG5uXgeGUrdyTzuslhF5Sf70Ui5wR//tIZgFKbCHBxq/aMy42jas8xKmY2Cm7qyUGpbRS4fu1KrHEwyGyjmNbmwFZk30bqivXlCbZ6igptDYqD71HXJDCAcY0Aki5ssRwQoSaxxglkhPSUtxrJzvn3kGwzGsRQE6hUGIhiUJA0uScKfkkbdk+4DH/xA7jtJyHGqXYEfBPNLAKCIApqYU9hcxhzDvWXPEhp5xkYx62qgTACpI9AZNsPODtXYxQ8YLsY9uQ1oHDYyfRNGWnTfR7RaLzyBsTBiJKWyJZvyL97FrnPnU506w8kVCpMBNqXXUrzgUcxc8kD9O+1hHgsNzMDEiYhgJABanF/fIGCexeQ/8TxJDZ/zTYxbAuNHIgX0KXPCo5bfC9Tey8mN5aTJgIRbJgDlUMVd+Ub5N+3DH3kSHas/4RtSEphRNwc+vVcQq8WA9NEIBpEwAlzoEqYtR9QntL6+p2xvZYSa9qPGBAzDuVOFv+HBkkoGFGqTXLjl6x95gx+/PqJ33MjIZLRQuRQTTQaJ9F5JvHuC7DZRWxTHxdDAkmbhL4x4RRQeTSWS3nnuZR1m4+XVUBClBiWmFaUcMD6GUQgEk6BkjEaLyTZ5yj2L3yaRJ8j0HqF4fogbMewZct3fPHM6az6+vGM1gFHEDIi+xD8zkvw2s/ARuNgFBOatwjuuo+IfXgH+9e8V6l1wCETGg3CjrgCdaKIgCgYCxjFWf0O7se342z8rEq7oSMKmm4zipdgxP3jg9ZiVr+C89ntmG3fVWs3dADSToOCUVDfx6x6FueLO2HXL9XcDUMDkkkO2oPww0PI13cie9fX6IHEQSGdCf3lmUBVI815wICHONQ1ESdeYeKgEfRHiZdCTn3qitLcQ8nJLsEz7vdG4AEQpOdxgFDbiAhT+iwlmAJxHzT5yd1XirCKZmNh2GWQ04DaIj+vPvPGnEOXFsPwJbqyrLDgKgHocPreDgec7KctNFNRNLE1vJwSyIZlMMbf+gj7hd+VE15iK1QQ1kuMQ8N4YXCjzrfeSrUHxs85u8X3Qkir8zTPet7xvjGzFW3jG4kqYP9uICj/3h+OhX0xlGJseIUPhSZT7e+LsA/4ZVx9zHnFewB+BQL7BcuXk7EZAAAAAElFTkSuQmCC',
|
|
6
|
+
opencode: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAACshmLzAAABl0lEQVRYCe1XS2rDMBAd/70xBJoubJqVF83Sm+YooVcIPUDJTUpOUHIa71Ov0ht4Y/yJ6zEYHFnyZBKKNx4IkTSjNw/pzQhrT4tFDROaPmHuNvVMYPITMCkNVFVFhYz6DcMY9SsJ1HUNmqbB1+EAYRiOgqicSZLAx24HHZYsTkmgC46iCF7X627K+vc8j4wnNVAUBQmiCrhlL3kCIjilCerORTwWgTzP4X27hd/zGcRESOxltYLv4xFs2xbzKOcsAiimn9MJUFymeb21LEvIsqwVnDKbxHGNIgkQl8ymrDC5SADj0Mc1NgGjSW5Z1oAAliz6uMbaoes6vG027V3LNOD7PmAMx1gEENhxHHBdVypC9HGNTQCF2P36yWRrfb9qzDsvFcoD6zMBtgaw48nasWqduh0WAaz1IAjAalqtIZRbdbnA83LZPuFU0r6fRQC73+d+398/GP97H+AmGDAUFsgqwLZ7r92yl7yCOI4hTdO7OOCrSZlGfRnJFE+B9v3im9H34Zg8AQpABOTOSQ1wAbnxM4HJT+APfgFr0DSNhC0AAAAASUVORK5CYII=',
|
|
7
|
+
} as const
|
|
8
|
+
|
|
9
|
+
export type AiToolKey = 'claude' | 'gemini' | 'codex' | 'copilot' | 'opencode'
|
|
10
|
+
|
|
11
|
+
export const AI_TOOL_NAMES: Record<AiToolKey, string> = {
|
|
12
|
+
claude: 'Claude Code',
|
|
13
|
+
gemini: 'Gemini CLI',
|
|
14
|
+
codex: 'Codex CLI',
|
|
15
|
+
copilot: 'GitHub Copilot',
|
|
16
|
+
opencode: 'Opencode',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function AiToolIcon({ tool, size, showName, className, style }: {
|
|
20
|
+
tool: string
|
|
21
|
+
size?: number
|
|
22
|
+
showName?: boolean
|
|
23
|
+
className?: string
|
|
24
|
+
style?: React.CSSProperties
|
|
25
|
+
}) {
|
|
26
|
+
const src = AI_TOOL_LOGOS[tool as keyof typeof AI_TOOL_LOGOS]
|
|
27
|
+
if (!src) return null
|
|
28
|
+
const img = (
|
|
29
|
+
<img
|
|
30
|
+
src={src}
|
|
31
|
+
alt=""
|
|
32
|
+
{...(size ? { width: size, height: size } : {})}
|
|
33
|
+
className={className}
|
|
34
|
+
style={{ objectFit: 'contain', ...style }}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
if (!showName) return img
|
|
38
|
+
return (
|
|
39
|
+
<span style={{ display: 'inline-flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
|
|
40
|
+
{img}
|
|
41
|
+
<span className="text-[11px] text-neutral-400">{AI_TOOL_NAMES[tool as AiToolKey] ?? tool}</span>
|
|
42
|
+
</span>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type FormColor =
|
|
2
|
+
| 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow'
|
|
3
|
+
| 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky'
|
|
4
|
+
|
|
5
|
+
interface FormColorConfig {
|
|
6
|
+
/** Idle border class (e.g. 'border-blue-500/30') */
|
|
7
|
+
border: string
|
|
8
|
+
/** Hover classes (e.g. 'hover:bg-blue-500/20 hover:border-blue-500/40') */
|
|
9
|
+
hover: string
|
|
10
|
+
/** Focus border class (e.g. 'focus:border-blue-500') */
|
|
11
|
+
focus: string
|
|
12
|
+
/** Selected row background (e.g. 'bg-blue-600/20') */
|
|
13
|
+
selectedBg: string
|
|
14
|
+
/** Accent text for icons/checks (e.g. 'text-blue-400') */
|
|
15
|
+
accent: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const FORM_COLORS: Record<FormColor, FormColorConfig> = {
|
|
19
|
+
blue: { border: 'border-blue-500/30', hover: 'hover:bg-blue-500/20 hover:border-blue-500/40', focus: 'focus:border-blue-500', selectedBg: 'bg-blue-600/20', accent: 'text-blue-400' },
|
|
20
|
+
green: { border: 'border-green-500/30', hover: 'hover:bg-green-500/20 hover:border-green-500/40', focus: 'focus:border-green-500', selectedBg: 'bg-green-600/20', accent: 'text-green-400' },
|
|
21
|
+
red: { border: 'border-red-500/30', hover: 'hover:bg-red-500/20 hover:border-red-500/40', focus: 'focus:border-red-500', selectedBg: 'bg-red-600/20', accent: 'text-red-400' },
|
|
22
|
+
orange: { border: 'border-orange-500/30', hover: 'hover:bg-orange-500/20 hover:border-orange-500/40', focus: 'focus:border-orange-500', selectedBg: 'bg-orange-600/20', accent: 'text-orange-400' },
|
|
23
|
+
cyan: { border: 'border-cyan-500/30', hover: 'hover:bg-cyan-500/20 hover:border-cyan-500/40', focus: 'focus:border-cyan-500', selectedBg: 'bg-cyan-600/20', accent: 'text-cyan-400' },
|
|
24
|
+
yellow: { border: 'border-yellow-500/30', hover: 'hover:bg-yellow-500/20 hover:border-yellow-500/40', focus: 'focus:border-yellow-500', selectedBg: 'bg-yellow-600/20', accent: 'text-yellow-400' },
|
|
25
|
+
purple: { border: 'border-purple-500/30', hover: 'hover:bg-purple-500/20 hover:border-purple-500/40', focus: 'focus:border-purple-500', selectedBg: 'bg-purple-600/20', accent: 'text-purple-400' },
|
|
26
|
+
indigo: { border: 'border-indigo-500/30', hover: 'hover:bg-indigo-500/20 hover:border-indigo-500/40', focus: 'focus:border-indigo-500', selectedBg: 'bg-indigo-600/20', accent: 'text-indigo-400' },
|
|
27
|
+
emerald: { border: 'border-emerald-500/30', hover: 'hover:bg-emerald-500/20 hover:border-emerald-500/40', focus: 'focus:border-emerald-500', selectedBg: 'bg-emerald-600/20', accent: 'text-emerald-400' },
|
|
28
|
+
amber: { border: 'border-amber-500/30', hover: 'hover:bg-amber-500/20 hover:border-amber-500/40', focus: 'focus:border-amber-500', selectedBg: 'bg-amber-600/20', accent: 'text-amber-400' },
|
|
29
|
+
violet: { border: 'border-violet-500/30', hover: 'hover:bg-violet-500/20 hover:border-violet-500/40', focus: 'focus:border-violet-500', selectedBg: 'bg-violet-600/20', accent: 'text-violet-400' },
|
|
30
|
+
neutral: { border: 'border-neutral-500/30', hover: 'hover:bg-neutral-500/20 hover:border-neutral-500/40', focus: 'focus:border-neutral-500', selectedBg: 'bg-neutral-600/20', accent: 'text-neutral-400' },
|
|
31
|
+
sky: { border: 'border-sky-500/30', hover: 'hover:bg-sky-500/20 hover:border-sky-500/40', focus: 'focus:border-sky-500', selectedBg: 'bg-sky-600/20', accent: 'text-sky-400' },
|
|
32
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { FormColor } from './form-colors.ts'
|
|
2
|
+
|
|
3
|
+
/* ── Neutral scale types ──────────────────────────────────── */
|
|
4
|
+
|
|
5
|
+
export const SCALE_KEYS = ['black', '950', '900', '800', '700', '600', '500', '400', '300', '200'] as const
|
|
6
|
+
export type ScaleKey = (typeof SCALE_KEYS)[number]
|
|
7
|
+
|
|
8
|
+
/* ── Theme definitions ────────────────────────────────────── */
|
|
9
|
+
|
|
10
|
+
export type ThemeId = 'oled' | 'dark' | 'night' | 'soft' | 'bright' | 'paper' | 'muted'
|
|
11
|
+
|
|
12
|
+
export const DARK_THEMES: ThemeId[] = ['oled', 'dark', 'night', 'soft']
|
|
13
|
+
export const LIGHT_THEMES: ThemeId[] = ['bright', 'paper', 'muted']
|
|
14
|
+
|
|
15
|
+
export const BASE_THEMES: Record<ThemeId, { label: string; maxSat: number; lightness: Record<ScaleKey, number> }> = {
|
|
16
|
+
oled: { label: 'OLED', maxSat: 4, lightness: { black: 0, '950': 0, '900': 2, '800': 4, '700': 10, '600': 20, '500': 40, '400': 60, '300': 80, '200': 88 } },
|
|
17
|
+
dark: { label: 'Dark', maxSat: 8, lightness: { black: 0, '950': 1.5, '900': 4, '800': 7, '700': 13, '600': 22, '500': 41, '400': 61, '300': 81, '200': 88 } },
|
|
18
|
+
night: { label: 'Night', maxSat: 12, lightness: { black: 0, '950': 3, '900': 7, '800': 11, '700': 17, '600': 25, '500': 43, '400': 62, '300': 82, '200': 89 } },
|
|
19
|
+
soft: { label: 'Soft', maxSat: 15, lightness: { black: 0, '950': 5, '900': 10, '800': 15, '700': 22, '600': 30, '500': 46, '400': 64, '300': 83, '200': 90 } },
|
|
20
|
+
bright: { label: 'Bright', maxSat: 6, lightness: { black: 100, '950': 98, '900': 96, '800': 92, '700': 85, '600': 68, '500': 52, '400': 40, '300': 24, '200': 14 } },
|
|
21
|
+
paper: { label: 'Paper', maxSat: 10, lightness: { black: 98, '950': 96, '900': 93, '800': 88, '700': 82, '600': 65, '500': 50, '400': 38, '300': 22, '200': 12 } },
|
|
22
|
+
muted: { label: 'Muted', maxSat: 14, lightness: { black: 95, '950': 93, '900': 89, '800': 84, '700': 78, '600': 62, '500': 48, '400': 36, '300': 20, '200': 10 } },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* ── Accent definitions ───────────────────────────────────── */
|
|
26
|
+
|
|
27
|
+
export interface AccentDef {
|
|
28
|
+
id: FormColor
|
|
29
|
+
hue: number | null
|
|
30
|
+
dotColor: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const ACCENT_DEFS: AccentDef[] = [
|
|
34
|
+
{ id: 'blue', hue: 220, dotColor: '#3b82f6' },
|
|
35
|
+
{ id: 'violet', hue: 260, dotColor: '#8b5cf6' },
|
|
36
|
+
{ id: 'orange', hue: 25, dotColor: '#f97316' },
|
|
37
|
+
{ id: 'cyan', hue: 185, dotColor: '#06b6d4' },
|
|
38
|
+
{ id: 'emerald', hue: 155, dotColor: '#10b981' },
|
|
39
|
+
{ id: 'amber', hue: 38, dotColor: '#f59e0b' },
|
|
40
|
+
{ id: 'sky', hue: 200, dotColor: '#0ea5e9' },
|
|
41
|
+
{ id: 'indigo', hue: 235, dotColor: '#6366f1' },
|
|
42
|
+
{ id: 'purple', hue: 275, dotColor: '#a855f7' },
|
|
43
|
+
{ id: 'green', hue: 142, dotColor: '#22c55e' },
|
|
44
|
+
{ id: 'yellow', hue: 55, dotColor: '#eab308' },
|
|
45
|
+
{ id: 'red', hue: 0, dotColor: '#ef4444' },
|
|
46
|
+
{ id: 'neutral', hue: null, dotColor: '#737373' },
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
/* ── Color utilities ──────────────────────────────────────── */
|
|
50
|
+
|
|
51
|
+
export function satCurve(l: number): number {
|
|
52
|
+
return Math.min(l / 15, 1) * Math.max(0, 1 - l / 110)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function hslToHex(h: number, s: number, l: number): string {
|
|
56
|
+
const a = (s / 100) * Math.min(l / 100, 1 - l / 100)
|
|
57
|
+
const f = (n: number) => {
|
|
58
|
+
const k = (n + h / 30) % 12
|
|
59
|
+
const c = l / 100 - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
|
|
60
|
+
return Math.round(255 * Math.max(0, Math.min(1, c))).toString(16).padStart(2, '0')
|
|
61
|
+
}
|
|
62
|
+
return `#${f(0)}${f(8)}${f(4)}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function generateScale(theme: ThemeId, hue: number | null, maxSat: number): Record<ScaleKey, string> {
|
|
66
|
+
const lightness = BASE_THEMES[theme].lightness
|
|
67
|
+
const result = {} as Record<ScaleKey, string>
|
|
68
|
+
for (const key of SCALE_KEYS) {
|
|
69
|
+
const l = lightness[key]
|
|
70
|
+
if (hue === null || maxSat === 0) {
|
|
71
|
+
const g = Math.round(255 * l / 100).toString(16).padStart(2, '0')
|
|
72
|
+
result[key] = `#${g}${g}${g}`
|
|
73
|
+
} else {
|
|
74
|
+
result[key] = hslToHex(hue, maxSat * satCurve(l), l)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* ── Theme application helpers ────────────────────────────── */
|
|
81
|
+
|
|
82
|
+
export function isLightTheme(themeId: ThemeId): boolean {
|
|
83
|
+
return LIGHT_THEMES.includes(themeId)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function applyTheme(themeId: ThemeId, accentHue: number | null, root: HTMLElement = document.documentElement): void {
|
|
87
|
+
const scale = generateScale(themeId, accentHue, BASE_THEMES[themeId].maxSat)
|
|
88
|
+
const light = isLightTheme(themeId)
|
|
89
|
+
|
|
90
|
+
for (const key of SCALE_KEYS) {
|
|
91
|
+
const prop = key === 'black' ? '--color-black' : `--color-neutral-${key}`
|
|
92
|
+
root.style.setProperty(prop, scale[key])
|
|
93
|
+
}
|
|
94
|
+
root.style.setProperty('--color-white', light ? '#0a0a0a' : '#ffffff')
|
|
95
|
+
root.style.setProperty('--color-neutral-100', light ? '#1a1a1a' : '#f0f0f0')
|
|
96
|
+
root.style.setProperty('--color-neutral-50', light ? '#0f0f0f' : '#fafafa')
|
|
97
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type ToolrAppId = 'toolr' | 'configr' | 'reviewr' | 'vibr' | 'learnr' | 'planr' | 'seedr'
|
|
2
|
+
|
|
3
|
+
export const TOOLR_APPS: Record<ToolrAppId, { name: string; color: string }> = {
|
|
4
|
+
toolr: { name: 'Toolr', color: '#60a5fa' },
|
|
5
|
+
configr: { name: 'Configr', color: '#a78bfa' },
|
|
6
|
+
reviewr: { name: 'Reviewr', color: '#fb923c' },
|
|
7
|
+
learnr: { name: 'Learnr', color: '#facc15' },
|
|
8
|
+
seedr: { name: 'Seedr', color: '#2dd4bf' },
|
|
9
|
+
planr: { name: 'Planr', color: '#f472b6' },
|
|
10
|
+
vibr: { name: 'Vibr', color: '#f87171' },
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ToolrAppLogo({ app, color: colorOverride, size, className }: { app?: ToolrAppId; color?: string; size?: number; className?: string }) {
|
|
14
|
+
const color = colorOverride ?? (app ? TOOLR_APPS[app]?.color : undefined) ?? '#60a5fa'
|
|
15
|
+
return (
|
|
16
|
+
<svg
|
|
17
|
+
{...(size ? { width: size, height: size } : {})}
|
|
18
|
+
viewBox="0 0 32 32"
|
|
19
|
+
fill="none"
|
|
20
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
21
|
+
className={className}
|
|
22
|
+
>
|
|
23
|
+
<path d="M5 26V4H16C21.5 4 26 8 26 13C26 18 21.5 22 16 22H5" stroke={color} strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
24
|
+
<path d="M14 22L24 28" stroke={color} strokeWidth="3.5" strokeLinecap="round" />
|
|
25
|
+
<rect x="3" y="29" width="5.5" height="2.5" rx="0.5" fill={color} />
|
|
26
|
+
<rect x="10" y="29" width="5.5" height="2.5" rx="0.5" fill={color} opacity={0.7} />
|
|
27
|
+
<rect x="17" y="29" width="5.5" height="2.5" rx="0.5" fill={color} opacity={0.45} />
|
|
28
|
+
<rect x="24" y="29" width="5.5" height="2.5" rx="0.5" fill={color} opacity={0.25} />
|
|
29
|
+
</svg>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Tools Paths — Section barrel export
|
|
3
|
+
*
|
|
4
|
+
* This section provides a complete, reusable AI tool binary path configuration
|
|
5
|
+
* panel. It handles tool detection, path validation, enable/disable toggles,
|
|
6
|
+
* and per-tool refresh — all via an API adapter interface.
|
|
7
|
+
*
|
|
8
|
+
* File structure:
|
|
9
|
+
* - tools-paths-panel.tsx — Main panel component (drop-in usage)
|
|
10
|
+
* - use-tools-paths.ts — Detection state & actions hook (used by panel, also standalone)
|
|
11
|
+
* - types.ts — Shared types and API adapter interface
|
|
12
|
+
*
|
|
13
|
+
* Quick start for consuming apps:
|
|
14
|
+
* import { ToolsPathsPanel, type ToolsPathsApi, type AiToolConfig } from '@toolr/ui-design'
|
|
15
|
+
*
|
|
16
|
+
* const api: ToolsPathsApi = {
|
|
17
|
+
* detectAll: () => invoke('detect_all_tools'),
|
|
18
|
+
* detectTool: (id) => invoke('detect_tool', { toolId: id }),
|
|
19
|
+
* validatePath: (path) => invoke('validate_binary_path', { path }),
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* <ToolsPathsPanel
|
|
23
|
+
* api={api}
|
|
24
|
+
* tools={toolConfigs}
|
|
25
|
+
* onToolConfigChange={(id, partial) => updateTool(id, partial)}
|
|
26
|
+
* renderToolIcon={(id) => <AiToolIcon tool={id} size={20} />}
|
|
27
|
+
* />
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// Main panel component
|
|
31
|
+
export { ToolsPathsPanel, type ToolsPathsPanelProps } from './tools-paths-panel.tsx'
|
|
32
|
+
|
|
33
|
+
// Hook for custom UIs
|
|
34
|
+
export { useToolsPaths, type UseToolsPathsOptions, type UseToolsPathsReturn } from './use-tools-paths.ts'
|
|
35
|
+
|
|
36
|
+
// Types and API interface
|
|
37
|
+
export type { AiToolConfig, ToolDetectionResult, ToolsPathsApi } from './types.ts'
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolsPathsPanel — AI tool binary path configuration panel
|
|
3
|
+
*
|
|
4
|
+
* Part of: Sections > AI Tools Paths
|
|
5
|
+
*
|
|
6
|
+
* This is the main "drop in and it works" component. Provide an API adapter,
|
|
7
|
+
* tool configs, and a change callback — it handles the full detection and
|
|
8
|
+
* configuration UI.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* <ToolsPathsPanel
|
|
12
|
+
* api={toolsPathsApi}
|
|
13
|
+
* tools={toolConfigs}
|
|
14
|
+
* onToolConfigChange={(id, partial) => updateTool(id, partial)}
|
|
15
|
+
* />
|
|
16
|
+
*
|
|
17
|
+
* AI agent notes:
|
|
18
|
+
* - Replicates the configr Settings > AI Tools > Paths page layout
|
|
19
|
+
* - Each tool row shows: icon, name, status badge, toggle, path input, refresh
|
|
20
|
+
* - The component manages detection state internally via useToolsPaths hook
|
|
21
|
+
* - Tool configs (enabled, path, detected) are owned by the consumer
|
|
22
|
+
* - The renderToolIcon prop allows each app to provide its own tool icons
|
|
23
|
+
* - Uses ui-design components (Input, Toggle, IconButton, Tooltip)
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { ReactNode } from 'react'
|
|
27
|
+
import { Check, X, RefreshCw, Terminal } from 'lucide-react'
|
|
28
|
+
import { cn } from '../../lib/cn.ts'
|
|
29
|
+
import { Input } from '../../ui/input.tsx'
|
|
30
|
+
import { Toggle } from '../../ui/toggle.tsx'
|
|
31
|
+
import { IconButton } from '../../ui/icon-button.tsx'
|
|
32
|
+
import { useToolsPaths } from './use-tools-paths.ts'
|
|
33
|
+
import type { AiToolConfig, ToolsPathsApi } from './types.ts'
|
|
34
|
+
|
|
35
|
+
export interface ToolsPathsPanelProps {
|
|
36
|
+
/** API adapter for backend operations (detection, validation) */
|
|
37
|
+
api: ToolsPathsApi
|
|
38
|
+
/** Current tool configurations */
|
|
39
|
+
tools: AiToolConfig[]
|
|
40
|
+
/** Called when a tool's config changes. Consumer updates their state. */
|
|
41
|
+
onToolConfigChange: (toolId: string, config: Partial<AiToolConfig>) => void
|
|
42
|
+
/** Optional render function for tool icons. Receives tool ID, returns ReactNode. */
|
|
43
|
+
renderToolIcon?: (toolId: string) => ReactNode
|
|
44
|
+
className?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ToolsPathsPanel({
|
|
48
|
+
api,
|
|
49
|
+
tools,
|
|
50
|
+
onToolConfigChange,
|
|
51
|
+
renderToolIcon,
|
|
52
|
+
className,
|
|
53
|
+
}: ToolsPathsPanelProps) {
|
|
54
|
+
const {
|
|
55
|
+
isDetecting,
|
|
56
|
+
hasScanned,
|
|
57
|
+
refreshingTools,
|
|
58
|
+
scannedTools,
|
|
59
|
+
detectAll,
|
|
60
|
+
detectTool,
|
|
61
|
+
validateAndUpdatePath,
|
|
62
|
+
toggleEnabled,
|
|
63
|
+
} = useToolsPaths({ api, tools, onToolConfigChange })
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className={cn('w-full', className)}>
|
|
67
|
+
{/* Header */}
|
|
68
|
+
<div className="flex items-center justify-between mb-4">
|
|
69
|
+
<p className="text-xs text-[#6c7086] leading-relaxed max-w-xl">
|
|
70
|
+
Configure CLI paths for AI coding assistants. Specify executable locations
|
|
71
|
+
to enable integration with your development workflow.
|
|
72
|
+
</p>
|
|
73
|
+
<IconButton
|
|
74
|
+
icon={
|
|
75
|
+
isDetecting ? (
|
|
76
|
+
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
77
|
+
) : hasScanned ? (
|
|
78
|
+
<Check className="w-4 h-4" />
|
|
79
|
+
) : (
|
|
80
|
+
<RefreshCw className="w-4 h-4" />
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
onClick={detectAll}
|
|
84
|
+
disabled={isDetecting}
|
|
85
|
+
size="sm"
|
|
86
|
+
color={hasScanned ? 'green' : 'neutral'}
|
|
87
|
+
tooltip={{
|
|
88
|
+
title: hasScanned ? 'Scan complete' : 'Scan for tools',
|
|
89
|
+
description: hasScanned ? 'CLI tools detected' : 'Auto-detect CLI tool locations',
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Tool list */}
|
|
95
|
+
<div className="bg-[#181825] border border-[#313244] rounded-lg divide-y divide-[#313244]">
|
|
96
|
+
{tools.map((tool) => {
|
|
97
|
+
const isRefreshing = refreshingTools.has(tool.id)
|
|
98
|
+
const isEnabled = tool.enabled
|
|
99
|
+
const displayPath = tool.binaryPath || tool.detectedPath || ''
|
|
100
|
+
const hasBeenScanned = scannedTools.has(tool.id) && tool.detected
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
key={tool.id}
|
|
105
|
+
className={cn('p-4 space-y-2', !tool.detected && 'opacity-60')}
|
|
106
|
+
>
|
|
107
|
+
{/* Name + status + toggle row */}
|
|
108
|
+
<div className="flex items-center justify-between">
|
|
109
|
+
<div className="relative flex items-center gap-2">
|
|
110
|
+
{renderToolIcon?.(tool.id)}
|
|
111
|
+
<span
|
|
112
|
+
className={cn(
|
|
113
|
+
'font-medium',
|
|
114
|
+
tool.detected && !isEnabled ? 'text-[#6c7086]' : 'text-[#cdd6f4]',
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
{tool.name}
|
|
118
|
+
</span>
|
|
119
|
+
{tool.detected && !isEnabled && (
|
|
120
|
+
<div
|
|
121
|
+
className="absolute inset-0 flex items-center pointer-events-none"
|
|
122
|
+
aria-hidden="true"
|
|
123
|
+
>
|
|
124
|
+
<div className="w-full h-0.5 bg-[#f38ba8]/70 rounded-full" />
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
<div className="flex items-center gap-3">
|
|
129
|
+
{tool.detected ? (
|
|
130
|
+
<span className="flex items-center gap-1 text-xs text-green-400">
|
|
131
|
+
<Check className="w-3.5 h-3.5" />
|
|
132
|
+
Installed
|
|
133
|
+
</span>
|
|
134
|
+
) : (
|
|
135
|
+
<span className="flex items-center gap-1 text-xs text-[#6c7086]">
|
|
136
|
+
<X className="w-3.5 h-3.5" />
|
|
137
|
+
Not Found
|
|
138
|
+
</span>
|
|
139
|
+
)}
|
|
140
|
+
{tool.detected && (
|
|
141
|
+
<Toggle
|
|
142
|
+
checked={isEnabled}
|
|
143
|
+
onChange={() => toggleEnabled(tool.id)}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Path input row */}
|
|
150
|
+
<div className="space-y-1">
|
|
151
|
+
<div className="flex items-center gap-2">
|
|
152
|
+
<Terminal className="w-3 h-3 text-[#6c7086]" />
|
|
153
|
+
<div className="flex-1">
|
|
154
|
+
<Input
|
|
155
|
+
value={displayPath}
|
|
156
|
+
onChange={(newPath) => validateAndUpdatePath(tool.id, newPath)}
|
|
157
|
+
placeholder={`/usr/local/bin/${tool.id}`}
|
|
158
|
+
size="sm"
|
|
159
|
+
className={displayPath ? 'text-white' : 'text-[#a6adc8]'}
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
<IconButton
|
|
163
|
+
icon={
|
|
164
|
+
isRefreshing ? (
|
|
165
|
+
<RefreshCw className="w-3 h-3 text-[#cdd6f4] animate-spin" />
|
|
166
|
+
) : hasBeenScanned ? (
|
|
167
|
+
<Check className="w-3 h-3 text-green-400" />
|
|
168
|
+
) : (
|
|
169
|
+
<RefreshCw className="w-3 h-3 text-[#cdd6f4]" />
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
onClick={() => detectTool(tool.id)}
|
|
173
|
+
disabled={isRefreshing}
|
|
174
|
+
color={hasBeenScanned ? 'green' : 'neutral'}
|
|
175
|
+
size="sm"
|
|
176
|
+
tooltipPosition="bottom"
|
|
177
|
+
tooltip={{
|
|
178
|
+
title: hasBeenScanned ? 'Scan complete' : `Scan for ${tool.name}`,
|
|
179
|
+
description: hasBeenScanned ? 'Click to re-scan' : 'Check if CLI tool is installed',
|
|
180
|
+
}}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{/* Helper text */}
|
|
185
|
+
{isEnabled && !displayPath?.includes('/') && !tool.detected && (
|
|
186
|
+
<p className="text-xs text-red-400 pl-5">
|
|
187
|
+
Path required when enabled. Enter full path to binary.
|
|
188
|
+
</p>
|
|
189
|
+
)}
|
|
190
|
+
{tool.detected && !tool.binaryPath && (
|
|
191
|
+
<p className="text-xs text-[#6c7086] pl-5">Using auto-detected path</p>
|
|
192
|
+
)}
|
|
193
|
+
{!isEnabled && !displayPath?.includes('/') && !tool.detected && (
|
|
194
|
+
<p className="text-xs text-[#6c7086] pl-5">
|
|
195
|
+
Tool disabled. Enter path to enable.
|
|
196
|
+
</p>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Disabled warning */}
|
|
201
|
+
{tool.detected && !isEnabled && (
|
|
202
|
+
<p className="text-xs text-yellow-500">
|
|
203
|
+
Tool disabled — enable to use in your workflow
|
|
204
|
+
</p>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
)
|
|
208
|
+
})}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
)
|
|
212
|
+
}
|