@toolr/ui-design 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/components/hooks/use-click-outside.ts +10 -3
  2. package/components/hooks/use-modal-behavior.ts +24 -0
  3. package/components/hooks/use-navigation-history.ts +7 -2
  4. package/components/hooks/use-resizable-sidebar.ts +38 -0
  5. package/components/lib/form-colors.ts +40 -0
  6. package/components/sections/captured-issues/captured-issues-panel.tsx +3 -3
  7. package/components/sections/captured-issues/use-captured-issues.ts +9 -3
  8. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +4 -40
  9. package/components/sections/prompt-editor/index.ts +0 -7
  10. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +4 -40
  11. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +4 -36
  12. package/components/sections/snippets-editor/snippets-editor.tsx +6 -39
  13. package/components/settings/SettingsHeader.tsx +0 -1
  14. package/components/settings/SettingsTreeNav.tsx +9 -12
  15. package/components/ui/action-dialog.tsx +7 -51
  16. package/components/ui/badge.tsx +4 -20
  17. package/components/ui/breadcrumb.tsx +6 -66
  18. package/components/ui/checkbox.tsx +2 -16
  19. package/components/ui/collapsible-section.tsx +3 -41
  20. package/components/ui/confirm-badge.tsx +3 -20
  21. package/components/ui/cookie-consent.tsx +17 -1
  22. package/components/ui/debounce-border-overlay.tsx +31 -0
  23. package/components/ui/detail-section.tsx +2 -19
  24. package/components/ui/editor-placeholder-card.tsx +10 -9
  25. package/components/ui/execution-details-panel.tsx +2 -7
  26. package/components/ui/file-structure-section.tsx +3 -18
  27. package/components/ui/file-tree.tsx +2 -14
  28. package/components/ui/files-panel.tsx +3 -11
  29. package/components/ui/form-actions.tsx +6 -5
  30. package/components/ui/icon-button.tsx +2 -1
  31. package/components/ui/input.tsx +10 -26
  32. package/components/ui/label.tsx +3 -17
  33. package/components/ui/modal.tsx +3 -15
  34. package/components/ui/nav-card.tsx +2 -17
  35. package/components/ui/navigation-bar.tsx +8 -69
  36. package/components/ui/registry-browser.tsx +6 -20
  37. package/components/ui/registry-card.tsx +3 -7
  38. package/components/ui/resizable-textarea.tsx +13 -35
  39. package/components/ui/segmented-toggle.tsx +2 -1
  40. package/components/ui/select.tsx +2 -11
  41. package/components/ui/selection-grid.tsx +2 -50
  42. package/components/ui/setting-row.tsx +1 -3
  43. package/components/ui/settings-info-box.tsx +5 -22
  44. package/components/ui/status-card.tsx +2 -13
  45. package/components/ui/tab-bar.tsx +3 -29
  46. package/components/ui/toggle.tsx +3 -19
  47. package/components/ui/tooltip.tsx +6 -18
  48. package/dist/index.d.ts +60 -137
  49. package/dist/index.js +1426 -2334
  50. package/index.ts +8 -7
  51. package/package.json +1 -1
  52. package/components/sections/prompt-editor/use-prompt-editor.ts +0 -131
@@ -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
 
@@ -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 {
@@ -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'> {
@@ -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,26 +184,7 @@ 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 && (
@@ -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
@@ -94,7 +80,7 @@ const sizeConfig = {
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
 
@@ -1,6 +1,7 @@
1
- import { useEffect, useRef, useState } from 'react'
1
+ import { useRef, useState } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
3
  import { Info, AlertTriangle, AlertCircle, Check } from 'lucide-react'
4
+ import { useModalBehavior } from '../hooks/use-modal-behavior.ts'
4
5
  import { IconButton, type ActionItem } from './icon-button.tsx'
5
6
  import { FormActions } from './form-actions.tsx'
6
7
  import type { ReactNode } from 'react'
@@ -42,20 +43,7 @@ interface ModalProps {
42
43
  function Modal({ isOpen, onClose, title, children, kind = 'info', size = 'md', hideCloseButton = false, headerActions, testId }: ModalProps) {
43
44
  const modalRef = useRef<HTMLDivElement>(null)
44
45
 
45
- useEffect(() => {
46
- if (!isOpen) return
47
- const handleEscape = (e: KeyboardEvent) => {
48
- if (e.key === 'Escape') onClose()
49
- }
50
- document.addEventListener('keydown', handleEscape)
51
- return () => document.removeEventListener('keydown', handleEscape)
52
- }, [isOpen, onClose])
53
-
54
- useEffect(() => {
55
- if (!isOpen) return
56
- document.body.style.overflow = 'hidden'
57
- return () => { document.body.style.overflow = '' }
58
- }, [isOpen])
46
+ useModalBehavior(isOpen, onClose)
59
47
 
60
48
  if (!isOpen) return null
61
49
 
@@ -1,24 +1,9 @@
1
1
  /** Navigation card with icon, description, label, and hover lift effect. */
2
2
 
3
- import {
4
- Settings, Puzzle, Zap, Shield, Folder, Code, Database, Globe,
5
- Terminal, Star, Bell, User, Users, File, Tag, Sparkles, Search,
6
- Lock, Mail, Cloud, Heart, Bookmark, Pin, Bot, Plug,
7
- } from 'lucide-react'
8
- import type { LucideIcon } from 'lucide-react'
9
- import type { IconName } from './icon-button.tsx'
3
+ import { iconMap, type IconName } from './icon-button.tsx'
10
4
  import { Label, type LabelColor } from './label.tsx'
11
5
  import { cn } from '../lib/cn.ts'
12
6
 
13
- const iconSubset: Partial<Record<IconName, LucideIcon>> = {
14
- settings: Settings, puzzle: Puzzle, zap: Zap, shield: Shield,
15
- folder: Folder, code: Code, database: Database, globe: Globe,
16
- terminal: Terminal, star: Star, bell: Bell, user: User,
17
- users: Users, file: File, tag: Tag, sparkles: Sparkles,
18
- search: Search, lock: Lock, mail: Mail, cloud: Cloud,
19
- heart: Heart, bookmark: Bookmark, pin: Pin, bot: Bot, plug: Plug,
20
- }
21
-
22
7
  export interface NavCardProps {
23
8
  title: string
24
9
  description?: string
@@ -45,7 +30,7 @@ export function NavCard({
45
30
  disabled = false,
46
31
  className,
47
32
  }: NavCardProps) {
48
- const Icon = IconComponent ?? (icon ? iconSubset[icon] : undefined)
33
+ const Icon = IconComponent ?? (icon ? iconMap[icon] : undefined)
49
34
 
50
35
  return (
51
36
  <button
@@ -1,58 +1,14 @@
1
1
  /** Navigation bar with back/forward controls, history dropdown, and inline breadcrumb path. */
2
2
 
3
3
  import { useState, useRef, useCallback } from 'react'
4
- import {
5
- ChevronLeft, ChevronRight, History,
6
- Menu, Home, Layers,
7
- Settings, Folder, File, Code, Terminal, Database,
8
- Globe, Star, Users, User, Tag, Search, Heart,
9
- Zap, Shield, ShieldCheck, Sparkles, Eye, Lock,
10
- Cloud, Wand2, Bell, Bookmark, Pin, Mail, Send,
11
- Image, Bot, Puzzle, Plug, Webhook,
12
- } from 'lucide-react'
4
+ import { ChevronLeft, ChevronRight, History } from 'lucide-react'
13
5
  import type { LucideIcon } from 'lucide-react'
14
- import type { IconName } from './icon-button.tsx'
6
+ import { iconMap, type IconName } from './icon-button.tsx'
15
7
  import type { BreadcrumbSegment } from './breadcrumb.tsx'
8
+ import { ACCENT_NAV, type AccentColor } from '../lib/form-colors.ts'
16
9
  import { cn } from '../lib/cn.ts'
17
10
  import { useClickOutside } from '../hooks/use-click-outside.ts'
18
11
 
19
- const iconSubset: Partial<Record<IconName, LucideIcon>> = {
20
- menu: Menu,
21
- home: Home,
22
- layers: Layers,
23
- folder: Folder,
24
- file: File,
25
- settings: Settings,
26
- code: Code,
27
- terminal: Terminal,
28
- database: Database,
29
- globe: Globe,
30
- star: Star,
31
- users: Users,
32
- user: User,
33
- tag: Tag,
34
- zap: Zap,
35
- shield: Shield,
36
- 'shield-check': ShieldCheck,
37
- sparkles: Sparkles,
38
- eye: Eye,
39
- lock: Lock,
40
- search: Search,
41
- heart: Heart,
42
- cloud: Cloud,
43
- wand: Wand2,
44
- bell: Bell,
45
- bookmark: Bookmark,
46
- pin: Pin,
47
- mail: Mail,
48
- send: Send,
49
- image: Image,
50
- bot: Bot,
51
- puzzle: Puzzle,
52
- plug: Plug,
53
- webhook: Webhook,
54
- }
55
-
56
12
  export interface NavigationBarProps {
57
13
  segments: BreadcrumbSegment[]
58
14
  canGoBack?: boolean
@@ -76,23 +32,6 @@ const sizeConfig = {
76
32
  lg: { text: 'text-lg', segIcon: 'w-5 h-5', navIcon: 'w-5 h-5', navBtn: 'w-9 h-9 rounded-md', px: 'px-3', py: 'py-1.5', sep: 'w-4 h-4', divH: 'h-6' },
77
33
  }
78
34
 
79
- const colorMap: Record<string, { bg: string; text: string }> = {
80
- blue: { bg: 'bg-blue-500/10', text: 'text-blue-400' },
81
- green: { bg: 'bg-green-500/10', text: 'text-green-400' },
82
- purple: { bg: 'bg-purple-500/10', text: 'text-purple-400' },
83
- red: { bg: 'bg-red-500/10', text: 'text-red-400' },
84
- orange: { bg: 'bg-orange-500/10', text: 'text-orange-400' },
85
- cyan: { bg: 'bg-cyan-500/10', text: 'text-cyan-400' },
86
- yellow: { bg: 'bg-yellow-500/10', text: 'text-yellow-400' },
87
- amber: { bg: 'bg-amber-500/10', text: 'text-amber-400' },
88
- emerald: { bg: 'bg-emerald-500/10', text: 'text-emerald-400' },
89
- indigo: { bg: 'bg-indigo-500/10', text: 'text-indigo-400' },
90
- violet: { bg: 'bg-violet-500/10', text: 'text-violet-400' },
91
- sky: { bg: 'bg-sky-500/10', text: 'text-sky-400' },
92
- pink: { bg: 'bg-pink-500/10', text: 'text-pink-400' },
93
- teal: { bg: 'bg-teal-500/10', text: 'text-teal-400' },
94
- neutral: { bg: 'bg-neutral-500/10', text: 'text-neutral-400' },
95
- }
96
35
 
97
36
  function NavButton({ icon: Icon, onClick, disabled, size, active }: {
98
37
  icon: LucideIcon
@@ -134,9 +73,9 @@ function SegmentSeparator({ type, size }: { type: 'chevron' | 'slash' | 'dot'; s
134
73
  }
135
74
 
136
75
  function SegmentIcon({ icon, color, size }: { icon: IconName; color?: string; size: keyof typeof sizeConfig }) {
137
- const Icon = iconSubset[icon]
76
+ const Icon = iconMap[icon]
138
77
  if (!Icon) return null
139
- const c = color && colorMap[color] ? colorMap[color] : null
78
+ const c = color && ACCENT_NAV[color as AccentColor] ? ACCENT_NAV[color as AccentColor] : null
140
79
  return (
141
80
  <span className={c?.text || ''}>
142
81
  <Icon className={sizeConfig[size].segIcon} />
@@ -160,7 +99,7 @@ export function NavigationBar({
160
99
  }: NavigationBarProps) {
161
100
  const s = sizeConfig[size]
162
101
  const hasNav = !!(onBack || onForward)
163
- const LeadIcon = leadingAction ? iconSubset[leadingAction.icon] : null
102
+ const LeadIcon = leadingAction ? iconMap[leadingAction.icon] : null
164
103
 
165
104
  const [historyOpen, setHistoryOpen] = useState(false)
166
105
  const historyRef = useRef<HTMLDivElement>(null)
@@ -221,7 +160,7 @@ export function NavigationBar({
221
160
  {seg.icon && <SegmentIcon icon={seg.icon} color={seg.color} size="xs" />}
222
161
  <span className={cn(
223
162
  'text-sm truncate',
224
- seg.color && colorMap[seg.color] ? colorMap[seg.color].text : 'text-neutral-300',
163
+ seg.color && ACCENT_NAV[seg.color as AccentColor] ? ACCENT_NAV[seg.color as AccentColor].text : 'text-neutral-300',
225
164
  )}>
226
165
  {seg.label}
227
166
  </span>
@@ -243,7 +182,7 @@ export function NavigationBar({
243
182
  {segments.map((segment, index) => {
244
183
  const isLast = index === segments.length - 1
245
184
  const isClickable = !isLast && !!segment.onClick
246
- const colors = segment.color && colorMap[segment.color] ? colorMap[segment.color] : null
185
+ const colors = segment.color && ACCENT_NAV[segment.color as AccentColor] ? ACCENT_NAV[segment.color as AccentColor] : null
247
186
 
248
187
  return (
249
188
  <div key={segment.id} className="flex items-center gap-1">
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode, useState, useEffect, useRef, useCallback } from 'react'
2
2
  import { Search, ArrowRight, RefreshCw, Loader2, X, AlertTriangle } from 'lucide-react'
3
+ import { DebounceBorderOverlay } from './debounce-border-overlay.tsx'
3
4
  import { IconButton } from './icon-button.tsx'
4
5
  import { Input } from './input.tsx'
5
6
  import { RegistryCard, type RegistryCardProps } from './registry-card.tsx'
@@ -123,6 +124,10 @@ export function RegistryBrowser({
123
124
  }, 150)
124
125
  }, [onScrollChange])
125
126
 
127
+ useEffect(() => {
128
+ return () => clearTimeout(scrollTimerRef.current)
129
+ }, [])
130
+
126
131
  const loadMore = useCallback(() => {
127
132
  setVisibleCount((prev: number) => Math.min(prev + PAGE_SIZE, totalCount))
128
133
  }, [totalCount])
@@ -170,26 +175,7 @@ export function RegistryBrowser({
170
175
  </button>
171
176
  )}
172
177
  {debounceKey != null && debounceKey > 0 && (
173
- <svg
174
- key={debounceKey}
175
- className="absolute inset-0 pointer-events-none text-emerald-400/70"
176
- style={{ width: '100%', height: '100%' }}
177
- >
178
- <rect
179
- x="1" y="1" rx="5" ry="5"
180
- fill="none"
181
- stroke="currentColor"
182
- strokeWidth="1.5"
183
- pathLength="100"
184
- strokeDasharray="100"
185
- strokeDashoffset="0"
186
- style={{
187
- width: 'calc(100% - 2px)',
188
- height: 'calc(100% - 2px)',
189
- animation: `debounce-border-drain ${debounceDurationMs}ms linear forwards`,
190
- }}
191
- />
192
- </svg>
178
+ <DebounceBorderOverlay debounceKey={debounceKey} durationMs={debounceDurationMs} />
193
179
  )}
194
180
  </div>
195
181
 
@@ -5,7 +5,7 @@ import {
5
5
  } from 'lucide-react'
6
6
  import { Tooltip } from './tooltip.tsx'
7
7
  import { IconButton, type IconName } from './icon-button.tsx'
8
- import { Label, type LabelColor } from './label.tsx'
8
+ import { Label, type LabelColor, smartCapitalize } from './label.tsx'
9
9
  import { AiToolIcon, AI_TOOL_NAMES, type AiToolKey } from '../lib/ai-tools.tsx'
10
10
  export { AiToolIcon, AI_TOOL_NAMES, type AiToolKey }
11
11
 
@@ -72,10 +72,6 @@ function getCategoryLabelColor(category: string): LabelColor {
72
72
  return CATEGORY_LABEL_COLORS[Math.abs(hash) % CATEGORY_LABEL_COLORS.length]
73
73
  }
74
74
 
75
- export function formatCategory(category: string): string {
76
- return category.replace(/(^|-)(\w)/g, (_, sep, ch) => sep + ch.toUpperCase())
77
- }
78
-
79
75
  // ── Time helpers ──────────────────────────────────────────────────────────────
80
76
 
81
77
  function formatRelativeTime(isoDate: string): string {
@@ -153,10 +149,10 @@ function CardClickable({ children, onClick }: { children: ReactNode; onClick: (e
153
149
  function CategoryBadge({ category, onFilter }: { category: string; onFilter?: (category: string) => void }) {
154
150
  return (
155
151
  <Label
156
- text={formatCategory(category)}
152
+ text={smartCapitalize(category)}
157
153
  color={getCategoryLabelColor(category)}
158
154
  icon="tag"
159
- tooltip={{ description: onFilter ? `${formatCategory(category)} \u00b7 Click to filter` : formatCategory(category) }}
155
+ tooltip={{ description: onFilter ? `${smartCapitalize(category)} \u00b7 Click to filter` : smartCapitalize(category) }}
160
156
  size="sm"
161
157
  onClick={onFilter ? () => onFilter(category) : undefined}
162
158
  />
@@ -115,10 +115,10 @@ function useResize(minHeight: number, onHeightChange?: (height: number) => void)
115
115
  // Code variant — Monaco editor with resize handle
116
116
  // ---------------------------------------------------------------------------
117
117
 
118
- const MONACO_THEME = 'resizable-textarea-dark'
119
- let themeRegistered = false
118
+ const MONACO_THEME_PREFIX = 'resizable-textarea'
119
+ const registeredThemes = new Set<string>()
120
120
 
121
- const codeWrapperClasses = {
121
+ const wrapperVariantClasses = {
122
122
  filled: 'bg-neutral-800 border rounded-lg overflow-hidden',
123
123
  outline: 'bg-transparent border rounded-lg overflow-hidden',
124
124
  }
@@ -139,7 +139,7 @@ function ResizableCode({
139
139
 
140
140
  return (
141
141
  <div
142
- className={`relative ${codeWrapperClasses[variant]} ${FORM_COLORS[color].border} ${wrapperClassName ?? ''}`}
142
+ className={`relative ${wrapperVariantClasses[variant]} ${FORM_COLORS[color].border} ${wrapperClassName ?? ''}`}
143
143
  data-resizable-wrapper
144
144
  style={{ height: height ?? minHeight }}
145
145
  >
@@ -148,7 +148,7 @@ function ResizableCode({
148
148
  language={language}
149
149
  value={value}
150
150
  onChange={(v) => onChange?.(v ?? '')}
151
- theme={MONACO_THEME}
151
+ theme={`${MONACO_THEME_PREFIX}-${variant}`}
152
152
  options={{
153
153
  minimap: { enabled: false },
154
154
  fontSize: 13,
@@ -170,8 +170,9 @@ function ResizableCode({
170
170
  glyphMargin: false,
171
171
  }}
172
172
  beforeMount={(monaco) => {
173
- if (!themeRegistered) {
174
- monaco.editor.defineTheme(MONACO_THEME, {
173
+ const themeName = `${MONACO_THEME_PREFIX}-${variant}`
174
+ if (!registeredThemes.has(themeName)) {
175
+ monaco.editor.defineTheme(themeName, {
175
176
  base: 'vs-dark',
176
177
  inherit: true,
177
178
  rules: [],
@@ -182,7 +183,7 @@ function ResizableCode({
182
183
  'editor.lineHighlightBorder': '#00000000',
183
184
  },
184
185
  })
185
- themeRegistered = true
186
+ registeredThemes.add(themeName)
186
187
  }
187
188
  }}
188
189
  />
@@ -202,11 +203,6 @@ function ResizableCode({
202
203
  // Children variant — wraps any element with resize handle
203
204
  // ---------------------------------------------------------------------------
204
205
 
205
- const childrenVariantClasses = {
206
- filled: 'bg-neutral-800 border rounded-lg overflow-hidden',
207
- outline: 'bg-transparent border rounded-lg overflow-hidden',
208
- }
209
-
210
206
  function ResizableChildren({
211
207
  children,
212
208
  wrapperClassName,
@@ -224,7 +220,7 @@ function ResizableChildren({
224
220
 
225
221
  return (
226
222
  <div
227
- className={`relative ${childrenVariantClasses[variant]} ${FORM_COLORS[color].border} ${wrapperClassName ?? ''}`}
223
+ className={`relative ${wrapperVariantClasses[variant]} ${FORM_COLORS[color].border} ${wrapperClassName ?? ''}`}
228
224
  data-resizable-wrapper
229
225
  style={height != null ? { height } : undefined}
230
226
  >
@@ -250,25 +246,7 @@ function ResizableField({
250
246
  color = 'blue',
251
247
  ...props
252
248
  }: ResizableTextareaBaseProps & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'color'>) {
253
- const taRef = useRef<HTMLTextAreaElement>(null)
254
- const dragRef = useRef<{ startY: number; startH: number } | null>(null)
255
- const onResizeStart = useCallback((e: React.MouseEvent) => {
256
- e.preventDefault()
257
- const ta = taRef.current
258
- if (!ta) return
259
- dragRef.current = { startY: e.clientY, startH: ta.offsetHeight }
260
- const onMove = (ev: MouseEvent) => {
261
- if (!dragRef.current || !ta) return
262
- ta.style.height = `${Math.max(40, dragRef.current.startH + ev.clientY - dragRef.current.startY)}px`
263
- }
264
- const onUp = () => {
265
- dragRef.current = null
266
- document.removeEventListener('mousemove', onMove)
267
- document.removeEventListener('mouseup', onUp)
268
- }
269
- document.addEventListener('mousemove', onMove)
270
- document.addEventListener('mouseup', onUp)
271
- }, [])
249
+ const { height, onResizeStart } = useResize(40)
272
250
 
273
251
  const className = `w-full rounded-lg focus:outline-none transition-colors ${variantClasses[variant]} ${FORM_COLORS[color].border} ${FORM_COLORS[color].focus} ${props.className ?? ''}`
274
252
 
@@ -277,8 +255,8 @@ function ResizableField({
277
255
  }
278
256
 
279
257
  return (
280
- <div className={`relative ${wrapperClassName ?? ''}`}>
281
- <textarea ref={taRef} {...props} className={className} style={{ resize: 'none', ...props.style }} />
258
+ <div className={`relative ${wrapperClassName ?? ''}`} data-resizable-wrapper>
259
+ <textarea {...props} className={className} style={{ resize: 'none', ...props.style, ...(height != null ? { height } : {}) }} />
282
260
  <div
283
261
  className="absolute bottom-[8px] right-[3px] w-4 h-3 cursor-row-resize flex items-end justify-end"
284
262
  onMouseDown={onResizeStart}
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from 'react'
2
+ import type { AccentColor } from '../lib/form-colors.ts'
2
3
  import { Tooltip, type TooltipContent, type TooltipPosition } from './tooltip.tsx'
3
4
 
4
5
  export interface SegmentedToggleOption<T extends string> {
@@ -12,7 +13,7 @@ export interface SegmentedToggleProps<T extends string> {
12
13
  options: SegmentedToggleOption<T>[]
13
14
  value: T
14
15
  onChange: (value: T) => void
15
- accentColor?: 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow' | 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky' | 'pink' | 'teal'
16
+ accentColor?: AccentColor
16
17
  /** Visual style: 'filled' (default) has a container background, 'outline' is transparent */
17
18
  variant?: 'filled' | 'outline'
18
19
  size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
@@ -1,6 +1,7 @@
1
1
  import { useState, useRef, useEffect, useCallback, type ReactNode } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
3
  import { ChevronDown, Check } from 'lucide-react'
4
+ import { useClickOutside } from '../hooks/use-click-outside.ts'
4
5
  import { useDropdownMaxHeight } from '../hooks/use-dropdown-max-height.ts'
5
6
  import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
6
7
 
@@ -74,17 +75,7 @@ export function Select<T extends string | number = string>({
74
75
  }, [])
75
76
 
76
77
  // Close on click outside both the trigger and the portal menu
77
- useEffect(() => {
78
- if (!isOpen) return
79
- const handleClick = (event: MouseEvent) => {
80
- const target = event.target as Node
81
- if (ref.current?.contains(target)) return
82
- if (menuRef.current?.contains(target)) return
83
- close()
84
- }
85
- document.addEventListener('mousedown', handleClick)
86
- return () => document.removeEventListener('mousedown', handleClick)
87
- }, [isOpen, close])
78
+ useClickOutside([ref, menuRef], isOpen, close)
88
79
 
89
80
  useEffect(() => {
90
81
  if (highlightIdx >= 0 && menuRef.current) {
@@ -1,54 +1,8 @@
1
- import {
2
- Check, Settings, Code, Folder, File, Terminal, Globe, Database, Cloud,
3
- Sparkles, Zap, Shield, ShieldCheck, Wand2, Star, Heart, Bell,
4
- Search, Filter, Eye, Lock, User, Users, Image, Tag, Pin, Mail,
5
- Send, Bookmark, Play, Pause, Bot, Plug, Puzzle, Webhook, Scan,
6
- } from 'lucide-react'
7
- import type { LucideIcon } from 'lucide-react'
8
- import type { IconName } from './icon-button.tsx'
1
+ import { iconMap, type IconName } from './icon-button.tsx'
9
2
  import type { ConfirmBadgeColor } from './confirm-badge.tsx'
10
3
  import { cn } from '../lib/cn.ts'
11
4
  import { AiToolIcon, AI_TOOL_NAMES, type AiToolKey } from '../lib/ai-tools.tsx'
12
5
 
13
- const iconMap: Partial<Record<IconName, LucideIcon>> = {
14
- 'check': Check,
15
- 'settings': Settings,
16
- 'code': Code,
17
- 'folder': Folder,
18
- 'file': File,
19
- 'terminal': Terminal,
20
- 'globe': Globe,
21
- 'database': Database,
22
- 'cloud': Cloud,
23
- 'sparkles': Sparkles,
24
- 'zap': Zap,
25
- 'shield': Shield,
26
- 'shield-check': ShieldCheck,
27
- 'wand': Wand2,
28
- 'star': Star,
29
- 'heart': Heart,
30
- 'bell': Bell,
31
- 'search': Search,
32
- 'filter': Filter,
33
- 'eye': Eye,
34
- 'lock': Lock,
35
- 'user': User,
36
- 'users': Users,
37
- 'image': Image,
38
- 'tag': Tag,
39
- 'pin': Pin,
40
- 'mail': Mail,
41
- 'send': Send,
42
- 'bookmark': Bookmark,
43
- 'play': Play,
44
- 'pause': Pause,
45
- 'bot': Bot,
46
- 'plug': Plug,
47
- 'puzzle': Puzzle,
48
- 'webhook': Webhook,
49
- 'scan': Scan,
50
- }
51
-
52
6
  /* ── Preset logos (shared AiToolIcon) ─────────────────────── */
53
7
 
54
8
  type IconProps = { className?: string; style?: React.CSSProperties }
@@ -142,9 +96,7 @@ function resolveColor(color?: string): string {
142
96
  }
143
97
 
144
98
  function autoColumns(itemCount: number): number {
145
- if (itemCount <= 2) return itemCount
146
- if (itemCount <= 4) return itemCount
147
- return 5
99
+ return Math.min(itemCount, 5)
148
100
  }
149
101
 
150
102
  /* ── Component ─────────────────────────────────────────────── */
@@ -11,7 +11,7 @@
11
11
  * - input: renders a text Input
12
12
  */
13
13
 
14
- import { Toggle, type ToggleColor, type ToggleSize, type ToggleVariant } from './toggle.tsx'
14
+ import { Toggle, type ToggleColor, type ToggleSize } from './toggle.tsx'
15
15
  import { Select, type SelectOption } from './select.tsx'
16
16
  import { Input } from './input.tsx'
17
17
 
@@ -28,7 +28,6 @@ interface SettingRowToggle extends SettingRowBase {
28
28
  onChange: (checked: boolean) => void
29
29
  color?: ToggleColor
30
30
  size?: ToggleSize
31
- variant?: ToggleVariant
32
31
  }
33
32
 
34
33
  interface SettingRowSelect extends SettingRowBase {
@@ -67,7 +66,6 @@ export function SettingRow(props: SettingRowProps) {
67
66
  disabled={disabled}
68
67
  color={props.color}
69
68
  size={props.size}
70
- variant={props.variant}
71
69
  />
72
70
  )}
73
71
  {props.type === 'select' && (