@toolr/ui-design 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +63 -0
  2. package/components/content/info-panel-primitives.tsx +297 -0
  3. package/components/diagrams/diagram-utils.tsx +908 -0
  4. package/components/hooks/use-click-outside.ts +27 -0
  5. package/components/hooks/use-dropdown-max-height.ts +20 -0
  6. package/components/hooks/use-navigation-history.ts +94 -0
  7. package/components/lib/ai-tools.tsx +44 -0
  8. package/components/lib/cn.ts +6 -0
  9. package/components/lib/form-colors.ts +32 -0
  10. package/components/lib/theme-engine.ts +97 -0
  11. package/components/lib/toolr-brand.tsx +31 -0
  12. package/components/sections/ai-tools-paths/index.ts +37 -0
  13. package/components/sections/ai-tools-paths/tools-paths-panel.tsx +212 -0
  14. package/components/sections/ai-tools-paths/types.ts +111 -0
  15. package/components/sections/ai-tools-paths/use-tools-paths.ts +159 -0
  16. package/components/sections/captured-issues/captured-issues-panel.tsx +214 -0
  17. package/components/sections/captured-issues/index.ts +38 -0
  18. package/components/sections/captured-issues/types.ts +113 -0
  19. package/components/sections/captured-issues/use-captured-issues.ts +111 -0
  20. package/components/sections/golden-snapshots/file-diff-viewer.tsx +420 -0
  21. package/components/sections/golden-snapshots/golden-sync-panel.tsx +223 -0
  22. package/components/sections/golden-snapshots/index.ts +145 -0
  23. package/components/sections/golden-snapshots/snapshot-manager.tsx +200 -0
  24. package/components/sections/golden-snapshots/status-overview.tsx +305 -0
  25. package/components/sections/golden-snapshots/types.ts +288 -0
  26. package/components/sections/golden-snapshots/use-golden-sync.ts +477 -0
  27. package/components/sections/golden-snapshots/version-manager.tsx +186 -0
  28. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +210 -0
  29. package/components/sections/prompt-editor/index.ts +121 -0
  30. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +276 -0
  31. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +514 -0
  32. package/components/sections/prompt-editor/types.ts +101 -0
  33. package/components/sections/prompt-editor/use-prompt-editor.ts +131 -0
  34. package/components/sections/report-bug/error-logger.ts +392 -0
  35. package/components/sections/report-bug/index.ts +59 -0
  36. package/components/sections/report-bug/issue-reporter-api.ts +83 -0
  37. package/components/sections/report-bug/report-bug-form.tsx +282 -0
  38. package/components/sections/report-bug/screenshot-uploader.tsx +228 -0
  39. package/components/sections/report-bug/use-report-bug.ts +170 -0
  40. package/components/sections/snapshot-browser/index.ts +53 -0
  41. package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +147 -0
  42. package/components/sections/snapshot-browser/snapshot-tree.tsx +451 -0
  43. package/components/sections/snapshot-browser/types.ts +106 -0
  44. package/components/sections/snapshot-browser/use-snapshot-browser.ts +125 -0
  45. package/components/sections/snippets-editor/index.ts +31 -0
  46. package/components/sections/snippets-editor/snippets-editor.tsx +381 -0
  47. package/components/sections/snippets-editor/types.ts +48 -0
  48. package/components/sections/snippets-editor/use-snippets-editor.ts +217 -0
  49. package/components/ui/action-dialog.tsx +309 -0
  50. package/components/ui/ai-action-button.tsx +137 -0
  51. package/components/ui/ai-execution-action-buttons.tsx +106 -0
  52. package/components/ui/badge.tsx +67 -0
  53. package/components/ui/bottom-panel-header.tsx +240 -0
  54. package/components/ui/breadcrumb.tsx +168 -0
  55. package/components/ui/checkbox.tsx +102 -0
  56. package/components/ui/collapsible-section.tsx +100 -0
  57. package/components/ui/confirm-badge.tsx +71 -0
  58. package/components/ui/detail-section.tsx +67 -0
  59. package/components/ui/detail-view-wrapper.tsx +55 -0
  60. package/components/ui/editor-placeholder-card.tsx +197 -0
  61. package/components/ui/editor-toolbar.tsx +123 -0
  62. package/components/ui/execution-details-panel.tsx +93 -0
  63. package/components/ui/extension-list-card.tsx +105 -0
  64. package/components/ui/file-structure-section.tsx +373 -0
  65. package/components/ui/file-tree.tsx +171 -0
  66. package/components/ui/files-panel.tsx +251 -0
  67. package/components/ui/filter-dropdown.tsx +173 -0
  68. package/components/ui/form-actions.tsx +127 -0
  69. package/components/ui/frontmatter-form-header.tsx +80 -0
  70. package/components/ui/icon-button.tsx +388 -0
  71. package/components/ui/input.tsx +211 -0
  72. package/components/ui/label.tsx +159 -0
  73. package/components/ui/layout-tab-bar.tsx +289 -0
  74. package/components/ui/modal.tsx +194 -0
  75. package/components/ui/nav-card.tsx +81 -0
  76. package/components/ui/navigation-bar.tsx +285 -0
  77. package/components/ui/number-input.tsx +165 -0
  78. package/components/ui/registry-browser.tsx +261 -0
  79. package/components/ui/registry-card.tsx +710 -0
  80. package/components/ui/registry-detail.tsx +224 -0
  81. package/components/ui/resizable-textarea.tsx +290 -0
  82. package/components/ui/scope-badge.tsx +67 -0
  83. package/components/ui/segmented-toggle.tsx +133 -0
  84. package/components/ui/select.tsx +172 -0
  85. package/components/ui/selection-grid.tsx +313 -0
  86. package/components/ui/setting-row.tsx +97 -0
  87. package/components/ui/snapshot-card.tsx +107 -0
  88. package/components/ui/snippets-panel.tsx +161 -0
  89. package/components/ui/sort-dropdown.tsx +109 -0
  90. package/components/ui/status-card.tsx +96 -0
  91. package/components/ui/tab-bar.tsx +340 -0
  92. package/components/ui/toggle.tsx +142 -0
  93. package/components/ui/tooltip.tsx +326 -0
  94. package/dist/content.d.ts +110 -0
  95. package/dist/content.js +195 -0
  96. package/dist/diagrams.d.ts +371 -0
  97. package/dist/diagrams.js +702 -0
  98. package/dist/index.d.ts +2714 -0
  99. package/dist/index.js +11220 -0
  100. package/dist/preset.d.ts +24 -0
  101. package/dist/preset.js +17 -0
  102. package/dist/tokens/tokens/primitives.css +45 -0
  103. package/dist/tokens/tokens/semantic.css +46 -0
  104. package/dist/tokens/tokens/theme.css +11 -0
  105. package/dist/tokens/tokens/tokens.json +65 -0
  106. package/index.ts +123 -0
  107. package/package.json +63 -0
  108. package/tailwind-preset.ts +22 -0
  109. package/tokens/primitives.css +45 -0
  110. package/tokens/semantic.css +46 -0
  111. package/tokens/theme.css +11 -0
  112. package/tokens/tokens.json +65 -0
@@ -0,0 +1,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,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -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
+ }