@toolr/ui-design 0.1.0

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 (112) hide show
  1. package/README.md +63 -0
  2. package/components/content/info-panel-primitives.tsx +297 -0
  3. package/components/diagrams/diagram-utils.tsx +908 -0
  4. package/components/hooks/use-click-outside.ts +27 -0
  5. package/components/hooks/use-dropdown-max-height.ts +20 -0
  6. package/components/hooks/use-navigation-history.ts +94 -0
  7. package/components/lib/ai-tools.tsx +44 -0
  8. package/components/lib/cn.ts +6 -0
  9. package/components/lib/form-colors.ts +32 -0
  10. package/components/lib/theme-engine.ts +97 -0
  11. package/components/lib/toolr-brand.tsx +31 -0
  12. package/components/sections/ai-tools-paths/index.ts +37 -0
  13. package/components/sections/ai-tools-paths/tools-paths-panel.tsx +212 -0
  14. package/components/sections/ai-tools-paths/types.ts +111 -0
  15. package/components/sections/ai-tools-paths/use-tools-paths.ts +159 -0
  16. package/components/sections/captured-issues/captured-issues-panel.tsx +214 -0
  17. package/components/sections/captured-issues/index.ts +38 -0
  18. package/components/sections/captured-issues/types.ts +113 -0
  19. package/components/sections/captured-issues/use-captured-issues.ts +111 -0
  20. package/components/sections/golden-snapshots/file-diff-viewer.tsx +420 -0
  21. package/components/sections/golden-snapshots/golden-sync-panel.tsx +223 -0
  22. package/components/sections/golden-snapshots/index.ts +145 -0
  23. package/components/sections/golden-snapshots/snapshot-manager.tsx +200 -0
  24. package/components/sections/golden-snapshots/status-overview.tsx +305 -0
  25. package/components/sections/golden-snapshots/types.ts +288 -0
  26. package/components/sections/golden-snapshots/use-golden-sync.ts +477 -0
  27. package/components/sections/golden-snapshots/version-manager.tsx +186 -0
  28. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +210 -0
  29. package/components/sections/prompt-editor/index.ts +121 -0
  30. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +276 -0
  31. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +514 -0
  32. package/components/sections/prompt-editor/types.ts +101 -0
  33. package/components/sections/prompt-editor/use-prompt-editor.ts +131 -0
  34. package/components/sections/report-bug/error-logger.ts +392 -0
  35. package/components/sections/report-bug/index.ts +59 -0
  36. package/components/sections/report-bug/issue-reporter-api.ts +83 -0
  37. package/components/sections/report-bug/report-bug-form.tsx +282 -0
  38. package/components/sections/report-bug/screenshot-uploader.tsx +228 -0
  39. package/components/sections/report-bug/use-report-bug.ts +170 -0
  40. package/components/sections/snapshot-browser/index.ts +53 -0
  41. package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +147 -0
  42. package/components/sections/snapshot-browser/snapshot-tree.tsx +451 -0
  43. package/components/sections/snapshot-browser/types.ts +106 -0
  44. package/components/sections/snapshot-browser/use-snapshot-browser.ts +125 -0
  45. package/components/sections/snippets-editor/index.ts +31 -0
  46. package/components/sections/snippets-editor/snippets-editor.tsx +381 -0
  47. package/components/sections/snippets-editor/types.ts +48 -0
  48. package/components/sections/snippets-editor/use-snippets-editor.ts +217 -0
  49. package/components/ui/action-dialog.tsx +309 -0
  50. package/components/ui/ai-action-button.tsx +137 -0
  51. package/components/ui/ai-execution-action-buttons.tsx +106 -0
  52. package/components/ui/badge.tsx +67 -0
  53. package/components/ui/bottom-panel-header.tsx +240 -0
  54. package/components/ui/breadcrumb.tsx +168 -0
  55. package/components/ui/checkbox.tsx +102 -0
  56. package/components/ui/collapsible-section.tsx +100 -0
  57. package/components/ui/confirm-badge.tsx +71 -0
  58. package/components/ui/detail-section.tsx +67 -0
  59. package/components/ui/detail-view-wrapper.tsx +55 -0
  60. package/components/ui/editor-placeholder-card.tsx +197 -0
  61. package/components/ui/editor-toolbar.tsx +123 -0
  62. package/components/ui/execution-details-panel.tsx +93 -0
  63. package/components/ui/extension-list-card.tsx +105 -0
  64. package/components/ui/file-structure-section.tsx +373 -0
  65. package/components/ui/file-tree.tsx +171 -0
  66. package/components/ui/files-panel.tsx +251 -0
  67. package/components/ui/filter-dropdown.tsx +173 -0
  68. package/components/ui/form-actions.tsx +127 -0
  69. package/components/ui/frontmatter-form-header.tsx +80 -0
  70. package/components/ui/icon-button.tsx +388 -0
  71. package/components/ui/input.tsx +211 -0
  72. package/components/ui/label.tsx +159 -0
  73. package/components/ui/layout-tab-bar.tsx +289 -0
  74. package/components/ui/modal.tsx +194 -0
  75. package/components/ui/nav-card.tsx +81 -0
  76. package/components/ui/navigation-bar.tsx +285 -0
  77. package/components/ui/number-input.tsx +165 -0
  78. package/components/ui/registry-browser.tsx +261 -0
  79. package/components/ui/registry-card.tsx +710 -0
  80. package/components/ui/registry-detail.tsx +224 -0
  81. package/components/ui/resizable-textarea.tsx +290 -0
  82. package/components/ui/scope-badge.tsx +67 -0
  83. package/components/ui/segmented-toggle.tsx +133 -0
  84. package/components/ui/select.tsx +172 -0
  85. package/components/ui/selection-grid.tsx +313 -0
  86. package/components/ui/setting-row.tsx +97 -0
  87. package/components/ui/snapshot-card.tsx +107 -0
  88. package/components/ui/snippets-panel.tsx +161 -0
  89. package/components/ui/sort-dropdown.tsx +109 -0
  90. package/components/ui/status-card.tsx +96 -0
  91. package/components/ui/tab-bar.tsx +340 -0
  92. package/components/ui/toggle.tsx +142 -0
  93. package/components/ui/tooltip.tsx +326 -0
  94. package/dist/content.d.ts +110 -0
  95. package/dist/content.js +195 -0
  96. package/dist/diagrams.d.ts +371 -0
  97. package/dist/diagrams.js +702 -0
  98. package/dist/index.d.ts +2714 -0
  99. package/dist/index.js +11220 -0
  100. package/dist/preset.d.ts +24 -0
  101. package/dist/preset.js +17 -0
  102. package/dist/tokens/tokens/primitives.css +45 -0
  103. package/dist/tokens/tokens/semantic.css +46 -0
  104. package/dist/tokens/tokens/theme.css +11 -0
  105. package/dist/tokens/tokens/tokens.json +65 -0
  106. package/index.ts +123 -0
  107. package/package.json +63 -0
  108. package/tailwind-preset.ts +22 -0
  109. package/tokens/primitives.css +45 -0
  110. package/tokens/semantic.css +46 -0
  111. package/tokens/theme.css +11 -0
  112. package/tokens/tokens.json +65 -0
@@ -0,0 +1,127 @@
1
+ import { IconButton, type IconName, type IconButtonProps, type IconButtonStatus } from './icon-button.tsx'
2
+
3
+ export interface FormActionsProps {
4
+ /** Cancel handler — renders X button. Optional (e.g. AlertModal has no cancel). */
5
+ onCancel?: () => void
6
+ cancelTooltip?: string
7
+
8
+ /** Back handler — optional, renders left-arrow on the left side */
9
+ onBack?: () => void
10
+ backTooltip?: string
11
+
12
+ /** Minimize handler — optional, renders minimize icon before Cancel */
13
+ onMinimize?: () => void
14
+ minimizeTooltip?: string
15
+
16
+ /** Confirm handler — optional, renders between Cancel and Next */
17
+ onConfirm?: () => void
18
+ confirmTooltip?: string
19
+ confirmIcon?: IconName
20
+ confirmColor?: IconButtonProps['color']
21
+ confirmDisabled?: boolean
22
+ confirmStatus?: IconButtonStatus
23
+
24
+ /** Next handler — optional, renders right-arrow for wizard navigation */
25
+ onNext?: () => void
26
+ nextTooltip?: string
27
+
28
+ /** Status text shown on the left side of the footer */
29
+ statusText?: string
30
+
31
+ border?: boolean
32
+ padding?: 'compact' | 'normal' | 'modal'
33
+ }
34
+
35
+ const PADDING_CLASSES = {
36
+ compact: 'pt-2',
37
+ normal: 'pt-2 border-t border-neutral-700',
38
+ modal: 'px-4 py-3 border-t border-neutral-700',
39
+ } as const
40
+
41
+ const DEFAULT_BORDER = {
42
+ compact: false,
43
+ normal: true,
44
+ modal: true,
45
+ } as const
46
+
47
+ export function FormActions({
48
+ onCancel,
49
+ cancelTooltip = 'Cancel',
50
+ onBack,
51
+ backTooltip = 'Back',
52
+ onMinimize,
53
+ minimizeTooltip = 'Minimize',
54
+ onConfirm,
55
+ confirmTooltip = 'Confirm',
56
+ confirmIcon = 'check',
57
+ confirmColor = 'blue',
58
+ confirmDisabled,
59
+ confirmStatus,
60
+ onNext,
61
+ nextTooltip = 'Next',
62
+ statusText,
63
+ border,
64
+ padding = 'normal',
65
+ }: FormActionsProps) {
66
+ const showBorder = border ?? DEFAULT_BORDER[padding]
67
+ const base = PADDING_CLASSES[padding]
68
+ const paddingClass = showBorder
69
+ ? base
70
+ : base.replace(/\s*border-t\s+border-\[#374151\]/g, '')
71
+
72
+ const hasLeft = onBack || statusText
73
+
74
+ return (
75
+ <div className={`flex items-center ${hasLeft ? 'justify-between' : 'justify-end'} gap-2 ${paddingClass}`}>
76
+ {hasLeft && (
77
+ <div className="flex items-center gap-2">
78
+ {onBack && (
79
+ <IconButton
80
+ icon="arrow-left"
81
+ color="neutral"
82
+ onClick={onBack}
83
+ tooltip={{ description: backTooltip }}
84
+ />
85
+ )}
86
+ {statusText && <span className="text-xs text-neutral-500">{statusText}</span>}
87
+ </div>
88
+ )}
89
+ <div className="flex items-center gap-2">
90
+ {onMinimize && (
91
+ <IconButton
92
+ icon="minimize"
93
+ color="neutral"
94
+ onClick={onMinimize}
95
+ tooltip={{ description: minimizeTooltip }}
96
+ />
97
+ )}
98
+ {onCancel && (
99
+ <IconButton
100
+ icon="x"
101
+ color="neutral"
102
+ onClick={onCancel}
103
+ tooltip={{ description: cancelTooltip }}
104
+ />
105
+ )}
106
+ {onConfirm && (
107
+ <IconButton
108
+ icon={confirmIcon}
109
+ color={confirmColor}
110
+ onClick={onConfirm}
111
+ disabled={confirmDisabled}
112
+ status={confirmStatus}
113
+ tooltip={{ description: confirmTooltip }}
114
+ />
115
+ )}
116
+ {onNext && (
117
+ <IconButton
118
+ icon="arrow-right"
119
+ color="blue"
120
+ onClick={onNext}
121
+ tooltip={{ description: nextTooltip }}
122
+ />
123
+ )}
124
+ </div>
125
+ </div>
126
+ )
127
+ }
@@ -0,0 +1,80 @@
1
+ import type { ReactNode } from 'react'
2
+ import { ChevronRight } from 'lucide-react'
3
+ import { Checkbox } from './checkbox.tsx'
4
+
5
+ export interface FrontmatterFormHeaderProps {
6
+ collapsed: boolean
7
+ onToggle: () => void
8
+ /** Summary text shown when collapsed */
9
+ renderSummary: () => string
10
+ children: ReactNode
11
+ /** Whether frontmatter is currently enabled (content has ---) */
12
+ frontmatterEnabled?: boolean
13
+ /** Toggle frontmatter on/off */
14
+ onFrontmatterToggle?: (enabled: boolean) => void
15
+ readOnly?: boolean
16
+ }
17
+
18
+ export function FrontmatterFormHeader({
19
+ collapsed,
20
+ onToggle,
21
+ renderSummary,
22
+ children,
23
+ frontmatterEnabled,
24
+ onFrontmatterToggle,
25
+ readOnly,
26
+ }: FrontmatterFormHeaderProps) {
27
+ const hasFm = frontmatterEnabled !== false
28
+
29
+ return (
30
+ <div className="bg-neutral-900 border-b border-neutral-800 select-none">
31
+ {/* Header bar — always visible, always expandable */}
32
+ <button
33
+ type="button"
34
+ onClick={onToggle}
35
+ className="flex items-center gap-2 w-full px-3 py-3 hover:bg-neutral-800/50 cursor-pointer transition-colors"
36
+ >
37
+ <ChevronRight
38
+ className={`w-3.5 h-3.5 text-neutral-500 transition-transform duration-150 ${
39
+ collapsed ? '' : 'rotate-90'
40
+ }`}
41
+ />
42
+ <span className="text-xs font-medium text-neutral-400 uppercase tracking-wide">
43
+ Configuration
44
+ </span>
45
+ {collapsed && hasFm && (
46
+ <span className="text-[11px] text-neutral-500 font-mono ml-2 truncate">
47
+ {renderSummary()}
48
+ </span>
49
+ )}
50
+ {collapsed && !hasFm && (
51
+ <span className="text-[11px] text-neutral-600 ml-2">No frontmatter</span>
52
+ )}
53
+ </button>
54
+
55
+ {/* Expanded area */}
56
+ {!collapsed && (
57
+ <div className="px-3 pb-3 pt-1">
58
+ {/* Frontmatter checkbox — always first */}
59
+ {onFrontmatterToggle && (
60
+ <div className="inline-flex items-center gap-1.5 mb-3">
61
+ <Checkbox
62
+ checked={hasFm}
63
+ onChange={(checked) => onFrontmatterToggle(checked)}
64
+ disabled={readOnly}
65
+ />
66
+ <span
67
+ className="text-xs text-neutral-400 cursor-pointer"
68
+ onClick={() => !readOnly && onFrontmatterToggle(!hasFm)}
69
+ >
70
+ Add YAML frontmatter to file
71
+ </span>
72
+ </div>
73
+ )}
74
+ {/* Form content — only when frontmatter is enabled */}
75
+ {hasFm && children}
76
+ </div>
77
+ )}
78
+ </div>
79
+ )
80
+ }
@@ -0,0 +1,388 @@
1
+ /**
2
+ * IconButton - Icon-only button with tooltip and variant support
3
+ *
4
+ * Used by:
5
+ * - Toolbars - action buttons (save, edit, delete)
6
+ * - Panel headers - expand/collapse, settings
7
+ * - Card actions - quick actions on cards
8
+ *
9
+ * Features:
10
+ * - Multiple size variants (xss, xs, sm, md, lg)
11
+ * - Color variants (gray, green, red, blue, etc.)
12
+ * - Optional badge with count
13
+ * - Strikethrough state for disabled appearance
14
+ * - Portal-based tooltip via Tooltip component
15
+ */
16
+
17
+ import type { MouseEvent, ReactNode } from 'react'
18
+ import {
19
+ ArrowDownToLine,
20
+ ArrowLeft, ArrowRight, ArrowUp, ArrowDown,
21
+ ChevronLeft, ChevronRight, ChevronUp, ChevronDown,
22
+ ChevronsUpDown, ChevronsDownUp,
23
+ Check, X, Plus, Minus, Pencil, Trash2, Copy, Save,
24
+ RefreshCw, RotateCcw, Undo2, Redo2,
25
+ Search, Filter, Download, Upload, ExternalLink, Link2, Unlink2,
26
+ Eye, EyeOff, Lock, Unlock, Settings, MoreHorizontal, MoreVertical,
27
+ Loader2, CheckCircle2, AlertTriangle, XCircle,
28
+ Info, HelpCircle,
29
+ User, Users, Folder, File, FileText, Image, Code, Terminal,
30
+ Star, Heart, Bell, Bookmark, Tag, Pin, Mail, Send,
31
+ Globe, Database, Cloud,
32
+ Wand2, Shield, ShieldCheck, Zap, Sparkles,
33
+ Play, Pause, Square, StopCircle, CirclePlay,
34
+ Menu, GripVertical, Maximize2, Minimize2,
35
+ Scan,
36
+ Webhook, Bot, Puzzle, Plug,
37
+ PanelBottomClose,
38
+ Package, Wrench, Store, ScrollText, Cpu, FlaskConical, Layers, Timer, Camera,
39
+ AlertCircle, FileCode, Gauge, Home, PieChart, Settings2,
40
+ } from 'lucide-react'
41
+ import type { LucideIcon } from 'lucide-react'
42
+ import { Tooltip, type TooltipContent } from './tooltip.tsx'
43
+
44
+ export const iconMap = {
45
+ 'arrow-down-to-line': ArrowDownToLine,
46
+ 'arrow-left': ArrowLeft,
47
+ 'arrow-right': ArrowRight,
48
+ 'arrow-up': ArrowUp,
49
+ 'arrow-down': ArrowDown,
50
+ 'chevron-left': ChevronLeft,
51
+ 'chevron-right': ChevronRight,
52
+ 'chevron-up': ChevronUp,
53
+ 'chevron-down': ChevronDown,
54
+ 'chevrons-up-down': ChevronsUpDown,
55
+ 'chevrons-down-up': ChevronsDownUp,
56
+ 'check': Check,
57
+ 'x': X,
58
+ 'plus': Plus,
59
+ 'minus': Minus,
60
+ 'pencil': Pencil,
61
+ 'trash': Trash2,
62
+ 'copy': Copy,
63
+ 'save': Save,
64
+ 'refresh': RefreshCw,
65
+ 'rotate': RotateCcw,
66
+ 'undo': Undo2,
67
+ 'redo': Redo2,
68
+ 'search': Search,
69
+ 'filter': Filter,
70
+ 'download': Download,
71
+ 'upload': Upload,
72
+ 'external-link': ExternalLink,
73
+ 'link': Link2,
74
+ 'unlink': Unlink2,
75
+ 'eye': Eye,
76
+ 'eye-off': EyeOff,
77
+ 'lock': Lock,
78
+ 'unlock': Unlock,
79
+ 'settings': Settings,
80
+ 'more-h': MoreHorizontal,
81
+ 'more-v': MoreVertical,
82
+ 'loader': Loader2,
83
+ 'check-circle': CheckCircle2,
84
+ 'alert-triangle': AlertTriangle,
85
+ 'x-circle': XCircle,
86
+ 'info': Info,
87
+ 'help': HelpCircle,
88
+ 'user': User,
89
+ 'users': Users,
90
+ 'folder': Folder,
91
+ 'file': File,
92
+ 'file-text': FileText,
93
+ 'image': Image,
94
+ 'code': Code,
95
+ 'terminal': Terminal,
96
+ 'star': Star,
97
+ 'heart': Heart,
98
+ 'bell': Bell,
99
+ 'bookmark': Bookmark,
100
+ 'tag': Tag,
101
+ 'pin': Pin,
102
+ 'mail': Mail,
103
+ 'send': Send,
104
+ 'globe': Globe,
105
+ 'database': Database,
106
+ 'cloud': Cloud,
107
+ 'wand': Wand2,
108
+ 'shield': Shield,
109
+ 'shield-check': ShieldCheck,
110
+ 'zap': Zap,
111
+ 'sparkles': Sparkles,
112
+ 'play': Play,
113
+ 'pause': Pause,
114
+ 'stop': Square,
115
+ 'stop-circle': StopCircle,
116
+ 'circle-play': CirclePlay,
117
+ 'scan': Scan,
118
+ 'menu': Menu,
119
+ 'grip': GripVertical,
120
+ 'maximize': Maximize2,
121
+ 'minimize': Minimize2,
122
+ 'webhook': Webhook,
123
+ 'bot': Bot,
124
+ 'puzzle': Puzzle,
125
+ 'plug': Plug,
126
+ 'panel-bottom-close': PanelBottomClose,
127
+ 'package': Package,
128
+ 'wrench': Wrench,
129
+ 'store': Store,
130
+ 'scroll-text': ScrollText,
131
+ 'cpu': Cpu,
132
+ 'flask-conical': FlaskConical,
133
+ 'layers': Layers,
134
+ 'timer': Timer,
135
+ 'camera': Camera,
136
+ 'alert-circle': AlertCircle,
137
+ 'file-code': FileCode,
138
+ 'gauge': Gauge,
139
+ 'home': Home,
140
+ 'pie-chart': PieChart,
141
+ 'settings-2': Settings2,
142
+ } as const
143
+
144
+ export type IconName = keyof typeof iconMap
145
+
146
+ export interface ActionItem {
147
+ icon: IconName
148
+ onClick: () => void
149
+ color?: IconButtonColor
150
+ size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
151
+ variant?: IconButtonVariant
152
+ tooltip?: TooltipContent
153
+ disabled?: boolean
154
+ status?: IconButtonStatus
155
+ active?: boolean
156
+ href?: string
157
+ testId?: string
158
+ }
159
+
160
+ export type IconButtonStatus = 'loading' | 'success' | 'warning' | 'error'
161
+
162
+ export type IconButtonColor = 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow' | 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky'
163
+ export type IconButtonVariant = 'filled' | 'outline'
164
+
165
+ export interface IconButtonProps {
166
+ icon: IconName | ReactNode
167
+ onClick?: (e?: MouseEvent) => void
168
+ /** When provided, renders an <a> tag instead of <button>. Opens in a new tab. */
169
+ href?: string
170
+ size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
171
+ color?: IconButtonColor
172
+ variant?: IconButtonVariant
173
+ active?: boolean
174
+ disabled?: boolean
175
+ /** Async action status. Overrides icon, color, and active state when set. */
176
+ status?: IconButtonStatus
177
+ /** Tooltip shown on hover. Title and description are required. */
178
+ tooltip?: TooltipContent
179
+ tooltipPosition?: 'bottom' | 'bottom-left' | 'left' | 'right' | 'top' | 'top-left' | 'top-right'
180
+ badge?: number | string
181
+ badgeColor?: 'red' | 'blue' | 'green' | 'orange'
182
+ strikethrough?: boolean
183
+ className?: string
184
+ /** Test ID for E2E testing */
185
+ testId?: string
186
+ }
187
+
188
+ const sizeClasses = {
189
+ xss: 'w-[18px] h-[18px] rounded-[3px]',
190
+ xs: 'w-6 h-6 rounded-[5px]',
191
+ sm: 'w-7 h-7 rounded-md',
192
+ md: 'w-8 h-8 rounded-md',
193
+ lg: 'w-9 h-9 rounded-md',
194
+ }
195
+
196
+ const iconSizeClasses = {
197
+ xss: 'w-2.5 h-2.5',
198
+ xs: 'w-3 h-3',
199
+ sm: 'w-3.5 h-3.5',
200
+ md: 'w-4 h-4',
201
+ lg: 'w-5 h-5',
202
+ }
203
+
204
+ // Color variants
205
+ const colorClasses = {
206
+ green: { text: 'text-green-400', border: 'border-green-500/30', hover: 'hover:bg-green-500/20 hover:border-green-500/40 hover:text-green-300', active: 'bg-green-500/20 text-green-300 border-green-500/40' },
207
+ red: { text: 'text-red-400', border: 'border-red-500/30', hover: 'hover:bg-red-500/20 hover:border-red-500/40 hover:text-red-300', active: 'bg-red-500/20 text-red-300 border-red-500/40' },
208
+ blue: { text: 'text-blue-400', border: 'border-blue-500/30', hover: 'hover:bg-blue-500/20 hover:border-blue-500/40 hover:text-blue-300', active: 'bg-blue-500/20 text-blue-300 border-blue-500/40' },
209
+ orange: { text: 'text-orange-400', border: 'border-orange-500/30', hover: 'hover:bg-orange-500/20 hover:border-orange-500/40 hover:text-orange-300', active: 'bg-orange-500/20 text-orange-300 border-orange-500/40' },
210
+ cyan: { text: 'text-cyan-400', border: 'border-cyan-500/30', hover: 'hover:bg-cyan-500/20 hover:border-cyan-500/40 hover:text-cyan-300', active: 'bg-cyan-500/20 text-cyan-300 border-cyan-500/40' },
211
+ yellow: { text: 'text-yellow-400', border: 'border-yellow-500/30', hover: 'hover:bg-yellow-500/20 hover:border-yellow-500/40 hover:text-yellow-300', active: 'bg-yellow-500/20 text-yellow-300 border-yellow-500/40' },
212
+ purple: { text: 'text-purple-400', border: 'border-purple-500/30', hover: 'hover:bg-purple-500/20 hover:border-purple-500/40 hover:text-purple-300', active: 'bg-purple-500/20 text-purple-300 border-purple-500/40' },
213
+ indigo: { text: 'text-indigo-400', border: 'border-indigo-500/30', hover: 'hover:bg-indigo-500/20 hover:border-indigo-500/40 hover:text-indigo-300', active: 'bg-indigo-500/20 text-indigo-300 border-indigo-500/40' },
214
+ emerald: { text: 'text-emerald-400', border: 'border-emerald-500/30', hover: 'hover:bg-emerald-500/20 hover:border-emerald-500/40 hover:text-emerald-300', active: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40' },
215
+ amber: { text: 'text-amber-400', border: 'border-amber-500/30', hover: 'hover:bg-amber-500/20 hover:border-amber-500/40 hover:text-amber-300', active: 'bg-amber-500/20 text-amber-300 border-amber-500/40' },
216
+ violet: { text: 'text-violet-400', border: 'border-violet-500/30', hover: 'hover:bg-violet-500/20 hover:border-violet-500/40 hover:text-violet-300', active: 'bg-violet-500/20 text-violet-300 border-violet-500/40' },
217
+ neutral: { text: 'text-neutral-400', border: 'border-neutral-500/30', hover: 'hover:bg-neutral-500/20 hover:border-neutral-500/40 hover:text-neutral-300', active: 'bg-neutral-500/20 text-neutral-300 border-neutral-500/40' },
218
+ sky: { text: 'text-sky-400', border: 'border-sky-500/30', hover: 'hover:bg-sky-500/20 hover:border-sky-500/40 hover:text-sky-300', active: 'bg-sky-500/20 text-sky-300 border-sky-500/40' },
219
+ }
220
+
221
+ const badgeColorClasses = {
222
+ red: 'bg-red-500',
223
+ blue: 'bg-blue-500',
224
+ green: 'bg-green-500',
225
+ orange: 'bg-orange-500',
226
+ }
227
+
228
+ // Map IconButton tooltip positions to Tooltip component positions
229
+ const mapTooltipPosition = (
230
+ pos: 'bottom' | 'bottom-left' | 'left' | 'right' | 'top' | 'top-left' | 'top-right'
231
+ ): { position: 'top' | 'bottom' | 'left' | 'right'; align: 'start' | 'center' | 'end' } => {
232
+ switch (pos) {
233
+ case 'left':
234
+ return { position: 'left', align: 'center' }
235
+ case 'right':
236
+ return { position: 'right', align: 'center' }
237
+ case 'top':
238
+ return { position: 'top', align: 'center' }
239
+ case 'top-left':
240
+ return { position: 'top', align: 'end' }
241
+ case 'top-right':
242
+ return { position: 'top', align: 'start' }
243
+ case 'bottom-left':
244
+ return { position: 'bottom', align: 'end' }
245
+ case 'bottom':
246
+ default:
247
+ return { position: 'bottom', align: 'end' }
248
+ }
249
+ }
250
+
251
+ const statusIcons: Record<IconButtonStatus, LucideIcon> = {
252
+ loading: Loader2,
253
+ success: CheckCircle2,
254
+ warning: AlertTriangle,
255
+ error: XCircle,
256
+ }
257
+
258
+ const statusConfig = {
259
+ loading: { color: undefined, active: true, animation: 'animate-spin' },
260
+ success: { color: 'green' as const, active: true, animation: 'animate-pulse' },
261
+ warning: { color: 'amber' as const, active: true, animation: 'animate-pulse' },
262
+ error: { color: 'red' as const, active: true, animation: 'animate-pulse' },
263
+ }
264
+
265
+ function resolveIcon(icon: IconName | ReactNode, status: IconButtonStatus | undefined): LucideIcon | null {
266
+ if (status) return statusIcons[status]
267
+ if (typeof icon === 'string') return iconMap[icon as IconName]
268
+ return null
269
+ }
270
+
271
+ export function IconButton({
272
+ icon,
273
+ onClick,
274
+ href,
275
+ size = 'sm',
276
+ color = 'neutral',
277
+ variant = 'outline',
278
+ active = false,
279
+ disabled = false,
280
+ status,
281
+ tooltip,
282
+ tooltipPosition = 'bottom',
283
+ badge,
284
+ badgeColor = 'red',
285
+ strikethrough = false,
286
+ className = '',
287
+ testId,
288
+ }: IconButtonProps) {
289
+ const resolvedColor = status ? (statusConfig[status].color ?? color) : color
290
+ const resolvedActive = status ? statusConfig[status].active : active
291
+
292
+ const colorStyle = colorClasses[resolvedColor] ?? colorClasses.neutral
293
+ const { position, align } = mapTooltipPosition(tooltipPosition)
294
+
295
+ const isOutline = variant === 'outline'
296
+ const borderClass = isOutline ? colorStyle.border : 'border-neutral-600'
297
+
298
+ const sharedClassName = `
299
+ relative flex items-center justify-center border transition-colors ${isOutline ? '' : 'bg-neutral-800'}
300
+ ${sizeClasses[size]}
301
+ ${colorStyle.text} ${borderClass}
302
+ ${resolvedActive ? colorStyle.active : ''}
303
+ ${!disabled && !resolvedActive ? colorStyle.hover : ''}
304
+ ${className}
305
+ `
306
+
307
+ const iconClass = iconSizeClasses[size]
308
+ const animationClass = status ? statusConfig[status].animation : ''
309
+ const Icon = resolveIcon(icon, status)
310
+ const isReactNodeIcon = !Icon && typeof icon !== 'string'
311
+
312
+ const inner = (
313
+ <>
314
+ <span className={`relative flex items-center justify-center ${iconClass} ${animationClass}`}>
315
+ {Icon ? <Icon className={iconClass} /> : isReactNodeIcon ? icon : null}
316
+ {strikethrough && !status && (
317
+ <span className="absolute inset-0 flex items-center justify-center">
318
+ <span className="w-5 h-0.5 bg-current rotate-[-45deg] rounded-full" />
319
+ </span>
320
+ )}
321
+ </span>
322
+ {badge !== undefined && (
323
+ <span
324
+ 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]}`}
325
+ >
326
+ {typeof badge === 'number' && badge > 99 ? '99+' : badge}
327
+ </span>
328
+ )}
329
+ </>
330
+ )
331
+
332
+ const button = href ? (
333
+ <a
334
+ href={href}
335
+ target="_blank"
336
+ rel="noopener noreferrer"
337
+ aria-label={tooltip?.title}
338
+ data-testid={testId}
339
+ className={`${sharedClassName} cursor-pointer no-underline`}
340
+ >
341
+ {inner}
342
+ </a>
343
+ ) : (
344
+ <button
345
+ type="button"
346
+ onClick={onClick}
347
+ disabled={disabled}
348
+ aria-label={tooltip?.title}
349
+ data-testid={testId}
350
+ className={`${sharedClassName} cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed`}
351
+ >
352
+ {inner}
353
+ </button>
354
+ )
355
+
356
+ // Wrap with Tooltip if tooltip prop is provided
357
+ if (tooltip) {
358
+ return (
359
+ <Tooltip content={tooltip} position={position} align={align}>
360
+ {button}
361
+ </Tooltip>
362
+ )
363
+ }
364
+
365
+ return button
366
+ }
367
+
368
+ export interface CollapseButtonProps {
369
+ collapsed: boolean
370
+ onToggle: () => void
371
+ size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
372
+ tooltipPosition?: 'bottom' | 'bottom-left' | 'left' | 'right' | 'top' | 'top-left' | 'top-right'
373
+ }
374
+
375
+ export function CollapseButton({ collapsed, onToggle, size = 'xss', tooltipPosition = 'bottom-left' }: CollapseButtonProps) {
376
+ return (
377
+ <IconButton
378
+ icon={collapsed ? 'chevrons-up-down' : 'chevrons-down-up'}
379
+ onClick={onToggle}
380
+ tooltip={{
381
+ title: collapsed ? 'Expand all' : 'Collapse all',
382
+ description: collapsed ? 'Expand all folders' : 'Collapse all folders',
383
+ }}
384
+ tooltipPosition={tooltipPosition}
385
+ size={size}
386
+ />
387
+ )
388
+ }