@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.
Files changed (82) hide show
  1. package/agent-rules.json +91 -0
  2. package/ai-manifest.json +190 -0
  3. package/components/content/info-panel-primitives.tsx +14 -14
  4. package/components/hooks/use-click-outside.ts +10 -3
  5. package/components/hooks/use-modal-behavior.ts +24 -0
  6. package/components/hooks/use-navigation-history.ts +7 -2
  7. package/components/hooks/use-resizable-sidebar.ts +38 -0
  8. package/components/lib/ai-tools.tsx +1 -1
  9. package/components/lib/form-colors.ts +40 -0
  10. package/components/sections/ai-tools-paths/tools-paths-panel.tsx +7 -7
  11. package/components/sections/captured-issues/captured-issues-panel.tsx +13 -13
  12. package/components/sections/captured-issues/use-captured-issues.ts +9 -3
  13. package/components/sections/golden-snapshots/file-diff-viewer.tsx +13 -13
  14. package/components/sections/golden-snapshots/golden-sync-panel.tsx +5 -5
  15. package/components/sections/golden-snapshots/snapshot-manager.tsx +11 -11
  16. package/components/sections/golden-snapshots/status-overview.tsx +20 -20
  17. package/components/sections/golden-snapshots/version-manager.tsx +8 -8
  18. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +8 -44
  19. package/components/sections/prompt-editor/index.ts +0 -7
  20. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +9 -45
  21. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +11 -43
  22. package/components/sections/report-bug/report-bug-form.tsx +14 -14
  23. package/components/sections/report-bug/screenshot-uploader.tsx +6 -6
  24. package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +3 -3
  25. package/components/sections/snapshot-browser/snapshot-tree.tsx +8 -8
  26. package/components/sections/snippets-editor/snippets-editor.tsx +74 -48
  27. package/components/settings/SettingsHeader.tsx +1 -2
  28. package/components/settings/SettingsTreeNav.tsx +31 -16
  29. package/components/ui/action-dialog.tsx +12 -56
  30. package/components/ui/badge.tsx +8 -24
  31. package/components/ui/bottom-panel-header.tsx +4 -4
  32. package/components/ui/breadcrumb.tsx +8 -68
  33. package/components/ui/checkbox.tsx +2 -16
  34. package/components/ui/collapsible-section.tsx +4 -42
  35. package/components/ui/confirm-badge.tsx +3 -20
  36. package/components/ui/cookie-consent.tsx +21 -5
  37. package/components/ui/debounce-border-overlay.tsx +31 -0
  38. package/components/ui/detail-section.tsx +5 -22
  39. package/components/ui/editor-placeholder-card.tsx +17 -16
  40. package/components/ui/editor-toolbar.tsx +12 -0
  41. package/components/ui/execution-details-panel.tsx +8 -13
  42. package/components/ui/extension-list-card.tsx +3 -3
  43. package/components/ui/file-structure-section.tsx +20 -35
  44. package/components/ui/file-tree.tsx +4 -14
  45. package/components/ui/files-panel.tsx +28 -18
  46. package/components/ui/filter-dropdown.tsx +5 -5
  47. package/components/ui/form-actions.tsx +7 -6
  48. package/components/ui/frontmatter-form-header.tsx +4 -4
  49. package/components/ui/icon-button.tsx +3 -2
  50. package/components/ui/input.tsx +15 -31
  51. package/components/ui/label.tsx +7 -21
  52. package/components/ui/layout-tab-bar.tsx +4 -4
  53. package/components/ui/modal.tsx +5 -17
  54. package/components/ui/nav-card.tsx +5 -20
  55. package/components/ui/navigation-bar.tsx +13 -74
  56. package/components/ui/number-input.tsx +4 -4
  57. package/components/ui/registry-browser.tsx +10 -24
  58. package/components/ui/registry-card.tsx +16 -20
  59. package/components/ui/registry-detail.tsx +6 -6
  60. package/components/ui/resizable-textarea.tsx +13 -35
  61. package/components/ui/segmented-toggle.tsx +6 -5
  62. package/components/ui/select.tsx +7 -16
  63. package/components/ui/selection-grid.tsx +6 -54
  64. package/components/ui/setting-row.tsx +2 -4
  65. package/components/ui/settings-card.tsx +3 -3
  66. package/components/ui/settings-info-box.tsx +6 -23
  67. package/components/ui/settings-section-title.tsx +1 -1
  68. package/components/ui/snapshot-card.tsx +7 -7
  69. package/components/ui/snippets-panel.tsx +10 -10
  70. package/components/ui/sort-dropdown.tsx +2 -2
  71. package/components/ui/status-card.tsx +6 -17
  72. package/components/ui/tab-bar.tsx +5 -31
  73. package/components/ui/toggle.tsx +3 -19
  74. package/components/ui/tooltip.tsx +9 -21
  75. package/dist/content.js +14 -14
  76. package/dist/index.d.ts +71 -141
  77. package/dist/index.js +1634 -2450
  78. package/dist/tokens/primitives.css +9 -2
  79. package/index.ts +8 -7
  80. package/package.json +13 -3
  81. package/tokens/primitives.css +9 -2
  82. 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 { 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'
@@ -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
- // Resizable sidebar
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-sm font-medium text-neutral-300">{title}</h3>
107
- <span className="px-2 py-0.5 text-xs rounded-full bg-neutral-700 text-neutral-400">
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-xs text-neutral-500 hidden sm:block">{description}</p>
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: MIN_SIDEBAR }}>
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-xs text-neutral-500 mb-1">
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-xss text-neutral-600">
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="w-1 cursor-col-resize bg-transparent hover:bg-blue-500/30 transition-colors flex-shrink-0"
167
- onMouseDown={onDividerMouseDown}
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-sm text-neutral-500 mb-2">Select a snippet to edit</p>
190
- <p className="text-xs text-neutral-600 leading-relaxed">
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="text-blue-400">+</span> to create a new one.
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
- ? 'bg-neutral-850 border-l-blue-400'
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-xs font-mono font-medium text-neutral-300 truncate">
252
+ <p className="text-sm font-mono font-medium text-neutral-300 truncate">
229
253
  {snippet.name}
230
254
  </p>
231
- <p className="text-xss text-neutral-500 truncate mt-0.5">
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-xss text-neutral-600 truncate mt-0.5 font-mono">
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-xs text-neutral-500 mb-1.5">
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-xss text-neutral-600">
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-xs text-neutral-500 mb-1.5">
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-xs text-neutral-500 mb-1.5">Value</label>
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-xss text-neutral-600">
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-xs text-red-400">{formError}</p>
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-xs text-neutral-400 transition-colors hover:bg-neutral-700 hover:text-neutral-300 disabled:opacity-50"
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="flex items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-xs text-white transition-colors hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-50"
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-sm text-neutral-400 leading-relaxed">{description}</p>
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-sm rounded-md transition-colors cursor-pointer
76
- ${isSelected ? 'bg-blue-500/20 text-blue-400' : isInSelectedPath ? 'text-neutral-300' : 'text-neutral-400'}
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
- const newExpanded = new Set(expandedPaths)
149
- let changed = false
150
-
151
- for (const p of parents) {
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
- if (changed) {
159
- setExpandedPaths(newExpanded)
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-xs text-neutral-500 text-center py-4 px-2">
249
+ <p className="text-sm text-neutral-500 text-center py-4 px-2">
236
250
  No settings match &quot;{searchQuery}&quot;
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
- ArrowLeft, ArrowRight, ArrowUp, ArrowDown,
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, type ExecutionDetailRow } from './execution-details-panel.tsx'
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: ExecutionDetailRow[]
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
- useEffect(() => {
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 ? dialogIconMap[icon] : null
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-sm font-semibold text-neutral-200">
170
+ <span className="text-md font-semibold text-neutral-200">
215
171
  {title}
216
172
  </span>
217
173
  {subtitle && (
218
- <span className="text-xs text-neutral-500">{subtitle}</span>
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-xs text-neutral-500 mb-2">{selectionLabel}</div>
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-xs text-neutral-500">{scenarioLabel}</span>
213
+ <span className="text-sm text-neutral-500">{scenarioLabel}</span>
258
214
  <button
259
215
  type="button"
260
216
  onClick={handleSelectAllScenarios}
261
- className="text-xs text-blue-400 hover:text-blue-300 transition-colors cursor-pointer"
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>
@@ -13,7 +13,9 @@
13
13
  * - 5 size variants (xss, xs, sm, md, lg)
14
14
  */
15
15
 
16
- export type BadgeColor = 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow' | 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky' | 'pink' | 'teal'
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-xss',
47
- sm: 'min-w-[18px] h-[18px] px-1 text-xss',
48
- md: 'min-w-[20px] h-[20px] px-1.5 text-xss',
49
- lg: 'min-w-[22px] h-[22px] px-1.5 text-xs',
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 ${colorClasses[color]} ${sizeClasses[size]} ${className}`}
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-sm
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-sm border-b-2 transition-colors cursor-pointer ${baseClasses}`}
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-xs`}>
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-xs max-w-full`}>
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>