@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.
- package/components/hooks/use-click-outside.ts +10 -3
- package/components/hooks/use-modal-behavior.ts +24 -0
- package/components/hooks/use-navigation-history.ts +7 -2
- package/components/hooks/use-resizable-sidebar.ts +38 -0
- package/components/lib/form-colors.ts +40 -0
- package/components/sections/captured-issues/captured-issues-panel.tsx +3 -3
- package/components/sections/captured-issues/use-captured-issues.ts +9 -3
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +4 -40
- package/components/sections/prompt-editor/index.ts +0 -7
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +4 -40
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +4 -36
- package/components/sections/snippets-editor/snippets-editor.tsx +6 -39
- package/components/settings/SettingsHeader.tsx +0 -1
- package/components/settings/SettingsTreeNav.tsx +9 -12
- package/components/ui/action-dialog.tsx +7 -51
- package/components/ui/badge.tsx +4 -20
- package/components/ui/breadcrumb.tsx +6 -66
- package/components/ui/checkbox.tsx +2 -16
- package/components/ui/collapsible-section.tsx +3 -41
- package/components/ui/confirm-badge.tsx +3 -20
- package/components/ui/cookie-consent.tsx +17 -1
- package/components/ui/debounce-border-overlay.tsx +31 -0
- package/components/ui/detail-section.tsx +2 -19
- package/components/ui/editor-placeholder-card.tsx +10 -9
- package/components/ui/execution-details-panel.tsx +2 -7
- package/components/ui/file-structure-section.tsx +3 -18
- package/components/ui/file-tree.tsx +2 -14
- package/components/ui/files-panel.tsx +3 -11
- package/components/ui/form-actions.tsx +6 -5
- package/components/ui/icon-button.tsx +2 -1
- package/components/ui/input.tsx +10 -26
- package/components/ui/label.tsx +3 -17
- package/components/ui/modal.tsx +3 -15
- package/components/ui/nav-card.tsx +2 -17
- package/components/ui/navigation-bar.tsx +8 -69
- package/components/ui/registry-browser.tsx +6 -20
- package/components/ui/registry-card.tsx +3 -7
- package/components/ui/resizable-textarea.tsx +13 -35
- package/components/ui/segmented-toggle.tsx +2 -1
- package/components/ui/select.tsx +2 -11
- package/components/ui/selection-grid.tsx +2 -50
- package/components/ui/setting-row.tsx +1 -3
- package/components/ui/settings-info-box.tsx +5 -22
- package/components/ui/status-card.tsx +2 -13
- package/components/ui/tab-bar.tsx +3 -29
- package/components/ui/toggle.tsx +3 -19
- package/components/ui/tooltip.tsx +6 -18
- package/dist/index.d.ts +60 -137
- package/dist/index.js +1426 -2334
- package/index.ts +8 -7
- package/package.json +1 -1
- 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
|
-
|
|
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
|
|
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,
|
|
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-
|
|
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-
|
|
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 =
|
|
54
|
-
|
|
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,
|
|
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
|
|
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:
|
|
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,
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
402
|
+
style={{ width: sidebarWidth, minWidth: 220, maxWidth: 400 }}
|
|
435
403
|
>
|
|
436
404
|
{/* Resize handle on left edge */}
|
|
437
405
|
<div
|
|
438
|
-
onPointerDown={
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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>
|
|
@@ -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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
|