@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.
Files changed (61) hide show
  1. package/components/hooks/use-click-outside.ts +10 -3
  2. package/components/hooks/use-modal-behavior.ts +53 -0
  3. package/components/hooks/use-navigation-history.ts +7 -2
  4. package/components/hooks/use-resizable-sidebar.ts +38 -0
  5. package/components/lib/form-colors.ts +40 -0
  6. package/components/sections/captured-issues/captured-issues-panel.tsx +3 -3
  7. package/components/sections/captured-issues/use-captured-issues.ts +9 -3
  8. package/components/sections/golden-snapshots/file-diff-viewer.tsx +1 -1
  9. package/components/sections/golden-snapshots/status-overview.tsx +1 -1
  10. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +4 -40
  11. package/components/sections/prompt-editor/index.ts +0 -7
  12. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +4 -40
  13. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +4 -36
  14. package/components/sections/snippets-editor/snippets-editor.tsx +6 -39
  15. package/components/settings/SettingsHeader.tsx +0 -1
  16. package/components/settings/SettingsTreeNav.tsx +9 -12
  17. package/components/ui/action-dialog.tsx +19 -55
  18. package/components/ui/ai-action-button.tsx +2 -4
  19. package/components/ui/badge.tsx +15 -23
  20. package/components/ui/breadcrumb.tsx +11 -71
  21. package/components/ui/checkbox.tsx +19 -27
  22. package/components/ui/collapsible-section.tsx +4 -41
  23. package/components/ui/confirm-badge.tsx +14 -23
  24. package/components/ui/cookie-consent.tsx +18 -2
  25. package/components/ui/debounce-border-overlay.tsx +31 -0
  26. package/components/ui/detail-section.tsx +2 -19
  27. package/components/ui/editor-placeholder-card.tsx +10 -9
  28. package/components/ui/execution-details-panel.tsx +2 -7
  29. package/components/ui/extension-list-card.tsx +1 -1
  30. package/components/ui/file-structure-section.tsx +3 -18
  31. package/components/ui/file-tree.tsx +6 -18
  32. package/components/ui/files-panel.tsx +3 -11
  33. package/components/ui/filter-dropdown.tsx +5 -2
  34. package/components/ui/form-actions.tsx +11 -8
  35. package/components/ui/icon-button.tsx +7 -6
  36. package/components/ui/input.tsx +18 -29
  37. package/components/ui/label.tsx +7 -17
  38. package/components/ui/layout-tab-bar.tsx +5 -5
  39. package/components/ui/modal.tsx +10 -18
  40. package/components/ui/nav-card.tsx +3 -18
  41. package/components/ui/navigation-bar.tsx +12 -73
  42. package/components/ui/number-input.tsx +6 -0
  43. package/components/ui/registry-browser.tsx +6 -20
  44. package/components/ui/registry-card.tsx +3 -7
  45. package/components/ui/resizable-textarea.tsx +13 -35
  46. package/components/ui/segmented-toggle.tsx +4 -1
  47. package/components/ui/select.tsx +8 -14
  48. package/components/ui/selection-grid.tsx +6 -50
  49. package/components/ui/setting-row.tsx +5 -5
  50. package/components/ui/settings-card.tsx +2 -2
  51. package/components/ui/settings-info-box.tsx +6 -24
  52. package/components/ui/sort-dropdown.tsx +8 -5
  53. package/components/ui/status-card.tsx +2 -13
  54. package/components/ui/tab-bar.tsx +17 -33
  55. package/components/ui/toggle.tsx +22 -30
  56. package/components/ui/tooltip.tsx +11 -23
  57. package/dist/index.d.ts +71 -142
  58. package/dist/index.js +1630 -2436
  59. package/index.ts +8 -7
  60. package/package.json +9 -1
  61. 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 { useEffect } from 'react'
14
+ import { useId, useRef } from 'react'
15
15
  import { createPortal } from 'react-dom'
16
- import {
17
- ArrowLeft, ArrowRight, ArrowUp, ArrowDown,
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, type ExecutionDetailRow } from './execution-details-panel.tsx'
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: ExecutionDetailRow[]
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
- useEffect(() => {
167
- const handleEscape = (e: KeyboardEvent) => {
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
- useEffect(() => {
175
- document.body.style.overflow = 'hidden'
176
- return () => { document.body.style.overflow = '' }
177
- }, [])
137
+ useModalBehavior(true, () => onCancel?.(), dialogRef)
178
138
 
179
- const Icon = icon ? dialogIconMap[icon] : null
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)] backdrop-blur-sm" onClick={onCancel} />
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-2xl w-full max-w-[800px] mx-4 flex flex-col',
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={`${className ?? ''} ${blinkClass}`.trim() || undefined}
130
+ active={isRunning || isCompleted}
131
+ className={className}
134
132
  testId={testId}
135
133
  />
136
134
  )
@@ -13,7 +13,11 @@
13
13
  * - 5 size variants (xss, xs, sm, md, lg)
14
14
  */
15
15
 
16
- export type BadgeColor = 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow' | 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky' | 'pink' | 'teal'
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={`inline-flex items-center justify-center border rounded-full font-medium leading-none tabular-nums ${colorClasses[color]} ${sizeClasses[size]} ${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
- 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, 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 = iconSubset[icon]
35
+ const Icon = iconMap[icon]
96
36
  if (!Icon) return null
97
37
  const s = sizeConfig[size]
98
- const c = color && colorMap[color] ? colorMap[color] : null
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 && colorMap[segment.color] ? colorMap[segment.color] : null
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
- ${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
- `}
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={`${s.icon} ${c.icon}`} />}
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
- ChevronRight, Settings, Folder, Code, Terminal, Database,
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 ? iconSubset[icon] : undefined
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 = 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow' | 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky' | 'pink' | 'teal'
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={`inline-flex items-center justify-center border ${colorClasses[color]} ${sizeClasses[size]} ${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/95 backdrop-blur-sm border-t border-neutral-700/50">
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 bg-${accentColor}-600 border-${accentColor}-500 hover:bg-${accentColor}-500`}
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 ? iconSubset[icon] : undefined
30
+ const Icon = icon ? iconMap[icon] : undefined
48
31
 
49
32
  return (
50
33
  <div className={className}>