@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.
- package/agent-rules.json +91 -0
- package/ai-manifest.json +190 -0
- package/components/content/info-panel-primitives.tsx +14 -14
- package/components/hooks/use-click-outside.ts +10 -3
- package/components/hooks/use-modal-behavior.ts +24 -0
- package/components/hooks/use-navigation-history.ts +7 -2
- package/components/hooks/use-resizable-sidebar.ts +38 -0
- package/components/lib/ai-tools.tsx +1 -1
- package/components/lib/form-colors.ts +40 -0
- package/components/sections/ai-tools-paths/tools-paths-panel.tsx +7 -7
- package/components/sections/captured-issues/captured-issues-panel.tsx +13 -13
- package/components/sections/captured-issues/use-captured-issues.ts +9 -3
- package/components/sections/golden-snapshots/file-diff-viewer.tsx +13 -13
- package/components/sections/golden-snapshots/golden-sync-panel.tsx +5 -5
- package/components/sections/golden-snapshots/snapshot-manager.tsx +11 -11
- package/components/sections/golden-snapshots/status-overview.tsx +20 -20
- package/components/sections/golden-snapshots/version-manager.tsx +8 -8
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +8 -44
- package/components/sections/prompt-editor/index.ts +0 -7
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +9 -45
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +11 -43
- package/components/sections/report-bug/report-bug-form.tsx +14 -14
- package/components/sections/report-bug/screenshot-uploader.tsx +6 -6
- package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +3 -3
- package/components/sections/snapshot-browser/snapshot-tree.tsx +8 -8
- package/components/sections/snippets-editor/snippets-editor.tsx +74 -48
- package/components/settings/SettingsHeader.tsx +1 -2
- package/components/settings/SettingsTreeNav.tsx +31 -16
- package/components/ui/action-dialog.tsx +12 -56
- package/components/ui/badge.tsx +8 -24
- package/components/ui/bottom-panel-header.tsx +4 -4
- package/components/ui/breadcrumb.tsx +8 -68
- package/components/ui/checkbox.tsx +2 -16
- package/components/ui/collapsible-section.tsx +4 -42
- package/components/ui/confirm-badge.tsx +3 -20
- package/components/ui/cookie-consent.tsx +21 -5
- package/components/ui/debounce-border-overlay.tsx +31 -0
- package/components/ui/detail-section.tsx +5 -22
- package/components/ui/editor-placeholder-card.tsx +17 -16
- package/components/ui/editor-toolbar.tsx +12 -0
- package/components/ui/execution-details-panel.tsx +8 -13
- package/components/ui/extension-list-card.tsx +3 -3
- package/components/ui/file-structure-section.tsx +20 -35
- package/components/ui/file-tree.tsx +4 -14
- package/components/ui/files-panel.tsx +28 -18
- package/components/ui/filter-dropdown.tsx +5 -5
- package/components/ui/form-actions.tsx +7 -6
- package/components/ui/frontmatter-form-header.tsx +4 -4
- package/components/ui/icon-button.tsx +3 -2
- package/components/ui/input.tsx +15 -31
- package/components/ui/label.tsx +7 -21
- package/components/ui/layout-tab-bar.tsx +4 -4
- package/components/ui/modal.tsx +5 -17
- package/components/ui/nav-card.tsx +5 -20
- package/components/ui/navigation-bar.tsx +13 -74
- package/components/ui/number-input.tsx +4 -4
- package/components/ui/registry-browser.tsx +10 -24
- package/components/ui/registry-card.tsx +16 -20
- package/components/ui/registry-detail.tsx +6 -6
- package/components/ui/resizable-textarea.tsx +13 -35
- package/components/ui/segmented-toggle.tsx +6 -5
- package/components/ui/select.tsx +7 -16
- package/components/ui/selection-grid.tsx +6 -54
- package/components/ui/setting-row.tsx +2 -4
- package/components/ui/settings-card.tsx +3 -3
- package/components/ui/settings-info-box.tsx +6 -23
- package/components/ui/settings-section-title.tsx +1 -1
- package/components/ui/snapshot-card.tsx +7 -7
- package/components/ui/snippets-panel.tsx +10 -10
- package/components/ui/sort-dropdown.tsx +2 -2
- package/components/ui/status-card.tsx +6 -17
- package/components/ui/tab-bar.tsx +5 -31
- package/components/ui/toggle.tsx +3 -19
- package/components/ui/tooltip.tsx +9 -21
- package/dist/content.js +14 -14
- package/dist/index.d.ts +71 -141
- package/dist/index.js +1634 -2450
- package/dist/tokens/primitives.css +9 -2
- package/index.ts +8 -7
- package/package.json +13 -3
- package/tokens/primitives.css +9 -2
- package/components/sections/prompt-editor/use-prompt-editor.ts +0 -131
package/components/ui/modal.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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 ?
|
|
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-
|
|
62
|
+
<h3 className="text-md font-medium text-neutral-200">{title}</h3>
|
|
78
63
|
|
|
79
64
|
{description && (
|
|
80
|
-
<p className="mt-1 text-
|
|
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-
|
|
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
|
|
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-
|
|
74
|
-
sm: { text: 'text-
|
|
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 =
|
|
76
|
+
const Icon = iconMap[icon]
|
|
138
77
|
if (!Icon) return null
|
|
139
|
-
const c = color &&
|
|
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 ?
|
|
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-
|
|
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-
|
|
224
|
-
seg.color &&
|
|
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-
|
|
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 &&
|
|
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-
|
|
21
|
-
sm: { wrapper: 'h-7', input: 'px-2 text-
|
|
22
|
-
md: { wrapper: 'h-8', input: 'px-3 text-
|
|
23
|
-
lg: { wrapper: 'h-9', input: 'px-3 text-
|
|
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
|
-
<
|
|
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-
|
|
220
|
+
<p className="text-md">{error}</p>
|
|
235
221
|
{onRetry && (
|
|
236
|
-
<button onClick={onRetry} className="mt-2 text-
|
|
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-
|
|
229
|
+
<p className="text-md">{emptyMessage}</p>
|
|
244
230
|
{hasActiveFilters && onResetFilters && (
|
|
245
|
-
<button onClick={onResetFilters} className="mt-2 text-
|
|
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={
|
|
152
|
+
text={smartCapitalize(category)}
|
|
157
153
|
color={getCategoryLabelColor(category)}
|
|
158
154
|
icon="tag"
|
|
159
|
-
tooltip={{ description: onFilter ? `${
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
631
|
+
<h3 className="text-md font-medium text-white">{name}</h3>
|
|
636
632
|
|
|
637
633
|
{/* Subtitle */}
|
|
638
|
-
{subtitle && <div className="text-
|
|
634
|
+
{subtitle && <div className="text-sm text-neutral-400 mb-1.5">{subtitle}</div>}
|
|
639
635
|
|
|
640
636
|
{/* Description */}
|
|
641
|
-
{description && <p className="text-
|
|
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-
|
|
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-
|
|
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-
|
|
696
|
-
<p className="text-
|
|
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-
|
|
701
|
-
<button type="button" onClick={() => { setShowScopeConfirm(false); props.onInstall() }} className="px-3 py-1.5 text-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
185
|
-
<p className="text-
|
|
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
|
|
119
|
-
|
|
118
|
+
const MONACO_THEME_PREFIX = 'resizable-textarea'
|
|
119
|
+
const registeredThemes = new Set<string>()
|
|
120
120
|
|
|
121
|
-
const
|
|
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 ${
|
|
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={
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
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
|
|
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?:
|
|
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-
|
|
39
|
-
sm: 'h-7 px-2.5 text-
|
|
40
|
-
md: 'h-8 px-3 text-
|
|
41
|
-
lg: 'h-9 px-3.5 text-
|
|
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 = {
|