@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
@@ -1,19 +1,11 @@
1
1
  import { useState, useEffect, useCallback, useRef, useMemo, type ReactNode } from 'react'
2
2
  import { FileCode, FolderTree, Loader2, AlertCircle, AlignLeft, Code2, Type } from 'lucide-react'
3
- import { CollapseButton, type IconButtonColor } from './icon-button.tsx'
3
+ import { type AccentColor, ACCENT_ICON } from '../lib/form-colors.ts'
4
+ import { CollapseButton } from './icon-button.tsx'
4
5
  import { SegmentedToggle } from './segmented-toggle.tsx'
5
6
  import { FileTree, collectDirPaths, type FileTreeNode } from './file-tree.tsx'
6
7
 
7
8
  export type PreviewMode = 'format' | 'language' | 'plain'
8
- export type AccentColor = 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow' | 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky' | 'pink' | 'teal'
9
-
10
- const ACCENT_ICON: Record<AccentColor, string> = {
11
- blue: 'text-blue-400', green: 'text-green-400', red: 'text-red-400',
12
- orange: 'text-orange-400', cyan: 'text-cyan-400', yellow: 'text-yellow-400',
13
- purple: 'text-purple-400', indigo: 'text-indigo-400', emerald: 'text-emerald-400',
14
- amber: 'text-amber-400', violet: 'text-violet-400', neutral: 'text-neutral-400',
15
- sky: 'text-sky-400', pink: 'text-pink-400', teal: 'text-teal-400',
16
- }
17
9
 
18
10
  const ACCENT_BORDER: Record<AccentColor, string> = {
19
11
  blue: 'border-blue-500/25', green: 'border-green-500/25', red: 'border-red-500/25',
@@ -23,13 +15,6 @@ const ACCENT_BORDER: Record<AccentColor, string> = {
23
15
  sky: 'border-sky-500/25', pink: 'border-pink-500/25', teal: 'border-teal-500/25',
24
16
  }
25
17
 
26
- const ACCENT_BUTTON: Record<AccentColor, IconButtonColor> = {
27
- blue: 'blue', green: 'green', red: 'red',
28
- orange: 'orange', cyan: 'cyan', yellow: 'yellow',
29
- purple: 'purple', indigo: 'indigo', emerald: 'emerald',
30
- amber: 'amber', violet: 'violet', neutral: 'neutral',
31
- sky: 'sky', pink: 'pink', teal: 'teal',
32
- }
33
18
 
34
19
  const ACCENT_HANDLE: Record<AccentColor, string> = {
35
20
  blue: 'bg-blue-500/30 group-hover:bg-blue-400/50', green: 'bg-green-500/30 group-hover:bg-green-400/50', red: 'bg-red-500/30 group-hover:bg-red-400/50',
@@ -89,7 +74,7 @@ function renderMarkdownContent(content: string) {
89
74
  while (i < lines.length && lines[i] !== '---') { fmLines.push(lines[i]); i++ }
90
75
  i++ // skip closing ---
91
76
  nodes.push(
92
- <div key="fm" className="mb-3 font-mono text-xss text-neutral-500 border-l-2 border-neutral-700 pl-2 py-0.5">
77
+ <div key="fm" className="mb-3 font-mono text-xs text-neutral-500 border-l-2 border-neutral-700 pl-2 py-0.5">
93
78
  <div className="text-neutral-600">---</div>
94
79
  {fmLines.map((l, j) => <div key={j}>{l}</div>)}
95
80
  <div className="text-neutral-600">---</div>
@@ -104,20 +89,20 @@ function renderMarkdownContent(content: string) {
104
89
  i++
105
90
  while (i < lines.length && !lines[i].startsWith('```')) { codeLines.push(lines[i]); i++ }
106
91
  nodes.push(
107
- <pre key={i} className="mb-2 p-2 bg-[var(--background)]/30 rounded text-xss font-mono text-neutral-300 overflow-x-auto">
92
+ <pre key={i} className="mb-2 p-2 bg-[var(--background)]/30 rounded text-xs font-mono text-neutral-300 overflow-x-auto">
108
93
  {codeLines.join('\n')}
109
94
  </pre>
110
95
  )
111
96
  } else if (line.startsWith('### ')) {
112
- nodes.push(<h3 key={i} className="text-xss font-semibold text-neutral-300 mt-2 mb-0.5">{line.slice(4)}</h3>)
97
+ nodes.push(<h3 key={i} className="text-xs font-semibold text-neutral-300 mt-2 mb-0.5">{line.slice(4)}</h3>)
113
98
  } else if (line.startsWith('## ')) {
114
- nodes.push(<h2 key={i} className="text-xs font-semibold text-neutral-200 mt-2.5 mb-1">{line.slice(3)}</h2>)
99
+ nodes.push(<h2 key={i} className="text-sm font-semibold text-neutral-200 mt-2.5 mb-1">{line.slice(3)}</h2>)
115
100
  } else if (line.startsWith('# ')) {
116
- nodes.push(<h1 key={i} className="text-sm font-semibold text-neutral-100 mb-1.5">{line.slice(2)}</h1>)
101
+ nodes.push(<h1 key={i} className="text-md font-semibold text-neutral-100 mb-1.5">{line.slice(2)}</h1>)
117
102
  } else if (line === '' || line === '\r') {
118
103
  nodes.push(<div key={i} className="h-1.5" />)
119
104
  } else {
120
- nodes.push(<p key={i} className="text-xss text-neutral-400 leading-relaxed">{line}</p>)
105
+ nodes.push(<p key={i} className="text-xs text-neutral-400 leading-relaxed">{line}</p>)
121
106
  }
122
107
  i++
123
108
  }
@@ -302,8 +287,8 @@ export function FileStructureSection({
302
287
  if (isLoading) {
303
288
  return (
304
289
  <div>
305
- <h3 className="text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
306
- <div className="flex items-center gap-2 text-xs text-neutral-500 py-4">
290
+ <h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
291
+ <div className="flex items-center gap-2 text-sm text-neutral-500 py-4">
307
292
  <Loader2 className="w-3.5 h-3.5 animate-spin" />
308
293
  Loading file tree...
309
294
  </div>
@@ -318,8 +303,8 @@ export function FileStructureSection({
318
303
  if (error) {
319
304
  return (
320
305
  <div>
321
- <h3 className="text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
322
- <div className="flex items-center gap-2 text-xs text-red-400 py-4">
306
+ <h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
307
+ <div className="flex items-center gap-2 text-sm text-red-400 py-4">
323
308
  <AlertCircle className="w-3.5 h-3.5 shrink-0" />
324
309
  {error}
325
310
  </div>
@@ -336,7 +321,7 @@ export function FileStructureSection({
336
321
  if (mode === 'format') return renderMarkdownContent(content)
337
322
  if (mode === 'language' && renderPreview) return renderPreview(content, filePath, resolvedLanguage)
338
323
  return (
339
- <pre className="p-3 text-xs font-mono text-white leading-relaxed whitespace-pre-wrap">
324
+ <pre className="p-3 text-sm font-mono text-white leading-relaxed whitespace-pre-wrap">
340
325
  <code>{content}</code>
341
326
  </pre>
342
327
  )
@@ -346,11 +331,11 @@ export function FileStructureSection({
346
331
  <div className={`flex flex-col bg-neutral-900 border ${ACCENT_BORDER[accentColor]} rounded-lg overflow-hidden ${variant === 'split' && effectiveFilePath ? 'w-1/3 shrink-0' : 'flex-1'}`}>
347
332
  <div className={`flex items-center px-3 py-2 border-b ${ACCENT_BORDER[accentColor]} shrink-0 gap-2 min-w-0`}>
348
333
  <FolderTree className={`w-3.5 h-3.5 shrink-0 ${ACCENT_ICON[accentColor]}`} />
349
- <span className="text-xs text-neutral-200 truncate flex-1">Files</span>
334
+ <span className="text-sm text-neutral-200 truncate flex-1">Files</span>
350
335
  <CollapseButton
351
336
  collapsed={allCollapsed}
352
337
  onToggle={() => setExpandedPaths(allCollapsed ? new Set(allDirPaths) : new Set())}
353
- color={ACCENT_BUTTON[accentColor]}
338
+ color={accentColor}
354
339
  />
355
340
  </div>
356
341
  <div className={`${variant === 'split' ? 'flex-1 overflow-y-auto' : ''} p-3`}>
@@ -371,7 +356,7 @@ export function FileStructureSection({
371
356
  <div className={`flex-1 flex flex-col bg-neutral-900 border ${ACCENT_BORDER[accentColor]} rounded-lg overflow-hidden`}>
372
357
  <div className={`flex items-center px-3 py-2 border-b ${ACCENT_BORDER[accentColor]} shrink-0 gap-2 min-w-0`}>
373
358
  <FileCode className={`w-3.5 h-3.5 shrink-0 ${ACCENT_ICON[accentColor]}`} />
374
- <span className="text-xs text-neutral-200 truncate flex-1">{selectedFileName}</span>
359
+ <span className="text-sm text-neutral-200 truncate flex-1">{selectedFileName}</span>
375
360
  {showToggle && (
376
361
  <SegmentedToggle
377
362
  options={toggleOptions}
@@ -384,12 +369,12 @@ export function FileStructureSection({
384
369
  </div>
385
370
  <div className="flex-1 overflow-auto">
386
371
  {fileIsLoading ? (
387
- <div className="flex items-center gap-2 text-xs text-neutral-500 p-3">
372
+ <div className="flex items-center gap-2 text-sm text-neutral-500 p-3">
388
373
  <Loader2 className="w-3.5 h-3.5 animate-spin" />
389
374
  Loading...
390
375
  </div>
391
376
  ) : fileError ? (
392
- <p className="text-xs text-red-400 p-3">{fileError}</p>
377
+ <p className="text-sm text-red-400 p-3">{fileError}</p>
393
378
  ) : fileContent !== null ? (
394
379
  renderContent(fileContent, effectiveFilePath)
395
380
  ) : null}
@@ -409,7 +394,7 @@ export function FileStructureSection({
409
394
  if (variant === 'list') {
410
395
  return (
411
396
  <div ref={sectionRef}>
412
- <h3 className="text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
397
+ <h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
413
398
  <div className="space-y-3">
414
399
  {treePanel}
415
400
  {previewPanel && (
@@ -427,7 +412,7 @@ export function FileStructureSection({
427
412
 
428
413
  return (
429
414
  <div ref={sectionRef}>
430
- <h3 className="text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
415
+ <h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
431
416
  <div className="flex gap-3" style={{ height: `${effectiveHeight}px` }}>
432
417
  {treePanel}
433
418
  {previewPanel}
@@ -1,4 +1,5 @@
1
1
  import { FileCode, Folder, FolderOpen, ChevronRight, ChevronDown } from 'lucide-react'
2
+ import { ACCENT_ICON, type AccentColor } from '../lib/form-colors.ts'
2
3
 
3
4
  export interface FileTreeNode {
4
5
  name: string
@@ -27,18 +28,7 @@ const ACCENT_SELECTED: Record<string, string> = {
27
28
  emerald: 'bg-emerald-400/20 text-neutral-200',
28
29
  teal: 'bg-teal-400/20 text-neutral-200',
29
30
  sky: 'bg-sky-400/20 text-neutral-200',
30
- }
31
-
32
- const ACCENT_ICON: Record<string, string> = {
33
- blue: 'text-blue-400',
34
- purple: 'text-purple-400',
35
- orange: 'text-orange-400',
36
- green: 'text-green-400',
37
- pink: 'text-pink-400',
38
- amber: 'text-amber-400',
39
- emerald: 'text-emerald-400',
40
- teal: 'text-teal-400',
41
- sky: 'text-sky-400',
31
+ violet: 'bg-violet-400/20 text-neutral-200',
42
32
  }
43
33
 
44
34
  function nodeHasFiles(node: FileTreeNode): boolean {
@@ -120,9 +110,9 @@ function FileTreeNodeItem({ node, path, selectedPath, onSelectFile, expandedPath
120
110
  const isDir = node.type === 'directory'
121
111
  const isSelected = !isDir && selectedPath === path
122
112
  const expanded = isDir && expandedPaths.has(path)
123
- const base = 'flex items-center gap-1.5 py-0.5 px-1 rounded text-xs transition-colors overflow-hidden whitespace-nowrap'
113
+ const base = 'flex items-center gap-1.5 py-0.5 px-1 rounded text-sm transition-colors overflow-hidden whitespace-nowrap'
124
114
  const selectedClass = ACCENT_SELECTED[accentColor] ?? ACCENT_SELECTED.blue
125
- const iconColorClass = ACCENT_ICON[accentColor] ?? ACCENT_ICON.blue
115
+ const iconColorClass = ACCENT_ICON[accentColor as AccentColor] ?? ACCENT_ICON.blue
126
116
  const rowClass = isSelected
127
117
  ? `${base} ${selectedClass}`
128
118
  : isDir
@@ -3,15 +3,20 @@
3
3
  import { useState, useMemo } from 'react'
4
4
  import { Folder, FolderOpen, File, FileCode, FileText, FileJson, Image, ChevronRight, Search, MoreVertical } from 'lucide-react'
5
5
  import type { LucideIcon } from 'lucide-react'
6
- import type { IconName } from './icon-button.tsx'
6
+ import { iconMap, type IconName } from './icon-button.tsx'
7
7
  import { cn } from '../lib/cn.ts'
8
8
 
9
- const iconSubset: Partial<Record<IconName, LucideIcon>> = {
10
- folder: Folder,
11
- file: File,
12
- code: FileCode,
13
- image: Image,
14
- search: Search,
9
+ const ACCENT_SELECTED: Record<string, string> = {
10
+ blue: 'bg-blue-400/15 text-blue-400',
11
+ purple: 'bg-purple-400/15 text-purple-400',
12
+ orange: 'bg-orange-400/15 text-orange-400',
13
+ green: 'bg-green-400/15 text-green-400',
14
+ pink: 'bg-pink-400/15 text-pink-400',
15
+ amber: 'bg-amber-400/15 text-amber-400',
16
+ emerald: 'bg-emerald-400/15 text-emerald-400',
17
+ teal: 'bg-teal-400/15 text-teal-400',
18
+ sky: 'bg-sky-400/15 text-sky-400',
19
+ violet: 'bg-violet-400/15 text-violet-400',
15
20
  }
16
21
 
17
22
  const EXTENSION_ICON_MAP: Record<string, LucideIcon> = {
@@ -48,6 +53,7 @@ export interface FilesPanelProps {
48
53
  onAction?: (action: string, path: string) => void
49
54
  showSearch?: boolean
50
55
  className?: string
56
+ accentColor?: string
51
57
  }
52
58
 
53
59
  function collectAllFolderPaths(entries: FileEntry[]): Set<string> {
@@ -74,7 +80,7 @@ function countFiles(entries: FileEntry[]): number {
74
80
  }
75
81
 
76
82
  function getFileIcon(entry: FileEntry): LucideIcon {
77
- if (entry.icon && iconSubset[entry.icon]) return iconSubset[entry.icon]!
83
+ if (entry.icon && iconMap[entry.icon]) return iconMap[entry.icon]!
78
84
  if (entry.type === 'folder') return Folder
79
85
  const ext = entry.name.split('.').pop()?.toLowerCase()
80
86
  if (ext && EXTENSION_ICON_MAP[ext]) return EXTENSION_ICON_MAP[ext]
@@ -82,7 +88,7 @@ function getFileIcon(entry: FileEntry): LucideIcon {
82
88
  }
83
89
 
84
90
  function getFolderIcon(expanded: boolean, entry: FileEntry): LucideIcon {
85
- if (entry.icon && iconSubset[entry.icon]) return iconSubset[entry.icon]!
91
+ if (entry.icon && iconMap[entry.icon]) return iconMap[entry.icon]!
86
92
  return expanded ? FolderOpen : Folder
87
93
  }
88
94
 
@@ -110,9 +116,10 @@ interface FileNodeProps {
110
116
  onToggleExpand: (path: string) => void
111
117
  onSelect?: (path: string) => void
112
118
  onAction?: (action: string, path: string) => void
119
+ accentColor: string
113
120
  }
114
121
 
115
- function FileNode({ entry, depth, selectedPath, expandedPaths, onToggleExpand, onSelect, onAction }: FileNodeProps) {
122
+ function FileNode({ entry, depth, selectedPath, expandedPaths, onToggleExpand, onSelect, onAction, accentColor }: FileNodeProps) {
116
123
  const isFolder = entry.type === 'folder'
117
124
  const isExpanded = isFolder && expandedPaths.has(entry.path)
118
125
  const isSelected = !isFolder && selectedPath === entry.path
@@ -124,9 +131,9 @@ function FileNode({ entry, depth, selectedPath, expandedPaths, onToggleExpand, o
124
131
  type="button"
125
132
  onClick={isFolder ? () => onToggleExpand(entry.path) : () => onSelect?.(entry.path)}
126
133
  className={cn(
127
- 'group flex items-center gap-1.5 w-full py-1 px-2 rounded text-xs transition-colors cursor-pointer',
134
+ 'group flex items-center gap-1.5 w-full py-1 px-2 rounded text-sm transition-colors cursor-pointer',
128
135
  isSelected
129
- ? 'bg-blue-400/15 text-blue-400'
136
+ ? ACCENT_SELECTED[accentColor] ?? ACCENT_SELECTED.blue
130
137
  : 'text-neutral-400 hover:bg-neutral-700/40 hover:text-neutral-200',
131
138
  )}
132
139
  style={{ paddingLeft: `${depth * 16 + 8}px` }}
@@ -144,7 +151,7 @@ function FileNode({ entry, depth, selectedPath, expandedPaths, onToggleExpand, o
144
151
  />
145
152
  <span className="truncate">{entry.name}</span>
146
153
  {entry.badge && (
147
- <span className="ml-auto shrink-0 px-1.5 py-0.5 text-xss rounded bg-neutral-700 text-neutral-500">
154
+ <span className="ml-auto shrink-0 px-1.5 py-0.5 text-xs rounded bg-neutral-700 text-neutral-500">
148
155
  {entry.badge}
149
156
  </span>
150
157
  )}
@@ -172,6 +179,7 @@ function FileNode({ entry, depth, selectedPath, expandedPaths, onToggleExpand, o
172
179
  onToggleExpand={onToggleExpand}
173
180
  onSelect={onSelect}
174
181
  onAction={onAction}
182
+ accentColor={accentColor}
175
183
  />
176
184
  ))}
177
185
  </ul>
@@ -187,6 +195,7 @@ export function FilesPanel({
187
195
  onAction,
188
196
  showSearch = false,
189
197
  className,
198
+ accentColor = 'blue',
190
199
  }: FilesPanelProps) {
191
200
  const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => collectAllFolderPaths(files))
192
201
  const [searchQuery, setSearchQuery] = useState('')
@@ -210,19 +219,19 @@ export function FilesPanel({
210
219
  return (
211
220
  <div className={cn('flex flex-col bg-neutral-800 rounded-lg overflow-hidden', className)}>
212
221
  <div className="flex items-center justify-between px-3 py-2 border-b border-neutral-700">
213
- <span className="text-xss font-semibold uppercase tracking-wider text-neutral-500">Files</span>
214
- <span className="text-xss text-neutral-500">{fileCount} files</span>
222
+ <span className="text-xs font-semibold uppercase tracking-wider text-neutral-500">Files</span>
223
+ <span className="text-xs text-neutral-500">{fileCount} files</span>
215
224
  </div>
216
225
  {showSearch && (
217
226
  <div className="px-2 py-2 border-b border-neutral-700">
218
- <div className="flex items-center gap-1.5 px-2 py-1 bg-[var(--background)] border border-neutral-700 rounded text-xs">
227
+ <div className="flex items-center gap-1.5 px-2 py-1 bg-[var(--background)] border border-neutral-700 rounded text-sm">
219
228
  <Search className="w-3 h-3 text-neutral-500 shrink-0" />
220
229
  <input
221
230
  type="text"
222
231
  placeholder="Search files..."
223
232
  value={searchQuery}
224
233
  onChange={(e) => setSearchQuery(e.target.value)}
225
- className="flex-1 bg-transparent text-neutral-200 placeholder-neutral-500 outline-none text-xs"
234
+ className="flex-1 bg-transparent text-neutral-200 placeholder-neutral-500 outline-none text-sm"
226
235
  />
227
236
  </div>
228
237
  </div>
@@ -239,11 +248,12 @@ export function FilesPanel({
239
248
  onToggleExpand={handleToggleExpand}
240
249
  onSelect={onSelect}
241
250
  onAction={onAction}
251
+ accentColor={accentColor}
242
252
  />
243
253
  ))}
244
254
  </ul>
245
255
  {displayedFiles.length === 0 && (
246
- <p className="text-xss text-neutral-500 text-center py-4">No files found</p>
256
+ <p className="text-xs text-neutral-500 text-center py-4">No files found</p>
247
257
  )}
248
258
  </div>
249
259
  </div>
@@ -89,7 +89,7 @@ export function FilterDropdown({
89
89
  <div className="relative flex items-center" ref={ref} onKeyDown={handleKeyDown}>
90
90
  <button
91
91
  onClick={() => setIsOpen(!isOpen)}
92
- className={`flex items-center gap-1.5 h-7 px-2 rounded-md border ${v.bg} text-xs transition-colors cursor-pointer ${
92
+ className={`flex items-center gap-1.5 h-7 px-2 rounded-md border ${v.bg} text-sm transition-colors cursor-pointer ${
93
93
  isActive
94
94
  ? `${clearable ? 'rounded-r-none border-r-0' : ''} ${FORM_COLORS[color].border} text-neutral-200 ${FORM_COLORS[color].hover}`
95
95
  : isOpen
@@ -124,7 +124,7 @@ export function FilterDropdown({
124
124
  onChange={(e) => setSearch(e.target.value)}
125
125
  onKeyDown={handleKeyDown}
126
126
  placeholder="Search..."
127
- className={`w-full pl-6 pr-2 py-1 text-xs bg-[var(--popover)] border border-neutral-600 rounded text-neutral-200 placeholder-neutral-500 outline-none ${FORM_COLORS[color].focus}`}
127
+ className={`w-full pl-6 pr-2 py-1 text-sm bg-[var(--popover)] border border-neutral-600 rounded text-neutral-200 placeholder-neutral-500 outline-none ${FORM_COLORS[color].focus}`}
128
128
  />
129
129
  </div>
130
130
  </div>
@@ -133,7 +133,7 @@ export function FilterDropdown({
133
133
  <button
134
134
  data-idx={0}
135
135
  onClick={() => handleSelect('all')}
136
- className={`w-full flex items-center gap-2 px-3 py-1.5 text-xs text-left transition-colors cursor-pointer ${
136
+ className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer ${
137
137
  highlightIdx === 0
138
138
  ? `${FORM_COLORS[color].selectedBg} text-neutral-200`
139
139
  : !isActive ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
@@ -152,7 +152,7 @@ export function FilterDropdown({
152
152
  key={opt.value}
153
153
  data-idx={idx}
154
154
  onClick={() => handleSelect(opt.value)}
155
- className={`w-full flex items-center gap-2 px-3 py-1.5 text-xs text-left transition-colors cursor-pointer ${
155
+ className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer ${
156
156
  isHighlighted
157
157
  ? `${FORM_COLORS[color].selectedBg} text-neutral-200`
158
158
  : isSelected ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
@@ -164,7 +164,7 @@ export function FilterDropdown({
164
164
  )
165
165
  })}
166
166
  {showSearch && search && filtered.length === 0 && (
167
- <div className="px-3 py-2 text-xs text-neutral-500">No matches</div>
167
+ <div className="px-3 py-2 text-sm text-neutral-500">No matches</div>
168
168
  )}
169
169
  </div>
170
170
  )}
@@ -34,10 +34,12 @@ export interface FormActionsProps {
34
34
 
35
35
  const PADDING_CLASSES = {
36
36
  compact: 'pt-2',
37
- normal: 'pt-2 border-t border-neutral-700',
38
- modal: 'px-4 py-3 border-t border-neutral-700',
37
+ normal: 'pt-2',
38
+ modal: 'px-4 py-3',
39
39
  } as const
40
40
 
41
+ const BORDER_CLASS = 'border-t border-neutral-700'
42
+
41
43
  const DEFAULT_BORDER = {
42
44
  compact: false,
43
45
  normal: true,
@@ -64,10 +66,9 @@ export function FormActions({
64
66
  padding = 'normal',
65
67
  }: FormActionsProps) {
66
68
  const showBorder = border ?? DEFAULT_BORDER[padding]
67
- const base = PADDING_CLASSES[padding]
68
69
  const paddingClass = showBorder
69
- ? base
70
- : base.replace(/\s*border-t\s+border-neutral-700/g, '')
70
+ ? `${PADDING_CLASSES[padding]} ${BORDER_CLASS}`
71
+ : PADDING_CLASSES[padding]
71
72
 
72
73
  const hasLeft = onBack || statusText
73
74
 
@@ -83,7 +84,7 @@ export function FormActions({
83
84
  tooltip={{ description: backTooltip }}
84
85
  />
85
86
  )}
86
- {statusText && <span className="text-xs text-neutral-500">{statusText}</span>}
87
+ {statusText && <span className="text-sm text-neutral-500">{statusText}</span>}
87
88
  </div>
88
89
  )}
89
90
  <div className="flex items-center gap-2">
@@ -39,16 +39,16 @@ export function FrontmatterFormHeader({
39
39
  collapsed ? '' : 'rotate-90'
40
40
  }`}
41
41
  />
42
- <span className="text-xs font-medium text-neutral-400 uppercase tracking-wide">
42
+ <span className="text-sm font-medium text-neutral-400 uppercase tracking-wide">
43
43
  Configuration
44
44
  </span>
45
45
  {collapsed && hasFm && (
46
- <span className="text-xss text-neutral-500 font-mono ml-2 truncate">
46
+ <span className="text-xs text-neutral-500 font-mono ml-2 truncate">
47
47
  {renderSummary()}
48
48
  </span>
49
49
  )}
50
50
  {collapsed && !hasFm && (
51
- <span className="text-xss text-neutral-600 ml-2">No frontmatter</span>
51
+ <span className="text-xs text-neutral-600 ml-2">No frontmatter</span>
52
52
  )}
53
53
  </button>
54
54
 
@@ -64,7 +64,7 @@ export function FrontmatterFormHeader({
64
64
  disabled={readOnly}
65
65
  />
66
66
  <span
67
- className="text-xs text-neutral-400 cursor-pointer"
67
+ className="text-sm text-neutral-400 cursor-pointer"
68
68
  onClick={() => !readOnly && onFrontmatterToggle(!hasFm)}
69
69
  >
70
70
  Add YAML frontmatter to file
@@ -39,6 +39,7 @@ import {
39
39
  AlertCircle, FileCode, Gauge, Home, PieChart, Settings2,
40
40
  } from 'lucide-react'
41
41
  import type { LucideIcon } from 'lucide-react'
42
+ import { type AccentColor } from '../lib/form-colors.ts'
42
43
  import { Tooltip, type TooltipContent } from './tooltip.tsx'
43
44
 
44
45
  export const iconMap = {
@@ -159,7 +160,7 @@ export interface ActionItem {
159
160
 
160
161
  export type IconButtonStatus = 'loading' | 'success' | 'warning' | 'error'
161
162
 
162
- export type IconButtonColor = 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow' | 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky' | 'pink' | 'teal'
163
+ export type IconButtonColor = AccentColor
163
164
  export type IconButtonVariant = 'filled' | 'outline'
164
165
 
165
166
  export interface IconButtonProps {
@@ -323,7 +324,7 @@ export function IconButton({
323
324
  </span>
324
325
  {badge !== undefined && (
325
326
  <span
326
- className={`absolute -top-1 -right-1 min-w-[18px] h-[18px] flex items-center justify-center px-1 text-xs font-bold text-white rounded-full ${badgeColorClasses[badgeColor]}`}
327
+ className={`absolute -top-1 -right-1 min-w-[18px] h-[18px] flex items-center justify-center px-1 text-sm font-bold text-white rounded-full ${badgeColorClasses[badgeColor]}`}
327
328
  >
328
329
  {typeof badge === 'number' && badge > 99 ? '99+' : badge}
329
330
  </span>
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { forwardRef, useEffect, useRef, useState, type InputHTMLAttributes, type ReactNode } from 'react'
21
21
  import { Search, X, Eye, EyeOff } from 'lucide-react'
22
+ import { DebounceBorderOverlay } from './debounce-border-overlay.tsx'
22
23
  import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
23
24
 
24
25
  export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'size' | 'type'> {
@@ -39,10 +40,10 @@ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>,
39
40
 
40
41
  const sizeClasses = {
41
42
  xss: 'px-1 py-0.5 text-xss',
42
- xs: 'px-1.5 py-0.5 text-xs',
43
- sm: 'px-2 py-1 text-xs',
44
- md: 'px-3 py-1.5 text-sm',
45
- lg: 'px-3 py-2 text-sm',
43
+ xs: 'px-1.5 py-0.5 text-sm',
44
+ sm: 'px-2 py-1 text-sm',
45
+ md: 'px-3 py-1.5 text-md',
46
+ lg: 'px-3 py-2 text-md',
46
47
  }
47
48
 
48
49
  const variantClasses = {
@@ -50,6 +51,13 @@ const variantClasses = {
50
51
  outline: 'bg-transparent',
51
52
  }
52
53
 
54
+ const SEARCH_AUTO_PROPS = {
55
+ autoComplete: 'off' as const,
56
+ autoCorrect: 'off' as const,
57
+ autoCapitalize: 'off' as const,
58
+ spellCheck: false as const,
59
+ }
60
+
53
61
  export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
54
62
  value,
55
63
  onChange,
@@ -115,12 +123,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
115
123
  return () => clearTimeout(timerRef.current)
116
124
  }, [])
117
125
 
118
- const searchAutoProps = isSearch ? {
119
- autoComplete: 'off' as const,
120
- autoCorrect: 'off' as const,
121
- autoCapitalize: 'off' as const,
122
- spellCheck: false as const,
123
- } : {}
126
+ const searchAutoProps = isSearch ? SEARCH_AUTO_PROPS : undefined
124
127
 
125
128
  const showClear = isSearch && displayValue && !disabled
126
129
 
@@ -181,30 +184,11 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
181
184
  </button>
182
185
  )}
183
186
  {debounceMs > 0 && debounceKey > 0 && (
184
- <svg
185
- key={debounceKey}
186
- className="absolute inset-0 pointer-events-none text-emerald-400/70"
187
- style={{ width: '100%', height: '100%' }}
188
- >
189
- <rect
190
- x="1" y="1" rx="5" ry="5"
191
- fill="none"
192
- stroke="currentColor"
193
- strokeWidth="1.5"
194
- pathLength="100"
195
- strokeDasharray="100"
196
- strokeDashoffset="0"
197
- style={{
198
- width: 'calc(100% - 2px)',
199
- height: 'calc(100% - 2px)',
200
- animation: `debounce-border-drain ${debounceMs}ms linear forwards`,
201
- }}
202
- />
203
- </svg>
187
+ <DebounceBorderOverlay debounceKey={debounceKey} durationMs={debounceMs} />
204
188
  )}
205
189
  </div>
206
190
  {typeof error === 'string' && error && (
207
- <p className="text-xs text-red-400 mt-1 text-right">{error}</p>
191
+ <p className="text-sm text-red-400 mt-1 text-right">{error}</p>
208
192
  )}
209
193
  </div>
210
194
  )
@@ -12,25 +12,11 @@
12
12
  * - Clickable with hover state
13
13
  */
14
14
 
15
+ import { type AccentColor } from '../lib/form-colors.ts'
15
16
  import { iconMap, type IconName } from './icon-button.tsx'
16
17
  import { Tooltip } from './tooltip.tsx'
17
18
 
18
- export type LabelColor =
19
- | 'neutral'
20
- | 'green'
21
- | 'red'
22
- | 'blue'
23
- | 'yellow'
24
- | 'orange'
25
- | 'purple'
26
- | 'amber'
27
- | 'emerald'
28
- | 'cyan'
29
- | 'indigo'
30
- | 'teal'
31
- | 'violet'
32
- | 'pink'
33
- | 'sky'
19
+ export type LabelColor = AccentColor
34
20
 
35
21
  export interface LabelProps {
36
22
  text: string
@@ -87,14 +73,14 @@ const progressFillColors: Record<LabelColor, string> = {
87
73
 
88
74
  const sizeConfig = {
89
75
  xss: { height: 14, padding: 'px-1', text: 'text-xss', iconSize: 'w-2 h-2', gap: 'gap-0.5' },
90
- xs: { height: 16, padding: 'px-1.5', text: 'text-xss', iconSize: 'w-2.5 h-2.5', gap: 'gap-1' },
91
- sm: { height: 18, padding: 'px-1.5', text: 'text-xss', iconSize: 'w-2.5 h-2.5', gap: 'gap-1.5' },
92
- md: { height: 20, padding: 'px-1.5', text: 'text-xss', iconSize: 'w-3 h-3', gap: 'gap-1' },
93
- lg: { height: 22, padding: 'px-2', text: 'text-xs', iconSize: 'w-3 h-3', gap: 'gap-1' },
76
+ xs: { height: 16, padding: 'px-1.5', text: 'text-xs', iconSize: 'w-2.5 h-2.5', gap: 'gap-1' },
77
+ sm: { height: 18, padding: 'px-1.5', text: 'text-xs', iconSize: 'w-2.5 h-2.5', gap: 'gap-1.5' },
78
+ md: { height: 20, padding: 'px-1.5', text: 'text-xs', iconSize: 'w-3 h-3', gap: 'gap-1' },
79
+ lg: { height: 22, padding: 'px-2', text: 'text-sm', iconSize: 'w-3 h-3', gap: 'gap-1' },
94
80
  }
95
81
 
96
82
  /** Smart capitalize: capitalizes first letter of each word separated by spaces or dashes */
97
- function smartCapitalize(value: string): string {
83
+ export function smartCapitalize(value: string): string {
98
84
  return value.replace(/(^|[\s-])(\w)/g, (_match, separator: string, char: string) => separator + char.toUpperCase())
99
85
  }
100
86
 
@@ -220,14 +220,14 @@ export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, cla
220
220
  {/* Line 1: Icon + Title */}
221
221
  <div className="flex items-center gap-1.5 w-full transition-[padding] duration-150 group-hover:pr-5">
222
222
  {tab.icon && <span className={cn('shrink-0 inline-flex', isActive ? iconC : 'text-neutral-400')} style={{ width: 14, height: 14 }}>{tab.icon}</span>}
223
- <span className={cn('truncate text-sm font-medium', isActive ? titleC : 'text-neutral-300')}>{tab.title}</span>
223
+ <span className={cn('truncate text-md font-medium', isActive ? titleC : 'text-neutral-300')}>{tab.title}</span>
224
224
  </div>
225
225
 
226
226
  {/* Line 2: Subtitle with optional icon */}
227
227
  {tab.subtitle && (
228
228
  <div className="flex items-center gap-1 w-full">
229
229
  {tab.subtitleIcon && <span className={cn('shrink-0 inline-flex', isActive ? subIconC : 'text-neutral-500')} style={{ width: 10, height: 10 }}>{tab.subtitleIcon}</span>}
230
- <span className={cn('truncate text-xs', isActive ? subC : 'text-neutral-500')}>{tab.subtitle}</span>
230
+ <span className={cn('truncate text-sm', isActive ? subC : 'text-neutral-500')}>{tab.subtitle}</span>
231
231
  </div>
232
232
  )}
233
233
 
@@ -272,12 +272,12 @@ export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, cla
272
272
  <div className={cn('flex flex-col gap-0.5 px-2.5 py-1.5 rounded-t-lg border-t-2 shadow-xl bg-neutral-800', ghostBorder)}>
273
273
  <div className="flex items-center gap-1.5">
274
274
  {t.icon && <span className={cn('shrink-0 inline-flex', ghostIconC)} style={{ width: 14, height: 14 }}>{t.icon}</span>}
275
- <span className={cn('text-sm font-medium', ghostTitleC)}>{t.title}</span>
275
+ <span className={cn('text-md font-medium', ghostTitleC)}>{t.title}</span>
276
276
  </div>
277
277
  {t.subtitle && (
278
278
  <div className="flex items-center gap-1">
279
279
  {t.subtitleIcon && <span className={cn('shrink-0 inline-flex', ghostSubIconC)} style={{ width: 10, height: 10 }}>{t.subtitleIcon}</span>}
280
- <span className={cn('text-xs', ghostSubC)}>{t.subtitle}</span>
280
+ <span className={cn('text-sm', ghostSubC)}>{t.subtitle}</span>
281
281
  </div>
282
282
  )}
283
283
  </div>