@toolr/ui-design 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/hooks/use-click-outside.ts +10 -3
- package/components/hooks/use-modal-behavior.ts +53 -0
- package/components/hooks/use-navigation-history.ts +7 -2
- package/components/hooks/use-resizable-sidebar.ts +38 -0
- package/components/lib/form-colors.ts +40 -0
- package/components/sections/captured-issues/captured-issues-panel.tsx +3 -3
- package/components/sections/captured-issues/use-captured-issues.ts +9 -3
- package/components/sections/golden-snapshots/file-diff-viewer.tsx +1 -1
- package/components/sections/golden-snapshots/status-overview.tsx +1 -1
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +4 -40
- package/components/sections/prompt-editor/index.ts +0 -7
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +4 -40
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +4 -36
- package/components/sections/snippets-editor/snippets-editor.tsx +6 -39
- package/components/settings/SettingsHeader.tsx +0 -1
- package/components/settings/SettingsTreeNav.tsx +9 -12
- package/components/ui/action-dialog.tsx +19 -55
- package/components/ui/ai-action-button.tsx +2 -4
- package/components/ui/badge.tsx +15 -23
- package/components/ui/breadcrumb.tsx +11 -71
- package/components/ui/checkbox.tsx +19 -27
- package/components/ui/collapsible-section.tsx +4 -41
- package/components/ui/confirm-badge.tsx +14 -23
- package/components/ui/cookie-consent.tsx +18 -2
- package/components/ui/debounce-border-overlay.tsx +31 -0
- package/components/ui/detail-section.tsx +2 -19
- package/components/ui/editor-placeholder-card.tsx +10 -9
- package/components/ui/execution-details-panel.tsx +2 -7
- package/components/ui/extension-list-card.tsx +1 -1
- package/components/ui/file-structure-section.tsx +3 -18
- package/components/ui/file-tree.tsx +6 -18
- package/components/ui/files-panel.tsx +3 -11
- package/components/ui/filter-dropdown.tsx +5 -2
- package/components/ui/form-actions.tsx +11 -8
- package/components/ui/icon-button.tsx +7 -6
- package/components/ui/input.tsx +18 -29
- package/components/ui/label.tsx +7 -17
- package/components/ui/layout-tab-bar.tsx +5 -5
- package/components/ui/modal.tsx +10 -18
- package/components/ui/nav-card.tsx +3 -18
- package/components/ui/navigation-bar.tsx +12 -73
- package/components/ui/number-input.tsx +6 -0
- package/components/ui/registry-browser.tsx +6 -20
- package/components/ui/registry-card.tsx +3 -7
- package/components/ui/resizable-textarea.tsx +13 -35
- package/components/ui/segmented-toggle.tsx +4 -1
- package/components/ui/select.tsx +8 -14
- package/components/ui/selection-grid.tsx +6 -50
- package/components/ui/setting-row.tsx +5 -5
- package/components/ui/settings-card.tsx +2 -2
- package/components/ui/settings-info-box.tsx +6 -24
- package/components/ui/sort-dropdown.tsx +8 -5
- package/components/ui/status-card.tsx +2 -13
- package/components/ui/tab-bar.tsx +17 -33
- package/components/ui/toggle.tsx +22 -30
- package/components/ui/tooltip.tsx +11 -23
- package/dist/index.d.ts +71 -142
- package/dist/index.js +1630 -2436
- package/index.ts +8 -7
- package/package.json +9 -1
- package/components/sections/prompt-editor/use-prompt-editor.ts +0 -131
|
@@ -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)
|
|
@@ -200,7 +139,7 @@ export function NavigationBar({
|
|
|
200
139
|
active={historyOpen}
|
|
201
140
|
/>
|
|
202
141
|
{historyOpen && hasHistoryEntries && (
|
|
203
|
-
<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-
|
|
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-lg z-50">
|
|
204
143
|
<div className="px-3 py-1.5 border-b border-neutral-700/50">
|
|
205
144
|
<p className="text-sm font-medium text-neutral-500">History</p>
|
|
206
145
|
</div>
|
|
@@ -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,17 +182,17 @@ 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
|
-
<div key={segment.id} className="flex items-center gap-1">
|
|
188
|
+
<div key={segment.id} className="flex items-center gap-1 min-w-0">
|
|
250
189
|
{index > 0 && <SegmentSeparator type={separator} size={size} />}
|
|
251
190
|
{isClickable ? (
|
|
252
191
|
<button
|
|
253
192
|
type="button"
|
|
254
193
|
onClick={segment.onClick}
|
|
255
194
|
className={cn(
|
|
256
|
-
'flex items-center gap-1.5 px-2 py-0.5 rounded-md transition-colors cursor-pointer',
|
|
195
|
+
'flex items-center gap-1.5 px-2 py-0.5 rounded-md transition-colors cursor-pointer min-w-0',
|
|
257
196
|
s.text,
|
|
258
197
|
'font-medium hover:text-white',
|
|
259
198
|
colors ? [colors.text, `hover:${colors.bg}`] : ['text-neutral-300', 'hover:bg-neutral-700/50'],
|
|
@@ -265,7 +204,7 @@ export function NavigationBar({
|
|
|
265
204
|
) : (
|
|
266
205
|
<div
|
|
267
206
|
className={cn(
|
|
268
|
-
'flex items-center gap-1.5 px-2 py-0.5 rounded-md',
|
|
207
|
+
'flex items-center gap-1.5 px-2 py-0.5 rounded-md min-w-0',
|
|
269
208
|
s.text,
|
|
270
209
|
isLast
|
|
271
210
|
? ['font-medium bg-neutral-700/50', colors ? colors.text : 'text-white']
|
|
@@ -13,6 +13,8 @@ export interface NumberInputProps {
|
|
|
13
13
|
size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
|
|
14
14
|
disabled?: boolean
|
|
15
15
|
className?: string
|
|
16
|
+
/** Accessible label — required for screen readers */
|
|
17
|
+
'aria-label'?: string
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
const SIZE_CONFIG = {
|
|
@@ -39,6 +41,7 @@ export function NumberInput({
|
|
|
39
41
|
size = 'sm',
|
|
40
42
|
disabled = false,
|
|
41
43
|
className = '',
|
|
44
|
+
'aria-label': ariaLabel,
|
|
42
45
|
}: NumberInputProps) {
|
|
43
46
|
const [focused, setFocused] = useState(false)
|
|
44
47
|
const [editText, setEditText] = useState<string | null>(null)
|
|
@@ -92,6 +95,7 @@ export function NumberInput({
|
|
|
92
95
|
ref={inputRef}
|
|
93
96
|
type="text"
|
|
94
97
|
inputMode="numeric"
|
|
98
|
+
aria-label={ariaLabel}
|
|
95
99
|
value={editText ?? value}
|
|
96
100
|
onChange={(e) => setEditText(e.target.value)}
|
|
97
101
|
onFocus={() => {
|
|
@@ -128,6 +132,7 @@ export function NumberInput({
|
|
|
128
132
|
>
|
|
129
133
|
<button
|
|
130
134
|
type="button"
|
|
135
|
+
aria-label="Increase value"
|
|
131
136
|
tabIndex={-1}
|
|
132
137
|
onMouseDown={(e) => e.preventDefault()}
|
|
133
138
|
onClick={() => nudge(1)}
|
|
@@ -145,6 +150,7 @@ export function NumberInput({
|
|
|
145
150
|
<div className={`border-t ${fc.border}`} />
|
|
146
151
|
<button
|
|
147
152
|
type="button"
|
|
153
|
+
aria-label="Decrease value"
|
|
148
154
|
tabIndex={-1}
|
|
149
155
|
onMouseDown={(e) => e.preventDefault()}
|
|
150
156
|
onClick={() => nudge(-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'
|
|
@@ -135,6 +136,8 @@ export function SegmentedToggle<T extends string>({
|
|
|
135
136
|
return (
|
|
136
137
|
<Tooltip key={option.value} content={option.tooltip} position={tooltipPosition}>
|
|
137
138
|
<button
|
|
139
|
+
aria-pressed={isActive}
|
|
140
|
+
aria-label={option.label || (typeof option.tooltip.description === 'string' ? option.tooltip.description : undefined)}
|
|
138
141
|
onClick={() => onChange(option.value)}
|
|
139
142
|
disabled={disabled}
|
|
140
143
|
className={`flex items-center justify-center ${sizeClass} ${rounding} font-medium transition-all cursor-pointer ${
|
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) {
|
|
@@ -119,6 +110,8 @@ export function Select<T extends string | number = string>({
|
|
|
119
110
|
<button
|
|
120
111
|
ref={buttonRef}
|
|
121
112
|
type="button"
|
|
113
|
+
aria-expanded={isOpen}
|
|
114
|
+
aria-haspopup="listbox"
|
|
122
115
|
onClick={() => !disabled && (isOpen ? close() : open())}
|
|
123
116
|
disabled={disabled}
|
|
124
117
|
className={`flex items-center gap-1.5 min-w-0 rounded-lg border ${v.bg} ${FORM_COLORS[color].border} text-neutral-200 focus:outline-none ${FORM_COLORS[color].focus} transition-colors ${
|
|
@@ -126,7 +119,7 @@ export function Select<T extends string | number = string>({
|
|
|
126
119
|
} ${s}`}
|
|
127
120
|
>
|
|
128
121
|
{selectedOption?.icon}
|
|
129
|
-
<span className={`
|
|
122
|
+
<span className={`truncate ${selectedOption ? '' : 'text-neutral-500'}`}>
|
|
130
123
|
{selectedOption?.label ?? placeholder}
|
|
131
124
|
</span>
|
|
132
125
|
<ChevronDown className={`w-3 h-3 ml-auto text-neutral-500 transition-transform shrink-0 ${isOpen ? 'rotate-180' : ''}`} />
|
|
@@ -134,7 +127,8 @@ export function Select<T extends string | number = string>({
|
|
|
134
127
|
{isOpen && menuPos && createPortal(
|
|
135
128
|
<div
|
|
136
129
|
ref={menuRef}
|
|
137
|
-
|
|
130
|
+
role="listbox"
|
|
131
|
+
className={`fixed z-[9999] whitespace-nowrap ${v.menuBg} border ${FORM_COLORS[color].border} rounded-lg shadow-lg overflow-hidden`}
|
|
138
132
|
style={{
|
|
139
133
|
top: menuPos.top,
|
|
140
134
|
left: align === 'right' ? undefined : menuPos.left,
|
|
@@ -160,7 +154,7 @@ export function Select<T extends string | number = string>({
|
|
|
160
154
|
>
|
|
161
155
|
<Check className={`w-3 h-3 shrink-0 ${isSelected ? FORM_COLORS[color].accent : 'invisible'}`} />
|
|
162
156
|
{opt.icon}
|
|
163
|
-
<span>{opt.label}</span>
|
|
157
|
+
<span className="truncate">{opt.label}</span>
|
|
164
158
|
</button>
|
|
165
159
|
)
|
|
166
160
|
})}
|
|
@@ -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 ─────────────────────────────────────────────── */
|
|
@@ -228,6 +180,8 @@ function GridCard({ item, selected, onClick }: CardProps) {
|
|
|
228
180
|
return (
|
|
229
181
|
<button
|
|
230
182
|
type="button"
|
|
183
|
+
aria-pressed={selected}
|
|
184
|
+
aria-label={item.name}
|
|
231
185
|
onClick={onClick}
|
|
232
186
|
disabled={item.disabled}
|
|
233
187
|
className={cn(
|
|
@@ -267,6 +221,8 @@ function ListCard({ item, selected, onClick }: CardProps) {
|
|
|
267
221
|
return (
|
|
268
222
|
<button
|
|
269
223
|
type="button"
|
|
224
|
+
aria-pressed={selected}
|
|
225
|
+
aria-label={item.name}
|
|
270
226
|
onClick={onClick}
|
|
271
227
|
disabled={item.disabled}
|
|
272
228
|
className={cn(
|
|
@@ -11,9 +11,10 @@
|
|
|
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
|
+
import { cn } from '../lib/cn.ts'
|
|
17
18
|
|
|
18
19
|
interface SettingRowBase {
|
|
19
20
|
label: string
|
|
@@ -28,7 +29,6 @@ interface SettingRowToggle extends SettingRowBase {
|
|
|
28
29
|
onChange: (checked: boolean) => void
|
|
29
30
|
color?: ToggleColor
|
|
30
31
|
size?: ToggleSize
|
|
31
|
-
variant?: ToggleVariant
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
interface SettingRowSelect extends SettingRowBase {
|
|
@@ -52,10 +52,10 @@ interface SettingRowInput extends SettingRowBase {
|
|
|
52
52
|
export type SettingRowProps = SettingRowToggle | SettingRowSelect | SettingRowInput
|
|
53
53
|
|
|
54
54
|
export function SettingRow(props: SettingRowProps) {
|
|
55
|
-
const { label, description, disabled, className
|
|
55
|
+
const { label, description, disabled, className } = props
|
|
56
56
|
|
|
57
57
|
return (
|
|
58
|
-
<div className={
|
|
58
|
+
<div className={cn('flex items-start justify-between gap-4', className)}>
|
|
59
59
|
<div>
|
|
60
60
|
<label className="text-neutral-200 leading-7">{label}</label>
|
|
61
61
|
{description && <p className="text-md text-neutral-500">{description}</p>}
|
|
@@ -67,7 +67,7 @@ export function SettingRow(props: SettingRowProps) {
|
|
|
67
67
|
disabled={disabled}
|
|
68
68
|
color={props.color}
|
|
69
69
|
size={props.size}
|
|
70
|
-
|
|
70
|
+
aria-label={label}
|
|
71
71
|
/>
|
|
72
72
|
)}
|
|
73
73
|
{props.type === 'select' && (
|
|
@@ -16,8 +16,8 @@ export function SettingsCard({ children, className, title, description, testId }
|
|
|
16
16
|
>
|
|
17
17
|
{title && (
|
|
18
18
|
<div>
|
|
19
|
-
<h3 className="text-md font-medium text-neutral-200">{title}</h3>
|
|
20
|
-
{description && <p className="text-md text-neutral-500 mt-1">{description}</p>}
|
|
19
|
+
<h3 className="text-md font-medium text-neutral-200 truncate">{title}</h3>
|
|
20
|
+
{description && <p className="text-md text-neutral-500 mt-1 line-clamp-2">{description}</p>}
|
|
21
21
|
</div>
|
|
22
22
|
)}
|
|
23
23
|
{!title && description && <p className="text-md text-neutral-500">{description}</p>}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Info, AlertTriangle, CheckCircle, AlertCircle } from 'lucide-react'
|
|
2
|
+
import { ACCENT_TEXT, type AccentColor } from '../lib/form-colors.ts'
|
|
2
3
|
import { cn } from '../lib/cn.ts'
|
|
3
4
|
|
|
4
|
-
export type SettingsInfoBoxColor =
|
|
5
|
+
export type SettingsInfoBoxColor = AccentColor
|
|
5
6
|
|
|
6
7
|
export interface SettingsInfoBoxProps {
|
|
7
8
|
children: React.ReactNode
|
|
@@ -10,7 +11,7 @@ export interface SettingsInfoBoxProps {
|
|
|
10
11
|
testId?: string
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
const
|
|
14
|
+
const INFO_BOX_ICONS: Record<SettingsInfoBoxColor, typeof Info> = {
|
|
14
15
|
neutral: Info,
|
|
15
16
|
blue: Info,
|
|
16
17
|
amber: AlertTriangle,
|
|
@@ -28,24 +29,6 @@ const iconMap: Record<SettingsInfoBoxColor, typeof Info> = {
|
|
|
28
29
|
teal: Info,
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const iconColorMap: Record<SettingsInfoBoxColor, string> = {
|
|
32
|
-
neutral: 'text-neutral-500',
|
|
33
|
-
blue: 'text-blue-400',
|
|
34
|
-
amber: 'text-amber-400',
|
|
35
|
-
green: 'text-green-400',
|
|
36
|
-
red: 'text-red-400',
|
|
37
|
-
orange: 'text-orange-400',
|
|
38
|
-
cyan: 'text-cyan-400',
|
|
39
|
-
yellow: 'text-yellow-400',
|
|
40
|
-
purple: 'text-purple-400',
|
|
41
|
-
indigo: 'text-indigo-400',
|
|
42
|
-
emerald: 'text-emerald-400',
|
|
43
|
-
violet: 'text-violet-400',
|
|
44
|
-
sky: 'text-sky-400',
|
|
45
|
-
pink: 'text-pink-400',
|
|
46
|
-
teal: 'text-teal-400',
|
|
47
|
-
}
|
|
48
|
-
|
|
49
32
|
const borderColorMap: Record<SettingsInfoBoxColor, string> = {
|
|
50
33
|
neutral: 'border-l-neutral-600',
|
|
51
34
|
blue: 'border-l-blue-500',
|
|
@@ -65,15 +48,14 @@ const borderColorMap: Record<SettingsInfoBoxColor, string> = {
|
|
|
65
48
|
}
|
|
66
49
|
|
|
67
50
|
export function SettingsInfoBox({ children, color = 'neutral', className, testId }: SettingsInfoBoxProps) {
|
|
68
|
-
const Icon =
|
|
51
|
+
const Icon = INFO_BOX_ICONS[color]
|
|
69
52
|
|
|
70
53
|
return (
|
|
71
54
|
<div
|
|
72
|
-
className={cn('flex items-start gap-3 border-l-2', borderColorMap[color], className)}
|
|
73
|
-
style={{ paddingLeft: 10 }}
|
|
55
|
+
className={cn('flex items-start gap-3 border-l-2 pl-2.5', borderColorMap[color], className)}
|
|
74
56
|
data-testid={testId}
|
|
75
57
|
>
|
|
76
|
-
<Icon className={cn('w-4 h-4 mt-0.5 shrink-0',
|
|
58
|
+
<Icon className={cn('w-4 h-4 mt-0.5 shrink-0', ACCENT_TEXT[color])} />
|
|
77
59
|
<div className="text-md text-neutral-500">{children}</div>
|
|
78
60
|
</div>
|
|
79
61
|
)
|