@toolr/ui-design 0.1.5 → 0.1.7

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 (82) hide show
  1. package/agent-rules.json +91 -0
  2. package/ai-manifest.json +190 -0
  3. package/components/content/info-panel-primitives.tsx +14 -14
  4. package/components/hooks/use-click-outside.ts +10 -3
  5. package/components/hooks/use-modal-behavior.ts +24 -0
  6. package/components/hooks/use-navigation-history.ts +7 -2
  7. package/components/hooks/use-resizable-sidebar.ts +38 -0
  8. package/components/lib/ai-tools.tsx +1 -1
  9. package/components/lib/form-colors.ts +40 -0
  10. package/components/sections/ai-tools-paths/tools-paths-panel.tsx +7 -7
  11. package/components/sections/captured-issues/captured-issues-panel.tsx +13 -13
  12. package/components/sections/captured-issues/use-captured-issues.ts +9 -3
  13. package/components/sections/golden-snapshots/file-diff-viewer.tsx +13 -13
  14. package/components/sections/golden-snapshots/golden-sync-panel.tsx +5 -5
  15. package/components/sections/golden-snapshots/snapshot-manager.tsx +11 -11
  16. package/components/sections/golden-snapshots/status-overview.tsx +20 -20
  17. package/components/sections/golden-snapshots/version-manager.tsx +8 -8
  18. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +8 -44
  19. package/components/sections/prompt-editor/index.ts +0 -7
  20. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +9 -45
  21. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +11 -43
  22. package/components/sections/report-bug/report-bug-form.tsx +14 -14
  23. package/components/sections/report-bug/screenshot-uploader.tsx +6 -6
  24. package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +3 -3
  25. package/components/sections/snapshot-browser/snapshot-tree.tsx +8 -8
  26. package/components/sections/snippets-editor/snippets-editor.tsx +74 -48
  27. package/components/settings/SettingsHeader.tsx +1 -2
  28. package/components/settings/SettingsTreeNav.tsx +31 -16
  29. package/components/ui/action-dialog.tsx +12 -56
  30. package/components/ui/badge.tsx +8 -24
  31. package/components/ui/bottom-panel-header.tsx +4 -4
  32. package/components/ui/breadcrumb.tsx +8 -68
  33. package/components/ui/checkbox.tsx +2 -16
  34. package/components/ui/collapsible-section.tsx +4 -42
  35. package/components/ui/confirm-badge.tsx +3 -20
  36. package/components/ui/cookie-consent.tsx +21 -5
  37. package/components/ui/debounce-border-overlay.tsx +31 -0
  38. package/components/ui/detail-section.tsx +5 -22
  39. package/components/ui/editor-placeholder-card.tsx +17 -16
  40. package/components/ui/editor-toolbar.tsx +12 -0
  41. package/components/ui/execution-details-panel.tsx +8 -13
  42. package/components/ui/extension-list-card.tsx +3 -3
  43. package/components/ui/file-structure-section.tsx +20 -35
  44. package/components/ui/file-tree.tsx +4 -14
  45. package/components/ui/files-panel.tsx +28 -18
  46. package/components/ui/filter-dropdown.tsx +5 -5
  47. package/components/ui/form-actions.tsx +7 -6
  48. package/components/ui/frontmatter-form-header.tsx +4 -4
  49. package/components/ui/icon-button.tsx +3 -2
  50. package/components/ui/input.tsx +15 -31
  51. package/components/ui/label.tsx +7 -21
  52. package/components/ui/layout-tab-bar.tsx +4 -4
  53. package/components/ui/modal.tsx +5 -17
  54. package/components/ui/nav-card.tsx +5 -20
  55. package/components/ui/navigation-bar.tsx +13 -74
  56. package/components/ui/number-input.tsx +4 -4
  57. package/components/ui/registry-browser.tsx +10 -24
  58. package/components/ui/registry-card.tsx +16 -20
  59. package/components/ui/registry-detail.tsx +6 -6
  60. package/components/ui/resizable-textarea.tsx +13 -35
  61. package/components/ui/segmented-toggle.tsx +6 -5
  62. package/components/ui/select.tsx +7 -16
  63. package/components/ui/selection-grid.tsx +6 -54
  64. package/components/ui/setting-row.tsx +2 -4
  65. package/components/ui/settings-card.tsx +3 -3
  66. package/components/ui/settings-info-box.tsx +6 -23
  67. package/components/ui/settings-section-title.tsx +1 -1
  68. package/components/ui/snapshot-card.tsx +7 -7
  69. package/components/ui/snippets-panel.tsx +10 -10
  70. package/components/ui/sort-dropdown.tsx +2 -2
  71. package/components/ui/status-card.tsx +6 -17
  72. package/components/ui/tab-bar.tsx +5 -31
  73. package/components/ui/toggle.tsx +3 -19
  74. package/components/ui/tooltip.tsx +9 -21
  75. package/dist/content.js +14 -14
  76. package/dist/index.d.ts +71 -141
  77. package/dist/index.js +1634 -2450
  78. package/dist/tokens/primitives.css +9 -2
  79. package/index.ts +8 -7
  80. package/package.json +13 -3
  81. package/tokens/primitives.css +9 -2
  82. package/components/sections/prompt-editor/use-prompt-editor.ts +0 -131
@@ -1,6 +1,7 @@
1
- import { useEffect, useRef, useState } from 'react'
1
+ import { useRef, useState } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
3
  import { Info, AlertTriangle, AlertCircle, Check } from 'lucide-react'
4
+ import { useModalBehavior } from '../hooks/use-modal-behavior.ts'
4
5
  import { IconButton, type ActionItem } from './icon-button.tsx'
5
6
  import { FormActions } from './form-actions.tsx'
6
7
  import type { ReactNode } from 'react'
@@ -42,20 +43,7 @@ interface ModalProps {
42
43
  function Modal({ isOpen, onClose, title, children, kind = 'info', size = 'md', hideCloseButton = false, headerActions, testId }: ModalProps) {
43
44
  const modalRef = useRef<HTMLDivElement>(null)
44
45
 
45
- useEffect(() => {
46
- if (!isOpen) return
47
- const handleEscape = (e: KeyboardEvent) => {
48
- if (e.key === 'Escape') onClose()
49
- }
50
- document.addEventListener('keydown', handleEscape)
51
- return () => document.removeEventListener('keydown', handleEscape)
52
- }, [isOpen, onClose])
53
-
54
- useEffect(() => {
55
- if (!isOpen) return
56
- document.body.style.overflow = 'hidden'
57
- return () => { document.body.style.overflow = '' }
58
- }, [isOpen])
46
+ useModalBehavior(isOpen, onClose)
59
47
 
60
48
  if (!isOpen) return null
61
49
 
@@ -139,11 +127,11 @@ export function ConfirmModal({
139
127
  {message}
140
128
  {warning && (
141
129
  <div className="mt-3 p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg">
142
- <p className="text-amber-300 text-sm font-medium">{warning}</p>
130
+ <p className="text-amber-300 text-md font-medium">{warning}</p>
143
131
  {warningItems && warningItems.length > 0 && (
144
132
  <ul className="mt-2 space-y-1">
145
133
  {warningItems.map((item, i) => (
146
- <li key={i} className="text-amber-300/80 text-sm ml-4 list-disc">{item}</li>
134
+ <li key={i} className="text-amber-300/80 text-md ml-4 list-disc">{item}</li>
147
135
  ))}
148
136
  </ul>
149
137
  )}
@@ -1,24 +1,9 @@
1
1
  /** Navigation card with icon, description, label, and hover lift effect. */
2
2
 
3
- import {
4
- Settings, Puzzle, Zap, Shield, Folder, Code, Database, Globe,
5
- Terminal, Star, Bell, User, Users, File, Tag, Sparkles, Search,
6
- Lock, Mail, Cloud, Heart, Bookmark, Pin, Bot, Plug,
7
- } from 'lucide-react'
8
- import type { LucideIcon } from 'lucide-react'
9
- import type { IconName } from './icon-button.tsx'
3
+ import { iconMap, type IconName } from './icon-button.tsx'
10
4
  import { Label, type LabelColor } from './label.tsx'
11
5
  import { cn } from '../lib/cn.ts'
12
6
 
13
- const iconSubset: Partial<Record<IconName, LucideIcon>> = {
14
- settings: Settings, puzzle: Puzzle, zap: Zap, shield: Shield,
15
- folder: Folder, code: Code, database: Database, globe: Globe,
16
- terminal: Terminal, star: Star, bell: Bell, user: User,
17
- users: Users, file: File, tag: Tag, sparkles: Sparkles,
18
- search: Search, lock: Lock, mail: Mail, cloud: Cloud,
19
- heart: Heart, bookmark: Bookmark, pin: Pin, bot: Bot, plug: Plug,
20
- }
21
-
22
7
  export interface NavCardProps {
23
8
  title: string
24
9
  description?: string
@@ -45,7 +30,7 @@ export function NavCard({
45
30
  disabled = false,
46
31
  className,
47
32
  }: NavCardProps) {
48
- const Icon = IconComponent ?? (icon ? iconSubset[icon] : undefined)
33
+ const Icon = IconComponent ?? (icon ? iconMap[icon] : undefined)
49
34
 
50
35
  return (
51
36
  <button
@@ -74,14 +59,14 @@ export function NavCard({
74
59
  </div>
75
60
  )}
76
61
 
77
- <h3 className="text-sm font-medium text-neutral-200">{title}</h3>
62
+ <h3 className="text-md font-medium text-neutral-200">{title}</h3>
78
63
 
79
64
  {description && (
80
- <p className="mt-1 text-xs text-neutral-500 leading-relaxed line-clamp-2">{description}</p>
65
+ <p className="mt-1 text-sm text-neutral-500 leading-relaxed line-clamp-2">{description}</p>
81
66
  )}
82
67
 
83
68
  {stats && (
84
- <p className="mt-2 text-xs text-neutral-600">{stats}</p>
69
+ <p className="mt-2 text-sm text-neutral-600">{stats}</p>
85
70
  )}
86
71
  </button>
87
72
  )
@@ -1,58 +1,14 @@
1
1
  /** Navigation bar with back/forward controls, history dropdown, and inline breadcrumb path. */
2
2
 
3
3
  import { useState, useRef, useCallback } from 'react'
4
- import {
5
- ChevronLeft, ChevronRight, History,
6
- Menu, Home, Layers,
7
- Settings, Folder, File, Code, Terminal, Database,
8
- Globe, Star, Users, User, Tag, Search, Heart,
9
- Zap, Shield, ShieldCheck, Sparkles, Eye, Lock,
10
- Cloud, Wand2, Bell, Bookmark, Pin, Mail, Send,
11
- Image, Bot, Puzzle, Plug, Webhook,
12
- } from 'lucide-react'
4
+ import { ChevronLeft, ChevronRight, History } from 'lucide-react'
13
5
  import type { LucideIcon } from 'lucide-react'
14
- import type { IconName } from './icon-button.tsx'
6
+ import { iconMap, type IconName } from './icon-button.tsx'
15
7
  import type { BreadcrumbSegment } from './breadcrumb.tsx'
8
+ import { ACCENT_NAV, type AccentColor } from '../lib/form-colors.ts'
16
9
  import { cn } from '../lib/cn.ts'
17
10
  import { useClickOutside } from '../hooks/use-click-outside.ts'
18
11
 
19
- const iconSubset: Partial<Record<IconName, LucideIcon>> = {
20
- menu: Menu,
21
- home: Home,
22
- layers: Layers,
23
- folder: Folder,
24
- file: File,
25
- settings: Settings,
26
- code: Code,
27
- terminal: Terminal,
28
- database: Database,
29
- globe: Globe,
30
- star: Star,
31
- users: Users,
32
- user: User,
33
- tag: Tag,
34
- zap: Zap,
35
- shield: Shield,
36
- 'shield-check': ShieldCheck,
37
- sparkles: Sparkles,
38
- eye: Eye,
39
- lock: Lock,
40
- search: Search,
41
- heart: Heart,
42
- cloud: Cloud,
43
- wand: Wand2,
44
- bell: Bell,
45
- bookmark: Bookmark,
46
- pin: Pin,
47
- mail: Mail,
48
- send: Send,
49
- image: Image,
50
- bot: Bot,
51
- puzzle: Puzzle,
52
- plug: Plug,
53
- webhook: Webhook,
54
- }
55
-
56
12
  export interface NavigationBarProps {
57
13
  segments: BreadcrumbSegment[]
58
14
  canGoBack?: boolean
@@ -70,29 +26,12 @@ export interface NavigationBarProps {
70
26
 
71
27
  const sizeConfig = {
72
28
  xss: { text: 'text-xss', segIcon: 'w-2.5 h-2.5', navIcon: 'w-2.5 h-2.5', navBtn: 'w-[18px] h-[18px] rounded-[3px]', px: 'px-1', py: 'py-0.5', sep: 'w-2 h-2', divH: 'h-3' },
73
- xs: { text: 'text-xs', segIcon: 'w-3 h-3', navIcon: 'w-3 h-3', navBtn: 'w-6 h-6 rounded-[5px]', px: 'px-1.5', py: 'py-0.5', sep: 'w-2.5 h-2.5', divH: 'h-3.5' },
74
- sm: { text: 'text-sm', segIcon: 'w-3.5 h-3.5', navIcon: 'w-3.5 h-3.5', navBtn: 'w-7 h-7 rounded-md', px: 'px-2', py: 'py-1', sep: 'w-3 h-3', divH: 'h-4' },
29
+ xs: { text: 'text-sm', segIcon: 'w-3 h-3', navIcon: 'w-3 h-3', navBtn: 'w-6 h-6 rounded-[5px]', px: 'px-1.5', py: 'py-0.5', sep: 'w-2.5 h-2.5', divH: 'h-3.5' },
30
+ sm: { text: 'text-md', segIcon: 'w-3.5 h-3.5', navIcon: 'w-3.5 h-3.5', navBtn: 'w-7 h-7 rounded-md', px: 'px-2', py: 'py-1', sep: 'w-3 h-3', divH: 'h-4' },
75
31
  md: { text: 'text-base', segIcon: 'w-4 h-4', navIcon: 'w-4 h-4', navBtn: 'w-8 h-8 rounded-md', px: 'px-2.5', py: 'py-1', sep: 'w-3.5 h-3.5', divH: 'h-5' },
76
32
  lg: { text: 'text-lg', segIcon: 'w-5 h-5', navIcon: 'w-5 h-5', navBtn: 'w-9 h-9 rounded-md', px: 'px-3', py: 'py-1.5', sep: 'w-4 h-4', divH: 'h-6' },
77
33
  }
78
34
 
79
- const colorMap: Record<string, { bg: string; text: string }> = {
80
- blue: { bg: 'bg-blue-500/10', text: 'text-blue-400' },
81
- green: { bg: 'bg-green-500/10', text: 'text-green-400' },
82
- purple: { bg: 'bg-purple-500/10', text: 'text-purple-400' },
83
- red: { bg: 'bg-red-500/10', text: 'text-red-400' },
84
- orange: { bg: 'bg-orange-500/10', text: 'text-orange-400' },
85
- cyan: { bg: 'bg-cyan-500/10', text: 'text-cyan-400' },
86
- yellow: { bg: 'bg-yellow-500/10', text: 'text-yellow-400' },
87
- amber: { bg: 'bg-amber-500/10', text: 'text-amber-400' },
88
- emerald: { bg: 'bg-emerald-500/10', text: 'text-emerald-400' },
89
- indigo: { bg: 'bg-indigo-500/10', text: 'text-indigo-400' },
90
- violet: { bg: 'bg-violet-500/10', text: 'text-violet-400' },
91
- sky: { bg: 'bg-sky-500/10', text: 'text-sky-400' },
92
- pink: { bg: 'bg-pink-500/10', text: 'text-pink-400' },
93
- teal: { bg: 'bg-teal-500/10', text: 'text-teal-400' },
94
- neutral: { bg: 'bg-neutral-500/10', text: 'text-neutral-400' },
95
- }
96
35
 
97
36
  function NavButton({ icon: Icon, onClick, disabled, size, active }: {
98
37
  icon: LucideIcon
@@ -134,9 +73,9 @@ function SegmentSeparator({ type, size }: { type: 'chevron' | 'slash' | 'dot'; s
134
73
  }
135
74
 
136
75
  function SegmentIcon({ icon, color, size }: { icon: IconName; color?: string; size: keyof typeof sizeConfig }) {
137
- const Icon = iconSubset[icon]
76
+ const Icon = iconMap[icon]
138
77
  if (!Icon) return null
139
- const c = color && colorMap[color] ? colorMap[color] : null
78
+ const c = color && ACCENT_NAV[color as AccentColor] ? ACCENT_NAV[color as AccentColor] : null
140
79
  return (
141
80
  <span className={c?.text || ''}>
142
81
  <Icon className={sizeConfig[size].segIcon} />
@@ -160,7 +99,7 @@ export function NavigationBar({
160
99
  }: NavigationBarProps) {
161
100
  const s = sizeConfig[size]
162
101
  const hasNav = !!(onBack || onForward)
163
- const LeadIcon = leadingAction ? iconSubset[leadingAction.icon] : null
102
+ const LeadIcon = leadingAction ? iconMap[leadingAction.icon] : null
164
103
 
165
104
  const [historyOpen, setHistoryOpen] = useState(false)
166
105
  const historyRef = useRef<HTMLDivElement>(null)
@@ -202,7 +141,7 @@ export function NavigationBar({
202
141
  {historyOpen && hasHistoryEntries && (
203
142
  <div className="absolute left-0 top-full mt-1 w-max min-w-[200px] max-w-[420px] bg-neutral-800 border border-neutral-700 rounded-lg shadow-xl z-50">
204
143
  <div className="px-3 py-1.5 border-b border-neutral-700/50">
205
- <p className="text-xs font-medium text-neutral-500">History</p>
144
+ <p className="text-sm font-medium text-neutral-500">History</p>
206
145
  </div>
207
146
  <div className="max-h-[300px] overflow-y-auto py-1">
208
147
  {historyEntries.map((entry, i) => (
@@ -220,8 +159,8 @@ export function NavigationBar({
220
159
  {segIdx > 0 && <ChevronRight className="w-2.5 h-2.5 text-neutral-600 flex-shrink-0" />}
221
160
  {seg.icon && <SegmentIcon icon={seg.icon} color={seg.color} size="xs" />}
222
161
  <span className={cn(
223
- 'text-xs truncate',
224
- seg.color && colorMap[seg.color] ? colorMap[seg.color].text : 'text-neutral-300',
162
+ 'text-sm truncate',
163
+ seg.color && ACCENT_NAV[seg.color as AccentColor] ? ACCENT_NAV[seg.color as AccentColor].text : 'text-neutral-300',
225
164
  )}>
226
165
  {seg.label}
227
166
  </span>
@@ -231,7 +170,7 @@ export function NavigationBar({
231
170
  ))}
232
171
  </div>
233
172
  <div className="px-3 py-1.5 border-t border-neutral-700/50">
234
- <p className="text-xs text-neutral-600">Click to navigate</p>
173
+ <p className="text-sm text-neutral-600">Click to navigate</p>
235
174
  </div>
236
175
  </div>
237
176
  )}
@@ -243,7 +182,7 @@ export function NavigationBar({
243
182
  {segments.map((segment, index) => {
244
183
  const isLast = index === segments.length - 1
245
184
  const isClickable = !isLast && !!segment.onClick
246
- const colors = segment.color && colorMap[segment.color] ? colorMap[segment.color] : null
185
+ const colors = segment.color && ACCENT_NAV[segment.color as AccentColor] ? ACCENT_NAV[segment.color as AccentColor] : null
247
186
 
248
187
  return (
249
188
  <div key={segment.id} className="flex items-center gap-1">
@@ -17,10 +17,10 @@ export interface NumberInputProps {
17
17
 
18
18
  const SIZE_CONFIG = {
19
19
  xss: { wrapper: 'h-[18px]', input: 'px-1 text-xss', chevron: 'w-2.5 h-2.5', stepperW: 'w-4' },
20
- xs: { wrapper: 'h-6', input: 'px-1.5 text-xs', chevron: 'w-2.5 h-2.5', stepperW: 'w-5' },
21
- sm: { wrapper: 'h-7', input: 'px-2 text-xs', chevron: 'w-3 h-3', stepperW: 'w-5' },
22
- md: { wrapper: 'h-8', input: 'px-3 text-sm', chevron: 'w-3 h-3', stepperW: 'w-6' },
23
- lg: { wrapper: 'h-9', input: 'px-3 text-sm', chevron: 'w-3.5 h-3.5', stepperW: 'w-7' },
20
+ xs: { wrapper: 'h-6', input: 'px-1.5 text-sm', chevron: 'w-2.5 h-2.5', stepperW: 'w-5' },
21
+ sm: { wrapper: 'h-7', input: 'px-2 text-sm', chevron: 'w-3 h-3', stepperW: 'w-5' },
22
+ md: { wrapper: 'h-8', input: 'px-3 text-md', chevron: 'w-3 h-3', stepperW: 'w-6' },
23
+ lg: { wrapper: 'h-9', input: 'px-3 text-md', chevron: 'w-3.5 h-3.5', stepperW: 'w-7' },
24
24
  }
25
25
 
26
26
  const VARIANT_CLASSES = {
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode, useState, useEffect, useRef, useCallback } from 'react'
2
2
  import { Search, ArrowRight, RefreshCw, Loader2, X, AlertTriangle } from 'lucide-react'
3
+ import { DebounceBorderOverlay } from './debounce-border-overlay.tsx'
3
4
  import { IconButton } from './icon-button.tsx'
4
5
  import { Input } from './input.tsx'
5
6
  import { RegistryCard, type RegistryCardProps } from './registry-card.tsx'
@@ -123,6 +124,10 @@ export function RegistryBrowser({
123
124
  }, 150)
124
125
  }, [onScrollChange])
125
126
 
127
+ useEffect(() => {
128
+ return () => clearTimeout(scrollTimerRef.current)
129
+ }, [])
130
+
126
131
  const loadMore = useCallback(() => {
127
132
  setVisibleCount((prev: number) => Math.min(prev + PAGE_SIZE, totalCount))
128
133
  }, [totalCount])
@@ -170,26 +175,7 @@ export function RegistryBrowser({
170
175
  </button>
171
176
  )}
172
177
  {debounceKey != null && debounceKey > 0 && (
173
- <svg
174
- key={debounceKey}
175
- className="absolute inset-0 pointer-events-none text-emerald-400/70"
176
- style={{ width: '100%', height: '100%' }}
177
- >
178
- <rect
179
- x="1" y="1" rx="5" ry="5"
180
- fill="none"
181
- stroke="currentColor"
182
- strokeWidth="1.5"
183
- pathLength="100"
184
- strokeDasharray="100"
185
- strokeDashoffset="0"
186
- style={{
187
- width: 'calc(100% - 2px)',
188
- height: 'calc(100% - 2px)',
189
- animation: `debounce-border-drain ${debounceDurationMs}ms linear forwards`,
190
- }}
191
- />
192
- </svg>
178
+ <DebounceBorderOverlay debounceKey={debounceKey} durationMs={debounceDurationMs} />
193
179
  )}
194
180
  </div>
195
181
 
@@ -231,18 +217,18 @@ export function RegistryBrowser({
231
217
  ) : error ? (
232
218
  <div className="flex flex-col items-center justify-center py-12 text-neutral-500">
233
219
  <AlertTriangle className="w-6 h-6 text-amber-400 mb-2" />
234
- <p className="text-sm">{error}</p>
220
+ <p className="text-md">{error}</p>
235
221
  {onRetry && (
236
- <button onClick={onRetry} className="mt-2 text-xs text-blue-400 hover:underline cursor-pointer">
222
+ <button onClick={onRetry} className="mt-2 text-sm text-blue-400 hover:underline cursor-pointer">
237
223
  Try again
238
224
  </button>
239
225
  )}
240
226
  </div>
241
227
  ) : isEmpty ? (
242
228
  <div className="flex flex-col items-center justify-center py-12 text-neutral-500">
243
- <p className="text-sm">{emptyMessage}</p>
229
+ <p className="text-md">{emptyMessage}</p>
244
230
  {hasActiveFilters && onResetFilters && (
245
- <button onClick={onResetFilters} className="mt-2 text-xs text-blue-400 hover:underline cursor-pointer">
231
+ <button onClick={onResetFilters} className="mt-2 text-sm text-blue-400 hover:underline cursor-pointer">
246
232
  Reset filters
247
233
  </button>
248
234
  )}
@@ -5,7 +5,7 @@ import {
5
5
  } from 'lucide-react'
6
6
  import { Tooltip } from './tooltip.tsx'
7
7
  import { IconButton, type IconName } from './icon-button.tsx'
8
- import { Label, type LabelColor } from './label.tsx'
8
+ import { Label, type LabelColor, smartCapitalize } from './label.tsx'
9
9
  import { AiToolIcon, AI_TOOL_NAMES, type AiToolKey } from '../lib/ai-tools.tsx'
10
10
  export { AiToolIcon, AI_TOOL_NAMES, type AiToolKey }
11
11
 
@@ -72,10 +72,6 @@ function getCategoryLabelColor(category: string): LabelColor {
72
72
  return CATEGORY_LABEL_COLORS[Math.abs(hash) % CATEGORY_LABEL_COLORS.length]
73
73
  }
74
74
 
75
- export function formatCategory(category: string): string {
76
- return category.replace(/(^|-)(\w)/g, (_, sep, ch) => sep + ch.toUpperCase())
77
- }
78
-
79
75
  // ── Time helpers ──────────────────────────────────────────────────────────────
80
76
 
81
77
  function formatRelativeTime(isoDate: string): string {
@@ -153,10 +149,10 @@ function CardClickable({ children, onClick }: { children: ReactNode; onClick: (e
153
149
  function CategoryBadge({ category, onFilter }: { category: string; onFilter?: (category: string) => void }) {
154
150
  return (
155
151
  <Label
156
- text={formatCategory(category)}
152
+ text={smartCapitalize(category)}
157
153
  color={getCategoryLabelColor(category)}
158
154
  icon="tag"
159
- tooltip={{ description: onFilter ? `${formatCategory(category)} \u00b7 Click to filter` : formatCategory(category) }}
155
+ tooltip={{ description: onFilter ? `${smartCapitalize(category)} \u00b7 Click to filter` : smartCapitalize(category) }}
160
156
  size="sm"
161
157
  onClick={onFilter ? () => onFilter(category) : undefined}
162
158
  />
@@ -412,7 +408,7 @@ export function RegistryCard(props: RegistryCardProps) {
412
408
  <Tooltip key={`pkg-${key}`} content={{ description: `${count} ${count === 1 ? label : labelPlural}` }} position="top">
413
409
  <span className="flex items-center gap-0.5">
414
410
  <Icon className={`w-3 h-3 ${color}`} />
415
- <span className="text-xss text-neutral-500">{count}</span>
411
+ <span className="text-xs text-neutral-500">{count}</span>
416
412
  </span>
417
413
  </Tooltip>
418
414
  )]
@@ -469,7 +465,7 @@ export function RegistryCard(props: RegistryCardProps) {
469
465
  ...(props.stars != null && props.stars > 0 ? [(
470
466
  <CardClickable key="stars" onClick={() => props.onSortBy?.('stars')}>
471
467
  <Tooltip content={{ description: `${props.stars.toLocaleString()} stars \u00b7 Click to sort` }} position="top">
472
- <span className="flex items-center gap-1 text-xss text-amber-400/80">
468
+ <span className="flex items-center gap-1 text-xs text-amber-400/80">
473
469
  <Star className="w-3 h-3" />
474
470
  {formatCount(props.stars)}
475
471
  </span>
@@ -479,7 +475,7 @@ export function RegistryCard(props: RegistryCardProps) {
479
475
  ...(props.downloads != null && props.downloads > 0 ? [(
480
476
  <CardClickable key="downloads" onClick={() => props.onSortBy?.('downloads')}>
481
477
  <Tooltip content={{ description: `${props.downloads.toLocaleString()} downloads \u00b7 Click to sort` }} position="top">
482
- <span className="flex items-center gap-1 text-xss text-emerald-400/80">
478
+ <span className="flex items-center gap-1 text-xs text-emerald-400/80">
483
479
  <Download className="w-3 h-3" />
484
480
  {formatCount(props.downloads)}
485
481
  </span>
@@ -581,7 +577,7 @@ export function RegistryCard(props: RegistryCardProps) {
581
577
  if (props.installs != null && props.installs > 0) {
582
578
  bottomStats = [(
583
579
  <Tooltip key="installs" content={{ description: `${props.installs.toLocaleString()} installs` }} position="top">
584
- <span className="flex items-center gap-1 text-xss text-neutral-500">
580
+ <span className="flex items-center gap-1 text-xs text-neutral-500">
585
581
  <Download className="w-3 h-3" />
586
582
  {props.installs.toLocaleString()}
587
583
  </span>
@@ -632,17 +628,17 @@ export function RegistryCard(props: RegistryCardProps) {
632
628
  </div>
633
629
 
634
630
  {/* Name */}
635
- <h3 className="text-sm font-medium text-white">{name}</h3>
631
+ <h3 className="text-md font-medium text-white">{name}</h3>
636
632
 
637
633
  {/* Subtitle */}
638
- {subtitle && <div className="text-xs text-neutral-400 mb-1.5">{subtitle}</div>}
634
+ {subtitle && <div className="text-sm text-neutral-400 mb-1.5">{subtitle}</div>}
639
635
 
640
636
  {/* Description */}
641
- {description && <p className="text-xs text-neutral-500 mb-3 flex-grow line-clamp-2">{description}</p>}
637
+ {description && <p className="text-sm text-neutral-500 mb-3 flex-grow line-clamp-2">{description}</p>}
642
638
 
643
639
  {/* Error/warning message */}
644
640
  {errorMessage && (
645
- <p className={`text-xss mb-2 break-all ${flash === 'warning' ? 'text-amber-400' : 'text-red-400'}`}>{errorMessage}</p>
641
+ <p className={`text-xs mb-2 break-all ${flash === 'warning' ? 'text-amber-400' : 'text-red-400'}`}>{errorMessage}</p>
646
642
  )}
647
643
 
648
644
  {/* Bottom row */}
@@ -650,7 +646,7 @@ export function RegistryCard(props: RegistryCardProps) {
650
646
  const dateNode = updatedAt ? (
651
647
  <Tooltip content={{ description: onDateClick ? `Last updated ${formatFullDate(updatedAt)} \u00b7 Click to sort by date` : `Last updated ${formatFullDate(updatedAt)}` }} position="top">
652
648
  <span
653
- className={`flex items-center gap-1 text-xss text-neutral-500 whitespace-nowrap${onDateClick ? ' cursor-pointer hover:brightness-125 transition-all' : ''}`}
649
+ className={`flex items-center gap-1 text-xs text-neutral-500 whitespace-nowrap${onDateClick ? ' cursor-pointer hover:brightness-125 transition-all' : ''}`}
654
650
  onClick={onDateClick ? (e: MouseEvent) => { e.stopPropagation(); onDateClick() } : undefined}
655
651
  >
656
652
  <Clock className="w-3 h-3" />
@@ -692,13 +688,13 @@ export function RegistryCard(props: RegistryCardProps) {
692
688
  <div onClick={(e) => e.stopPropagation()}>
693
689
  <div className="fixed inset-0 bg-[var(--background)]/50 z-50 flex items-center justify-center" onClick={() => setShowScopeConfirm(false)}>
694
690
  <div className="bg-neutral-800 border border-neutral-700 rounded-lg p-4 max-w-sm" onClick={(e) => e.stopPropagation()}>
695
- <h3 className="text-sm font-medium text-neutral-200 mb-2">{ALREADY_AT_USER}</h3>
696
- <p className="text-xs text-neutral-400 mb-4">
691
+ <h3 className="text-md font-medium text-neutral-200 mb-2">{ALREADY_AT_USER}</h3>
692
+ <p className="text-sm text-neutral-400 mb-4">
697
693
  <strong className="text-neutral-300">{name}</strong> is already installed at user level and available to all projects. Do you also want to install it at project level?
698
694
  </p>
699
695
  <div className="flex justify-end gap-2">
700
- <button type="button" onClick={() => setShowScopeConfirm(false)} className="px-3 py-1.5 text-xs text-neutral-400 hover:text-neutral-200 transition-colors cursor-pointer">Cancel</button>
701
- <button type="button" onClick={() => { setShowScopeConfirm(false); props.onInstall() }} className="px-3 py-1.5 text-xs bg-blue-600 text-white rounded hover:bg-blue-500 transition-colors cursor-pointer">Install to project</button>
696
+ <button type="button" onClick={() => setShowScopeConfirm(false)} className="px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-200 transition-colors cursor-pointer">Cancel</button>
697
+ <button type="button" onClick={() => { setShowScopeConfirm(false); props.onInstall() }} className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded hover:bg-blue-500 transition-colors cursor-pointer">Install to project</button>
702
698
  </div>
703
699
  </div>
704
700
  </div>
@@ -42,7 +42,7 @@ export interface RegistryDetailProps {
42
42
  children?: ReactNode
43
43
  }
44
44
 
45
- const MARKDOWN_CLASSES = 'text-sm text-neutral-400 leading-relaxed [&_strong]:text-neutral-200 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:bg-neutral-700/40 [&_code]:border [&_code]:border-neutral-500/40 [&_code]:text-neutral-200 [&_code]:font-mono [&_code]:text-xs [&_h1]:text-lg [&_h1]:font-semibold [&_h1]:text-neutral-200 [&_h1]:mb-2 [&_h2]:text-base [&_h2]:font-semibold [&_h2]:text-neutral-200 [&_h2]:mb-2 [&_h3]:text-sm [&_h3]:font-medium [&_h3]:text-neutral-200 [&_h3]:mb-1 [&_ul]:list-disc [&_ul]:pl-4 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:mb-1 [&_p]:mb-2 [&_pre]:bg-neutral-900 [&_pre]:rounded [&_pre]:p-3 [&_pre]:overflow-x-auto [&_pre]:text-xs'
45
+ const MARKDOWN_CLASSES = 'text-md text-neutral-400 leading-relaxed [&_strong]:text-neutral-200 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:bg-neutral-700/40 [&_code]:border [&_code]:border-neutral-500/40 [&_code]:text-neutral-200 [&_code]:font-mono [&_code]:text-sm [&_h1]:text-lg [&_h1]:font-semibold [&_h1]:text-neutral-200 [&_h1]:mb-2 [&_h2]:text-base [&_h2]:font-semibold [&_h2]:text-neutral-200 [&_h2]:mb-2 [&_h3]:text-md [&_h3]:font-medium [&_h3]:text-neutral-200 [&_h3]:mb-1 [&_ul]:list-disc [&_ul]:pl-4 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:mb-1 [&_p]:mb-2 [&_pre]:bg-neutral-900 [&_pre]:rounded [&_pre]:p-3 [&_pre]:overflow-x-auto [&_pre]:text-sm'
46
46
 
47
47
  // ── CollapsibleSection ────────────────────────────────────────────────────────
48
48
 
@@ -78,7 +78,7 @@ function CollapsibleTextSection({ children, header }: { children: string; header
78
78
  {(header || overflows) && (
79
79
  <div className="flex items-center justify-between mb-2">
80
80
  {header && (
81
- <h3 className="text-xs font-medium text-neutral-500 uppercase tracking-wider">{header}</h3>
81
+ <h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider">{header}</h3>
82
82
  )}
83
83
  {overflows && (
84
84
  <IconButton
@@ -117,12 +117,12 @@ function CompatibleWithSection({ tools }: { tools: string[] }) {
117
117
 
118
118
  return (
119
119
  <div>
120
- <h3 className="text-xs font-medium text-neutral-500 uppercase tracking-wider mb-3">Compatible with</h3>
120
+ <h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-3">Compatible with</h3>
121
121
  <div className="flex items-start gap-3">
122
122
  {tools.map((tool) => (
123
123
  <div key={tool} className="flex flex-col items-center gap-1">
124
124
  <AiToolIcon tool={tool} size={18} />
125
- <span className="text-xss text-neutral-400">{AI_TOOL_NAMES[tool as AiToolKey] ?? tool}</span>
125
+ <span className="text-xs text-neutral-400">{AI_TOOL_NAMES[tool as AiToolKey] ?? tool}</span>
126
126
  </div>
127
127
  ))}
128
128
  </div>
@@ -181,8 +181,8 @@ export function RegistryDetail({
181
181
  {/* Description */}
182
182
  {description && (
183
183
  <div>
184
- <h3 className="text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">Description</h3>
185
- <p className="text-sm text-neutral-400 leading-relaxed">{description}</p>
184
+ <h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-2">Description</h3>
185
+ <p className="text-md text-neutral-400 leading-relaxed">{description}</p>
186
186
  </div>
187
187
  )}
188
188
 
@@ -115,10 +115,10 @@ function useResize(minHeight: number, onHeightChange?: (height: number) => void)
115
115
  // Code variant — Monaco editor with resize handle
116
116
  // ---------------------------------------------------------------------------
117
117
 
118
- const MONACO_THEME = 'resizable-textarea-dark'
119
- let themeRegistered = false
118
+ const MONACO_THEME_PREFIX = 'resizable-textarea'
119
+ const registeredThemes = new Set<string>()
120
120
 
121
- const codeWrapperClasses = {
121
+ const wrapperVariantClasses = {
122
122
  filled: 'bg-neutral-800 border rounded-lg overflow-hidden',
123
123
  outline: 'bg-transparent border rounded-lg overflow-hidden',
124
124
  }
@@ -139,7 +139,7 @@ function ResizableCode({
139
139
 
140
140
  return (
141
141
  <div
142
- className={`relative ${codeWrapperClasses[variant]} ${FORM_COLORS[color].border} ${wrapperClassName ?? ''}`}
142
+ className={`relative ${wrapperVariantClasses[variant]} ${FORM_COLORS[color].border} ${wrapperClassName ?? ''}`}
143
143
  data-resizable-wrapper
144
144
  style={{ height: height ?? minHeight }}
145
145
  >
@@ -148,7 +148,7 @@ function ResizableCode({
148
148
  language={language}
149
149
  value={value}
150
150
  onChange={(v) => onChange?.(v ?? '')}
151
- theme={MONACO_THEME}
151
+ theme={`${MONACO_THEME_PREFIX}-${variant}`}
152
152
  options={{
153
153
  minimap: { enabled: false },
154
154
  fontSize: 13,
@@ -170,8 +170,9 @@ function ResizableCode({
170
170
  glyphMargin: false,
171
171
  }}
172
172
  beforeMount={(monaco) => {
173
- if (!themeRegistered) {
174
- monaco.editor.defineTheme(MONACO_THEME, {
173
+ const themeName = `${MONACO_THEME_PREFIX}-${variant}`
174
+ if (!registeredThemes.has(themeName)) {
175
+ monaco.editor.defineTheme(themeName, {
175
176
  base: 'vs-dark',
176
177
  inherit: true,
177
178
  rules: [],
@@ -182,7 +183,7 @@ function ResizableCode({
182
183
  'editor.lineHighlightBorder': '#00000000',
183
184
  },
184
185
  })
185
- themeRegistered = true
186
+ registeredThemes.add(themeName)
186
187
  }
187
188
  }}
188
189
  />
@@ -202,11 +203,6 @@ function ResizableCode({
202
203
  // Children variant — wraps any element with resize handle
203
204
  // ---------------------------------------------------------------------------
204
205
 
205
- const childrenVariantClasses = {
206
- filled: 'bg-neutral-800 border rounded-lg overflow-hidden',
207
- outline: 'bg-transparent border rounded-lg overflow-hidden',
208
- }
209
-
210
206
  function ResizableChildren({
211
207
  children,
212
208
  wrapperClassName,
@@ -224,7 +220,7 @@ function ResizableChildren({
224
220
 
225
221
  return (
226
222
  <div
227
- className={`relative ${childrenVariantClasses[variant]} ${FORM_COLORS[color].border} ${wrapperClassName ?? ''}`}
223
+ className={`relative ${wrapperVariantClasses[variant]} ${FORM_COLORS[color].border} ${wrapperClassName ?? ''}`}
228
224
  data-resizable-wrapper
229
225
  style={height != null ? { height } : undefined}
230
226
  >
@@ -250,25 +246,7 @@ function ResizableField({
250
246
  color = 'blue',
251
247
  ...props
252
248
  }: ResizableTextareaBaseProps & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'color'>) {
253
- const taRef = useRef<HTMLTextAreaElement>(null)
254
- const dragRef = useRef<{ startY: number; startH: number } | null>(null)
255
- const onResizeStart = useCallback((e: React.MouseEvent) => {
256
- e.preventDefault()
257
- const ta = taRef.current
258
- if (!ta) return
259
- dragRef.current = { startY: e.clientY, startH: ta.offsetHeight }
260
- const onMove = (ev: MouseEvent) => {
261
- if (!dragRef.current || !ta) return
262
- ta.style.height = `${Math.max(40, dragRef.current.startH + ev.clientY - dragRef.current.startY)}px`
263
- }
264
- const onUp = () => {
265
- dragRef.current = null
266
- document.removeEventListener('mousemove', onMove)
267
- document.removeEventListener('mouseup', onUp)
268
- }
269
- document.addEventListener('mousemove', onMove)
270
- document.addEventListener('mouseup', onUp)
271
- }, [])
249
+ const { height, onResizeStart } = useResize(40)
272
250
 
273
251
  const className = `w-full rounded-lg focus:outline-none transition-colors ${variantClasses[variant]} ${FORM_COLORS[color].border} ${FORM_COLORS[color].focus} ${props.className ?? ''}`
274
252
 
@@ -277,8 +255,8 @@ function ResizableField({
277
255
  }
278
256
 
279
257
  return (
280
- <div className={`relative ${wrapperClassName ?? ''}`}>
281
- <textarea ref={taRef} {...props} className={className} style={{ resize: 'none', ...props.style }} />
258
+ <div className={`relative ${wrapperClassName ?? ''}`} data-resizable-wrapper>
259
+ <textarea {...props} className={className} style={{ resize: 'none', ...props.style, ...(height != null ? { height } : {}) }} />
282
260
  <div
283
261
  className="absolute bottom-[8px] right-[3px] w-4 h-3 cursor-row-resize flex items-end justify-end"
284
262
  onMouseDown={onResizeStart}
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from 'react'
2
+ import type { AccentColor } from '../lib/form-colors.ts'
2
3
  import { Tooltip, type TooltipContent, type TooltipPosition } from './tooltip.tsx'
3
4
 
4
5
  export interface SegmentedToggleOption<T extends string> {
@@ -12,7 +13,7 @@ export interface SegmentedToggleProps<T extends string> {
12
13
  options: SegmentedToggleOption<T>[]
13
14
  value: T
14
15
  onChange: (value: T) => void
15
- accentColor?: 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow' | 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky' | 'pink' | 'teal'
16
+ accentColor?: AccentColor
16
17
  /** Visual style: 'filled' (default) has a container background, 'outline' is transparent */
17
18
  variant?: 'filled' | 'outline'
18
19
  size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
@@ -35,10 +36,10 @@ const ICON_SIZE_CLASSES = {
35
36
  /** Text label button sizes — horizontal padding instead of fixed width */
36
37
  const TEXT_SIZE_CLASSES = {
37
38
  xss: 'h-[18px] px-1.5 text-xss',
38
- xs: 'h-6 px-2 text-xss',
39
- sm: 'h-7 px-2.5 text-xs',
40
- md: 'h-8 px-3 text-xs',
41
- lg: 'h-9 px-3.5 text-sm',
39
+ xs: 'h-6 px-2 text-xs',
40
+ sm: 'h-7 px-2.5 text-sm',
41
+ md: 'h-8 px-3 text-sm',
42
+ lg: 'h-9 px-3.5 text-md',
42
43
  }
43
44
 
44
45
  const ROUNDING_CLASSES = {