@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,106 @@
1
+ import { IconButton, type IconName } from './icon-button.tsx'
2
+
3
+ export type ExecutionStatus = 'running' | 'success' | 'partial' | 'error'
4
+
5
+ export interface AiExecutionActionButtonsProps {
6
+ /** Whether execution is currently running */
7
+ isRunning: boolean
8
+ /** Whether all execution is complete */
9
+ allDone: boolean
10
+ /** Called when minimize/pin button clicked - hide modal, continue process */
11
+ onMinimize: () => void
12
+ /** Called when cancel/stop button clicked - kill process. If not provided, cancel button is hidden */
13
+ onCancel?: () => void
14
+ /** Called when done/close button clicked */
15
+ onClose: () => void
16
+ /** Completion status - affects done button icon/color */
17
+ status?: ExecutionStatus
18
+ /** Prefix for test IDs (e.g., 'scan-modal' results in 'scan-modal-minimize-btn') */
19
+ testIdPrefix?: string
20
+ }
21
+
22
+ export function AiExecutionActionButtons({
23
+ isRunning,
24
+ allDone,
25
+ onMinimize,
26
+ onCancel,
27
+ onClose,
28
+ status = 'success',
29
+ testIdPrefix,
30
+ }: AiExecutionActionButtonsProps) {
31
+ const getDoneButtonConfig = (): { icon: IconName; color: 'neutral' | 'blue' | 'red' | 'amber'; tooltip: { title?: string; description: string } } => {
32
+ if (!allDone) {
33
+ return {
34
+ icon: 'check-circle',
35
+ color: 'neutral',
36
+ tooltip: { description: 'Waiting for completion' },
37
+ }
38
+ }
39
+ switch (status) {
40
+ case 'error':
41
+ return {
42
+ icon: 'x-circle',
43
+ color: 'red',
44
+ tooltip: { description: 'All scans failed. No changes will be applied.' },
45
+ }
46
+ case 'partial':
47
+ return {
48
+ icon: 'alert-triangle',
49
+ color: 'amber',
50
+ tooltip: { description: 'Successful scans will be applied. Failed scans will be skipped.' },
51
+ }
52
+ default:
53
+ return {
54
+ icon: 'check-circle',
55
+ color: 'blue',
56
+ tooltip: { description: 'All scans completed successfully' },
57
+ }
58
+ }
59
+ }
60
+
61
+ const doneConfig = getDoneButtonConfig()
62
+ return (
63
+ <div className="flex items-center gap-2">
64
+ {/* While running, show minimize and cancel buttons */}
65
+ {isRunning && !allDone && (
66
+ <>
67
+ {/* Minimize button - hide modal but process continues */}
68
+ <IconButton
69
+ icon="minus"
70
+ onClick={onMinimize}
71
+ size="sm"
72
+ color="neutral"
73
+ tooltip={{ description: 'Hide modal. Process will continue in background.' }}
74
+ tooltipPosition="top"
75
+ testId={testIdPrefix ? `${testIdPrefix}-minimize-btn` : undefined}
76
+ />
77
+
78
+ {/* Cancel button - stop and kill the process */}
79
+ {onCancel && (
80
+ <IconButton
81
+ icon="stop-circle"
82
+ onClick={onCancel}
83
+ size="sm"
84
+ color="red"
85
+ tooltip={{ description: 'Stop and kill the running process' }}
86
+ tooltipPosition="top"
87
+ testId={testIdPrefix ? `${testIdPrefix}-cancel-btn` : undefined}
88
+ />
89
+ )}
90
+ </>
91
+ )}
92
+
93
+ {/* Done button - always shown, enabled when complete */}
94
+ <IconButton
95
+ icon={doneConfig.icon}
96
+ onClick={onClose}
97
+ disabled={isRunning && !allDone}
98
+ size="sm"
99
+ color={doneConfig.color}
100
+ tooltip={doneConfig.tooltip}
101
+ tooltipPosition="top"
102
+ testId={testIdPrefix ? `${testIdPrefix}-close-btn` : undefined}
103
+ />
104
+ </div>
105
+ )
106
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Badge - Outline-styled pill badge for displaying counts or short labels
3
+ *
4
+ * Used by:
5
+ * - TabBar, BottomPanelHeader - tab count badges
6
+ * - NavCard - card count badges
7
+ * - Any UI that needs a small inline indicator
8
+ *
9
+ * Features:
10
+ * - Outline variant matching IconButton outline style (border + text, no fill)
11
+ * - Accepts numbers (auto-caps at 99+) or short strings ("New")
12
+ * - 13 color variants
13
+ * - 5 size variants (xss, xs, sm, md, lg)
14
+ */
15
+
16
+ export type BadgeColor = 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow' | 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky'
17
+
18
+ export interface BadgeProps {
19
+ value: number | string
20
+ color?: BadgeColor
21
+ size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
22
+ className?: string
23
+ testId?: string
24
+ }
25
+
26
+ const colorClasses: Record<BadgeColor, string> = {
27
+ green: 'border-green-500/30 text-green-400',
28
+ red: 'border-red-500/30 text-red-400',
29
+ blue: 'border-blue-500/30 text-blue-400',
30
+ orange: 'border-orange-500/30 text-orange-400',
31
+ cyan: 'border-cyan-500/30 text-cyan-400',
32
+ yellow: 'border-yellow-500/30 text-yellow-400',
33
+ purple: 'border-purple-500/30 text-purple-400',
34
+ indigo: 'border-indigo-500/30 text-indigo-400',
35
+ emerald: 'border-emerald-500/30 text-emerald-400',
36
+ amber: 'border-amber-500/30 text-amber-400',
37
+ violet: 'border-violet-500/30 text-violet-400',
38
+ neutral: 'border-neutral-500/30 text-neutral-400',
39
+ sky: 'border-sky-500/30 text-sky-400',
40
+ }
41
+
42
+ const sizeClasses = {
43
+ xss: 'min-w-[14px] h-[14px] px-0.5 text-[9px]',
44
+ xs: 'min-w-[16px] h-[16px] px-1 text-[10px]',
45
+ sm: 'min-w-[18px] h-[18px] px-1 text-[10px]',
46
+ md: 'min-w-[20px] h-[20px] px-1.5 text-[11px]',
47
+ lg: 'min-w-[22px] h-[22px] px-1.5 text-xs',
48
+ }
49
+
50
+ export function Badge({
51
+ value,
52
+ color = 'neutral',
53
+ size = 'sm',
54
+ className = '',
55
+ testId,
56
+ }: BadgeProps) {
57
+ const display = typeof value === 'number' && value > 99 ? '99+' : value
58
+
59
+ return (
60
+ <span
61
+ data-testid={testId}
62
+ className={`inline-flex items-center justify-center border rounded-full font-medium leading-none tabular-nums ${colorClasses[color]} ${sizeClasses[size]} ${className}`}
63
+ >
64
+ {display}
65
+ </span>
66
+ )
67
+ }
@@ -0,0 +1,240 @@
1
+ import { type ReactNode, useState, useRef, useEffect, useCallback } from 'react'
2
+ import { RefreshCw } from 'lucide-react'
3
+ import { IconButton, type IconName, type ActionItem, iconMap } from './icon-button.tsx'
4
+ import { Badge, type BadgeColor } from './badge.tsx'
5
+ import { Tooltip } from './tooltip.tsx'
6
+
7
+ /** Status banner configuration for outdated/info messages */
8
+ export interface StatusBanner {
9
+ /** Message to display */
10
+ message: string
11
+ /** Banner type affects styling */
12
+ type: 'warning' | 'info'
13
+ /** Optional action button label */
14
+ actionLabel?: string
15
+ /** Optional action callback */
16
+ onAction?: () => void
17
+ }
18
+
19
+ // Tab configuration with icon, label, count badge, and color
20
+ export interface PanelTab<T extends string = string> {
21
+ /** Unique identifier for the tab */
22
+ id: T
23
+ /** Display label */
24
+ label: string
25
+ /** Icon name from iconMap */
26
+ icon?: IconName
27
+ /** Custom icon component (should accept className for sizing) */
28
+ IconComponent?: React.ComponentType<{ className?: string; style?: React.CSSProperties }>
29
+ /** Optional count badge */
30
+ count?: number
31
+ /** Color for the count badge */
32
+ countColor?: BadgeColor
33
+ /** Tailwind classes for active text/border color (e.g., 'text-orange-400') */
34
+ activeTextClass?: string
35
+ /** Tailwind classes for active border color (e.g., 'border-orange-400'). Defaults to 'border-current' */
36
+ activeBorderClass?: string
37
+ /** Hide label on small screens */
38
+ hideLabel?: boolean
39
+ /** Test ID for E2E testing */
40
+ testId?: string
41
+ }
42
+
43
+ export interface BottomPanelHeaderProps<T extends string = string> {
44
+ /** Array of tab configurations */
45
+ tabs: PanelTab<T>[]
46
+ /** Currently active tab ID */
47
+ activeTab: T
48
+ /** Callback when tab changes */
49
+ onTabChange: (tabId: T) => void
50
+ /** Action buttons to render (right side, before collapse button) */
51
+ actions?: ActionItem[]
52
+ /** Escape hatch: arbitrary ReactNode actions (e.g. AiActionButton) */
53
+ customActions?: ReactNode
54
+ /** Custom class name for the header container */
55
+ className?: string
56
+ /** Custom left content to replace tabs (e.g., for "ignored" mode) */
57
+ customLeftContent?: ReactNode
58
+ /** Optional status banner (e.g., "Results may be outdated") */
59
+ statusBanner?: StatusBanner
60
+ /** Callback when collapse button is clicked */
61
+ onCollapse?: () => void
62
+ }
63
+
64
+ // Default styling classes
65
+ const DEFAULT_ACTIVE_TEXT = 'text-neutral-300'
66
+ const DEFAULT_INACTIVE_CLASSES = 'border-transparent text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800/30'
67
+
68
+ // Layout mode: full → compact banner → compact tabs
69
+ type LayoutMode = 'full' | 'compact-banner' | 'compact-all'
70
+
71
+ // Width estimates for layout calculation
72
+ const TAB_ICON_PADDING = 56 // icon(16) + gap(8) + px-4(32)
73
+ const CHAR_WIDTH = 7.5 // approximate char width at text-sm
74
+ const COUNT_BADGE_WIDTH = 40 // badge with count
75
+ const BANNER_FULL_WIDTH = 200 // icon + text + padding
76
+ const BANNER_COMPACT_WIDTH = 36 // icon only
77
+
78
+ function estimateTabsFullWidth<T extends string>(tabs: PanelTab<T>[]): number {
79
+ return tabs.reduce((sum, tab) => {
80
+ const labelWidth = tab.label.length * CHAR_WIDTH
81
+ const badge = tab.count !== undefined ? COUNT_BADGE_WIDTH : 0
82
+ return sum + TAB_ICON_PADDING + labelWidth + badge
83
+ }, 0)
84
+ }
85
+
86
+ export function BottomPanelHeader<T extends string = string>({
87
+ tabs,
88
+ activeTab,
89
+ onTabChange,
90
+ actions,
91
+ customActions,
92
+ className = '',
93
+ customLeftContent,
94
+ statusBanner,
95
+ onCollapse,
96
+ }: BottomPanelHeaderProps<T>) {
97
+ const containerRef = useRef<HTMLDivElement>(null)
98
+ const actionsRef = useRef<HTMLDivElement>(null)
99
+ const [layoutMode, setLayoutMode] = useState<LayoutMode>('full')
100
+
101
+ const computeLayout = useCallback(() => {
102
+ const container = containerRef.current
103
+ const actionsEl = actionsRef.current
104
+ if (!container) return
105
+
106
+ const containerWidth = container.clientWidth
107
+ const actionsWidth = actionsEl?.offsetWidth ?? 0
108
+ const available = containerWidth - actionsWidth
109
+
110
+ const tabsFull = estimateTabsFullWidth(tabs)
111
+ const bannerFull = statusBanner ? BANNER_FULL_WIDTH : 0
112
+ const bannerCompact = statusBanner ? BANNER_COMPACT_WIDTH : 0
113
+
114
+ if (tabsFull + bannerFull <= available) {
115
+ setLayoutMode('full')
116
+ } else if (tabsFull + bannerCompact <= available) {
117
+ setLayoutMode('compact-banner')
118
+ } else {
119
+ setLayoutMode('compact-all')
120
+ }
121
+ }, [tabs, statusBanner])
122
+
123
+ useEffect(() => {
124
+ const container = containerRef.current
125
+ if (!container) return
126
+ computeLayout()
127
+ const observer = new ResizeObserver(computeLayout)
128
+ observer.observe(container)
129
+ return () => observer.disconnect()
130
+ }, [computeLayout])
131
+
132
+ const compactTabs = layoutMode === 'compact-all'
133
+ const compactBanner = layoutMode === 'compact-banner' || layoutMode === 'compact-all'
134
+
135
+ // Banner styling based on type
136
+ const bannerStyles = statusBanner?.type === 'warning'
137
+ ? 'bg-amber-500/10 border-amber-500/30 text-amber-300'
138
+ : 'bg-blue-500/10 border-blue-500/30 text-blue-300'
139
+
140
+ return (
141
+ <div
142
+ ref={containerRef}
143
+ className={`flex-shrink-0 h-[41px] flex justify-between bg-neutral-900 border-b border-neutral-800 ${className}`}
144
+ >
145
+ {/* Tabs or custom content - left aligned */}
146
+ <div className="flex flex-shrink-0">
147
+ {customLeftContent ?? tabs.map((tab) => {
148
+ const isActive = activeTab === tab.id
149
+ const TabIcon = tab.IconComponent || (tab.icon ? iconMap[tab.icon] : undefined)
150
+
151
+ // Determine classes based on active state
152
+ const textClass = isActive
153
+ ? tab.activeTextClass || DEFAULT_ACTIVE_TEXT
154
+ : ''
155
+ const borderClass = isActive
156
+ ? tab.activeBorderClass || 'border-current'
157
+ : ''
158
+ const baseClasses = isActive
159
+ ? `${textClass} ${borderClass} bg-neutral-800/50`
160
+ : DEFAULT_INACTIVE_CLASSES
161
+ const tabButton = (
162
+ <button
163
+ key={tab.id}
164
+ onClick={() => onTabChange(tab.id)}
165
+ data-testid={tab.testId}
166
+ className={`h-[41px] flex items-center justify-center gap-2 ${compactTabs ? 'px-3' : 'px-4'} text-sm border-b-2 transition-colors cursor-pointer ${baseClasses}`}
167
+ >
168
+ {compactTabs ? (
169
+ <span className="relative flex items-center justify-center w-[18px] h-[18px] flex-shrink-0">
170
+ {TabIcon && <TabIcon className="w-[18px] h-[18px]" />}
171
+ {tab.count !== undefined && (
172
+ <span className="absolute -top-1.5 -right-2">
173
+ <Badge value={tab.count} color={tab.countColor} size="xss" />
174
+ </span>
175
+ )}
176
+ </span>
177
+ ) : (
178
+ <>
179
+ <span className="flex items-center justify-center w-4 h-4 flex-shrink-0">
180
+ {TabIcon && <TabIcon className="w-4 h-4" />}
181
+ </span>
182
+ {tab.hideLabel ? (
183
+ <span className="hidden sm:inline">{tab.label}</span>
184
+ ) : (
185
+ <span>{tab.label}</span>
186
+ )}
187
+ {tab.count !== undefined && (
188
+ <Badge value={tab.count} color={tab.countColor} size="xss" />
189
+ )}
190
+ </>
191
+ )}
192
+ </button>
193
+ )
194
+
195
+ if (compactTabs) {
196
+ return (
197
+ <Tooltip key={tab.id} content={{ description: tab.label }} position="bottom">
198
+ {tabButton}
199
+ </Tooltip>
200
+ )
201
+ }
202
+
203
+ return tabButton
204
+ })}
205
+ </div>
206
+
207
+ {/* Center: Status banner */}
208
+ <div className="flex-1 min-w-0 flex items-center justify-center overflow-hidden">
209
+ {statusBanner && (
210
+ compactBanner ? (
211
+ <Tooltip content={{ description: statusBanner.message }} position="bottom">
212
+ <div className={`flex items-center px-2 py-1.5 ${bannerStyles} rounded text-xs`}>
213
+ <RefreshCw className="w-3 h-3 flex-shrink-0" />
214
+ </div>
215
+ </Tooltip>
216
+ ) : (
217
+ <div className={`flex items-center gap-2 px-2.5 py-1.5 ${bannerStyles} rounded text-xs max-w-full`}>
218
+ <RefreshCw className="w-3 h-3 flex-shrink-0" />
219
+ <span className="truncate">{statusBanner.message}</span>
220
+ </div>
221
+ )
222
+ )}
223
+ </div>
224
+
225
+ {/* Actions + Collapse - right aligned */}
226
+ <div ref={actionsRef} className="flex items-center gap-1.5 pr-3 flex-shrink-0">
227
+ {actions?.map((a, i) => <IconButton key={i} {...a} />)}
228
+ {customActions}
229
+ {onCollapse && (
230
+ <IconButton
231
+ icon="panel-bottom-close"
232
+ onClick={onCollapse}
233
+ size="sm"
234
+ tooltip={{ description: 'Collapse panel' }}
235
+ />
236
+ )}
237
+ </div>
238
+ </div>
239
+ )
240
+ }
@@ -0,0 +1,168 @@
1
+ /** Breadcrumb navigation with clickable segments, color-coded icons, and configurable separators. */
2
+
3
+ import {
4
+ ChevronRight,
5
+ Settings, Folder, File, Code, Terminal, Database,
6
+ Globe, Star, Users, User, Tag, Search, Heart,
7
+ Zap, Shield, ShieldCheck, Sparkles, Eye, Lock,
8
+ Cloud, Wand2, Bell, Bookmark, Pin, Mail, Send,
9
+ Image, Bot, Puzzle, Plug, Webhook,
10
+ } from 'lucide-react'
11
+ import type { LucideIcon } from 'lucide-react'
12
+ import type { IconName } from './icon-button.tsx'
13
+ import { cn } from '../lib/cn.ts'
14
+
15
+ const iconSubset: Partial<Record<IconName, LucideIcon>> = {
16
+ folder: Folder,
17
+ file: File,
18
+ settings: Settings,
19
+ code: Code,
20
+ terminal: Terminal,
21
+ database: Database,
22
+ globe: Globe,
23
+ star: Star,
24
+ users: Users,
25
+ user: User,
26
+ tag: Tag,
27
+ zap: Zap,
28
+ shield: Shield,
29
+ 'shield-check': ShieldCheck,
30
+ sparkles: Sparkles,
31
+ eye: Eye,
32
+ lock: Lock,
33
+ search: Search,
34
+ heart: Heart,
35
+ cloud: Cloud,
36
+ wand: Wand2,
37
+ bell: Bell,
38
+ bookmark: Bookmark,
39
+ pin: Pin,
40
+ mail: Mail,
41
+ send: Send,
42
+ image: Image,
43
+ bot: Bot,
44
+ puzzle: Puzzle,
45
+ plug: Plug,
46
+ webhook: Webhook,
47
+ }
48
+
49
+ export interface BreadcrumbSegment {
50
+ id: string
51
+ label: string
52
+ icon?: IconName
53
+ color?: string
54
+ onClick?: () => void
55
+ }
56
+
57
+ export interface BreadcrumbProps {
58
+ segments: BreadcrumbSegment[]
59
+ separator?: 'chevron' | 'slash' | 'dot'
60
+ size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
61
+ className?: string
62
+ }
63
+
64
+ const sizeConfig = {
65
+ xss: { text: 'text-[10px]', icon: 'w-2.5 h-2.5', px: 'px-1', py: 'py-0.5', gap: 'gap-0.5', sep: 'w-2 h-2' },
66
+ xs: { text: 'text-xs', icon: 'w-3 h-3', px: 'px-1.5', py: 'py-0.5', gap: 'gap-1', sep: 'w-2.5 h-2.5' },
67
+ sm: { text: 'text-sm', icon: 'w-3.5 h-3.5', px: 'px-2', py: 'py-1', gap: 'gap-1.5', sep: 'w-3 h-3' },
68
+ md: { text: 'text-base', icon: 'w-4 h-4', px: 'px-2.5', py: 'py-1', gap: 'gap-1.5', sep: 'w-3.5 h-3.5' },
69
+ lg: { text: 'text-lg', icon: 'w-5 h-5', px: 'px-3', py: 'py-1.5', gap: 'gap-2', sep: 'w-4 h-4' },
70
+ }
71
+
72
+ const colorMap: Record<string, { bg: string; text: string }> = {
73
+ blue: { bg: 'bg-blue-500/10', text: 'text-blue-400' },
74
+ green: { bg: 'bg-green-500/10', text: 'text-green-400' },
75
+ purple: { bg: 'bg-purple-500/10', text: 'text-purple-400' },
76
+ red: { bg: 'bg-red-500/10', text: 'text-red-400' },
77
+ orange: { bg: 'bg-orange-500/10', text: 'text-orange-400' },
78
+ cyan: { bg: 'bg-cyan-500/10', text: 'text-cyan-400' },
79
+ yellow: { bg: 'bg-yellow-500/10', text: 'text-yellow-400' },
80
+ amber: { bg: 'bg-amber-500/10', text: 'text-amber-400' },
81
+ emerald: { bg: 'bg-emerald-500/10', text: 'text-emerald-400' },
82
+ indigo: { bg: 'bg-indigo-500/10', text: 'text-indigo-400' },
83
+ violet: { bg: 'bg-violet-500/10', text: 'text-violet-400' },
84
+ sky: { bg: 'bg-sky-500/10', text: 'text-sky-400' },
85
+ pink: { bg: 'bg-pink-500/10', text: 'text-pink-400' },
86
+ teal: { bg: 'bg-teal-500/10', text: 'text-teal-400' },
87
+ neutral: { bg: 'bg-neutral-500/10', text: 'text-neutral-400' },
88
+ }
89
+
90
+ function SegmentIcon({ icon, color, size }: { icon: IconName; color?: string; size: 'xss' | 'xs' | 'sm' | 'md' | 'lg' }) {
91
+ const Icon = iconSubset[icon]
92
+ if (!Icon) return null
93
+ const s = sizeConfig[size]
94
+ const c = color && colorMap[color] ? colorMap[color] : null
95
+
96
+ return (
97
+ <span className={c?.text || ''}>
98
+ <Icon className={s.icon} />
99
+ </span>
100
+ )
101
+ }
102
+
103
+ function Separator({ type, size }: { type: 'chevron' | 'slash' | 'dot'; size: 'xss' | 'xs' | 'sm' | 'md' | 'lg' }) {
104
+ const s = sizeConfig[size]
105
+
106
+ if (type === 'chevron') {
107
+ return <ChevronRight className={cn(s.sep, 'text-neutral-600 flex-shrink-0')} />
108
+ }
109
+ if (type === 'slash') {
110
+ return <span className={cn(s.text, 'text-neutral-600 flex-shrink-0')}>/</span>
111
+ }
112
+ return <span className={cn(s.text, 'text-neutral-600 flex-shrink-0')}>&middot;</span>
113
+ }
114
+
115
+ export function Breadcrumb({
116
+ segments,
117
+ separator = 'chevron',
118
+ size = 'sm',
119
+ className,
120
+ }: BreadcrumbProps) {
121
+ const s = sizeConfig[size]
122
+
123
+ return (
124
+ <nav className={cn('flex items-center', className)}>
125
+ <div className={cn('flex items-center gap-1', s.px, s.py, 'bg-neutral-800/50 border border-neutral-700/50 rounded-lg')}>
126
+ {segments.map((segment, index) => {
127
+ const isLast = index === segments.length - 1
128
+ const isClickable = !isLast && !!segment.onClick
129
+ const colors = segment.color && colorMap[segment.color] ? colorMap[segment.color] : null
130
+
131
+ return (
132
+ <div key={segment.id} className="flex items-center gap-1">
133
+ {index > 0 && <Separator type={separator} size={size} />}
134
+ {isClickable ? (
135
+ <button
136
+ type="button"
137
+ onClick={segment.onClick}
138
+ className={cn(
139
+ 'flex items-center gap-1.5 px-2 py-0.5 rounded-md transition-colors cursor-pointer',
140
+ s.text,
141
+ 'font-medium hover:text-white',
142
+ colors ? [colors.text, `hover:${colors.bg}`] : ['text-neutral-300', 'hover:bg-neutral-700/50'],
143
+ )}
144
+ >
145
+ {segment.icon && <SegmentIcon icon={segment.icon} color={segment.color} size={size} />}
146
+ <span>{segment.label}</span>
147
+ </button>
148
+ ) : (
149
+ <div
150
+ className={cn(
151
+ 'flex items-center gap-1.5 px-2 py-0.5 rounded-md',
152
+ s.text,
153
+ isLast
154
+ ? ['font-medium bg-neutral-700/50', colors ? colors.text : 'text-white']
155
+ : ['font-medium', colors ? colors.text : 'text-neutral-300'],
156
+ )}
157
+ >
158
+ {segment.icon && <SegmentIcon icon={segment.icon} color={segment.color} size={size} />}
159
+ <span className="truncate max-w-[200px]">{segment.label}</span>
160
+ </div>
161
+ )}
162
+ </div>
163
+ )
164
+ })}
165
+ </div>
166
+ </nav>
167
+ )
168
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Checkbox - Controlled checkbox input with check icon
3
+ *
4
+ * Used by:
5
+ * - Settings forms - boolean preference toggles
6
+ * - Filter panels - multi-select filters
7
+ * - List items - selection state
8
+ */
9
+
10
+ import { Check } from 'lucide-react'
11
+
12
+ export type CheckboxSize = 'xss' | 'xs' | 'sm' | 'md' | 'lg'
13
+
14
+ export type CheckboxColor =
15
+ | 'blue'
16
+ | 'green'
17
+ | 'red'
18
+ | 'orange'
19
+ | 'cyan'
20
+ | 'yellow'
21
+ | 'purple'
22
+ | 'indigo'
23
+ | 'emerald'
24
+ | 'amber'
25
+ | 'violet'
26
+ | 'neutral'
27
+ | 'sky'
28
+ | 'pink'
29
+ | 'teal'
30
+
31
+ const CHECKBOX_COLORS: Record<CheckboxColor, { bg: string; border: string; icon: string; hover: string }> = {
32
+ blue: { bg: 'bg-blue-500/20', border: 'border-blue-500/40', icon: 'text-blue-300', hover: 'hover:bg-blue-500/15 hover:border-blue-500/30' },
33
+ green: { bg: 'bg-green-500/20', border: 'border-green-500/40', icon: 'text-green-300', hover: 'hover:bg-green-500/15 hover:border-green-500/30' },
34
+ red: { bg: 'bg-red-500/20', border: 'border-red-500/40', icon: 'text-red-300', hover: 'hover:bg-red-500/15 hover:border-red-500/30' },
35
+ orange: { bg: 'bg-orange-500/20', border: 'border-orange-500/40', icon: 'text-orange-300', hover: 'hover:bg-orange-500/15 hover:border-orange-500/30' },
36
+ cyan: { bg: 'bg-cyan-500/20', border: 'border-cyan-500/40', icon: 'text-cyan-300', hover: 'hover:bg-cyan-500/15 hover:border-cyan-500/30' },
37
+ yellow: { bg: 'bg-yellow-500/20', border: 'border-yellow-500/40', icon: 'text-yellow-300', hover: 'hover:bg-yellow-500/15 hover:border-yellow-500/30' },
38
+ purple: { bg: 'bg-purple-500/20', border: 'border-purple-500/40', icon: 'text-purple-300', hover: 'hover:bg-purple-500/15 hover:border-purple-500/30' },
39
+ indigo: { bg: 'bg-indigo-500/20', border: 'border-indigo-500/40', icon: 'text-indigo-300', hover: 'hover:bg-indigo-500/15 hover:border-indigo-500/30' },
40
+ emerald: { bg: 'bg-emerald-500/20', border: 'border-emerald-500/40', icon: 'text-emerald-300', hover: 'hover:bg-emerald-500/15 hover:border-emerald-500/30' },
41
+ amber: { bg: 'bg-amber-500/20', border: 'border-amber-500/40', icon: 'text-amber-300', hover: 'hover:bg-amber-500/15 hover:border-amber-500/30' },
42
+ violet: { bg: 'bg-violet-500/20', border: 'border-violet-500/40', icon: 'text-violet-300', hover: 'hover:bg-violet-500/15 hover:border-violet-500/30' },
43
+ neutral: { bg: 'bg-neutral-500/20', border: 'border-neutral-500/40', icon: 'text-neutral-300', hover: 'hover:bg-neutral-500/15 hover:border-neutral-500/30' },
44
+ sky: { bg: 'bg-sky-500/20', border: 'border-sky-500/40', icon: 'text-sky-300', hover: 'hover:bg-sky-500/15 hover:border-sky-500/30' },
45
+ pink: { bg: 'bg-pink-500/20', border: 'border-pink-500/40', icon: 'text-pink-300', hover: 'hover:bg-pink-500/15 hover:border-pink-500/30' },
46
+ teal: { bg: 'bg-teal-500/20', border: 'border-teal-500/40', icon: 'text-teal-300', hover: 'hover:bg-teal-500/15 hover:border-teal-500/30' },
47
+ }
48
+
49
+ const CHECKBOX_SIZES: Record<CheckboxSize, { box: string; icon: string }> = {
50
+ xss: { box: 'w-3 h-3', icon: 'w-2 h-2' },
51
+ xs: { box: 'w-3.5 h-3.5', icon: 'w-2 h-2' },
52
+ sm: { box: 'w-4 h-4', icon: 'w-2.5 h-2.5' },
53
+ md: { box: 'w-5 h-5', icon: 'w-3 h-3' },
54
+ lg: { box: 'w-6 h-6', icon: 'w-3.5 h-3.5' },
55
+ }
56
+
57
+ export type CheckboxVariant = 'outline' | 'filled'
58
+
59
+ export interface CheckboxProps {
60
+ checked: boolean
61
+ onChange: (checked: boolean) => void
62
+ disabled?: boolean
63
+ size?: CheckboxSize
64
+ color?: CheckboxColor
65
+ variant?: CheckboxVariant
66
+ className?: string
67
+ /** Test ID for E2E testing */
68
+ testId?: string
69
+ }
70
+
71
+ export function Checkbox({
72
+ checked,
73
+ onChange,
74
+ disabled = false,
75
+ size = 'sm',
76
+ color = 'blue',
77
+ variant = 'outline',
78
+ className = '',
79
+ testId,
80
+ }: CheckboxProps) {
81
+ const s = CHECKBOX_SIZES[size]
82
+ const c = CHECKBOX_COLORS[color]
83
+ const uncheckedStyle = variant === 'outline'
84
+ ? `${c.border} ${c.hover}`
85
+ : `bg-neutral-700 ${c.border} ${c.hover}`
86
+ return (
87
+ <button
88
+ type="button"
89
+ onClick={() => !disabled && onChange(!checked)}
90
+ disabled={disabled}
91
+ data-testid={testId}
92
+ className={`
93
+ ${s.box} rounded border flex items-center justify-center transition-colors flex-shrink-0
94
+ cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed
95
+ ${checked ? `${c.bg} ${c.border}` : uncheckedStyle}
96
+ ${className}
97
+ `}
98
+ >
99
+ {checked && <Check className={`${s.icon} ${c.icon}`} />}
100
+ </button>
101
+ )
102
+ }