@toolr/ui-design 0.1.6 → 0.1.7

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 (52) hide show
  1. package/components/hooks/use-click-outside.ts +10 -3
  2. package/components/hooks/use-modal-behavior.ts +24 -0
  3. package/components/hooks/use-navigation-history.ts +7 -2
  4. package/components/hooks/use-resizable-sidebar.ts +38 -0
  5. package/components/lib/form-colors.ts +40 -0
  6. package/components/sections/captured-issues/captured-issues-panel.tsx +3 -3
  7. package/components/sections/captured-issues/use-captured-issues.ts +9 -3
  8. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +4 -40
  9. package/components/sections/prompt-editor/index.ts +0 -7
  10. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +4 -40
  11. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +4 -36
  12. package/components/sections/snippets-editor/snippets-editor.tsx +6 -39
  13. package/components/settings/SettingsHeader.tsx +0 -1
  14. package/components/settings/SettingsTreeNav.tsx +9 -12
  15. package/components/ui/action-dialog.tsx +7 -51
  16. package/components/ui/badge.tsx +4 -20
  17. package/components/ui/breadcrumb.tsx +6 -66
  18. package/components/ui/checkbox.tsx +2 -16
  19. package/components/ui/collapsible-section.tsx +3 -41
  20. package/components/ui/confirm-badge.tsx +3 -20
  21. package/components/ui/cookie-consent.tsx +17 -1
  22. package/components/ui/debounce-border-overlay.tsx +31 -0
  23. package/components/ui/detail-section.tsx +2 -19
  24. package/components/ui/editor-placeholder-card.tsx +10 -9
  25. package/components/ui/execution-details-panel.tsx +2 -7
  26. package/components/ui/file-structure-section.tsx +3 -18
  27. package/components/ui/file-tree.tsx +2 -14
  28. package/components/ui/files-panel.tsx +3 -11
  29. package/components/ui/form-actions.tsx +6 -5
  30. package/components/ui/icon-button.tsx +2 -1
  31. package/components/ui/input.tsx +10 -26
  32. package/components/ui/label.tsx +3 -17
  33. package/components/ui/modal.tsx +3 -15
  34. package/components/ui/nav-card.tsx +2 -17
  35. package/components/ui/navigation-bar.tsx +8 -69
  36. package/components/ui/registry-browser.tsx +6 -20
  37. package/components/ui/registry-card.tsx +3 -7
  38. package/components/ui/resizable-textarea.tsx +13 -35
  39. package/components/ui/segmented-toggle.tsx +2 -1
  40. package/components/ui/select.tsx +2 -11
  41. package/components/ui/selection-grid.tsx +2 -50
  42. package/components/ui/setting-row.tsx +1 -3
  43. package/components/ui/settings-info-box.tsx +5 -22
  44. package/components/ui/status-card.tsx +2 -13
  45. package/components/ui/tab-bar.tsx +3 -29
  46. package/components/ui/toggle.tsx +3 -19
  47. package/components/ui/tooltip.tsx +6 -18
  48. package/dist/index.d.ts +60 -137
  49. package/dist/index.js +1426 -2334
  50. package/index.ts +8 -7
  51. package/package.json +1 -1
  52. package/components/sections/prompt-editor/use-prompt-editor.ts +0 -131
@@ -1,11 +1,13 @@
1
1
  import { useEffect, type RefObject } from 'react'
2
2
 
3
3
  /**
4
- * Close a menu/dropdown when clicking outside its ref element.
4
+ * Close a menu/dropdown when clicking outside its ref element(s).
5
+ * Accepts a single ref, an array of refs, or null.
6
+ * When given an array, closes only if the click is outside ALL refs.
5
7
  * If ref is null, closes on any mousedown event.
6
8
  */
7
9
  export function useClickOutside(
8
- ref: RefObject<HTMLElement | null> | null,
10
+ ref: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[] | null,
9
11
  isOpen: boolean,
10
12
  onClose: () => void
11
13
  ): void {
@@ -17,7 +19,12 @@ export function useClickOutside(
17
19
  onClose()
18
20
  return
19
21
  }
20
- if (ref.current && !ref.current.contains(event.target as Node)) {
22
+
23
+ const refs = Array.isArray(ref) ? ref : [ref]
24
+ const isOutsideAll = refs.every(
25
+ (r) => !r.current || !r.current.contains(event.target as Node)
26
+ )
27
+ if (isOutsideAll) {
21
28
  onClose()
22
29
  }
23
30
  }
@@ -0,0 +1,24 @@
1
+ import { useEffect } from 'react'
2
+
3
+ /**
4
+ * Shared modal behavior: Escape key to close + body overflow lock.
5
+ */
6
+ export function useModalBehavior(isOpen: boolean, onClose: () => void): void {
7
+ // Escape key handler
8
+ useEffect(() => {
9
+ if (!isOpen) return
10
+ const handler = (e: KeyboardEvent) => {
11
+ if (e.key === 'Escape') onClose()
12
+ }
13
+ document.addEventListener('keydown', handler)
14
+ return () => document.removeEventListener('keydown', handler)
15
+ }, [isOpen, onClose])
16
+
17
+ // Body overflow lock
18
+ useEffect(() => {
19
+ if (!isOpen) return
20
+ const prev = document.body.style.overflow
21
+ document.body.style.overflow = 'hidden'
22
+ return () => { document.body.style.overflow = prev }
23
+ }, [isOpen])
24
+ }
@@ -1,6 +1,6 @@
1
1
  /** Hook for managing back/forward navigation history with a breadcrumb segment stack. */
2
2
 
3
- import { useState, useCallback } from 'react'
3
+ import { useState, useCallback, useMemo } from 'react'
4
4
  import type { BreadcrumbSegment } from '../ui/breadcrumb.tsx'
5
5
 
6
6
  interface NavigationState {
@@ -81,6 +81,11 @@ export function useNavigationHistory(
81
81
  })
82
82
  }, [])
83
83
 
84
+ const history = useMemo(
85
+ () => [...state.backStack, ...(state.current ? [state.current] : [])],
86
+ [state.backStack, state.current],
87
+ )
88
+
84
89
  return {
85
90
  current: state.current,
86
91
  canGoBack: state.backStack.length > 0,
@@ -89,6 +94,6 @@ export function useNavigationHistory(
89
94
  goBack,
90
95
  goForward,
91
96
  goTo,
92
- history: [...state.backStack, ...(state.current ? [state.current] : [])],
97
+ history,
93
98
  }
94
99
  }
@@ -0,0 +1,38 @@
1
+ import { useState, useCallback, useRef } from 'react'
2
+
3
+ export interface UseResizableSidebarOptions {
4
+ min: number
5
+ max: number
6
+ defaultWidth: number
7
+ /** 'left' = drag right shrinks (sidebar on right), 'right' = drag right grows (sidebar on left) */
8
+ direction?: 'left' | 'right'
9
+ }
10
+
11
+ export function useResizableSidebar({ min, max, defaultWidth, direction = 'right' }: UseResizableSidebarOptions) {
12
+ const [width, setWidth] = useState(defaultWidth)
13
+ const widthRef = useRef(defaultWidth)
14
+
15
+ const onPointerDown = useCallback((e: React.PointerEvent) => {
16
+ e.preventDefault()
17
+ const el = e.currentTarget as HTMLElement
18
+ el.setPointerCapture(e.pointerId)
19
+ const startX = e.clientX
20
+ const startW = widthRef.current
21
+
22
+ const onMove = (ev: PointerEvent) => {
23
+ const delta = direction === 'left' ? startX - ev.clientX : ev.clientX - startX
24
+ const next = Math.max(min, Math.min(max, startW + delta))
25
+ widthRef.current = next
26
+ setWidth(next)
27
+ }
28
+ const onUp = () => {
29
+ el.removeEventListener('pointermove', onMove)
30
+ el.removeEventListener('pointerup', onUp)
31
+ el.releasePointerCapture(e.pointerId)
32
+ }
33
+ el.addEventListener('pointermove', onMove)
34
+ el.addEventListener('pointerup', onUp)
35
+ }, [min, max, direction])
36
+
37
+ return { width, onPointerDown }
38
+ }
@@ -16,6 +16,46 @@ interface FormColorConfig {
16
16
  accent: string
17
17
  }
18
18
 
19
+ export type AccentColor = FormColor
20
+
21
+ export const ACCENT_TEXT: Record<AccentColor, string> = {
22
+ blue: 'text-blue-400',
23
+ green: 'text-green-400',
24
+ red: 'text-red-400',
25
+ orange: 'text-orange-400',
26
+ cyan: 'text-cyan-400',
27
+ yellow: 'text-yellow-400',
28
+ purple: 'text-purple-400',
29
+ indigo: 'text-indigo-400',
30
+ emerald: 'text-emerald-400',
31
+ amber: 'text-amber-400',
32
+ violet: 'text-violet-400',
33
+ neutral: 'text-neutral-400',
34
+ sky: 'text-sky-400',
35
+ pink: 'text-pink-400',
36
+ teal: 'text-teal-400',
37
+ }
38
+
39
+ export const ACCENT_ICON = ACCENT_TEXT
40
+
41
+ export const ACCENT_NAV: Record<AccentColor, { bg: string; text: string }> = {
42
+ blue: { bg: 'bg-blue-500/10', text: 'text-blue-400' },
43
+ green: { bg: 'bg-green-500/10', text: 'text-green-400' },
44
+ red: { bg: 'bg-red-500/10', text: 'text-red-400' },
45
+ orange: { bg: 'bg-orange-500/10', text: 'text-orange-400' },
46
+ cyan: { bg: 'bg-cyan-500/10', text: 'text-cyan-400' },
47
+ yellow: { bg: 'bg-yellow-500/10', text: 'text-yellow-400' },
48
+ purple: { bg: 'bg-purple-500/10', text: 'text-purple-400' },
49
+ indigo: { bg: 'bg-indigo-500/10', text: 'text-indigo-400' },
50
+ emerald: { bg: 'bg-emerald-500/10', text: 'text-emerald-400' },
51
+ amber: { bg: 'bg-amber-500/10', text: 'text-amber-400' },
52
+ violet: { bg: 'bg-violet-500/10', text: 'text-violet-400' },
53
+ neutral: { bg: 'bg-neutral-500/10', text: 'text-neutral-400' },
54
+ sky: { bg: 'bg-sky-500/10', text: 'text-sky-400' },
55
+ pink: { bg: 'bg-pink-500/10', text: 'text-pink-400' },
56
+ teal: { bg: 'bg-teal-500/10', text: 'text-teal-400' },
57
+ }
58
+
19
59
  export const FORM_COLORS: Record<FormColor, FormColorConfig> = {
20
60
  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' },
21
61
  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' },
@@ -23,7 +23,7 @@
23
23
  * - Dark theme with Catppuccin-like colors matching configr
24
24
  */
25
25
 
26
- import { AlertTriangle, Check, X, Send, ChevronDown, Shield, Loader2 } from 'lucide-react'
26
+ import { AlertTriangle, Check, X, Send, ChevronDown, Loader2 } from 'lucide-react'
27
27
  import { cn } from '../../lib/cn.ts'
28
28
  import { Input } from '../../ui/input.tsx'
29
29
  import { ResizableTextarea } from '../../ui/resizable-textarea.tsx'
@@ -119,7 +119,7 @@ export function CapturedIssuesPanel({
119
119
  onChange={(e) => setDescription(e.target.value)}
120
120
  placeholder="What were you doing when this happened?"
121
121
  rows={3}
122
- className="w-full px-3 py-1.5 bg-neutral-750 border border-neutral-600 rounded-lg text-neutral-300 placeholder-neutral-500 focus:outline-none focus:border-blue-500 transition-colors resize-none text-md"
122
+ className="w-full px-3 py-1.5 bg-neutral-800 border border-neutral-600 rounded-lg text-neutral-300 placeholder-neutral-500 focus:outline-none focus:border-blue-500 transition-colors resize-none text-md"
123
123
  />
124
124
  <Input
125
125
  type="text"
@@ -174,7 +174,7 @@ export function CapturedIssuesPanel({
174
174
  {submittedErrors.length > 0 && (
175
175
  <div className="bg-neutral-900 border border-neutral-700 rounded-lg overflow-hidden">
176
176
  <details className="group">
177
- <summary className="flex items-center justify-between p-4 cursor-pointer hover:bg-neutral-850 transition-colors">
177
+ <summary className="flex items-center justify-between p-4 cursor-pointer hover:bg-neutral-800 transition-colors">
178
178
  <div className="flex items-center gap-2">
179
179
  <Check className="w-4 h-4 text-green-400" />
180
180
  <span className="text-md text-neutral-300">Previously Reported</span>
@@ -14,7 +14,7 @@
14
14
  * - Previously reported errors are loaded once on mount
15
15
  */
16
16
 
17
- import { useState, useCallback, useEffect } from 'react'
17
+ import { useState, useCallback, useEffect, useMemo } from 'react'
18
18
  import type { CapturedError, CapturedIssuesApi, SubmittedError } from './types.ts'
19
19
 
20
20
  export interface UseCapturedIssuesOptions {
@@ -50,8 +50,14 @@ export function useCapturedIssues(options: UseCapturedIssuesOptions): UseCapture
50
50
  const [submitError, setSubmitError] = useState<string | null>(null)
51
51
  const [submittedErrors, setSubmittedErrors] = useState<SubmittedError[]>([])
52
52
 
53
- const errorCount = errors.filter((e) => e.level === 'error').length
54
- const warnCount = errors.filter((e) => e.level === 'warning').length
53
+ const { errorCount, warnCount } = useMemo(() => {
54
+ let err = 0, warn = 0
55
+ for (const e of errors) {
56
+ if (e.level === 'error') err++
57
+ else if (e.level === 'warning') warn++
58
+ }
59
+ return { errorCount: err, warnCount: warn }
60
+ }, [errors])
55
61
 
56
62
  useEffect(() => {
57
63
  api.getSubmittedErrors().then(setSubmittedErrors).catch(() => {})
@@ -19,19 +19,12 @@
19
19
  * />
20
20
  */
21
21
 
22
- import { useState, useRef, useCallback } from 'react'
22
+ import { useState, useCallback } from 'react'
23
+ import { useResizableSidebar } from '../../hooks/use-resizable-sidebar.ts'
23
24
  import { FileCode, GripVertical, Crosshair } from 'lucide-react'
24
25
  import { TabbedPromptEditor } from './tabbed-prompt-editor.tsx'
25
26
  import type { ToolTab, PromptPlaceholder, FileTypeOption } from './types.ts'
26
27
 
27
- // ---------------------------------------------------------------------------
28
- // Constants
29
- // ---------------------------------------------------------------------------
30
-
31
- const MIN_SIDEBAR_WIDTH = 160
32
- const MAX_SIDEBAR_WIDTH = 320
33
- const DEFAULT_SIDEBAR_WIDTH = 200
34
-
35
28
  // ---------------------------------------------------------------------------
36
29
  // Props
37
30
  // ---------------------------------------------------------------------------
@@ -81,8 +74,7 @@ export function FileTypeTabbedPromptEditor({
81
74
  className = '',
82
75
  }: FileTypeTabbedPromptEditorProps) {
83
76
  const [selectedFileType, setSelectedFileType] = useState(fileTypes[0]?.id ?? '')
84
- const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH)
85
- const isDraggingRef = useRef(false)
77
+ const { width: sidebarWidth, onPointerDown: handleSidebarPointerDown } = useResizableSidebar({ min: 160, max: 320, defaultWidth: 200, direction: 'right' })
86
78
 
87
79
  // Derive prompts for current file type
88
80
  const currentPrompts = prompts[selectedFileType] ?? Object.fromEntries(tools.map((t) => [t.id, '']))
@@ -105,40 +97,12 @@ export function FileTypeTabbedPromptEditor({
105
97
  ? (tool: string, content: string) => onSave(selectedFileType, tool, content)
106
98
  : undefined
107
99
 
108
- // Sidebar resize via pointer events
109
- const handleSidebarPointerDown = useCallback((e: React.PointerEvent) => {
110
- e.preventDefault()
111
- e.stopPropagation()
112
- isDraggingRef.current = true
113
- const target = e.currentTarget as HTMLElement
114
- target.setPointerCapture(e.pointerId)
115
-
116
- const startX = e.clientX
117
- const startWidth = sidebarWidth
118
-
119
- const handlePointerMove = (moveEvent: PointerEvent) => {
120
- if (!isDraggingRef.current) return
121
- const deltaX = moveEvent.clientX - startX
122
- const newWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, startWidth + deltaX))
123
- setSidebarWidth(newWidth)
124
- }
125
-
126
- const handlePointerUp = () => {
127
- isDraggingRef.current = false
128
- target.removeEventListener('pointermove', handlePointerMove)
129
- target.removeEventListener('pointerup', handlePointerUp)
130
- }
131
-
132
- target.addEventListener('pointermove', handlePointerMove)
133
- target.addEventListener('pointerup', handlePointerUp)
134
- }, [sidebarWidth])
135
-
136
100
  return (
137
101
  <div className={`flex w-full max-w-full bg-neutral-900 border border-neutral-700 rounded-lg overflow-hidden ${className}`}>
138
102
  {/* Left Sidebar — File Type Selector */}
139
103
  <div
140
104
  className="relative shrink-0 bg-neutral-950 overflow-hidden flex flex-col"
141
- style={{ width: sidebarWidth, minWidth: MIN_SIDEBAR_WIDTH, maxWidth: MAX_SIDEBAR_WIDTH }}
105
+ style={{ width: sidebarWidth, minWidth: 160, maxWidth: 320 }}
142
106
  >
143
107
  {/* Header */}
144
108
  <div className="h-[52px] px-3 flex items-center border-b border-neutral-700 shrink-0">
@@ -97,13 +97,6 @@ export type {
97
97
  ScenarioOption,
98
98
  } from './types.ts'
99
99
 
100
- // Hook
101
- export {
102
- usePromptEditor,
103
- type UsePromptEditorOptions,
104
- type UsePromptEditorReturn,
105
- } from './use-prompt-editor.ts'
106
-
107
100
  // Components
108
101
  export {
109
102
  TabbedPromptEditor,
@@ -19,19 +19,12 @@
19
19
  * />
20
20
  */
21
21
 
22
- import { useState, useRef, useCallback, useMemo } from 'react'
22
+ import { useState, useCallback, useMemo } from 'react'
23
+ import { useResizableSidebar } from '../../hooks/use-resizable-sidebar.ts'
23
24
  import { ChevronDown, ChevronRight, GripVertical, Crosshair } from 'lucide-react'
24
25
  import { TabbedPromptEditor } from './tabbed-prompt-editor.tsx'
25
26
  import type { ToolTab, PromptPlaceholder, ScenarioOption } from './types.ts'
26
27
 
27
- // ---------------------------------------------------------------------------
28
- // Constants
29
- // ---------------------------------------------------------------------------
30
-
31
- const MIN_SIDEBAR_WIDTH = 180
32
- const MAX_SIDEBAR_WIDTH = 360
33
- const DEFAULT_SIDEBAR_WIDTH = 220
34
-
35
28
  // ---------------------------------------------------------------------------
36
29
  // Props
37
30
  // ---------------------------------------------------------------------------
@@ -82,8 +75,7 @@ export function SimulatorPromptEditor({
82
75
  const [expandedScenarios, setExpandedScenarios] = useState<Set<string>>(
83
76
  new Set([defaultScenarioId]),
84
77
  )
85
- const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH)
86
- const isDraggingRef = useRef(false)
78
+ const { width: sidebarWidth, onPointerDown: handleSidebarPointerDown } = useResizableSidebar({ min: 180, max: 360, defaultWidth: 220, direction: 'right' })
87
79
 
88
80
  // Ensure selected scenario is always expanded
89
81
  const effectiveExpanded = useMemo(() => {
@@ -138,40 +130,12 @@ export function SimulatorPromptEditor({
138
130
  }
139
131
  }
140
132
 
141
- // Sidebar resize via pointer events
142
- const handleSidebarPointerDown = useCallback((e: React.PointerEvent) => {
143
- e.preventDefault()
144
- e.stopPropagation()
145
- isDraggingRef.current = true
146
- const target = e.currentTarget as HTMLElement
147
- target.setPointerCapture(e.pointerId)
148
-
149
- const startX = e.clientX
150
- const startWidth = sidebarWidth
151
-
152
- const handlePointerMove = (moveEvent: PointerEvent) => {
153
- if (!isDraggingRef.current) return
154
- const deltaX = moveEvent.clientX - startX
155
- const newWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, startWidth + deltaX))
156
- setSidebarWidth(newWidth)
157
- }
158
-
159
- const handlePointerUp = () => {
160
- isDraggingRef.current = false
161
- target.removeEventListener('pointermove', handlePointerMove)
162
- target.removeEventListener('pointerup', handlePointerUp)
163
- }
164
-
165
- target.addEventListener('pointermove', handlePointerMove)
166
- target.addEventListener('pointerup', handlePointerUp)
167
- }, [sidebarWidth])
168
-
169
133
  return (
170
134
  <div className={`flex w-full max-w-full bg-neutral-900 border border-neutral-700 rounded-lg overflow-hidden ${className}`}>
171
135
  {/* Left Sidebar — Tree Selector */}
172
136
  <div
173
137
  className="relative shrink-0 bg-neutral-950 overflow-hidden flex flex-col"
174
- style={{ width: sidebarWidth, minWidth: MIN_SIDEBAR_WIDTH, maxWidth: MAX_SIDEBAR_WIDTH }}
138
+ style={{ width: sidebarWidth, minWidth: 180, maxWidth: 360 }}
175
139
  >
176
140
  {/* Header */}
177
141
  <div className="h-[52px] px-3 flex items-center border-b border-neutral-700 shrink-0">
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  import { useState, useRef, useCallback, useEffect, useMemo } from 'react'
23
+ import { useResizableSidebar } from '../../hooks/use-resizable-sidebar.ts'
23
24
  import Editor, { type Monaco } from '@monaco-editor/react'
24
25
  import type { editor, languages } from 'monaco-editor'
25
26
  import { Variable, Info, Search, X, AlertTriangle } from 'lucide-react'
@@ -37,9 +38,6 @@ import { AiToolIcon } from '../../lib/ai-tools.tsx'
37
38
  const THEME_NAME = 'prompt-editor-dark'
38
39
  let themeRegistered = false
39
40
 
40
- const MIN_SIDEBAR_WIDTH = 220
41
- const MAX_SIDEBAR_WIDTH = 400
42
- const DEFAULT_SIDEBAR_WIDTH = 280
43
41
  const DEFAULT_EDITOR_HEIGHT = 400
44
42
 
45
43
  // ---------------------------------------------------------------------------
@@ -85,12 +83,11 @@ export function TabbedPromptEditor({
85
83
  className = '',
86
84
  }: TabbedPromptEditorProps) {
87
85
  const [activeTab, setActiveTab] = useState(tools[0]?.id ?? '')
88
- const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH)
86
+ const { width: sidebarWidth, onPointerDown: handleSidebarPointerDown } = useResizableSidebar({ min: 220, max: 400, defaultWidth: 280, direction: 'left' })
89
87
  const [variableSearch, setVariableSearch] = useState('')
90
88
  const [localContent, setLocalContent] = useState<Record<string, string>>(prompts)
91
89
  const [isDirty, setIsDirty] = useState(false)
92
90
 
93
- const isDraggingSidebarRef = useRef(false)
94
91
  const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
95
92
  const monacoRef = useRef<Monaco | null>(null)
96
93
  const decorationsRef = useRef<string[]>([])
@@ -236,35 +233,6 @@ export function TabbedPromptEditor({
236
233
  }
237
234
  }, [variables, registerCompletionProvider])
238
235
 
239
- // --- Sidebar resize ---
240
-
241
- const handleSidebarMouseDown = useCallback((e: React.PointerEvent) => {
242
- e.preventDefault()
243
- e.stopPropagation()
244
- isDraggingSidebarRef.current = true
245
- const target = e.currentTarget as HTMLElement
246
- target.setPointerCapture(e.pointerId)
247
-
248
- const startX = e.clientX
249
- const startWidth = sidebarWidth
250
-
251
- const handlePointerMove = (moveEvent: PointerEvent) => {
252
- if (!isDraggingSidebarRef.current) return
253
- const deltaX = startX - moveEvent.clientX
254
- const newWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, startWidth + deltaX))
255
- setSidebarWidth(newWidth)
256
- }
257
-
258
- const handlePointerUp = () => {
259
- isDraggingSidebarRef.current = false
260
- target.removeEventListener('pointermove', handlePointerMove)
261
- target.removeEventListener('pointerup', handlePointerUp)
262
- }
263
-
264
- target.addEventListener('pointermove', handlePointerMove)
265
- target.addEventListener('pointerup', handlePointerUp)
266
- }, [sidebarWidth])
267
-
268
236
  // --- Content change ---
269
237
 
270
238
  const handleEditorChange = useCallback(
@@ -431,11 +399,11 @@ export function TabbedPromptEditor({
431
399
  {hasVariables && (
432
400
  <div
433
401
  className="relative shrink-0 bg-neutral-950 overflow-hidden flex flex-col border-l border-neutral-700"
434
- style={{ width: sidebarWidth, minWidth: MIN_SIDEBAR_WIDTH, maxWidth: MAX_SIDEBAR_WIDTH }}
402
+ style={{ width: sidebarWidth, minWidth: 220, maxWidth: 400 }}
435
403
  >
436
404
  {/* Resize handle on left edge */}
437
405
  <div
438
- onPointerDown={handleSidebarMouseDown}
406
+ onPointerDown={handleSidebarPointerDown}
439
407
  className="absolute left-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-violet-400/30 transition-colors z-10"
440
408
  />
441
409
 
@@ -22,8 +22,9 @@
22
22
  * - Dark theme styling matches configr's Catppuccin-inspired palette
23
23
  */
24
24
 
25
- import { useCallback, useRef, useState } from 'react'
25
+ import { useResizableSidebar } from '../../hooks/use-resizable-sidebar.ts'
26
26
  import { Plus, X, Braces, Trash2, RotateCcw, Save } from 'lucide-react'
27
+ import { ACCENT_TEXT, type AccentColor } from '../../lib/form-colors.ts'
27
28
  import { cn } from '../../lib/cn.ts'
28
29
  import { Input } from '../../ui/input.tsx'
29
30
  import { ResizableTextarea } from '../../ui/resizable-textarea.tsx'
@@ -55,18 +56,6 @@ const ACCENT_DIVIDER_HOVER: Record<string, string> = {
55
56
  violet: 'hover:bg-violet-500/30',
56
57
  }
57
58
 
58
- const ACCENT_TEXT: Record<string, string> = {
59
- blue: 'text-blue-400',
60
- purple: 'text-purple-400',
61
- orange: 'text-orange-400',
62
- green: 'text-green-400',
63
- pink: 'text-pink-400',
64
- amber: 'text-amber-400',
65
- emerald: 'text-emerald-400',
66
- teal: 'text-teal-400',
67
- sky: 'text-sky-400',
68
- violet: 'text-violet-400',
69
- }
70
59
 
71
60
  const ACCENT_BORDER: Record<string, string> = {
72
61
  blue: 'border-l-blue-400',
@@ -94,9 +83,6 @@ const ACCENT_BUTTON: Record<string, string> = {
94
83
  violet: 'bg-violet-600 hover:bg-violet-500',
95
84
  }
96
85
 
97
- const MIN_SIDEBAR = 200
98
- const MAX_SIDEBAR = 350
99
- const DEFAULT_SIDEBAR = 260
100
86
 
101
87
  export function SnippetsEditor({
102
88
  api,
@@ -125,26 +111,7 @@ export function SnippetsEditor({
125
111
  isSaving,
126
112
  } = useSnippetsEditor({ api, snippets })
127
113
 
128
- // Resizable sidebar
129
- const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR)
130
- const dragRef = useRef<{ startX: number; startW: number } | null>(null)
131
-
132
- const onDividerMouseDown = useCallback((e: React.MouseEvent) => {
133
- e.preventDefault()
134
- dragRef.current = { startX: e.clientX, startW: sidebarWidth }
135
- const onMove = (ev: MouseEvent) => {
136
- if (!dragRef.current) return
137
- const newW = Math.min(MAX_SIDEBAR, Math.max(MIN_SIDEBAR, dragRef.current.startW + ev.clientX - dragRef.current.startX))
138
- setSidebarWidth(newW)
139
- }
140
- const onUp = () => {
141
- dragRef.current = null
142
- document.removeEventListener('mousemove', onMove)
143
- document.removeEventListener('mouseup', onUp)
144
- }
145
- document.addEventListener('mousemove', onMove)
146
- document.addEventListener('mouseup', onUp)
147
- }, [sidebarWidth])
114
+ const { width: sidebarWidth, onPointerDown: onDividerPointerDown } = useResizableSidebar({ min: 200, max: 350, defaultWidth: 260, direction: 'right' })
148
115
 
149
116
  const hasSelection = isEditing || isAdding
150
117
  const nameHasError = formError !== null && (
@@ -168,7 +135,7 @@ export function SnippetsEditor({
168
135
  {/* Body: two columns */}
169
136
  <div className="flex flex-1 min-h-[400px]">
170
137
  {/* Left: Snippet list */}
171
- <div className="flex flex-col border-r border-neutral-700" style={{ width: sidebarWidth, minWidth: MIN_SIDEBAR }}>
138
+ <div className="flex flex-col border-r border-neutral-700" style={{ width: sidebarWidth, minWidth: 200 }}>
172
139
  {/* Search + Add */}
173
140
  <div className="flex items-center gap-1.5 p-2 border-b border-neutral-700">
174
141
  <div className="flex-1">
@@ -219,7 +186,7 @@ export function SnippetsEditor({
219
186
  {/* Resizable divider */}
220
187
  <div
221
188
  className={`w-1 cursor-col-resize bg-transparent ${ACCENT_DIVIDER_HOVER[accentColor] ?? ACCENT_DIVIDER_HOVER.blue} transition-colors flex-shrink-0`}
222
- onMouseDown={onDividerMouseDown}
189
+ onPointerDown={onDividerPointerDown}
223
190
  />
224
191
 
225
192
  {/* Right: Editor */}
@@ -245,7 +212,7 @@ export function SnippetsEditor({
245
212
  <p className="text-md text-neutral-500 mb-2">Select a snippet to edit</p>
246
213
  <p className="text-sm text-neutral-600 leading-relaxed">
247
214
  Choose a snippet from the list, or click{' '}
248
- <span className={ACCENT_TEXT[accentColor] ?? ACCENT_TEXT.blue}>+</span> to create a new one.
215
+ <span className={ACCENT_TEXT[accentColor as AccentColor] ?? ACCENT_TEXT.blue}>+</span> to create a new one.
249
216
  Reference snippets in prompts with{' '}
250
217
  <span className="font-mono text-purple-400">{'{{SNIPPET_NAME}}'}</span> syntax.
251
218
  </p>
@@ -10,7 +10,6 @@ export interface SettingsHeaderProps {
10
10
  title: string
11
11
  description: string
12
12
  }
13
- confirmReset?: boolean
14
13
  action?: ReactNode
15
14
  variant?: 'default' | 'info' | 'warning' | 'danger'
16
15
  }
@@ -162,19 +162,16 @@ export function SettingsTreeNav({ tree, selectedPath, onSelectPath, accentColor
162
162
  useEffect(() => {
163
163
  if (selectedPath) {
164
164
  const parents = getParentPaths(selectedPath)
165
- const newExpanded = new Set(expandedPaths)
166
- let changed = false
167
-
168
- for (const p of parents) {
169
- if (!newExpanded.has(p)) {
170
- newExpanded.add(p)
171
- changed = true
165
+ setExpandedPaths((prev) => {
166
+ let changed = false
167
+ for (const p of parents) {
168
+ if (!prev.has(p)) { changed = true; break }
172
169
  }
173
- }
174
-
175
- if (changed) {
176
- setExpandedPaths(newExpanded)
177
- }
170
+ if (!changed) return prev
171
+ const next = new Set(prev)
172
+ for (const p of parents) next.add(p)
173
+ return next
174
+ })
178
175
  }
179
176
  }, [selectedPath])
180
177