@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.
- package/README.md +63 -0
- package/components/content/info-panel-primitives.tsx +297 -0
- package/components/diagrams/diagram-utils.tsx +908 -0
- package/components/hooks/use-click-outside.ts +27 -0
- package/components/hooks/use-dropdown-max-height.ts +20 -0
- package/components/hooks/use-navigation-history.ts +94 -0
- package/components/lib/ai-tools.tsx +44 -0
- package/components/lib/cn.ts +6 -0
- package/components/lib/form-colors.ts +32 -0
- package/components/lib/theme-engine.ts +97 -0
- package/components/lib/toolr-brand.tsx +31 -0
- package/components/sections/ai-tools-paths/index.ts +37 -0
- package/components/sections/ai-tools-paths/tools-paths-panel.tsx +212 -0
- package/components/sections/ai-tools-paths/types.ts +111 -0
- package/components/sections/ai-tools-paths/use-tools-paths.ts +159 -0
- package/components/sections/captured-issues/captured-issues-panel.tsx +214 -0
- package/components/sections/captured-issues/index.ts +38 -0
- package/components/sections/captured-issues/types.ts +113 -0
- package/components/sections/captured-issues/use-captured-issues.ts +111 -0
- package/components/sections/golden-snapshots/file-diff-viewer.tsx +420 -0
- package/components/sections/golden-snapshots/golden-sync-panel.tsx +223 -0
- package/components/sections/golden-snapshots/index.ts +145 -0
- package/components/sections/golden-snapshots/snapshot-manager.tsx +200 -0
- package/components/sections/golden-snapshots/status-overview.tsx +305 -0
- package/components/sections/golden-snapshots/types.ts +288 -0
- package/components/sections/golden-snapshots/use-golden-sync.ts +477 -0
- package/components/sections/golden-snapshots/version-manager.tsx +186 -0
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +210 -0
- package/components/sections/prompt-editor/index.ts +121 -0
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +276 -0
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +514 -0
- package/components/sections/prompt-editor/types.ts +101 -0
- package/components/sections/prompt-editor/use-prompt-editor.ts +131 -0
- package/components/sections/report-bug/error-logger.ts +392 -0
- package/components/sections/report-bug/index.ts +59 -0
- package/components/sections/report-bug/issue-reporter-api.ts +83 -0
- package/components/sections/report-bug/report-bug-form.tsx +282 -0
- package/components/sections/report-bug/screenshot-uploader.tsx +228 -0
- package/components/sections/report-bug/use-report-bug.ts +170 -0
- package/components/sections/snapshot-browser/index.ts +53 -0
- package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +147 -0
- package/components/sections/snapshot-browser/snapshot-tree.tsx +451 -0
- package/components/sections/snapshot-browser/types.ts +106 -0
- package/components/sections/snapshot-browser/use-snapshot-browser.ts +125 -0
- package/components/sections/snippets-editor/index.ts +31 -0
- package/components/sections/snippets-editor/snippets-editor.tsx +381 -0
- package/components/sections/snippets-editor/types.ts +48 -0
- package/components/sections/snippets-editor/use-snippets-editor.ts +217 -0
- package/components/ui/action-dialog.tsx +309 -0
- package/components/ui/ai-action-button.tsx +137 -0
- package/components/ui/ai-execution-action-buttons.tsx +106 -0
- package/components/ui/badge.tsx +67 -0
- package/components/ui/bottom-panel-header.tsx +240 -0
- package/components/ui/breadcrumb.tsx +168 -0
- package/components/ui/checkbox.tsx +102 -0
- package/components/ui/collapsible-section.tsx +100 -0
- package/components/ui/confirm-badge.tsx +71 -0
- package/components/ui/detail-section.tsx +67 -0
- package/components/ui/detail-view-wrapper.tsx +55 -0
- package/components/ui/editor-placeholder-card.tsx +197 -0
- package/components/ui/editor-toolbar.tsx +123 -0
- package/components/ui/execution-details-panel.tsx +93 -0
- package/components/ui/extension-list-card.tsx +105 -0
- package/components/ui/file-structure-section.tsx +373 -0
- package/components/ui/file-tree.tsx +171 -0
- package/components/ui/files-panel.tsx +251 -0
- package/components/ui/filter-dropdown.tsx +173 -0
- package/components/ui/form-actions.tsx +127 -0
- package/components/ui/frontmatter-form-header.tsx +80 -0
- package/components/ui/icon-button.tsx +388 -0
- package/components/ui/input.tsx +211 -0
- package/components/ui/label.tsx +159 -0
- package/components/ui/layout-tab-bar.tsx +289 -0
- package/components/ui/modal.tsx +194 -0
- package/components/ui/nav-card.tsx +81 -0
- package/components/ui/navigation-bar.tsx +285 -0
- package/components/ui/number-input.tsx +165 -0
- package/components/ui/registry-browser.tsx +261 -0
- package/components/ui/registry-card.tsx +710 -0
- package/components/ui/registry-detail.tsx +224 -0
- package/components/ui/resizable-textarea.tsx +290 -0
- package/components/ui/scope-badge.tsx +67 -0
- package/components/ui/segmented-toggle.tsx +133 -0
- package/components/ui/select.tsx +172 -0
- package/components/ui/selection-grid.tsx +313 -0
- package/components/ui/setting-row.tsx +97 -0
- package/components/ui/snapshot-card.tsx +107 -0
- package/components/ui/snippets-panel.tsx +161 -0
- package/components/ui/sort-dropdown.tsx +109 -0
- package/components/ui/status-card.tsx +96 -0
- package/components/ui/tab-bar.tsx +340 -0
- package/components/ui/toggle.tsx +142 -0
- package/components/ui/tooltip.tsx +326 -0
- package/dist/content.d.ts +110 -0
- package/dist/content.js +195 -0
- package/dist/diagrams.d.ts +371 -0
- package/dist/diagrams.js +702 -0
- package/dist/index.d.ts +2714 -0
- package/dist/index.js +11220 -0
- package/dist/preset.d.ts +24 -0
- package/dist/preset.js +17 -0
- package/dist/tokens/tokens/primitives.css +45 -0
- package/dist/tokens/tokens/semantic.css +46 -0
- package/dist/tokens/tokens/theme.css +11 -0
- package/dist/tokens/tokens/tokens.json +65 -0
- package/index.ts +123 -0
- package/package.json +63 -0
- package/tailwind-preset.ts +22 -0
- package/tokens/primitives.css +45 -0
- package/tokens/semantic.css +46 -0
- package/tokens/theme.css +11 -0
- 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
|
+
}
|