@toolr/ui-design 0.1.6 → 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/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/form-colors.ts +40 -0
- package/components/sections/captured-issues/captured-issues-panel.tsx +3 -3
- package/components/sections/captured-issues/use-captured-issues.ts +9 -3
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +4 -40
- package/components/sections/prompt-editor/index.ts +0 -7
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +4 -40
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +4 -36
- package/components/sections/snippets-editor/snippets-editor.tsx +6 -39
- package/components/settings/SettingsHeader.tsx +0 -1
- package/components/settings/SettingsTreeNav.tsx +9 -12
- package/components/ui/action-dialog.tsx +7 -51
- package/components/ui/badge.tsx +4 -20
- package/components/ui/breadcrumb.tsx +6 -66
- package/components/ui/checkbox.tsx +2 -16
- package/components/ui/collapsible-section.tsx +3 -41
- package/components/ui/confirm-badge.tsx +3 -20
- package/components/ui/cookie-consent.tsx +17 -1
- package/components/ui/debounce-border-overlay.tsx +31 -0
- package/components/ui/detail-section.tsx +2 -19
- package/components/ui/editor-placeholder-card.tsx +10 -9
- package/components/ui/execution-details-panel.tsx +2 -7
- package/components/ui/file-structure-section.tsx +3 -18
- package/components/ui/file-tree.tsx +2 -14
- package/components/ui/files-panel.tsx +3 -11
- package/components/ui/form-actions.tsx +6 -5
- package/components/ui/icon-button.tsx +2 -1
- package/components/ui/input.tsx +10 -26
- package/components/ui/label.tsx +3 -17
- package/components/ui/modal.tsx +3 -15
- package/components/ui/nav-card.tsx +2 -17
- package/components/ui/navigation-bar.tsx +8 -69
- package/components/ui/registry-browser.tsx +6 -20
- package/components/ui/registry-card.tsx +3 -7
- package/components/ui/resizable-textarea.tsx +13 -35
- package/components/ui/segmented-toggle.tsx +2 -1
- package/components/ui/select.tsx +2 -11
- package/components/ui/selection-grid.tsx +2 -50
- package/components/ui/setting-row.tsx +1 -3
- package/components/ui/settings-info-box.tsx +5 -22
- package/components/ui/status-card.tsx +2 -13
- package/components/ui/tab-bar.tsx +3 -29
- package/components/ui/toggle.tsx +3 -19
- package/components/ui/tooltip.tsx +6 -18
- package/dist/index.d.ts +60 -137
- package/dist/index.js +1426 -2334
- package/index.ts +8 -7
- package/package.json +1 -1
- package/components/sections/prompt-editor/use-prompt-editor.ts +0 -131
|
@@ -34,10 +34,12 @@ export interface FormActionsProps {
|
|
|
34
34
|
|
|
35
35
|
const PADDING_CLASSES = {
|
|
36
36
|
compact: 'pt-2',
|
|
37
|
-
normal: 'pt-2
|
|
38
|
-
modal: 'px-4 py-3
|
|
37
|
+
normal: 'pt-2',
|
|
38
|
+
modal: 'px-4 py-3',
|
|
39
39
|
} as const
|
|
40
40
|
|
|
41
|
+
const BORDER_CLASS = 'border-t border-neutral-700'
|
|
42
|
+
|
|
41
43
|
const DEFAULT_BORDER = {
|
|
42
44
|
compact: false,
|
|
43
45
|
normal: true,
|
|
@@ -64,10 +66,9 @@ export function FormActions({
|
|
|
64
66
|
padding = 'normal',
|
|
65
67
|
}: FormActionsProps) {
|
|
66
68
|
const showBorder = border ?? DEFAULT_BORDER[padding]
|
|
67
|
-
const base = PADDING_CLASSES[padding]
|
|
68
69
|
const paddingClass = showBorder
|
|
69
|
-
?
|
|
70
|
-
:
|
|
70
|
+
? `${PADDING_CLASSES[padding]} ${BORDER_CLASS}`
|
|
71
|
+
: PADDING_CLASSES[padding]
|
|
71
72
|
|
|
72
73
|
const hasLeft = onBack || statusText
|
|
73
74
|
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
AlertCircle, FileCode, Gauge, Home, PieChart, Settings2,
|
|
40
40
|
} from 'lucide-react'
|
|
41
41
|
import type { LucideIcon } from 'lucide-react'
|
|
42
|
+
import { type AccentColor } from '../lib/form-colors.ts'
|
|
42
43
|
import { Tooltip, type TooltipContent } from './tooltip.tsx'
|
|
43
44
|
|
|
44
45
|
export const iconMap = {
|
|
@@ -159,7 +160,7 @@ export interface ActionItem {
|
|
|
159
160
|
|
|
160
161
|
export type IconButtonStatus = 'loading' | 'success' | 'warning' | 'error'
|
|
161
162
|
|
|
162
|
-
export type IconButtonColor =
|
|
163
|
+
export type IconButtonColor = AccentColor
|
|
163
164
|
export type IconButtonVariant = 'filled' | 'outline'
|
|
164
165
|
|
|
165
166
|
export interface IconButtonProps {
|
package/components/ui/input.tsx
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { forwardRef, useEffect, useRef, useState, type InputHTMLAttributes, type ReactNode } from 'react'
|
|
21
21
|
import { Search, X, Eye, EyeOff } from 'lucide-react'
|
|
22
|
+
import { DebounceBorderOverlay } from './debounce-border-overlay.tsx'
|
|
22
23
|
import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
|
|
23
24
|
|
|
24
25
|
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'size' | 'type'> {
|
|
@@ -50,6 +51,13 @@ const variantClasses = {
|
|
|
50
51
|
outline: 'bg-transparent',
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
const SEARCH_AUTO_PROPS = {
|
|
55
|
+
autoComplete: 'off' as const,
|
|
56
|
+
autoCorrect: 'off' as const,
|
|
57
|
+
autoCapitalize: 'off' as const,
|
|
58
|
+
spellCheck: false as const,
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
|
|
54
62
|
value,
|
|
55
63
|
onChange,
|
|
@@ -115,12 +123,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
|
|
|
115
123
|
return () => clearTimeout(timerRef.current)
|
|
116
124
|
}, [])
|
|
117
125
|
|
|
118
|
-
const searchAutoProps = isSearch ?
|
|
119
|
-
autoComplete: 'off' as const,
|
|
120
|
-
autoCorrect: 'off' as const,
|
|
121
|
-
autoCapitalize: 'off' as const,
|
|
122
|
-
spellCheck: false as const,
|
|
123
|
-
} : {}
|
|
126
|
+
const searchAutoProps = isSearch ? SEARCH_AUTO_PROPS : undefined
|
|
124
127
|
|
|
125
128
|
const showClear = isSearch && displayValue && !disabled
|
|
126
129
|
|
|
@@ -181,26 +184,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
|
|
|
181
184
|
</button>
|
|
182
185
|
)}
|
|
183
186
|
{debounceMs > 0 && debounceKey > 0 && (
|
|
184
|
-
<
|
|
185
|
-
key={debounceKey}
|
|
186
|
-
className="absolute inset-0 pointer-events-none text-emerald-400/70"
|
|
187
|
-
style={{ width: '100%', height: '100%' }}
|
|
188
|
-
>
|
|
189
|
-
<rect
|
|
190
|
-
x="1" y="1" rx="5" ry="5"
|
|
191
|
-
fill="none"
|
|
192
|
-
stroke="currentColor"
|
|
193
|
-
strokeWidth="1.5"
|
|
194
|
-
pathLength="100"
|
|
195
|
-
strokeDasharray="100"
|
|
196
|
-
strokeDashoffset="0"
|
|
197
|
-
style={{
|
|
198
|
-
width: 'calc(100% - 2px)',
|
|
199
|
-
height: 'calc(100% - 2px)',
|
|
200
|
-
animation: `debounce-border-drain ${debounceMs}ms linear forwards`,
|
|
201
|
-
}}
|
|
202
|
-
/>
|
|
203
|
-
</svg>
|
|
187
|
+
<DebounceBorderOverlay debounceKey={debounceKey} durationMs={debounceMs} />
|
|
204
188
|
)}
|
|
205
189
|
</div>
|
|
206
190
|
{typeof error === 'string' && error && (
|
package/components/ui/label.tsx
CHANGED
|
@@ -12,25 +12,11 @@
|
|
|
12
12
|
* - Clickable with hover state
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { type AccentColor } from '../lib/form-colors.ts'
|
|
15
16
|
import { iconMap, type IconName } from './icon-button.tsx'
|
|
16
17
|
import { Tooltip } from './tooltip.tsx'
|
|
17
18
|
|
|
18
|
-
export type LabelColor =
|
|
19
|
-
| 'neutral'
|
|
20
|
-
| 'green'
|
|
21
|
-
| 'red'
|
|
22
|
-
| 'blue'
|
|
23
|
-
| 'yellow'
|
|
24
|
-
| 'orange'
|
|
25
|
-
| 'purple'
|
|
26
|
-
| 'amber'
|
|
27
|
-
| 'emerald'
|
|
28
|
-
| 'cyan'
|
|
29
|
-
| 'indigo'
|
|
30
|
-
| 'teal'
|
|
31
|
-
| 'violet'
|
|
32
|
-
| 'pink'
|
|
33
|
-
| 'sky'
|
|
19
|
+
export type LabelColor = AccentColor
|
|
34
20
|
|
|
35
21
|
export interface LabelProps {
|
|
36
22
|
text: string
|
|
@@ -94,7 +80,7 @@ const sizeConfig = {
|
|
|
94
80
|
}
|
|
95
81
|
|
|
96
82
|
/** Smart capitalize: capitalizes first letter of each word separated by spaces or dashes */
|
|
97
|
-
function smartCapitalize(value: string): string {
|
|
83
|
+
export function smartCapitalize(value: string): string {
|
|
98
84
|
return value.replace(/(^|[\s-])(\w)/g, (_match, separator: string, char: string) => separator + char.toUpperCase())
|
|
99
85
|
}
|
|
100
86
|
|
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
|
|
|
@@ -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
|
|
@@ -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
|
|
@@ -76,23 +32,6 @@ const sizeConfig = {
|
|
|
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)
|
|
@@ -221,7 +160,7 @@ export function NavigationBar({
|
|
|
221
160
|
{seg.icon && <SegmentIcon icon={seg.icon} color={seg.color} size="xs" />}
|
|
222
161
|
<span className={cn(
|
|
223
162
|
'text-sm truncate',
|
|
224
|
-
seg.color &&
|
|
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>
|
|
@@ -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">
|
|
@@ -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
|
|
|
@@ -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
|
/>
|
|
@@ -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'
|
package/components/ui/select.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useRef, useEffect, useCallback, type ReactNode } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import { ChevronDown, Check } from 'lucide-react'
|
|
4
|
+
import { useClickOutside } from '../hooks/use-click-outside.ts'
|
|
4
5
|
import { useDropdownMaxHeight } from '../hooks/use-dropdown-max-height.ts'
|
|
5
6
|
import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
|
|
6
7
|
|
|
@@ -74,17 +75,7 @@ export function Select<T extends string | number = string>({
|
|
|
74
75
|
}, [])
|
|
75
76
|
|
|
76
77
|
// Close on click outside both the trigger and the portal menu
|
|
77
|
-
|
|
78
|
-
if (!isOpen) return
|
|
79
|
-
const handleClick = (event: MouseEvent) => {
|
|
80
|
-
const target = event.target as Node
|
|
81
|
-
if (ref.current?.contains(target)) return
|
|
82
|
-
if (menuRef.current?.contains(target)) return
|
|
83
|
-
close()
|
|
84
|
-
}
|
|
85
|
-
document.addEventListener('mousedown', handleClick)
|
|
86
|
-
return () => document.removeEventListener('mousedown', handleClick)
|
|
87
|
-
}, [isOpen, close])
|
|
78
|
+
useClickOutside([ref, menuRef], isOpen, close)
|
|
88
79
|
|
|
89
80
|
useEffect(() => {
|
|
90
81
|
if (highlightIdx >= 0 && menuRef.current) {
|
|
@@ -1,54 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Check, Settings, Code, Folder, File, Terminal, Globe, Database, Cloud,
|
|
3
|
-
Sparkles, Zap, Shield, ShieldCheck, Wand2, Star, Heart, Bell,
|
|
4
|
-
Search, Filter, Eye, Lock, User, Users, Image, Tag, Pin, Mail,
|
|
5
|
-
Send, Bookmark, Play, Pause, Bot, Plug, Puzzle, Webhook, Scan,
|
|
6
|
-
} from 'lucide-react'
|
|
7
|
-
import type { LucideIcon } from 'lucide-react'
|
|
8
|
-
import type { IconName } from './icon-button.tsx'
|
|
1
|
+
import { iconMap, type IconName } from './icon-button.tsx'
|
|
9
2
|
import type { ConfirmBadgeColor } from './confirm-badge.tsx'
|
|
10
3
|
import { cn } from '../lib/cn.ts'
|
|
11
4
|
import { AiToolIcon, AI_TOOL_NAMES, type AiToolKey } from '../lib/ai-tools.tsx'
|
|
12
5
|
|
|
13
|
-
const iconMap: Partial<Record<IconName, LucideIcon>> = {
|
|
14
|
-
'check': Check,
|
|
15
|
-
'settings': Settings,
|
|
16
|
-
'code': Code,
|
|
17
|
-
'folder': Folder,
|
|
18
|
-
'file': File,
|
|
19
|
-
'terminal': Terminal,
|
|
20
|
-
'globe': Globe,
|
|
21
|
-
'database': Database,
|
|
22
|
-
'cloud': Cloud,
|
|
23
|
-
'sparkles': Sparkles,
|
|
24
|
-
'zap': Zap,
|
|
25
|
-
'shield': Shield,
|
|
26
|
-
'shield-check': ShieldCheck,
|
|
27
|
-
'wand': Wand2,
|
|
28
|
-
'star': Star,
|
|
29
|
-
'heart': Heart,
|
|
30
|
-
'bell': Bell,
|
|
31
|
-
'search': Search,
|
|
32
|
-
'filter': Filter,
|
|
33
|
-
'eye': Eye,
|
|
34
|
-
'lock': Lock,
|
|
35
|
-
'user': User,
|
|
36
|
-
'users': Users,
|
|
37
|
-
'image': Image,
|
|
38
|
-
'tag': Tag,
|
|
39
|
-
'pin': Pin,
|
|
40
|
-
'mail': Mail,
|
|
41
|
-
'send': Send,
|
|
42
|
-
'bookmark': Bookmark,
|
|
43
|
-
'play': Play,
|
|
44
|
-
'pause': Pause,
|
|
45
|
-
'bot': Bot,
|
|
46
|
-
'plug': Plug,
|
|
47
|
-
'puzzle': Puzzle,
|
|
48
|
-
'webhook': Webhook,
|
|
49
|
-
'scan': Scan,
|
|
50
|
-
}
|
|
51
|
-
|
|
52
6
|
/* ── Preset logos (shared AiToolIcon) ─────────────────────── */
|
|
53
7
|
|
|
54
8
|
type IconProps = { className?: string; style?: React.CSSProperties }
|
|
@@ -142,9 +96,7 @@ function resolveColor(color?: string): string {
|
|
|
142
96
|
}
|
|
143
97
|
|
|
144
98
|
function autoColumns(itemCount: number): number {
|
|
145
|
-
|
|
146
|
-
if (itemCount <= 4) return itemCount
|
|
147
|
-
return 5
|
|
99
|
+
return Math.min(itemCount, 5)
|
|
148
100
|
}
|
|
149
101
|
|
|
150
102
|
/* ── Component ─────────────────────────────────────────────── */
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - input: renders a text Input
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { Toggle, type ToggleColor, type ToggleSize
|
|
14
|
+
import { Toggle, type ToggleColor, type ToggleSize } from './toggle.tsx'
|
|
15
15
|
import { Select, type SelectOption } from './select.tsx'
|
|
16
16
|
import { Input } from './input.tsx'
|
|
17
17
|
|
|
@@ -28,7 +28,6 @@ interface SettingRowToggle extends SettingRowBase {
|
|
|
28
28
|
onChange: (checked: boolean) => void
|
|
29
29
|
color?: ToggleColor
|
|
30
30
|
size?: ToggleSize
|
|
31
|
-
variant?: ToggleVariant
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
interface SettingRowSelect extends SettingRowBase {
|
|
@@ -67,7 +66,6 @@ export function SettingRow(props: SettingRowProps) {
|
|
|
67
66
|
disabled={disabled}
|
|
68
67
|
color={props.color}
|
|
69
68
|
size={props.size}
|
|
70
|
-
variant={props.variant}
|
|
71
69
|
/>
|
|
72
70
|
)}
|
|
73
71
|
{props.type === 'select' && (
|