@toolr/ui-design 0.1.6 → 0.1.8
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/components/hooks/use-click-outside.ts +10 -3
- package/components/hooks/use-modal-behavior.ts +53 -0
- package/components/hooks/use-navigation-history.ts +7 -2
- package/components/hooks/use-resizable-sidebar.ts +38 -0
- package/components/lib/form-colors.ts +40 -0
- package/components/sections/captured-issues/captured-issues-panel.tsx +3 -3
- package/components/sections/captured-issues/use-captured-issues.ts +9 -3
- package/components/sections/golden-snapshots/file-diff-viewer.tsx +1 -1
- package/components/sections/golden-snapshots/status-overview.tsx +1 -1
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +4 -40
- package/components/sections/prompt-editor/index.ts +0 -7
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +4 -40
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +4 -36
- package/components/sections/snippets-editor/snippets-editor.tsx +6 -39
- package/components/settings/SettingsHeader.tsx +0 -1
- package/components/settings/SettingsTreeNav.tsx +9 -12
- package/components/ui/action-dialog.tsx +19 -55
- package/components/ui/ai-action-button.tsx +2 -4
- package/components/ui/badge.tsx +15 -23
- package/components/ui/breadcrumb.tsx +11 -71
- package/components/ui/checkbox.tsx +19 -27
- package/components/ui/collapsible-section.tsx +4 -41
- package/components/ui/confirm-badge.tsx +14 -23
- package/components/ui/cookie-consent.tsx +18 -2
- package/components/ui/debounce-border-overlay.tsx +31 -0
- package/components/ui/detail-section.tsx +2 -19
- package/components/ui/editor-placeholder-card.tsx +10 -9
- package/components/ui/execution-details-panel.tsx +2 -7
- package/components/ui/extension-list-card.tsx +1 -1
- package/components/ui/file-structure-section.tsx +3 -18
- package/components/ui/file-tree.tsx +6 -18
- package/components/ui/files-panel.tsx +3 -11
- package/components/ui/filter-dropdown.tsx +5 -2
- package/components/ui/form-actions.tsx +11 -8
- package/components/ui/icon-button.tsx +7 -6
- package/components/ui/input.tsx +18 -29
- package/components/ui/label.tsx +7 -17
- package/components/ui/layout-tab-bar.tsx +5 -5
- package/components/ui/modal.tsx +10 -18
- package/components/ui/nav-card.tsx +3 -18
- package/components/ui/navigation-bar.tsx +12 -73
- package/components/ui/number-input.tsx +6 -0
- package/components/ui/registry-browser.tsx +6 -20
- package/components/ui/registry-card.tsx +3 -7
- package/components/ui/resizable-textarea.tsx +13 -35
- package/components/ui/segmented-toggle.tsx +4 -1
- package/components/ui/select.tsx +8 -14
- package/components/ui/selection-grid.tsx +6 -50
- package/components/ui/setting-row.tsx +5 -5
- package/components/ui/settings-card.tsx +2 -2
- package/components/ui/settings-info-box.tsx +6 -24
- package/components/ui/sort-dropdown.tsx +8 -5
- package/components/ui/status-card.tsx +2 -13
- package/components/ui/tab-bar.tsx +17 -33
- package/components/ui/toggle.tsx +22 -30
- package/components/ui/tooltip.tsx +11 -23
- package/dist/index.d.ts +71 -142
- package/dist/index.js +1630 -2436
- package/index.ts +8 -7
- package/package.json +9 -1
- package/components/sections/prompt-editor/use-prompt-editor.ts +0 -131
|
@@ -11,49 +11,17 @@
|
|
|
11
11
|
* Footer: FormActions (status text, cancel/submit)
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { useId, useRef } from 'react'
|
|
15
15
|
import { createPortal } from 'react-dom'
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
ChevronLeft, ChevronRight, ChevronUp, ChevronDown,
|
|
19
|
-
Check, X, Plus, Minus, Pencil, Trash2, Copy, Save,
|
|
20
|
-
RefreshCw, RotateCcw, Undo2, Redo2,
|
|
21
|
-
Search, Filter, Download, Upload, ExternalLink, Link2,
|
|
22
|
-
Eye, EyeOff, Lock, Unlock, Settings, MoreHorizontal, MoreVertical,
|
|
23
|
-
Info, HelpCircle,
|
|
24
|
-
User, Users, Folder, File, Image, Code, Terminal,
|
|
25
|
-
Star, Heart, Bell, Bookmark, Tag, Pin, Mail, Send,
|
|
26
|
-
Globe, Database, Cloud,
|
|
27
|
-
Wand2, Shield, ShieldCheck, Zap, Sparkles,
|
|
28
|
-
Play, Pause, Square, StopCircle,
|
|
29
|
-
Menu, GripVertical, Maximize2, Minimize2,
|
|
30
|
-
Scan, Webhook, Bot, Puzzle, Plug,
|
|
31
|
-
} from 'lucide-react'
|
|
32
|
-
import type { LucideIcon } from 'lucide-react'
|
|
33
|
-
import { IconButton } from './icon-button.tsx'
|
|
16
|
+
import { useModalBehavior } from '../hooks/use-modal-behavior.ts'
|
|
17
|
+
import { iconMap, IconButton } from './icon-button.tsx'
|
|
34
18
|
import type { IconName } from './icon-button.tsx'
|
|
35
19
|
import { FormActions } from './form-actions.tsx'
|
|
36
20
|
import { SelectionGrid, type SelectionCardItem, type CodingToolPresetConfig } from './selection-grid.tsx'
|
|
37
|
-
import { ExecutionDetailsPanel
|
|
21
|
+
import { ExecutionDetailsPanel } from './execution-details-panel.tsx'
|
|
22
|
+
import type { DetailRow } from './detail-section.tsx'
|
|
38
23
|
import { cn } from '../lib/cn.ts'
|
|
39
24
|
|
|
40
|
-
const dialogIconMap: Record<string, LucideIcon> = {
|
|
41
|
-
'arrow-left': ArrowLeft, 'arrow-right': ArrowRight, 'arrow-up': ArrowUp, 'arrow-down': ArrowDown,
|
|
42
|
-
'chevron-left': ChevronLeft, 'chevron-right': ChevronRight, 'chevron-up': ChevronUp, 'chevron-down': ChevronDown,
|
|
43
|
-
'check': Check, 'x': X, 'plus': Plus, 'minus': Minus, 'pencil': Pencil, 'trash': Trash2, 'copy': Copy, 'save': Save,
|
|
44
|
-
'refresh': RefreshCw, 'rotate': RotateCcw, 'undo': Undo2, 'redo': Redo2,
|
|
45
|
-
'search': Search, 'filter': Filter, 'download': Download, 'upload': Upload, 'external-link': ExternalLink, 'link': Link2,
|
|
46
|
-
'eye': Eye, 'eye-off': EyeOff, 'lock': Lock, 'unlock': Unlock, 'settings': Settings, 'more-h': MoreHorizontal, 'more-v': MoreVertical,
|
|
47
|
-
'info': Info, 'help': HelpCircle,
|
|
48
|
-
'user': User, 'users': Users, 'folder': Folder, 'file': File, 'image': Image, 'code': Code, 'terminal': Terminal,
|
|
49
|
-
'star': Star, 'heart': Heart, 'bell': Bell, 'bookmark': Bookmark, 'tag': Tag, 'pin': Pin, 'mail': Mail, 'send': Send,
|
|
50
|
-
'globe': Globe, 'database': Database, 'cloud': Cloud,
|
|
51
|
-
'wand': Wand2, 'shield': Shield, 'shield-check': ShieldCheck, 'zap': Zap, 'sparkles': Sparkles,
|
|
52
|
-
'play': Play, 'pause': Pause, 'stop': Square, 'stop-circle': StopCircle, 'scan': Scan,
|
|
53
|
-
'menu': Menu, 'grip': GripVertical, 'maximize': Maximize2, 'minimize': Minimize2,
|
|
54
|
-
'webhook': Webhook, 'bot': Bot, 'puzzle': Puzzle, 'plug': Plug,
|
|
55
|
-
}
|
|
56
|
-
|
|
57
25
|
export interface ActionDialogProps {
|
|
58
26
|
/** Dialog title */
|
|
59
27
|
title: string
|
|
@@ -114,7 +82,7 @@ export interface ActionDialogProps {
|
|
|
114
82
|
|
|
115
83
|
// ── Execution details section (mandatory) ────────────────────────────────
|
|
116
84
|
/** Execution detail rows (Tool, Permissions, Output, CLI Flags, Changes) */
|
|
117
|
-
executionDetails:
|
|
85
|
+
executionDetails: DetailRow[]
|
|
118
86
|
/** Whether direct file edits are allowed */
|
|
119
87
|
allowDirectEdits?: boolean
|
|
120
88
|
/** Callback to toggle direct edits - shows the toggle when provided */
|
|
@@ -163,20 +131,12 @@ export function ActionDialog({
|
|
|
163
131
|
children,
|
|
164
132
|
className,
|
|
165
133
|
}: ActionDialogProps) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (e.key === 'Escape') onCancel?.()
|
|
169
|
-
}
|
|
170
|
-
document.addEventListener('keydown', handleEscape)
|
|
171
|
-
return () => document.removeEventListener('keydown', handleEscape)
|
|
172
|
-
}, [onCancel])
|
|
134
|
+
const dialogRef = useRef<HTMLDivElement>(null)
|
|
135
|
+
const titleId = useId()
|
|
173
136
|
|
|
174
|
-
|
|
175
|
-
document.body.style.overflow = 'hidden'
|
|
176
|
-
return () => { document.body.style.overflow = '' }
|
|
177
|
-
}, [])
|
|
137
|
+
useModalBehavior(true, () => onCancel?.(), dialogRef)
|
|
178
138
|
|
|
179
|
-
const Icon = icon ?
|
|
139
|
+
const Icon = icon ? iconMap[icon] : null
|
|
180
140
|
const hasSelection = ((items && items.length > 0) || (presets && presets.length > 0)) && selectedIds && onSelect
|
|
181
141
|
const hasScenarios = scenarios && scenarios.length > 0 && selectedScenarioIds && onSelectScenarios
|
|
182
142
|
const hasExecutionDetails = executionDetails.length > 0 || onAllowDirectEditsChange || executionWarning
|
|
@@ -194,10 +154,14 @@ export function ActionDialog({
|
|
|
194
154
|
|
|
195
155
|
return createPortal(
|
|
196
156
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
197
|
-
<div className="absolute inset-0 bg-[var(--dialog-backdrop)]
|
|
157
|
+
<div className="absolute inset-0 bg-[var(--dialog-backdrop)]" onClick={onCancel} aria-hidden="true" />
|
|
198
158
|
<div
|
|
159
|
+
ref={dialogRef}
|
|
160
|
+
role="dialog"
|
|
161
|
+
aria-modal="true"
|
|
162
|
+
aria-labelledby={titleId}
|
|
199
163
|
className={cn(
|
|
200
|
-
'relative bg-neutral-950 border border-neutral-700 rounded-xl shadow-
|
|
164
|
+
'relative bg-neutral-950 border border-neutral-700 rounded-xl shadow-lg w-full max-w-[800px] mx-4 flex flex-col',
|
|
201
165
|
'max-h-[80vh]',
|
|
202
166
|
className,
|
|
203
167
|
)}
|
|
@@ -210,12 +174,12 @@ export function ActionDialog({
|
|
|
210
174
|
style={iconColor ? { color: iconColor } : undefined}
|
|
211
175
|
/>
|
|
212
176
|
)}
|
|
213
|
-
<div className="flex flex-col">
|
|
214
|
-
<span className="text-md font-semibold text-neutral-200">
|
|
177
|
+
<div className="flex flex-col min-w-0">
|
|
178
|
+
<span id={titleId} className="text-md font-semibold text-neutral-200 truncate">
|
|
215
179
|
{title}
|
|
216
180
|
</span>
|
|
217
181
|
{subtitle && (
|
|
218
|
-
<span className="text-sm text-neutral-500">{subtitle}</span>
|
|
182
|
+
<span className="text-sm text-neutral-500 truncate">{subtitle}</span>
|
|
219
183
|
)}
|
|
220
184
|
</div>
|
|
221
185
|
<div className="flex-1" />
|
|
@@ -119,8 +119,6 @@ export function AiActionButton({
|
|
|
119
119
|
}, [tooltip, forceDisabled, disabledReason, isRunning, isCompleted, runningTooltipTitle, completedTooltipTitle])
|
|
120
120
|
|
|
121
121
|
const isDisabled = forceDisabled
|
|
122
|
-
const blinkClass = isCompleted ? 'animate-pulse' : ''
|
|
123
|
-
|
|
124
122
|
return (
|
|
125
123
|
<IconButton
|
|
126
124
|
icon={resolvedIcon}
|
|
@@ -129,8 +127,8 @@ export function AiActionButton({
|
|
|
129
127
|
disabled={isDisabled}
|
|
130
128
|
onClick={isDisabled ? () => {} : onClick}
|
|
131
129
|
tooltip={resolvedTooltip}
|
|
132
|
-
active={isRunning}
|
|
133
|
-
className={
|
|
130
|
+
active={isRunning || isCompleted}
|
|
131
|
+
className={className}
|
|
134
132
|
testId={testId}
|
|
135
133
|
/>
|
|
136
134
|
)
|
package/components/ui/badge.tsx
CHANGED
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
* - 5 size variants (xss, xs, sm, md, lg)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
import { memo } from 'react'
|
|
17
|
+
import { FORM_COLORS, type AccentColor } from '../lib/form-colors.ts'
|
|
18
|
+
import { cn } from '../lib/cn.ts'
|
|
19
|
+
|
|
20
|
+
export type BadgeColor = AccentColor
|
|
17
21
|
|
|
18
22
|
export interface BadgeProps {
|
|
19
23
|
value: number | string
|
|
@@ -23,24 +27,6 @@ export interface BadgeProps {
|
|
|
23
27
|
testId?: string
|
|
24
28
|
}
|
|
25
29
|
|
|
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
|
-
pink: 'border-pink-500/30 text-pink-400',
|
|
41
|
-
teal: 'border-teal-500/30 text-teal-400',
|
|
42
|
-
}
|
|
43
|
-
|
|
44
30
|
const sizeClasses = {
|
|
45
31
|
xss: 'min-w-[14px] h-[14px] px-0.5 text-xss',
|
|
46
32
|
xs: 'min-w-[16px] h-[16px] px-1 text-xs',
|
|
@@ -49,11 +35,11 @@ const sizeClasses = {
|
|
|
49
35
|
lg: 'min-w-[22px] h-[22px] px-1.5 text-sm',
|
|
50
36
|
}
|
|
51
37
|
|
|
52
|
-
export function Badge({
|
|
38
|
+
export const Badge = memo(function Badge({
|
|
53
39
|
value,
|
|
54
40
|
color = 'neutral',
|
|
55
41
|
size = 'sm',
|
|
56
|
-
className
|
|
42
|
+
className,
|
|
57
43
|
testId,
|
|
58
44
|
}: BadgeProps) {
|
|
59
45
|
const display = typeof value === 'number' && value > 99 ? '99+' : value
|
|
@@ -61,9 +47,15 @@ export function Badge({
|
|
|
61
47
|
return (
|
|
62
48
|
<span
|
|
63
49
|
data-testid={testId}
|
|
64
|
-
className={
|
|
50
|
+
className={cn(
|
|
51
|
+
'inline-flex items-center justify-center border rounded-full font-medium leading-none tabular-nums',
|
|
52
|
+
FORM_COLORS[color].border,
|
|
53
|
+
FORM_COLORS[color].accent,
|
|
54
|
+
sizeClasses[size],
|
|
55
|
+
className,
|
|
56
|
+
)}
|
|
65
57
|
>
|
|
66
58
|
{display}
|
|
67
59
|
</span>
|
|
68
60
|
)
|
|
69
|
-
}
|
|
61
|
+
})
|
|
@@ -1,53 +1,10 @@
|
|
|
1
1
|
/** Breadcrumb navigation with clickable segments, color-coded icons, and configurable separators. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
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, House, Package,
|
|
10
|
-
} from 'lucide-react'
|
|
11
|
-
import type { LucideIcon } from 'lucide-react'
|
|
12
|
-
import type { IconName } from './icon-button.tsx'
|
|
3
|
+
import { ChevronRight } from 'lucide-react'
|
|
4
|
+
import { iconMap, type IconName } from './icon-button.tsx'
|
|
5
|
+
import { ACCENT_NAV, type AccentColor } from '../lib/form-colors.ts'
|
|
13
6
|
import { cn } from '../lib/cn.ts'
|
|
14
7
|
|
|
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
|
-
home: House,
|
|
48
|
-
'package': Package,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
8
|
export interface BreadcrumbSegment {
|
|
52
9
|
id: string
|
|
53
10
|
label: string
|
|
@@ -73,29 +30,12 @@ const sizeConfig = {
|
|
|
73
30
|
lg: { text: 'text-lg', icon: 'w-5 h-5', px: 'px-3', py: 'py-1.5', gap: 'gap-2', sep: 'w-4 h-4' },
|
|
74
31
|
}
|
|
75
32
|
|
|
76
|
-
const colorMap: Record<string, { bg: string; text: string }> = {
|
|
77
|
-
blue: { bg: 'bg-blue-500/10', text: 'text-blue-400' },
|
|
78
|
-
green: { bg: 'bg-green-500/10', text: 'text-green-400' },
|
|
79
|
-
purple: { bg: 'bg-purple-500/10', text: 'text-purple-400' },
|
|
80
|
-
red: { bg: 'bg-red-500/10', text: 'text-red-400' },
|
|
81
|
-
orange: { bg: 'bg-orange-500/10', text: 'text-orange-400' },
|
|
82
|
-
cyan: { bg: 'bg-cyan-500/10', text: 'text-cyan-400' },
|
|
83
|
-
yellow: { bg: 'bg-yellow-500/10', text: 'text-yellow-400' },
|
|
84
|
-
amber: { bg: 'bg-amber-500/10', text: 'text-amber-400' },
|
|
85
|
-
emerald: { bg: 'bg-emerald-500/10', text: 'text-emerald-400' },
|
|
86
|
-
indigo: { bg: 'bg-indigo-500/10', text: 'text-indigo-400' },
|
|
87
|
-
violet: { bg: 'bg-violet-500/10', text: 'text-violet-400' },
|
|
88
|
-
sky: { bg: 'bg-sky-500/10', text: 'text-sky-400' },
|
|
89
|
-
pink: { bg: 'bg-pink-500/10', text: 'text-pink-400' },
|
|
90
|
-
teal: { bg: 'bg-teal-500/10', text: 'text-teal-400' },
|
|
91
|
-
neutral: { bg: 'bg-neutral-500/10', text: 'text-neutral-400' },
|
|
92
|
-
}
|
|
93
33
|
|
|
94
34
|
function SegmentIcon({ icon, color, size }: { icon: IconName; color?: string; size: 'xss' | 'xs' | 'sm' | 'md' | 'lg' }) {
|
|
95
|
-
const Icon =
|
|
35
|
+
const Icon = iconMap[icon]
|
|
96
36
|
if (!Icon) return null
|
|
97
37
|
const s = sizeConfig[size]
|
|
98
|
-
const c = color &&
|
|
38
|
+
const c = color && ACCENT_NAV[color as AccentColor] ? ACCENT_NAV[color as AccentColor] : null
|
|
99
39
|
|
|
100
40
|
return (
|
|
101
41
|
<span className={c?.text || ''}>
|
|
@@ -127,7 +67,7 @@ export function Breadcrumb({
|
|
|
127
67
|
const isBox = variant === 'box'
|
|
128
68
|
|
|
129
69
|
return (
|
|
130
|
-
<nav className={cn('flex items-center', className)}>
|
|
70
|
+
<nav className={cn('flex items-center min-w-0', className)}>
|
|
131
71
|
<div className={cn(
|
|
132
72
|
'flex items-center gap-1',
|
|
133
73
|
isBox && [s.px, s.py, 'bg-neutral-800/50 border border-neutral-700/50 rounded-lg'],
|
|
@@ -135,18 +75,18 @@ export function Breadcrumb({
|
|
|
135
75
|
{segments.map((segment, index) => {
|
|
136
76
|
const isLast = index === segments.length - 1
|
|
137
77
|
const isClickable = !isLast && !!segment.onClick
|
|
138
|
-
const colors = segment.color &&
|
|
78
|
+
const colors = segment.color && ACCENT_NAV[segment.color as AccentColor] ? ACCENT_NAV[segment.color as AccentColor] : null
|
|
139
79
|
const isFirstPlain = !isBox && index === 0
|
|
140
80
|
|
|
141
81
|
return (
|
|
142
|
-
<div key={segment.id} className="flex items-center gap-1">
|
|
82
|
+
<div key={segment.id} className="flex items-center gap-1 min-w-0">
|
|
143
83
|
{index > 0 && <Separator type={separator} size={size} />}
|
|
144
84
|
{isClickable ? (
|
|
145
85
|
<button
|
|
146
86
|
type="button"
|
|
147
87
|
onClick={segment.onClick}
|
|
148
88
|
className={cn(
|
|
149
|
-
'flex items-center gap-1.5 pr-2 py-0.5 rounded-md transition-colors cursor-pointer',
|
|
89
|
+
'flex items-center gap-1.5 pr-2 py-0.5 rounded-md transition-colors cursor-pointer min-w-0',
|
|
150
90
|
isFirstPlain ? 'pl-0' : 'pl-2',
|
|
151
91
|
s.text,
|
|
152
92
|
'font-medium hover:text-white',
|
|
@@ -154,12 +94,12 @@ export function Breadcrumb({
|
|
|
154
94
|
)}
|
|
155
95
|
>
|
|
156
96
|
{segment.icon && <SegmentIcon icon={segment.icon} color={segment.color} size={size} />}
|
|
157
|
-
<span>{segment.label}</span>
|
|
97
|
+
<span className="truncate max-w-[200px]">{segment.label}</span>
|
|
158
98
|
</button>
|
|
159
99
|
) : (
|
|
160
100
|
<div
|
|
161
101
|
className={cn(
|
|
162
|
-
'flex items-center gap-1.5 pr-2 py-0.5 rounded-md',
|
|
102
|
+
'flex items-center gap-1.5 pr-2 py-0.5 rounded-md min-w-0',
|
|
163
103
|
isFirstPlain ? 'pl-0' : 'pl-2',
|
|
164
104
|
s.text,
|
|
165
105
|
isLast
|
|
@@ -8,25 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Check } from 'lucide-react'
|
|
11
|
+
import { type AccentColor } from '../lib/form-colors.ts'
|
|
12
|
+
import { cn } from '../lib/cn.ts'
|
|
11
13
|
|
|
12
14
|
export type CheckboxSize = 'xss' | 'xs' | 'sm' | 'md' | 'lg'
|
|
13
15
|
|
|
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'
|
|
16
|
+
export type CheckboxColor = AccentColor
|
|
30
17
|
|
|
31
18
|
const CHECKBOX_COLORS: Record<CheckboxColor, { bg: string; border: string; icon: string; hover: string }> = {
|
|
32
19
|
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' },
|
|
@@ -64,6 +51,8 @@ export interface CheckboxProps {
|
|
|
64
51
|
color?: CheckboxColor
|
|
65
52
|
variant?: CheckboxVariant
|
|
66
53
|
className?: string
|
|
54
|
+
/** Accessible label — required for screen readers */
|
|
55
|
+
'aria-label'?: string
|
|
67
56
|
/** Test ID for E2E testing */
|
|
68
57
|
testId?: string
|
|
69
58
|
}
|
|
@@ -75,28 +64,31 @@ export function Checkbox({
|
|
|
75
64
|
size = 'sm',
|
|
76
65
|
color = 'blue',
|
|
77
66
|
variant = 'outline',
|
|
78
|
-
className
|
|
67
|
+
className,
|
|
68
|
+
'aria-label': ariaLabel,
|
|
79
69
|
testId,
|
|
80
70
|
}: CheckboxProps) {
|
|
81
71
|
const s = CHECKBOX_SIZES[size]
|
|
82
72
|
const c = CHECKBOX_COLORS[color]
|
|
83
|
-
const uncheckedStyle = variant === 'outline'
|
|
84
|
-
? `${c.border} ${c.hover}`
|
|
85
|
-
: `bg-neutral-700 ${c.border} ${c.hover}`
|
|
86
73
|
return (
|
|
87
74
|
<button
|
|
88
75
|
type="button"
|
|
76
|
+
role="checkbox"
|
|
77
|
+
aria-checked={checked}
|
|
78
|
+
aria-label={ariaLabel}
|
|
89
79
|
onClick={() => !disabled && onChange(!checked)}
|
|
90
80
|
disabled={disabled}
|
|
91
81
|
data-testid={testId}
|
|
92
|
-
className={
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
className={cn(
|
|
83
|
+
'rounded border flex items-center justify-center transition-colors flex-shrink-0 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed',
|
|
84
|
+
s.box,
|
|
85
|
+
checked
|
|
86
|
+
? cn(c.bg, c.border)
|
|
87
|
+
: cn(variant === 'filled' && 'bg-neutral-700', c.border, c.hover),
|
|
88
|
+
className,
|
|
89
|
+
)}
|
|
98
90
|
>
|
|
99
|
-
{checked && <Check className={
|
|
91
|
+
{checked && <Check className={cn(s.icon, c.icon)} />}
|
|
100
92
|
</button>
|
|
101
93
|
)
|
|
102
94
|
}
|
|
@@ -1,47 +1,9 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
Star, Heart, Bell, Bookmark, Tag, Pin, Mail, Globe, Cloud,
|
|
5
|
-
Shield, Zap, Sparkles, Search, Filter, Eye, Lock, User, Users,
|
|
6
|
-
File, Image, Download, Upload, Play, Pause,
|
|
7
|
-
} from 'lucide-react'
|
|
8
|
-
import type { LucideIcon } from 'lucide-react'
|
|
9
|
-
import type { IconName } from './icon-button.tsx'
|
|
2
|
+
import { ChevronRight } from 'lucide-react'
|
|
3
|
+
import { iconMap, type IconName } from './icon-button.tsx'
|
|
10
4
|
import { Badge, type BadgeColor } from './badge.tsx'
|
|
11
5
|
import { cn } from '../lib/cn.ts'
|
|
12
6
|
|
|
13
|
-
const iconSubset: Partial<Record<IconName, LucideIcon>> = {
|
|
14
|
-
settings: Settings,
|
|
15
|
-
folder: Folder,
|
|
16
|
-
code: Code,
|
|
17
|
-
terminal: Terminal,
|
|
18
|
-
database: Database,
|
|
19
|
-
star: Star,
|
|
20
|
-
heart: Heart,
|
|
21
|
-
bell: Bell,
|
|
22
|
-
bookmark: Bookmark,
|
|
23
|
-
tag: Tag,
|
|
24
|
-
pin: Pin,
|
|
25
|
-
mail: Mail,
|
|
26
|
-
globe: Globe,
|
|
27
|
-
cloud: Cloud,
|
|
28
|
-
shield: Shield,
|
|
29
|
-
zap: Zap,
|
|
30
|
-
sparkles: Sparkles,
|
|
31
|
-
search: Search,
|
|
32
|
-
filter: Filter,
|
|
33
|
-
eye: Eye,
|
|
34
|
-
lock: Lock,
|
|
35
|
-
user: User,
|
|
36
|
-
users: Users,
|
|
37
|
-
file: File,
|
|
38
|
-
image: Image,
|
|
39
|
-
download: Download,
|
|
40
|
-
upload: Upload,
|
|
41
|
-
play: Play,
|
|
42
|
-
pause: Pause,
|
|
43
|
-
}
|
|
44
|
-
|
|
45
7
|
export interface CollapsibleSectionProps {
|
|
46
8
|
title: string
|
|
47
9
|
icon?: IconName
|
|
@@ -64,12 +26,13 @@ export function CollapsibleSection({
|
|
|
64
26
|
className,
|
|
65
27
|
}: CollapsibleSectionProps) {
|
|
66
28
|
const [open, setOpen] = useState(defaultOpen)
|
|
67
|
-
const Icon = icon ?
|
|
29
|
+
const Icon = icon ? iconMap[icon] : undefined
|
|
68
30
|
|
|
69
31
|
return (
|
|
70
32
|
<div className={cn('border-b border-neutral-700', className)}>
|
|
71
33
|
<button
|
|
72
34
|
type="button"
|
|
35
|
+
aria-expanded={open}
|
|
73
36
|
onClick={() => setOpen(!open)}
|
|
74
37
|
className="flex w-full items-center gap-2 py-2.5 px-1 text-left hover:bg-neutral-700/30 transition-colors cursor-pointer"
|
|
75
38
|
>
|
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
* - 5 size variants (xss, xs, sm, md, lg)
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { memo } from 'react'
|
|
14
15
|
import { Check } from 'lucide-react'
|
|
16
|
+
import { FORM_COLORS, type AccentColor } from '../lib/form-colors.ts'
|
|
17
|
+
import { cn } from '../lib/cn.ts'
|
|
15
18
|
|
|
16
|
-
export type ConfirmBadgeColor =
|
|
19
|
+
export type ConfirmBadgeColor = AccentColor
|
|
17
20
|
|
|
18
21
|
export interface ConfirmBadgeProps {
|
|
19
22
|
color?: ConfirmBadgeColor
|
|
@@ -22,24 +25,6 @@ export interface ConfirmBadgeProps {
|
|
|
22
25
|
testId?: string
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
const colorClasses: Record<ConfirmBadgeColor, string> = {
|
|
26
|
-
green: 'border-green-500/30 text-green-400',
|
|
27
|
-
red: 'border-red-500/30 text-red-400',
|
|
28
|
-
blue: 'border-blue-500/30 text-blue-400',
|
|
29
|
-
orange: 'border-orange-500/30 text-orange-400',
|
|
30
|
-
cyan: 'border-cyan-500/30 text-cyan-400',
|
|
31
|
-
yellow: 'border-yellow-500/30 text-yellow-400',
|
|
32
|
-
purple: 'border-purple-500/30 text-purple-400',
|
|
33
|
-
indigo: 'border-indigo-500/30 text-indigo-400',
|
|
34
|
-
emerald: 'border-emerald-500/30 text-emerald-400',
|
|
35
|
-
amber: 'border-amber-500/30 text-amber-400',
|
|
36
|
-
violet: 'border-violet-500/30 text-violet-400',
|
|
37
|
-
neutral: 'border-neutral-500/30 text-neutral-400',
|
|
38
|
-
sky: 'border-sky-500/30 text-sky-400',
|
|
39
|
-
pink: 'border-pink-500/30 text-pink-400',
|
|
40
|
-
teal: 'border-teal-500/30 text-teal-400',
|
|
41
|
-
}
|
|
42
|
-
|
|
43
28
|
const sizeClasses = {
|
|
44
29
|
xss: 'w-[14px] h-[14px] rounded-full',
|
|
45
30
|
xs: 'w-[16px] h-[16px] rounded-full',
|
|
@@ -56,18 +41,24 @@ const iconSizeClasses = {
|
|
|
56
41
|
lg: 'w-4 h-4',
|
|
57
42
|
}
|
|
58
43
|
|
|
59
|
-
export function ConfirmBadge({
|
|
44
|
+
export const ConfirmBadge = memo(function ConfirmBadge({
|
|
60
45
|
color = 'neutral',
|
|
61
46
|
size = 'sm',
|
|
62
|
-
className
|
|
47
|
+
className,
|
|
63
48
|
testId,
|
|
64
49
|
}: ConfirmBadgeProps) {
|
|
65
50
|
return (
|
|
66
51
|
<span
|
|
67
52
|
data-testid={testId}
|
|
68
|
-
className={
|
|
53
|
+
className={cn(
|
|
54
|
+
'inline-flex items-center justify-center border',
|
|
55
|
+
FORM_COLORS[color].border,
|
|
56
|
+
FORM_COLORS[color].accent,
|
|
57
|
+
sizeClasses[size],
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
69
60
|
>
|
|
70
61
|
<Check className={iconSizeClasses[size]} />
|
|
71
62
|
</span>
|
|
72
63
|
)
|
|
73
|
-
}
|
|
64
|
+
})
|
|
@@ -16,6 +16,22 @@ export interface CookieConsentProps {
|
|
|
16
16
|
onConsent?: (choice: ConsentChoice) => void
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
const ACCENT_BUTTON_CLASSES: Record<string, string> = {
|
|
20
|
+
cyan: 'bg-cyan-600 border-cyan-500 hover:bg-cyan-500',
|
|
21
|
+
blue: 'bg-blue-600 border-blue-500 hover:bg-blue-500',
|
|
22
|
+
green: 'bg-green-600 border-green-500 hover:bg-green-500',
|
|
23
|
+
purple: 'bg-purple-600 border-purple-500 hover:bg-purple-500',
|
|
24
|
+
orange: 'bg-orange-600 border-orange-500 hover:bg-orange-500',
|
|
25
|
+
red: 'bg-red-600 border-red-500 hover:bg-red-500',
|
|
26
|
+
amber: 'bg-amber-600 border-amber-500 hover:bg-amber-500',
|
|
27
|
+
emerald: 'bg-emerald-600 border-emerald-500 hover:bg-emerald-500',
|
|
28
|
+
indigo: 'bg-indigo-600 border-indigo-500 hover:bg-indigo-500',
|
|
29
|
+
pink: 'bg-pink-600 border-pink-500 hover:bg-pink-500',
|
|
30
|
+
teal: 'bg-teal-600 border-teal-500 hover:bg-teal-500',
|
|
31
|
+
violet: 'bg-violet-600 border-violet-500 hover:bg-violet-500',
|
|
32
|
+
sky: 'bg-sky-600 border-sky-500 hover:bg-sky-500',
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
export function CookieConsent({
|
|
20
36
|
storageKey,
|
|
21
37
|
accentColor = 'cyan',
|
|
@@ -39,7 +55,7 @@ export function CookieConsent({
|
|
|
39
55
|
if (!isVisible) return null
|
|
40
56
|
|
|
41
57
|
return (
|
|
42
|
-
<div className="fixed bottom-0 left-0 right-0 z-50 p-4 bg-neutral-900
|
|
58
|
+
<div className="fixed bottom-0 left-0 right-0 z-50 p-4 bg-neutral-900 border-t border-neutral-700/50">
|
|
43
59
|
<div className="max-w-6xl mx-auto flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
|
44
60
|
<div className="flex-grow">
|
|
45
61
|
<p className="text-md text-neutral-200 mb-1">{heading}</p>
|
|
@@ -61,7 +77,7 @@ export function CookieConsent({
|
|
|
61
77
|
</button>
|
|
62
78
|
<button
|
|
63
79
|
onClick={() => handleConsent('accepted')}
|
|
64
|
-
className={`px-3 py-1.5 text-md h-[26px] inline-flex items-center justify-center font-medium rounded-md cursor-pointer text-white border transition-colors
|
|
80
|
+
className={`px-3 py-1.5 text-md h-[26px] inline-flex items-center justify-center font-medium rounded-md cursor-pointer text-white border transition-colors ${ACCENT_BUTTON_CLASSES[accentColor] ?? ACCENT_BUTTON_CLASSES.cyan}`}
|
|
65
81
|
>
|
|
66
82
|
Accept All
|
|
67
83
|
</button>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface DebounceBorderOverlayProps {
|
|
2
|
+
debounceKey: number
|
|
3
|
+
durationMs: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function DebounceBorderOverlay({ debounceKey, durationMs }: DebounceBorderOverlayProps) {
|
|
7
|
+
if (debounceKey <= 0) return null
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<svg
|
|
11
|
+
key={debounceKey}
|
|
12
|
+
className="absolute inset-0 pointer-events-none text-emerald-400/70"
|
|
13
|
+
style={{ width: '100%', height: '100%' }}
|
|
14
|
+
>
|
|
15
|
+
<rect
|
|
16
|
+
x="1" y="1" rx="5" ry="5"
|
|
17
|
+
fill="none"
|
|
18
|
+
stroke="currentColor"
|
|
19
|
+
strokeWidth="1.5"
|
|
20
|
+
pathLength="100"
|
|
21
|
+
strokeDasharray="100"
|
|
22
|
+
strokeDashoffset="0"
|
|
23
|
+
style={{
|
|
24
|
+
width: 'calc(100% - 2px)',
|
|
25
|
+
height: 'calc(100% - 2px)',
|
|
26
|
+
animation: `debounce-border-drain ${durationMs}ms linear forwards`,
|
|
27
|
+
}}
|
|
28
|
+
/>
|
|
29
|
+
</svg>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -7,26 +7,9 @@
|
|
|
7
7
|
* - Any key-value metadata layout
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
Info, Settings, Code, Shield, Terminal, Database, Globe, Zap,
|
|
12
|
-
Star, Cloud, Bell, Heart, Sparkles, Bot, Plug, Lock, Eye,
|
|
13
|
-
File, Folder, User, Users, Tag, Bookmark, Mail, Send, Search,
|
|
14
|
-
Play, ShieldCheck, Wand2, Copy,
|
|
15
|
-
} from 'lucide-react'
|
|
16
|
-
import type { LucideIcon } from 'lucide-react'
|
|
17
|
-
import type { IconName } from './icon-button.tsx'
|
|
10
|
+
import { iconMap, type IconName } from './icon-button.tsx'
|
|
18
11
|
import { cn } from '../lib/cn.ts'
|
|
19
12
|
|
|
20
|
-
const iconSubset: Partial<Record<IconName, LucideIcon>> = {
|
|
21
|
-
info: Info, settings: Settings, code: Code, shield: Shield,
|
|
22
|
-
terminal: Terminal, database: Database, globe: Globe, zap: Zap,
|
|
23
|
-
star: Star, cloud: Cloud, bell: Bell, heart: Heart,
|
|
24
|
-
sparkles: Sparkles, bot: Bot, plug: Plug, lock: Lock, eye: Eye,
|
|
25
|
-
file: File, folder: Folder, user: User, users: Users, tag: Tag,
|
|
26
|
-
bookmark: Bookmark, mail: Mail, send: Send, search: Search,
|
|
27
|
-
play: Play, 'shield-check': ShieldCheck, wand: Wand2, copy: Copy,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
13
|
export interface DetailRow {
|
|
31
14
|
label: string
|
|
32
15
|
value: string
|
|
@@ -44,7 +27,7 @@ export interface DetailSectionProps {
|
|
|
44
27
|
}
|
|
45
28
|
|
|
46
29
|
export function DetailSection({ title, icon, rows, className }: DetailSectionProps) {
|
|
47
|
-
const Icon = icon ?
|
|
30
|
+
const Icon = icon ? iconMap[icon] : undefined
|
|
48
31
|
|
|
49
32
|
return (
|
|
50
33
|
<div className={className}>
|