@toolr/ui-design 0.1.5 → 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/agent-rules.json +91 -0
- package/ai-manifest.json +190 -0
- package/components/content/info-panel-primitives.tsx +14 -14
- 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/ai-tools.tsx +1 -1
- package/components/lib/form-colors.ts +40 -0
- package/components/sections/ai-tools-paths/tools-paths-panel.tsx +7 -7
- package/components/sections/captured-issues/captured-issues-panel.tsx +13 -13
- package/components/sections/captured-issues/use-captured-issues.ts +9 -3
- package/components/sections/golden-snapshots/file-diff-viewer.tsx +13 -13
- package/components/sections/golden-snapshots/golden-sync-panel.tsx +5 -5
- package/components/sections/golden-snapshots/snapshot-manager.tsx +11 -11
- package/components/sections/golden-snapshots/status-overview.tsx +20 -20
- package/components/sections/golden-snapshots/version-manager.tsx +8 -8
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +8 -44
- package/components/sections/prompt-editor/index.ts +0 -7
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +9 -45
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +11 -43
- package/components/sections/report-bug/report-bug-form.tsx +14 -14
- package/components/sections/report-bug/screenshot-uploader.tsx +6 -6
- package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +3 -3
- package/components/sections/snapshot-browser/snapshot-tree.tsx +8 -8
- package/components/sections/snippets-editor/snippets-editor.tsx +74 -48
- package/components/settings/SettingsHeader.tsx +1 -2
- package/components/settings/SettingsTreeNav.tsx +31 -16
- package/components/ui/action-dialog.tsx +12 -56
- package/components/ui/badge.tsx +8 -24
- package/components/ui/bottom-panel-header.tsx +4 -4
- package/components/ui/breadcrumb.tsx +8 -68
- package/components/ui/checkbox.tsx +2 -16
- package/components/ui/collapsible-section.tsx +4 -42
- package/components/ui/confirm-badge.tsx +3 -20
- package/components/ui/cookie-consent.tsx +21 -5
- package/components/ui/debounce-border-overlay.tsx +31 -0
- package/components/ui/detail-section.tsx +5 -22
- package/components/ui/editor-placeholder-card.tsx +17 -16
- package/components/ui/editor-toolbar.tsx +12 -0
- package/components/ui/execution-details-panel.tsx +8 -13
- package/components/ui/extension-list-card.tsx +3 -3
- package/components/ui/file-structure-section.tsx +20 -35
- package/components/ui/file-tree.tsx +4 -14
- package/components/ui/files-panel.tsx +28 -18
- package/components/ui/filter-dropdown.tsx +5 -5
- package/components/ui/form-actions.tsx +7 -6
- package/components/ui/frontmatter-form-header.tsx +4 -4
- package/components/ui/icon-button.tsx +3 -2
- package/components/ui/input.tsx +15 -31
- package/components/ui/label.tsx +7 -21
- package/components/ui/layout-tab-bar.tsx +4 -4
- package/components/ui/modal.tsx +5 -17
- package/components/ui/nav-card.tsx +5 -20
- package/components/ui/navigation-bar.tsx +13 -74
- package/components/ui/number-input.tsx +4 -4
- package/components/ui/registry-browser.tsx +10 -24
- package/components/ui/registry-card.tsx +16 -20
- package/components/ui/registry-detail.tsx +6 -6
- package/components/ui/resizable-textarea.tsx +13 -35
- package/components/ui/segmented-toggle.tsx +6 -5
- package/components/ui/select.tsx +7 -16
- package/components/ui/selection-grid.tsx +6 -54
- package/components/ui/setting-row.tsx +2 -4
- package/components/ui/settings-card.tsx +3 -3
- package/components/ui/settings-info-box.tsx +6 -23
- package/components/ui/settings-section-title.tsx +1 -1
- package/components/ui/snapshot-card.tsx +7 -7
- package/components/ui/snippets-panel.tsx +10 -10
- package/components/ui/sort-dropdown.tsx +2 -2
- package/components/ui/status-card.tsx +6 -17
- package/components/ui/tab-bar.tsx +5 -31
- package/components/ui/toggle.tsx +3 -19
- package/components/ui/tooltip.tsx +9 -21
- package/dist/content.js +14 -14
- package/dist/index.d.ts +71 -141
- package/dist/index.js +1634 -2450
- package/dist/tokens/primitives.css +9 -2
- package/index.ts +8 -7
- package/package.json +13 -3
- package/tokens/primitives.css +9 -2
- package/components/sections/prompt-editor/use-prompt-editor.ts +0 -131
|
@@ -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'
|
|
@@ -39,11 +40,49 @@ export interface SnippetsEditorProps {
|
|
|
39
40
|
/** Section description, e.g. "Define snippets to reuse in skills prompts..." */
|
|
40
41
|
description?: string
|
|
41
42
|
className?: string
|
|
43
|
+
accentColor?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ACCENT_DIVIDER_HOVER: Record<string, string> = {
|
|
47
|
+
blue: 'hover:bg-blue-500/30',
|
|
48
|
+
purple: 'hover:bg-purple-500/30',
|
|
49
|
+
orange: 'hover:bg-orange-500/30',
|
|
50
|
+
green: 'hover:bg-green-500/30',
|
|
51
|
+
pink: 'hover:bg-pink-500/30',
|
|
52
|
+
amber: 'hover:bg-amber-500/30',
|
|
53
|
+
emerald: 'hover:bg-emerald-500/30',
|
|
54
|
+
teal: 'hover:bg-teal-500/30',
|
|
55
|
+
sky: 'hover:bg-sky-500/30',
|
|
56
|
+
violet: 'hover:bg-violet-500/30',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
const ACCENT_BORDER: Record<string, string> = {
|
|
61
|
+
blue: 'border-l-blue-400',
|
|
62
|
+
purple: 'border-l-purple-400',
|
|
63
|
+
orange: 'border-l-orange-400',
|
|
64
|
+
green: 'border-l-green-400',
|
|
65
|
+
pink: 'border-l-pink-400',
|
|
66
|
+
amber: 'border-l-amber-400',
|
|
67
|
+
emerald: 'border-l-emerald-400',
|
|
68
|
+
teal: 'border-l-teal-400',
|
|
69
|
+
sky: 'border-l-sky-400',
|
|
70
|
+
violet: 'border-l-violet-400',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ACCENT_BUTTON: Record<string, string> = {
|
|
74
|
+
blue: 'bg-blue-600 hover:bg-blue-500',
|
|
75
|
+
purple: 'bg-purple-600 hover:bg-purple-500',
|
|
76
|
+
orange: 'bg-orange-600 hover:bg-orange-500',
|
|
77
|
+
green: 'bg-green-600 hover:bg-green-500',
|
|
78
|
+
pink: 'bg-pink-600 hover:bg-pink-500',
|
|
79
|
+
amber: 'bg-amber-600 hover:bg-amber-500',
|
|
80
|
+
emerald: 'bg-emerald-600 hover:bg-emerald-500',
|
|
81
|
+
teal: 'bg-teal-600 hover:bg-teal-500',
|
|
82
|
+
sky: 'bg-sky-600 hover:bg-sky-500',
|
|
83
|
+
violet: 'bg-violet-600 hover:bg-violet-500',
|
|
42
84
|
}
|
|
43
85
|
|
|
44
|
-
const MIN_SIDEBAR = 200
|
|
45
|
-
const MAX_SIDEBAR = 350
|
|
46
|
-
const DEFAULT_SIDEBAR = 260
|
|
47
86
|
|
|
48
87
|
export function SnippetsEditor({
|
|
49
88
|
api,
|
|
@@ -51,6 +90,7 @@ export function SnippetsEditor({
|
|
|
51
90
|
title = 'Snippets',
|
|
52
91
|
description = 'Define reusable snippets for prompts using {{SNIPPET_NAME}} syntax.',
|
|
53
92
|
className,
|
|
93
|
+
accentColor = 'blue',
|
|
54
94
|
}: SnippetsEditorProps) {
|
|
55
95
|
const {
|
|
56
96
|
selectedName,
|
|
@@ -71,26 +111,7 @@ export function SnippetsEditor({
|
|
|
71
111
|
isSaving,
|
|
72
112
|
} = useSnippetsEditor({ api, snippets })
|
|
73
113
|
|
|
74
|
-
|
|
75
|
-
const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR)
|
|
76
|
-
const dragRef = useRef<{ startX: number; startW: number } | null>(null)
|
|
77
|
-
|
|
78
|
-
const onDividerMouseDown = useCallback((e: React.MouseEvent) => {
|
|
79
|
-
e.preventDefault()
|
|
80
|
-
dragRef.current = { startX: e.clientX, startW: sidebarWidth }
|
|
81
|
-
const onMove = (ev: MouseEvent) => {
|
|
82
|
-
if (!dragRef.current) return
|
|
83
|
-
const newW = Math.min(MAX_SIDEBAR, Math.max(MIN_SIDEBAR, dragRef.current.startW + ev.clientX - dragRef.current.startX))
|
|
84
|
-
setSidebarWidth(newW)
|
|
85
|
-
}
|
|
86
|
-
const onUp = () => {
|
|
87
|
-
dragRef.current = null
|
|
88
|
-
document.removeEventListener('mousemove', onMove)
|
|
89
|
-
document.removeEventListener('mouseup', onUp)
|
|
90
|
-
}
|
|
91
|
-
document.addEventListener('mousemove', onMove)
|
|
92
|
-
document.addEventListener('mouseup', onUp)
|
|
93
|
-
}, [sidebarWidth])
|
|
114
|
+
const { width: sidebarWidth, onPointerDown: onDividerPointerDown } = useResizableSidebar({ min: 200, max: 350, defaultWidth: 260, direction: 'right' })
|
|
94
115
|
|
|
95
116
|
const hasSelection = isEditing || isAdding
|
|
96
117
|
const nameHasError = formError !== null && (
|
|
@@ -103,18 +124,18 @@ export function SnippetsEditor({
|
|
|
103
124
|
<div className="flex items-center justify-between px-4 py-3 border-b border-neutral-700 bg-purple-500/5">
|
|
104
125
|
<div className="flex items-center gap-2">
|
|
105
126
|
<Braces className="w-4 h-4 text-purple-400" />
|
|
106
|
-
<h3 className="text-
|
|
107
|
-
<span className="px-2 py-0.5 text-
|
|
127
|
+
<h3 className="text-md font-medium text-neutral-300">{title}</h3>
|
|
128
|
+
<span className="px-2 py-0.5 text-sm rounded-full bg-neutral-700 text-neutral-400">
|
|
108
129
|
{snippets.length}
|
|
109
130
|
</span>
|
|
110
131
|
</div>
|
|
111
|
-
<p className="text-
|
|
132
|
+
<p className="text-sm text-neutral-500 hidden sm:block">{description}</p>
|
|
112
133
|
</div>
|
|
113
134
|
|
|
114
135
|
{/* Body: two columns */}
|
|
115
136
|
<div className="flex flex-1 min-h-[400px]">
|
|
116
137
|
{/* Left: Snippet list */}
|
|
117
|
-
<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 }}>
|
|
118
139
|
{/* Search + Add */}
|
|
119
140
|
<div className="flex items-center gap-1.5 p-2 border-b border-neutral-700">
|
|
120
141
|
<div className="flex-1">
|
|
@@ -141,10 +162,10 @@ export function SnippetsEditor({
|
|
|
141
162
|
{filteredSnippets.length === 0 && !isAdding && (
|
|
142
163
|
<div className="text-center py-10 px-4">
|
|
143
164
|
<Braces className="w-8 h-8 mx-auto text-purple-400/40 mb-3" />
|
|
144
|
-
<p className="text-
|
|
165
|
+
<p className="text-sm text-neutral-500 mb-1">
|
|
145
166
|
{searchQuery ? 'No matching snippets' : 'No snippets defined'}
|
|
146
167
|
</p>
|
|
147
|
-
<p className="text-
|
|
168
|
+
<p className="text-xs text-neutral-600">
|
|
148
169
|
{searchQuery ? 'Try a different search term' : 'Click + to add your first snippet'}
|
|
149
170
|
</p>
|
|
150
171
|
</div>
|
|
@@ -156,6 +177,7 @@ export function SnippetsEditor({
|
|
|
156
177
|
selected={selectedName === snippet.name}
|
|
157
178
|
onSelect={() => selectSnippet(snippet.name)}
|
|
158
179
|
onDelete={() => remove(snippet.name)}
|
|
180
|
+
accentColor={accentColor}
|
|
159
181
|
/>
|
|
160
182
|
))}
|
|
161
183
|
</div>
|
|
@@ -163,8 +185,8 @@ export function SnippetsEditor({
|
|
|
163
185
|
|
|
164
186
|
{/* Resizable divider */}
|
|
165
187
|
<div
|
|
166
|
-
className=
|
|
167
|
-
|
|
188
|
+
className={`w-1 cursor-col-resize bg-transparent ${ACCENT_DIVIDER_HOVER[accentColor] ?? ACCENT_DIVIDER_HOVER.blue} transition-colors flex-shrink-0`}
|
|
189
|
+
onPointerDown={onDividerPointerDown}
|
|
168
190
|
/>
|
|
169
191
|
|
|
170
192
|
{/* Right: Editor */}
|
|
@@ -181,15 +203,16 @@ export function SnippetsEditor({
|
|
|
181
203
|
onReset={resetForm}
|
|
182
204
|
onDelete={isEditing && selectedName ? () => remove(selectedName) : undefined}
|
|
183
205
|
onCancel={isAdding ? cancelForm : undefined}
|
|
206
|
+
accentColor={accentColor}
|
|
184
207
|
/>
|
|
185
208
|
) : (
|
|
186
209
|
<div className="flex-1 flex items-center justify-center p-6">
|
|
187
210
|
<div className="text-center max-w-xs">
|
|
188
211
|
<Braces className="w-10 h-10 mx-auto text-purple-400/30 mb-4" />
|
|
189
|
-
<p className="text-
|
|
190
|
-
<p className="text-
|
|
212
|
+
<p className="text-md text-neutral-500 mb-2">Select a snippet to edit</p>
|
|
213
|
+
<p className="text-sm text-neutral-600 leading-relaxed">
|
|
191
214
|
Choose a snippet from the list, or click{' '}
|
|
192
|
-
<span className=
|
|
215
|
+
<span className={ACCENT_TEXT[accentColor as AccentColor] ?? ACCENT_TEXT.blue}>+</span> to create a new one.
|
|
193
216
|
Reference snippets in prompts with{' '}
|
|
194
217
|
<span className="font-mono text-purple-400">{'{{SNIPPET_NAME}}'}</span> syntax.
|
|
195
218
|
</p>
|
|
@@ -211,28 +234,29 @@ interface SnippetListItemProps {
|
|
|
211
234
|
selected: boolean
|
|
212
235
|
onSelect: () => void
|
|
213
236
|
onDelete: () => void
|
|
237
|
+
accentColor: string
|
|
214
238
|
}
|
|
215
239
|
|
|
216
|
-
function SnippetListItem({ snippet, selected, onSelect, onDelete }: SnippetListItemProps) {
|
|
240
|
+
function SnippetListItem({ snippet, selected, onSelect, onDelete, accentColor }: SnippetListItemProps) {
|
|
217
241
|
return (
|
|
218
242
|
<div
|
|
219
243
|
className={cn(
|
|
220
244
|
'group flex items-start gap-2 px-3 py-2.5 cursor-pointer transition-colors border-l-2',
|
|
221
245
|
selected
|
|
222
|
-
?
|
|
246
|
+
? `bg-neutral-850 ${ACCENT_BORDER[accentColor] ?? ACCENT_BORDER.blue}`
|
|
223
247
|
: 'border-l-transparent hover:bg-neutral-850/50',
|
|
224
248
|
)}
|
|
225
249
|
onClick={onSelect}
|
|
226
250
|
>
|
|
227
251
|
<div className="flex-1 min-w-0">
|
|
228
|
-
<p className="text-
|
|
252
|
+
<p className="text-sm font-mono font-medium text-neutral-300 truncate">
|
|
229
253
|
{snippet.name}
|
|
230
254
|
</p>
|
|
231
|
-
<p className="text-
|
|
255
|
+
<p className="text-xs text-neutral-500 truncate mt-0.5">
|
|
232
256
|
{snippet.description}
|
|
233
257
|
</p>
|
|
234
258
|
{snippet.value && (
|
|
235
|
-
<p className="text-
|
|
259
|
+
<p className="text-xs text-neutral-600 truncate mt-0.5 font-mono">
|
|
236
260
|
{snippet.value.slice(0, 80)}{snippet.value.length > 80 ? '...' : ''}
|
|
237
261
|
</p>
|
|
238
262
|
)}
|
|
@@ -263,6 +287,7 @@ interface SnippetFormProps {
|
|
|
263
287
|
onReset: () => void
|
|
264
288
|
onDelete?: () => void
|
|
265
289
|
onCancel?: () => void
|
|
290
|
+
accentColor: string
|
|
266
291
|
}
|
|
267
292
|
|
|
268
293
|
function SnippetForm({
|
|
@@ -276,13 +301,14 @@ function SnippetForm({
|
|
|
276
301
|
onReset,
|
|
277
302
|
onDelete,
|
|
278
303
|
onCancel,
|
|
304
|
+
accentColor,
|
|
279
305
|
}: SnippetFormProps) {
|
|
280
306
|
return (
|
|
281
307
|
<div className="flex-1 flex flex-col">
|
|
282
308
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
283
309
|
{/* Name */}
|
|
284
310
|
<div>
|
|
285
|
-
<label className="block text-
|
|
311
|
+
<label className="block text-sm text-neutral-500 mb-1.5">
|
|
286
312
|
Snippet Name <span className="text-red-400">*</span>
|
|
287
313
|
</label>
|
|
288
314
|
<Input
|
|
@@ -292,14 +318,14 @@ function SnippetForm({
|
|
|
292
318
|
error={nameHasError}
|
|
293
319
|
autoFocus={!isEditing}
|
|
294
320
|
/>
|
|
295
|
-
<p className="mt-1 text-
|
|
321
|
+
<p className="mt-1 text-xs text-neutral-600">
|
|
296
322
|
Use in prompts as <span className="font-mono text-purple-400">{'{{' + (formData.name || 'NAME') + '}}'}</span>
|
|
297
323
|
</p>
|
|
298
324
|
</div>
|
|
299
325
|
|
|
300
326
|
{/* Description */}
|
|
301
327
|
<div>
|
|
302
|
-
<label className="block text-
|
|
328
|
+
<label className="block text-sm text-neutral-500 mb-1.5">
|
|
303
329
|
Description <span className="text-red-400">*</span>
|
|
304
330
|
</label>
|
|
305
331
|
<Input
|
|
@@ -311,7 +337,7 @@ function SnippetForm({
|
|
|
311
337
|
|
|
312
338
|
{/* Value */}
|
|
313
339
|
<div>
|
|
314
|
-
<label className="block text-
|
|
340
|
+
<label className="block text-sm text-neutral-500 mb-1.5">Value</label>
|
|
315
341
|
<ResizableTextarea
|
|
316
342
|
mode="code"
|
|
317
343
|
language="markdown"
|
|
@@ -319,7 +345,7 @@ function SnippetForm({
|
|
|
319
345
|
onChange={(val) => setFormField('value', val)}
|
|
320
346
|
minHeight={160}
|
|
321
347
|
/>
|
|
322
|
-
<p className="mt-1 text-
|
|
348
|
+
<p className="mt-1 text-xs text-neutral-600">
|
|
323
349
|
Can be a single value, multi-line text, or an entire document
|
|
324
350
|
</p>
|
|
325
351
|
</div>
|
|
@@ -327,7 +353,7 @@ function SnippetForm({
|
|
|
327
353
|
{/* Error */}
|
|
328
354
|
{formError && (
|
|
329
355
|
<div className="px-3 py-2 bg-red-500/10 border border-red-500/30 rounded-lg">
|
|
330
|
-
<p className="text-
|
|
356
|
+
<p className="text-sm text-red-400">{formError}</p>
|
|
331
357
|
</div>
|
|
332
358
|
)}
|
|
333
359
|
</div>
|
|
@@ -352,7 +378,7 @@ function SnippetForm({
|
|
|
352
378
|
type="button"
|
|
353
379
|
onClick={onCancel}
|
|
354
380
|
disabled={isSaving}
|
|
355
|
-
className="rounded-md border border-neutral-700 bg-transparent px-3 py-1.5 text-
|
|
381
|
+
className="rounded-md border border-neutral-700 bg-transparent px-3 py-1.5 text-sm text-neutral-400 transition-colors hover:bg-neutral-700 hover:text-neutral-300 disabled:opacity-50"
|
|
356
382
|
>
|
|
357
383
|
Cancel
|
|
358
384
|
</button>
|
|
@@ -369,7 +395,7 @@ function SnippetForm({
|
|
|
369
395
|
type="button"
|
|
370
396
|
onClick={onSave}
|
|
371
397
|
disabled={isSaving}
|
|
372
|
-
className=
|
|
398
|
+
className={`flex items-center gap-1.5 rounded-md ${ACCENT_BUTTON[accentColor] ?? ACCENT_BUTTON.blue} px-3 py-1.5 text-sm text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50`}
|
|
373
399
|
>
|
|
374
400
|
<Save className="w-3 h-3" />
|
|
375
401
|
{isEditing ? 'Save' : 'Add'}
|
|
@@ -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
|
}
|
|
@@ -55,7 +54,7 @@ export function SettingsHeader({
|
|
|
55
54
|
<div className={`flex-shrink-0 w-8 h-8 rounded-lg ${styles.iconBg} flex items-center justify-center`}>
|
|
56
55
|
{icon ?? <Info className={`w-4 h-4 ${styles.iconColor}`} />}
|
|
57
56
|
</div>
|
|
58
|
-
<p className="flex-1 text-
|
|
57
|
+
<p className="flex-1 text-md text-neutral-400 leading-relaxed">{description}</p>
|
|
59
58
|
<div className="flex-shrink-0 flex items-center gap-2">
|
|
60
59
|
{action}
|
|
61
60
|
{onReset && (
|
|
@@ -14,6 +14,20 @@ export interface SettingsTreeNavProps {
|
|
|
14
14
|
tree: SettingsTreeNode[]
|
|
15
15
|
selectedPath: string
|
|
16
16
|
onSelectPath: (path: string) => void
|
|
17
|
+
accentColor?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ACCENT_SELECTED: Record<string, string> = {
|
|
21
|
+
blue: 'bg-blue-500/20 text-blue-400',
|
|
22
|
+
purple: 'bg-purple-500/20 text-purple-400',
|
|
23
|
+
orange: 'bg-orange-500/20 text-orange-400',
|
|
24
|
+
green: 'bg-green-500/20 text-green-400',
|
|
25
|
+
pink: 'bg-pink-500/20 text-pink-400',
|
|
26
|
+
amber: 'bg-amber-500/20 text-amber-400',
|
|
27
|
+
emerald: 'bg-emerald-500/20 text-emerald-400',
|
|
28
|
+
teal: 'bg-teal-500/20 text-teal-400',
|
|
29
|
+
sky: 'bg-sky-500/20 text-sky-400',
|
|
30
|
+
violet: 'bg-violet-500/20 text-violet-400',
|
|
17
31
|
}
|
|
18
32
|
|
|
19
33
|
interface TreeNodeProps {
|
|
@@ -25,6 +39,7 @@ interface TreeNodeProps {
|
|
|
25
39
|
onToggleExpand: (path: string) => void
|
|
26
40
|
depth: number
|
|
27
41
|
searchQuery?: string
|
|
42
|
+
accentColor: string
|
|
28
43
|
}
|
|
29
44
|
|
|
30
45
|
function highlightMatch(text: string, query: string | undefined) {
|
|
@@ -53,6 +68,7 @@ function TreeNode({
|
|
|
53
68
|
onToggleExpand,
|
|
54
69
|
depth,
|
|
55
70
|
searchQuery,
|
|
71
|
+
accentColor,
|
|
56
72
|
}: TreeNodeProps) {
|
|
57
73
|
const isLeaf = isLeafNode(node)
|
|
58
74
|
const isExpanded = expandedPaths.has(path)
|
|
@@ -72,8 +88,8 @@ function TreeNode({
|
|
|
72
88
|
<button
|
|
73
89
|
onClick={handleClick}
|
|
74
90
|
className={`
|
|
75
|
-
w-full flex items-center gap-2 px-2 py-1.5 text-
|
|
76
|
-
${isSelected ?
|
|
91
|
+
w-full flex items-center gap-2 px-2 py-1.5 text-md rounded-md transition-colors cursor-pointer
|
|
92
|
+
${isSelected ? ACCENT_SELECTED[accentColor] ?? ACCENT_SELECTED.blue : isInSelectedPath ? 'text-neutral-300' : 'text-neutral-400'}
|
|
77
93
|
${!isSelected && 'hover:bg-neutral-800 hover:text-neutral-200'}
|
|
78
94
|
`}
|
|
79
95
|
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
|
@@ -110,6 +126,7 @@ function TreeNode({
|
|
|
110
126
|
onToggleExpand={onToggleExpand}
|
|
111
127
|
depth={depth + 1}
|
|
112
128
|
searchQuery={searchQuery}
|
|
129
|
+
accentColor={accentColor}
|
|
113
130
|
/>
|
|
114
131
|
))}
|
|
115
132
|
</div>
|
|
@@ -118,7 +135,7 @@ function TreeNode({
|
|
|
118
135
|
)
|
|
119
136
|
}
|
|
120
137
|
|
|
121
|
-
export function SettingsTreeNav({ tree, selectedPath, onSelectPath }: SettingsTreeNavProps) {
|
|
138
|
+
export function SettingsTreeNav({ tree, selectedPath, onSelectPath, accentColor = 'blue' }: SettingsTreeNavProps) {
|
|
122
139
|
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => {
|
|
123
140
|
const expanded = new Set<string>()
|
|
124
141
|
if (selectedPath) {
|
|
@@ -145,19 +162,16 @@ export function SettingsTreeNav({ tree, selectedPath, onSelectPath }: SettingsTr
|
|
|
145
162
|
useEffect(() => {
|
|
146
163
|
if (selectedPath) {
|
|
147
164
|
const parents = getParentPaths(selectedPath)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (!newExpanded.has(p)) {
|
|
153
|
-
newExpanded.add(p)
|
|
154
|
-
changed = true
|
|
165
|
+
setExpandedPaths((prev) => {
|
|
166
|
+
let changed = false
|
|
167
|
+
for (const p of parents) {
|
|
168
|
+
if (!prev.has(p)) { changed = true; break }
|
|
155
169
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
170
|
+
if (!changed) return prev
|
|
171
|
+
const next = new Set(prev)
|
|
172
|
+
for (const p of parents) next.add(p)
|
|
173
|
+
return next
|
|
174
|
+
})
|
|
161
175
|
}
|
|
162
176
|
}, [selectedPath])
|
|
163
177
|
|
|
@@ -232,7 +246,7 @@ export function SettingsTreeNav({ tree, selectedPath, onSelectPath }: SettingsTr
|
|
|
232
246
|
|
|
233
247
|
<div className="flex-1 overflow-y-auto py-2">
|
|
234
248
|
{filteredTree.length === 0 && searchQuery ? (
|
|
235
|
-
<p className="text-
|
|
249
|
+
<p className="text-sm text-neutral-500 text-center py-4 px-2">
|
|
236
250
|
No settings match "{searchQuery}"
|
|
237
251
|
</p>
|
|
238
252
|
) : (
|
|
@@ -247,6 +261,7 @@ export function SettingsTreeNav({ tree, selectedPath, onSelectPath }: SettingsTr
|
|
|
247
261
|
onToggleExpand={handleToggleExpand}
|
|
248
262
|
depth={0}
|
|
249
263
|
searchQuery={searchQuery}
|
|
264
|
+
accentColor={accentColor}
|
|
250
265
|
/>
|
|
251
266
|
))
|
|
252
267
|
)}
|
|
@@ -11,49 +11,16 @@
|
|
|
11
11
|
* Footer: FormActions (status text, cancel/submit)
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { useEffect } from 'react'
|
|
15
14
|
import { createPortal } from 'react-dom'
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
ChevronLeft, ChevronRight, ChevronUp, ChevronDown,
|
|
19
|
-
Check, X, Plus, Minus, Pencil, Trash2, Copy, Save,
|
|
20
|
-
RefreshCw, RotateCcw, Undo2, Redo2,
|
|
21
|
-
Search, Filter, Download, Upload, ExternalLink, Link2,
|
|
22
|
-
Eye, EyeOff, Lock, Unlock, Settings, MoreHorizontal, MoreVertical,
|
|
23
|
-
Info, HelpCircle,
|
|
24
|
-
User, Users, Folder, File, Image, Code, Terminal,
|
|
25
|
-
Star, Heart, Bell, Bookmark, Tag, Pin, Mail, Send,
|
|
26
|
-
Globe, Database, Cloud,
|
|
27
|
-
Wand2, Shield, ShieldCheck, Zap, Sparkles,
|
|
28
|
-
Play, Pause, Square, StopCircle,
|
|
29
|
-
Menu, GripVertical, Maximize2, Minimize2,
|
|
30
|
-
Scan, Webhook, Bot, Puzzle, Plug,
|
|
31
|
-
} from 'lucide-react'
|
|
32
|
-
import type { LucideIcon } from 'lucide-react'
|
|
33
|
-
import { IconButton } from './icon-button.tsx'
|
|
15
|
+
import { useModalBehavior } from '../hooks/use-modal-behavior.ts'
|
|
16
|
+
import { iconMap, IconButton } from './icon-button.tsx'
|
|
34
17
|
import type { IconName } from './icon-button.tsx'
|
|
35
18
|
import { FormActions } from './form-actions.tsx'
|
|
36
19
|
import { SelectionGrid, type SelectionCardItem, type CodingToolPresetConfig } from './selection-grid.tsx'
|
|
37
|
-
import { ExecutionDetailsPanel
|
|
20
|
+
import { ExecutionDetailsPanel } from './execution-details-panel.tsx'
|
|
21
|
+
import type { DetailRow } from './detail-section.tsx'
|
|
38
22
|
import { cn } from '../lib/cn.ts'
|
|
39
23
|
|
|
40
|
-
const dialogIconMap: Record<string, LucideIcon> = {
|
|
41
|
-
'arrow-left': ArrowLeft, 'arrow-right': ArrowRight, 'arrow-up': ArrowUp, 'arrow-down': ArrowDown,
|
|
42
|
-
'chevron-left': ChevronLeft, 'chevron-right': ChevronRight, 'chevron-up': ChevronUp, 'chevron-down': ChevronDown,
|
|
43
|
-
'check': Check, 'x': X, 'plus': Plus, 'minus': Minus, 'pencil': Pencil, 'trash': Trash2, 'copy': Copy, 'save': Save,
|
|
44
|
-
'refresh': RefreshCw, 'rotate': RotateCcw, 'undo': Undo2, 'redo': Redo2,
|
|
45
|
-
'search': Search, 'filter': Filter, 'download': Download, 'upload': Upload, 'external-link': ExternalLink, 'link': Link2,
|
|
46
|
-
'eye': Eye, 'eye-off': EyeOff, 'lock': Lock, 'unlock': Unlock, 'settings': Settings, 'more-h': MoreHorizontal, 'more-v': MoreVertical,
|
|
47
|
-
'info': Info, 'help': HelpCircle,
|
|
48
|
-
'user': User, 'users': Users, 'folder': Folder, 'file': File, 'image': Image, 'code': Code, 'terminal': Terminal,
|
|
49
|
-
'star': Star, 'heart': Heart, 'bell': Bell, 'bookmark': Bookmark, 'tag': Tag, 'pin': Pin, 'mail': Mail, 'send': Send,
|
|
50
|
-
'globe': Globe, 'database': Database, 'cloud': Cloud,
|
|
51
|
-
'wand': Wand2, 'shield': Shield, 'shield-check': ShieldCheck, 'zap': Zap, 'sparkles': Sparkles,
|
|
52
|
-
'play': Play, 'pause': Pause, 'stop': Square, 'stop-circle': StopCircle, 'scan': Scan,
|
|
53
|
-
'menu': Menu, 'grip': GripVertical, 'maximize': Maximize2, 'minimize': Minimize2,
|
|
54
|
-
'webhook': Webhook, 'bot': Bot, 'puzzle': Puzzle, 'plug': Plug,
|
|
55
|
-
}
|
|
56
|
-
|
|
57
24
|
export interface ActionDialogProps {
|
|
58
25
|
/** Dialog title */
|
|
59
26
|
title: string
|
|
@@ -114,7 +81,7 @@ export interface ActionDialogProps {
|
|
|
114
81
|
|
|
115
82
|
// ── Execution details section (mandatory) ────────────────────────────────
|
|
116
83
|
/** Execution detail rows (Tool, Permissions, Output, CLI Flags, Changes) */
|
|
117
|
-
executionDetails:
|
|
84
|
+
executionDetails: DetailRow[]
|
|
118
85
|
/** Whether direct file edits are allowed */
|
|
119
86
|
allowDirectEdits?: boolean
|
|
120
87
|
/** Callback to toggle direct edits - shows the toggle when provided */
|
|
@@ -163,20 +130,9 @@ export function ActionDialog({
|
|
|
163
130
|
children,
|
|
164
131
|
className,
|
|
165
132
|
}: ActionDialogProps) {
|
|
166
|
-
|
|
167
|
-
const handleEscape = (e: KeyboardEvent) => {
|
|
168
|
-
if (e.key === 'Escape') onCancel?.()
|
|
169
|
-
}
|
|
170
|
-
document.addEventListener('keydown', handleEscape)
|
|
171
|
-
return () => document.removeEventListener('keydown', handleEscape)
|
|
172
|
-
}, [onCancel])
|
|
173
|
-
|
|
174
|
-
useEffect(() => {
|
|
175
|
-
document.body.style.overflow = 'hidden'
|
|
176
|
-
return () => { document.body.style.overflow = '' }
|
|
177
|
-
}, [])
|
|
133
|
+
useModalBehavior(true, () => onCancel?.())
|
|
178
134
|
|
|
179
|
-
const Icon = icon ?
|
|
135
|
+
const Icon = icon ? iconMap[icon] : null
|
|
180
136
|
const hasSelection = ((items && items.length > 0) || (presets && presets.length > 0)) && selectedIds && onSelect
|
|
181
137
|
const hasScenarios = scenarios && scenarios.length > 0 && selectedScenarioIds && onSelectScenarios
|
|
182
138
|
const hasExecutionDetails = executionDetails.length > 0 || onAllowDirectEditsChange || executionWarning
|
|
@@ -211,11 +167,11 @@ export function ActionDialog({
|
|
|
211
167
|
/>
|
|
212
168
|
)}
|
|
213
169
|
<div className="flex flex-col">
|
|
214
|
-
<span className="text-
|
|
170
|
+
<span className="text-md font-semibold text-neutral-200">
|
|
215
171
|
{title}
|
|
216
172
|
</span>
|
|
217
173
|
{subtitle && (
|
|
218
|
-
<span className="text-
|
|
174
|
+
<span className="text-sm text-neutral-500">{subtitle}</span>
|
|
219
175
|
)}
|
|
220
176
|
</div>
|
|
221
177
|
<div className="flex-1" />
|
|
@@ -236,7 +192,7 @@ export function ActionDialog({
|
|
|
236
192
|
{hasSelection && (
|
|
237
193
|
<div>
|
|
238
194
|
{selectionLabel && (
|
|
239
|
-
<div className="text-
|
|
195
|
+
<div className="text-sm text-neutral-500 mb-2">{selectionLabel}</div>
|
|
240
196
|
)}
|
|
241
197
|
<SelectionGrid
|
|
242
198
|
items={items}
|
|
@@ -254,11 +210,11 @@ export function ActionDialog({
|
|
|
254
210
|
{hasScenarios && (
|
|
255
211
|
<div>
|
|
256
212
|
<div className="flex items-center justify-between mb-2">
|
|
257
|
-
<span className="text-
|
|
213
|
+
<span className="text-sm text-neutral-500">{scenarioLabel}</span>
|
|
258
214
|
<button
|
|
259
215
|
type="button"
|
|
260
216
|
onClick={handleSelectAllScenarios}
|
|
261
|
-
className="text-
|
|
217
|
+
className="text-sm text-blue-400 hover:text-blue-300 transition-colors cursor-pointer"
|
|
262
218
|
>
|
|
263
219
|
{allScenariosSelected ? 'Deselect All' : 'Select All'}
|
|
264
220
|
</button>
|
package/components/ui/badge.tsx
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* - 5 size variants (xss, xs, sm, md, lg)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
import { FORM_COLORS, type AccentColor } from '../lib/form-colors.ts'
|
|
17
|
+
|
|
18
|
+
export type BadgeColor = AccentColor
|
|
17
19
|
|
|
18
20
|
export interface BadgeProps {
|
|
19
21
|
value: number | string
|
|
@@ -23,30 +25,12 @@ export interface BadgeProps {
|
|
|
23
25
|
testId?: string
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
const colorClasses: Record<BadgeColor, string> = {
|
|
27
|
-
green: 'border-green-500/30 text-green-400',
|
|
28
|
-
red: 'border-red-500/30 text-red-400',
|
|
29
|
-
blue: 'border-blue-500/30 text-blue-400',
|
|
30
|
-
orange: 'border-orange-500/30 text-orange-400',
|
|
31
|
-
cyan: 'border-cyan-500/30 text-cyan-400',
|
|
32
|
-
yellow: 'border-yellow-500/30 text-yellow-400',
|
|
33
|
-
purple: 'border-purple-500/30 text-purple-400',
|
|
34
|
-
indigo: 'border-indigo-500/30 text-indigo-400',
|
|
35
|
-
emerald: 'border-emerald-500/30 text-emerald-400',
|
|
36
|
-
amber: 'border-amber-500/30 text-amber-400',
|
|
37
|
-
violet: 'border-violet-500/30 text-violet-400',
|
|
38
|
-
neutral: 'border-neutral-500/30 text-neutral-400',
|
|
39
|
-
sky: 'border-sky-500/30 text-sky-400',
|
|
40
|
-
pink: 'border-pink-500/30 text-pink-400',
|
|
41
|
-
teal: 'border-teal-500/30 text-teal-400',
|
|
42
|
-
}
|
|
43
|
-
|
|
44
28
|
const sizeClasses = {
|
|
45
29
|
xss: 'min-w-[14px] h-[14px] px-0.5 text-xss',
|
|
46
|
-
xs: 'min-w-[16px] h-[16px] px-1 text-
|
|
47
|
-
sm: 'min-w-[18px] h-[18px] px-1 text-
|
|
48
|
-
md: 'min-w-[20px] h-[20px] px-1.5 text-
|
|
49
|
-
lg: 'min-w-[22px] h-[22px] px-1.5 text-
|
|
30
|
+
xs: 'min-w-[16px] h-[16px] px-1 text-xs',
|
|
31
|
+
sm: 'min-w-[18px] h-[18px] px-1 text-xs',
|
|
32
|
+
md: 'min-w-[20px] h-[20px] px-1.5 text-xs',
|
|
33
|
+
lg: 'min-w-[22px] h-[22px] px-1.5 text-sm',
|
|
50
34
|
}
|
|
51
35
|
|
|
52
36
|
export function Badge({
|
|
@@ -61,7 +45,7 @@ export function Badge({
|
|
|
61
45
|
return (
|
|
62
46
|
<span
|
|
63
47
|
data-testid={testId}
|
|
64
|
-
className={`inline-flex items-center justify-center border rounded-full font-medium leading-none tabular-nums ${
|
|
48
|
+
className={`inline-flex items-center justify-center border rounded-full font-medium leading-none tabular-nums ${FORM_COLORS[color].border} ${FORM_COLORS[color].accent} ${sizeClasses[size]} ${className}`}
|
|
65
49
|
>
|
|
66
50
|
{display}
|
|
67
51
|
</span>
|
|
@@ -70,7 +70,7 @@ type LayoutMode = 'full' | 'compact-banner' | 'compact-all'
|
|
|
70
70
|
|
|
71
71
|
// Width estimates for layout calculation
|
|
72
72
|
const TAB_ICON_PADDING = 56 // icon(16) + gap(8) + px-4(32)
|
|
73
|
-
const CHAR_WIDTH = 7.5 // approximate char width at text-
|
|
73
|
+
const CHAR_WIDTH = 7.5 // approximate char width at text-md
|
|
74
74
|
const COUNT_BADGE_WIDTH = 40 // badge with count
|
|
75
75
|
const BANNER_FULL_WIDTH = 200 // icon + text + padding
|
|
76
76
|
const BANNER_COMPACT_WIDTH = 36 // icon only
|
|
@@ -163,7 +163,7 @@ export function BottomPanelHeader<T extends string = string>({
|
|
|
163
163
|
key={tab.id}
|
|
164
164
|
onClick={() => onTabChange(tab.id)}
|
|
165
165
|
data-testid={tab.testId}
|
|
166
|
-
className={`h-[41px] flex items-center justify-center gap-2 ${compactTabs ? 'px-3' : 'px-4'} text-
|
|
166
|
+
className={`h-[41px] flex items-center justify-center gap-2 ${compactTabs ? 'px-3' : 'px-4'} text-md border-b-2 transition-colors cursor-pointer ${baseClasses}`}
|
|
167
167
|
>
|
|
168
168
|
{compactTabs ? (
|
|
169
169
|
<span className="relative flex items-center justify-center w-[18px] h-[18px] flex-shrink-0">
|
|
@@ -209,12 +209,12 @@ export function BottomPanelHeader<T extends string = string>({
|
|
|
209
209
|
{statusBanner && (
|
|
210
210
|
compactBanner ? (
|
|
211
211
|
<Tooltip content={{ description: statusBanner.message }} position="bottom">
|
|
212
|
-
<div className={`flex items-center px-2 py-1.5 ${bannerStyles} rounded text-
|
|
212
|
+
<div className={`flex items-center px-2 py-1.5 ${bannerStyles} rounded text-sm`}>
|
|
213
213
|
<RefreshCw className="w-3 h-3 flex-shrink-0" />
|
|
214
214
|
</div>
|
|
215
215
|
</Tooltip>
|
|
216
216
|
) : (
|
|
217
|
-
<div className={`flex items-center gap-2 px-2.5 py-1.5 ${bannerStyles} rounded text-
|
|
217
|
+
<div className={`flex items-center gap-2 px-2.5 py-1.5 ${bannerStyles} rounded text-sm max-w-full`}>
|
|
218
218
|
<RefreshCw className="w-3 h-3 flex-shrink-0" />
|
|
219
219
|
<span className="truncate">{statusBanner.message}</span>
|
|
220
220
|
</div>
|