@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,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')}>·</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
|
+
}
|