@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
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef, useMemo, type ReactNode } from 'react'
|
|
2
2
|
import { FileCode, FolderTree, Loader2, AlertCircle, AlignLeft, Code2, Type } from 'lucide-react'
|
|
3
|
-
import {
|
|
3
|
+
import { type AccentColor, ACCENT_ICON } from '../lib/form-colors.ts'
|
|
4
|
+
import { CollapseButton } from './icon-button.tsx'
|
|
4
5
|
import { SegmentedToggle } from './segmented-toggle.tsx'
|
|
5
6
|
import { FileTree, collectDirPaths, type FileTreeNode } from './file-tree.tsx'
|
|
6
7
|
|
|
7
8
|
export type PreviewMode = 'format' | 'language' | 'plain'
|
|
8
|
-
export type AccentColor = 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow' | 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky' | 'pink' | 'teal'
|
|
9
|
-
|
|
10
|
-
const ACCENT_ICON: Record<AccentColor, string> = {
|
|
11
|
-
blue: 'text-blue-400', green: 'text-green-400', red: 'text-red-400',
|
|
12
|
-
orange: 'text-orange-400', cyan: 'text-cyan-400', yellow: 'text-yellow-400',
|
|
13
|
-
purple: 'text-purple-400', indigo: 'text-indigo-400', emerald: 'text-emerald-400',
|
|
14
|
-
amber: 'text-amber-400', violet: 'text-violet-400', neutral: 'text-neutral-400',
|
|
15
|
-
sky: 'text-sky-400', pink: 'text-pink-400', teal: 'text-teal-400',
|
|
16
|
-
}
|
|
17
9
|
|
|
18
10
|
const ACCENT_BORDER: Record<AccentColor, string> = {
|
|
19
11
|
blue: 'border-blue-500/25', green: 'border-green-500/25', red: 'border-red-500/25',
|
|
@@ -23,13 +15,6 @@ const ACCENT_BORDER: Record<AccentColor, string> = {
|
|
|
23
15
|
sky: 'border-sky-500/25', pink: 'border-pink-500/25', teal: 'border-teal-500/25',
|
|
24
16
|
}
|
|
25
17
|
|
|
26
|
-
const ACCENT_BUTTON: Record<AccentColor, IconButtonColor> = {
|
|
27
|
-
blue: 'blue', green: 'green', red: 'red',
|
|
28
|
-
orange: 'orange', cyan: 'cyan', yellow: 'yellow',
|
|
29
|
-
purple: 'purple', indigo: 'indigo', emerald: 'emerald',
|
|
30
|
-
amber: 'amber', violet: 'violet', neutral: 'neutral',
|
|
31
|
-
sky: 'sky', pink: 'pink', teal: 'teal',
|
|
32
|
-
}
|
|
33
18
|
|
|
34
19
|
const ACCENT_HANDLE: Record<AccentColor, string> = {
|
|
35
20
|
blue: 'bg-blue-500/30 group-hover:bg-blue-400/50', green: 'bg-green-500/30 group-hover:bg-green-400/50', red: 'bg-red-500/30 group-hover:bg-red-400/50',
|
|
@@ -89,7 +74,7 @@ function renderMarkdownContent(content: string) {
|
|
|
89
74
|
while (i < lines.length && lines[i] !== '---') { fmLines.push(lines[i]); i++ }
|
|
90
75
|
i++ // skip closing ---
|
|
91
76
|
nodes.push(
|
|
92
|
-
<div key="fm" className="mb-3 font-mono text-
|
|
77
|
+
<div key="fm" className="mb-3 font-mono text-xs text-neutral-500 border-l-2 border-neutral-700 pl-2 py-0.5">
|
|
93
78
|
<div className="text-neutral-600">---</div>
|
|
94
79
|
{fmLines.map((l, j) => <div key={j}>{l}</div>)}
|
|
95
80
|
<div className="text-neutral-600">---</div>
|
|
@@ -104,20 +89,20 @@ function renderMarkdownContent(content: string) {
|
|
|
104
89
|
i++
|
|
105
90
|
while (i < lines.length && !lines[i].startsWith('```')) { codeLines.push(lines[i]); i++ }
|
|
106
91
|
nodes.push(
|
|
107
|
-
<pre key={i} className="mb-2 p-2 bg-[var(--background)]/30 rounded text-
|
|
92
|
+
<pre key={i} className="mb-2 p-2 bg-[var(--background)]/30 rounded text-xs font-mono text-neutral-300 overflow-x-auto">
|
|
108
93
|
{codeLines.join('\n')}
|
|
109
94
|
</pre>
|
|
110
95
|
)
|
|
111
96
|
} else if (line.startsWith('### ')) {
|
|
112
|
-
nodes.push(<h3 key={i} className="text-
|
|
97
|
+
nodes.push(<h3 key={i} className="text-xs font-semibold text-neutral-300 mt-2 mb-0.5">{line.slice(4)}</h3>)
|
|
113
98
|
} else if (line.startsWith('## ')) {
|
|
114
|
-
nodes.push(<h2 key={i} className="text-
|
|
99
|
+
nodes.push(<h2 key={i} className="text-sm font-semibold text-neutral-200 mt-2.5 mb-1">{line.slice(3)}</h2>)
|
|
115
100
|
} else if (line.startsWith('# ')) {
|
|
116
|
-
nodes.push(<h1 key={i} className="text-
|
|
101
|
+
nodes.push(<h1 key={i} className="text-md font-semibold text-neutral-100 mb-1.5">{line.slice(2)}</h1>)
|
|
117
102
|
} else if (line === '' || line === '\r') {
|
|
118
103
|
nodes.push(<div key={i} className="h-1.5" />)
|
|
119
104
|
} else {
|
|
120
|
-
nodes.push(<p key={i} className="text-
|
|
105
|
+
nodes.push(<p key={i} className="text-xs text-neutral-400 leading-relaxed">{line}</p>)
|
|
121
106
|
}
|
|
122
107
|
i++
|
|
123
108
|
}
|
|
@@ -302,8 +287,8 @@ export function FileStructureSection({
|
|
|
302
287
|
if (isLoading) {
|
|
303
288
|
return (
|
|
304
289
|
<div>
|
|
305
|
-
<h3 className="text-
|
|
306
|
-
<div className="flex items-center gap-2 text-
|
|
290
|
+
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
|
|
291
|
+
<div className="flex items-center gap-2 text-sm text-neutral-500 py-4">
|
|
307
292
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
308
293
|
Loading file tree...
|
|
309
294
|
</div>
|
|
@@ -318,8 +303,8 @@ export function FileStructureSection({
|
|
|
318
303
|
if (error) {
|
|
319
304
|
return (
|
|
320
305
|
<div>
|
|
321
|
-
<h3 className="text-
|
|
322
|
-
<div className="flex items-center gap-2 text-
|
|
306
|
+
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
|
|
307
|
+
<div className="flex items-center gap-2 text-sm text-red-400 py-4">
|
|
323
308
|
<AlertCircle className="w-3.5 h-3.5 shrink-0" />
|
|
324
309
|
{error}
|
|
325
310
|
</div>
|
|
@@ -336,7 +321,7 @@ export function FileStructureSection({
|
|
|
336
321
|
if (mode === 'format') return renderMarkdownContent(content)
|
|
337
322
|
if (mode === 'language' && renderPreview) return renderPreview(content, filePath, resolvedLanguage)
|
|
338
323
|
return (
|
|
339
|
-
<pre className="p-3 text-
|
|
324
|
+
<pre className="p-3 text-sm font-mono text-white leading-relaxed whitespace-pre-wrap">
|
|
340
325
|
<code>{content}</code>
|
|
341
326
|
</pre>
|
|
342
327
|
)
|
|
@@ -346,11 +331,11 @@ export function FileStructureSection({
|
|
|
346
331
|
<div className={`flex flex-col bg-neutral-900 border ${ACCENT_BORDER[accentColor]} rounded-lg overflow-hidden ${variant === 'split' && effectiveFilePath ? 'w-1/3 shrink-0' : 'flex-1'}`}>
|
|
347
332
|
<div className={`flex items-center px-3 py-2 border-b ${ACCENT_BORDER[accentColor]} shrink-0 gap-2 min-w-0`}>
|
|
348
333
|
<FolderTree className={`w-3.5 h-3.5 shrink-0 ${ACCENT_ICON[accentColor]}`} />
|
|
349
|
-
<span className="text-
|
|
334
|
+
<span className="text-sm text-neutral-200 truncate flex-1">Files</span>
|
|
350
335
|
<CollapseButton
|
|
351
336
|
collapsed={allCollapsed}
|
|
352
337
|
onToggle={() => setExpandedPaths(allCollapsed ? new Set(allDirPaths) : new Set())}
|
|
353
|
-
color={
|
|
338
|
+
color={accentColor}
|
|
354
339
|
/>
|
|
355
340
|
</div>
|
|
356
341
|
<div className={`${variant === 'split' ? 'flex-1 overflow-y-auto' : ''} p-3`}>
|
|
@@ -371,7 +356,7 @@ export function FileStructureSection({
|
|
|
371
356
|
<div className={`flex-1 flex flex-col bg-neutral-900 border ${ACCENT_BORDER[accentColor]} rounded-lg overflow-hidden`}>
|
|
372
357
|
<div className={`flex items-center px-3 py-2 border-b ${ACCENT_BORDER[accentColor]} shrink-0 gap-2 min-w-0`}>
|
|
373
358
|
<FileCode className={`w-3.5 h-3.5 shrink-0 ${ACCENT_ICON[accentColor]}`} />
|
|
374
|
-
<span className="text-
|
|
359
|
+
<span className="text-sm text-neutral-200 truncate flex-1">{selectedFileName}</span>
|
|
375
360
|
{showToggle && (
|
|
376
361
|
<SegmentedToggle
|
|
377
362
|
options={toggleOptions}
|
|
@@ -384,12 +369,12 @@ export function FileStructureSection({
|
|
|
384
369
|
</div>
|
|
385
370
|
<div className="flex-1 overflow-auto">
|
|
386
371
|
{fileIsLoading ? (
|
|
387
|
-
<div className="flex items-center gap-2 text-
|
|
372
|
+
<div className="flex items-center gap-2 text-sm text-neutral-500 p-3">
|
|
388
373
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
389
374
|
Loading...
|
|
390
375
|
</div>
|
|
391
376
|
) : fileError ? (
|
|
392
|
-
<p className="text-
|
|
377
|
+
<p className="text-sm text-red-400 p-3">{fileError}</p>
|
|
393
378
|
) : fileContent !== null ? (
|
|
394
379
|
renderContent(fileContent, effectiveFilePath)
|
|
395
380
|
) : null}
|
|
@@ -409,7 +394,7 @@ export function FileStructureSection({
|
|
|
409
394
|
if (variant === 'list') {
|
|
410
395
|
return (
|
|
411
396
|
<div ref={sectionRef}>
|
|
412
|
-
<h3 className="text-
|
|
397
|
+
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
|
|
413
398
|
<div className="space-y-3">
|
|
414
399
|
{treePanel}
|
|
415
400
|
{previewPanel && (
|
|
@@ -427,7 +412,7 @@ export function FileStructureSection({
|
|
|
427
412
|
|
|
428
413
|
return (
|
|
429
414
|
<div ref={sectionRef}>
|
|
430
|
-
<h3 className="text-
|
|
415
|
+
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
|
|
431
416
|
<div className="flex gap-3" style={{ height: `${effectiveHeight}px` }}>
|
|
432
417
|
{treePanel}
|
|
433
418
|
{previewPanel}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FileCode, Folder, FolderOpen, ChevronRight, ChevronDown } from 'lucide-react'
|
|
2
|
+
import { ACCENT_ICON, type AccentColor } from '../lib/form-colors.ts'
|
|
2
3
|
|
|
3
4
|
export interface FileTreeNode {
|
|
4
5
|
name: string
|
|
@@ -27,18 +28,7 @@ const ACCENT_SELECTED: Record<string, string> = {
|
|
|
27
28
|
emerald: 'bg-emerald-400/20 text-neutral-200',
|
|
28
29
|
teal: 'bg-teal-400/20 text-neutral-200',
|
|
29
30
|
sky: 'bg-sky-400/20 text-neutral-200',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const ACCENT_ICON: Record<string, string> = {
|
|
33
|
-
blue: 'text-blue-400',
|
|
34
|
-
purple: 'text-purple-400',
|
|
35
|
-
orange: 'text-orange-400',
|
|
36
|
-
green: 'text-green-400',
|
|
37
|
-
pink: 'text-pink-400',
|
|
38
|
-
amber: 'text-amber-400',
|
|
39
|
-
emerald: 'text-emerald-400',
|
|
40
|
-
teal: 'text-teal-400',
|
|
41
|
-
sky: 'text-sky-400',
|
|
31
|
+
violet: 'bg-violet-400/20 text-neutral-200',
|
|
42
32
|
}
|
|
43
33
|
|
|
44
34
|
function nodeHasFiles(node: FileTreeNode): boolean {
|
|
@@ -120,9 +110,9 @@ function FileTreeNodeItem({ node, path, selectedPath, onSelectFile, expandedPath
|
|
|
120
110
|
const isDir = node.type === 'directory'
|
|
121
111
|
const isSelected = !isDir && selectedPath === path
|
|
122
112
|
const expanded = isDir && expandedPaths.has(path)
|
|
123
|
-
const base = 'flex items-center gap-1.5 py-0.5 px-1 rounded text-
|
|
113
|
+
const base = 'flex items-center gap-1.5 py-0.5 px-1 rounded text-sm transition-colors overflow-hidden whitespace-nowrap'
|
|
124
114
|
const selectedClass = ACCENT_SELECTED[accentColor] ?? ACCENT_SELECTED.blue
|
|
125
|
-
const iconColorClass = ACCENT_ICON[accentColor] ?? ACCENT_ICON.blue
|
|
115
|
+
const iconColorClass = ACCENT_ICON[accentColor as AccentColor] ?? ACCENT_ICON.blue
|
|
126
116
|
const rowClass = isSelected
|
|
127
117
|
? `${base} ${selectedClass}`
|
|
128
118
|
: isDir
|
|
@@ -3,15 +3,20 @@
|
|
|
3
3
|
import { useState, useMemo } from 'react'
|
|
4
4
|
import { Folder, FolderOpen, File, FileCode, FileText, FileJson, Image, ChevronRight, Search, MoreVertical } from 'lucide-react'
|
|
5
5
|
import type { LucideIcon } from 'lucide-react'
|
|
6
|
-
import type
|
|
6
|
+
import { iconMap, type IconName } from './icon-button.tsx'
|
|
7
7
|
import { cn } from '../lib/cn.ts'
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
const ACCENT_SELECTED: Record<string, string> = {
|
|
10
|
+
blue: 'bg-blue-400/15 text-blue-400',
|
|
11
|
+
purple: 'bg-purple-400/15 text-purple-400',
|
|
12
|
+
orange: 'bg-orange-400/15 text-orange-400',
|
|
13
|
+
green: 'bg-green-400/15 text-green-400',
|
|
14
|
+
pink: 'bg-pink-400/15 text-pink-400',
|
|
15
|
+
amber: 'bg-amber-400/15 text-amber-400',
|
|
16
|
+
emerald: 'bg-emerald-400/15 text-emerald-400',
|
|
17
|
+
teal: 'bg-teal-400/15 text-teal-400',
|
|
18
|
+
sky: 'bg-sky-400/15 text-sky-400',
|
|
19
|
+
violet: 'bg-violet-400/15 text-violet-400',
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
const EXTENSION_ICON_MAP: Record<string, LucideIcon> = {
|
|
@@ -48,6 +53,7 @@ export interface FilesPanelProps {
|
|
|
48
53
|
onAction?: (action: string, path: string) => void
|
|
49
54
|
showSearch?: boolean
|
|
50
55
|
className?: string
|
|
56
|
+
accentColor?: string
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
function collectAllFolderPaths(entries: FileEntry[]): Set<string> {
|
|
@@ -74,7 +80,7 @@ function countFiles(entries: FileEntry[]): number {
|
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
function getFileIcon(entry: FileEntry): LucideIcon {
|
|
77
|
-
if (entry.icon &&
|
|
83
|
+
if (entry.icon && iconMap[entry.icon]) return iconMap[entry.icon]!
|
|
78
84
|
if (entry.type === 'folder') return Folder
|
|
79
85
|
const ext = entry.name.split('.').pop()?.toLowerCase()
|
|
80
86
|
if (ext && EXTENSION_ICON_MAP[ext]) return EXTENSION_ICON_MAP[ext]
|
|
@@ -82,7 +88,7 @@ function getFileIcon(entry: FileEntry): LucideIcon {
|
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
function getFolderIcon(expanded: boolean, entry: FileEntry): LucideIcon {
|
|
85
|
-
if (entry.icon &&
|
|
91
|
+
if (entry.icon && iconMap[entry.icon]) return iconMap[entry.icon]!
|
|
86
92
|
return expanded ? FolderOpen : Folder
|
|
87
93
|
}
|
|
88
94
|
|
|
@@ -110,9 +116,10 @@ interface FileNodeProps {
|
|
|
110
116
|
onToggleExpand: (path: string) => void
|
|
111
117
|
onSelect?: (path: string) => void
|
|
112
118
|
onAction?: (action: string, path: string) => void
|
|
119
|
+
accentColor: string
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
function FileNode({ entry, depth, selectedPath, expandedPaths, onToggleExpand, onSelect, onAction }: FileNodeProps) {
|
|
122
|
+
function FileNode({ entry, depth, selectedPath, expandedPaths, onToggleExpand, onSelect, onAction, accentColor }: FileNodeProps) {
|
|
116
123
|
const isFolder = entry.type === 'folder'
|
|
117
124
|
const isExpanded = isFolder && expandedPaths.has(entry.path)
|
|
118
125
|
const isSelected = !isFolder && selectedPath === entry.path
|
|
@@ -124,9 +131,9 @@ function FileNode({ entry, depth, selectedPath, expandedPaths, onToggleExpand, o
|
|
|
124
131
|
type="button"
|
|
125
132
|
onClick={isFolder ? () => onToggleExpand(entry.path) : () => onSelect?.(entry.path)}
|
|
126
133
|
className={cn(
|
|
127
|
-
'group flex items-center gap-1.5 w-full py-1 px-2 rounded text-
|
|
134
|
+
'group flex items-center gap-1.5 w-full py-1 px-2 rounded text-sm transition-colors cursor-pointer',
|
|
128
135
|
isSelected
|
|
129
|
-
?
|
|
136
|
+
? ACCENT_SELECTED[accentColor] ?? ACCENT_SELECTED.blue
|
|
130
137
|
: 'text-neutral-400 hover:bg-neutral-700/40 hover:text-neutral-200',
|
|
131
138
|
)}
|
|
132
139
|
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
|
@@ -144,7 +151,7 @@ function FileNode({ entry, depth, selectedPath, expandedPaths, onToggleExpand, o
|
|
|
144
151
|
/>
|
|
145
152
|
<span className="truncate">{entry.name}</span>
|
|
146
153
|
{entry.badge && (
|
|
147
|
-
<span className="ml-auto shrink-0 px-1.5 py-0.5 text-
|
|
154
|
+
<span className="ml-auto shrink-0 px-1.5 py-0.5 text-xs rounded bg-neutral-700 text-neutral-500">
|
|
148
155
|
{entry.badge}
|
|
149
156
|
</span>
|
|
150
157
|
)}
|
|
@@ -172,6 +179,7 @@ function FileNode({ entry, depth, selectedPath, expandedPaths, onToggleExpand, o
|
|
|
172
179
|
onToggleExpand={onToggleExpand}
|
|
173
180
|
onSelect={onSelect}
|
|
174
181
|
onAction={onAction}
|
|
182
|
+
accentColor={accentColor}
|
|
175
183
|
/>
|
|
176
184
|
))}
|
|
177
185
|
</ul>
|
|
@@ -187,6 +195,7 @@ export function FilesPanel({
|
|
|
187
195
|
onAction,
|
|
188
196
|
showSearch = false,
|
|
189
197
|
className,
|
|
198
|
+
accentColor = 'blue',
|
|
190
199
|
}: FilesPanelProps) {
|
|
191
200
|
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => collectAllFolderPaths(files))
|
|
192
201
|
const [searchQuery, setSearchQuery] = useState('')
|
|
@@ -210,19 +219,19 @@ export function FilesPanel({
|
|
|
210
219
|
return (
|
|
211
220
|
<div className={cn('flex flex-col bg-neutral-800 rounded-lg overflow-hidden', className)}>
|
|
212
221
|
<div className="flex items-center justify-between px-3 py-2 border-b border-neutral-700">
|
|
213
|
-
<span className="text-
|
|
214
|
-
<span className="text-
|
|
222
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-neutral-500">Files</span>
|
|
223
|
+
<span className="text-xs text-neutral-500">{fileCount} files</span>
|
|
215
224
|
</div>
|
|
216
225
|
{showSearch && (
|
|
217
226
|
<div className="px-2 py-2 border-b border-neutral-700">
|
|
218
|
-
<div className="flex items-center gap-1.5 px-2 py-1 bg-[var(--background)] border border-neutral-700 rounded text-
|
|
227
|
+
<div className="flex items-center gap-1.5 px-2 py-1 bg-[var(--background)] border border-neutral-700 rounded text-sm">
|
|
219
228
|
<Search className="w-3 h-3 text-neutral-500 shrink-0" />
|
|
220
229
|
<input
|
|
221
230
|
type="text"
|
|
222
231
|
placeholder="Search files..."
|
|
223
232
|
value={searchQuery}
|
|
224
233
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
225
|
-
className="flex-1 bg-transparent text-neutral-200 placeholder-neutral-500 outline-none text-
|
|
234
|
+
className="flex-1 bg-transparent text-neutral-200 placeholder-neutral-500 outline-none text-sm"
|
|
226
235
|
/>
|
|
227
236
|
</div>
|
|
228
237
|
</div>
|
|
@@ -239,11 +248,12 @@ export function FilesPanel({
|
|
|
239
248
|
onToggleExpand={handleToggleExpand}
|
|
240
249
|
onSelect={onSelect}
|
|
241
250
|
onAction={onAction}
|
|
251
|
+
accentColor={accentColor}
|
|
242
252
|
/>
|
|
243
253
|
))}
|
|
244
254
|
</ul>
|
|
245
255
|
{displayedFiles.length === 0 && (
|
|
246
|
-
<p className="text-
|
|
256
|
+
<p className="text-xs text-neutral-500 text-center py-4">No files found</p>
|
|
247
257
|
)}
|
|
248
258
|
</div>
|
|
249
259
|
</div>
|
|
@@ -89,7 +89,7 @@ export function FilterDropdown({
|
|
|
89
89
|
<div className="relative flex items-center" ref={ref} onKeyDown={handleKeyDown}>
|
|
90
90
|
<button
|
|
91
91
|
onClick={() => setIsOpen(!isOpen)}
|
|
92
|
-
className={`flex items-center gap-1.5 h-7 px-2 rounded-md border ${v.bg} text-
|
|
92
|
+
className={`flex items-center gap-1.5 h-7 px-2 rounded-md border ${v.bg} text-sm transition-colors cursor-pointer ${
|
|
93
93
|
isActive
|
|
94
94
|
? `${clearable ? 'rounded-r-none border-r-0' : ''} ${FORM_COLORS[color].border} text-neutral-200 ${FORM_COLORS[color].hover}`
|
|
95
95
|
: isOpen
|
|
@@ -124,7 +124,7 @@ export function FilterDropdown({
|
|
|
124
124
|
onChange={(e) => setSearch(e.target.value)}
|
|
125
125
|
onKeyDown={handleKeyDown}
|
|
126
126
|
placeholder="Search..."
|
|
127
|
-
className={`w-full pl-6 pr-2 py-1 text-
|
|
127
|
+
className={`w-full pl-6 pr-2 py-1 text-sm bg-[var(--popover)] border border-neutral-600 rounded text-neutral-200 placeholder-neutral-500 outline-none ${FORM_COLORS[color].focus}`}
|
|
128
128
|
/>
|
|
129
129
|
</div>
|
|
130
130
|
</div>
|
|
@@ -133,7 +133,7 @@ export function FilterDropdown({
|
|
|
133
133
|
<button
|
|
134
134
|
data-idx={0}
|
|
135
135
|
onClick={() => handleSelect('all')}
|
|
136
|
-
className={`w-full flex items-center gap-2 px-3 py-1.5 text-
|
|
136
|
+
className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer ${
|
|
137
137
|
highlightIdx === 0
|
|
138
138
|
? `${FORM_COLORS[color].selectedBg} text-neutral-200`
|
|
139
139
|
: !isActive ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
|
|
@@ -152,7 +152,7 @@ export function FilterDropdown({
|
|
|
152
152
|
key={opt.value}
|
|
153
153
|
data-idx={idx}
|
|
154
154
|
onClick={() => handleSelect(opt.value)}
|
|
155
|
-
className={`w-full flex items-center gap-2 px-3 py-1.5 text-
|
|
155
|
+
className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer ${
|
|
156
156
|
isHighlighted
|
|
157
157
|
? `${FORM_COLORS[color].selectedBg} text-neutral-200`
|
|
158
158
|
: isSelected ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
|
|
@@ -164,7 +164,7 @@ export function FilterDropdown({
|
|
|
164
164
|
)
|
|
165
165
|
})}
|
|
166
166
|
{showSearch && search && filtered.length === 0 && (
|
|
167
|
-
<div className="px-3 py-2 text-
|
|
167
|
+
<div className="px-3 py-2 text-sm text-neutral-500">No matches</div>
|
|
168
168
|
)}
|
|
169
169
|
</div>
|
|
170
170
|
)}
|
|
@@ -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
|
|
|
@@ -83,7 +84,7 @@ export function FormActions({
|
|
|
83
84
|
tooltip={{ description: backTooltip }}
|
|
84
85
|
/>
|
|
85
86
|
)}
|
|
86
|
-
{statusText && <span className="text-
|
|
87
|
+
{statusText && <span className="text-sm text-neutral-500">{statusText}</span>}
|
|
87
88
|
</div>
|
|
88
89
|
)}
|
|
89
90
|
<div className="flex items-center gap-2">
|
|
@@ -39,16 +39,16 @@ export function FrontmatterFormHeader({
|
|
|
39
39
|
collapsed ? '' : 'rotate-90'
|
|
40
40
|
}`}
|
|
41
41
|
/>
|
|
42
|
-
<span className="text-
|
|
42
|
+
<span className="text-sm font-medium text-neutral-400 uppercase tracking-wide">
|
|
43
43
|
Configuration
|
|
44
44
|
</span>
|
|
45
45
|
{collapsed && hasFm && (
|
|
46
|
-
<span className="text-
|
|
46
|
+
<span className="text-xs text-neutral-500 font-mono ml-2 truncate">
|
|
47
47
|
{renderSummary()}
|
|
48
48
|
</span>
|
|
49
49
|
)}
|
|
50
50
|
{collapsed && !hasFm && (
|
|
51
|
-
<span className="text-
|
|
51
|
+
<span className="text-xs text-neutral-600 ml-2">No frontmatter</span>
|
|
52
52
|
)}
|
|
53
53
|
</button>
|
|
54
54
|
|
|
@@ -64,7 +64,7 @@ export function FrontmatterFormHeader({
|
|
|
64
64
|
disabled={readOnly}
|
|
65
65
|
/>
|
|
66
66
|
<span
|
|
67
|
-
className="text-
|
|
67
|
+
className="text-sm text-neutral-400 cursor-pointer"
|
|
68
68
|
onClick={() => !readOnly && onFrontmatterToggle(!hasFm)}
|
|
69
69
|
>
|
|
70
70
|
Add YAML frontmatter to file
|
|
@@ -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 {
|
|
@@ -323,7 +324,7 @@ export function IconButton({
|
|
|
323
324
|
</span>
|
|
324
325
|
{badge !== undefined && (
|
|
325
326
|
<span
|
|
326
|
-
className={`absolute -top-1 -right-1 min-w-[18px] h-[18px] flex items-center justify-center px-1 text-
|
|
327
|
+
className={`absolute -top-1 -right-1 min-w-[18px] h-[18px] flex items-center justify-center px-1 text-sm font-bold text-white rounded-full ${badgeColorClasses[badgeColor]}`}
|
|
327
328
|
>
|
|
328
329
|
{typeof badge === 'number' && badge > 99 ? '99+' : badge}
|
|
329
330
|
</span>
|
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'> {
|
|
@@ -39,10 +40,10 @@ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>,
|
|
|
39
40
|
|
|
40
41
|
const sizeClasses = {
|
|
41
42
|
xss: 'px-1 py-0.5 text-xss',
|
|
42
|
-
xs: 'px-1.5 py-0.5 text-
|
|
43
|
-
sm: 'px-2 py-1 text-
|
|
44
|
-
md: 'px-3 py-1.5 text-
|
|
45
|
-
lg: 'px-3 py-2 text-
|
|
43
|
+
xs: 'px-1.5 py-0.5 text-sm',
|
|
44
|
+
sm: 'px-2 py-1 text-sm',
|
|
45
|
+
md: 'px-3 py-1.5 text-md',
|
|
46
|
+
lg: 'px-3 py-2 text-md',
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
const variantClasses = {
|
|
@@ -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,30 +184,11 @@ 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 && (
|
|
207
|
-
<p className="text-
|
|
191
|
+
<p className="text-sm text-red-400 mt-1 text-right">{error}</p>
|
|
208
192
|
)}
|
|
209
193
|
</div>
|
|
210
194
|
)
|
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
|
|
@@ -87,14 +73,14 @@ const progressFillColors: Record<LabelColor, string> = {
|
|
|
87
73
|
|
|
88
74
|
const sizeConfig = {
|
|
89
75
|
xss: { height: 14, padding: 'px-1', text: 'text-xss', iconSize: 'w-2 h-2', gap: 'gap-0.5' },
|
|
90
|
-
xs: { height: 16, padding: 'px-1.5', text: 'text-
|
|
91
|
-
sm: { height: 18, padding: 'px-1.5', text: 'text-
|
|
92
|
-
md: { height: 20, padding: 'px-1.5', text: 'text-
|
|
93
|
-
lg: { height: 22, padding: 'px-2', text: 'text-
|
|
76
|
+
xs: { height: 16, padding: 'px-1.5', text: 'text-xs', iconSize: 'w-2.5 h-2.5', gap: 'gap-1' },
|
|
77
|
+
sm: { height: 18, padding: 'px-1.5', text: 'text-xs', iconSize: 'w-2.5 h-2.5', gap: 'gap-1.5' },
|
|
78
|
+
md: { height: 20, padding: 'px-1.5', text: 'text-xs', iconSize: 'w-3 h-3', gap: 'gap-1' },
|
|
79
|
+
lg: { height: 22, padding: 'px-2', text: 'text-sm', iconSize: 'w-3 h-3', gap: 'gap-1' },
|
|
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
|
|
|
@@ -220,14 +220,14 @@ export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, cla
|
|
|
220
220
|
{/* Line 1: Icon + Title */}
|
|
221
221
|
<div className="flex items-center gap-1.5 w-full transition-[padding] duration-150 group-hover:pr-5">
|
|
222
222
|
{tab.icon && <span className={cn('shrink-0 inline-flex', isActive ? iconC : 'text-neutral-400')} style={{ width: 14, height: 14 }}>{tab.icon}</span>}
|
|
223
|
-
<span className={cn('truncate text-
|
|
223
|
+
<span className={cn('truncate text-md font-medium', isActive ? titleC : 'text-neutral-300')}>{tab.title}</span>
|
|
224
224
|
</div>
|
|
225
225
|
|
|
226
226
|
{/* Line 2: Subtitle with optional icon */}
|
|
227
227
|
{tab.subtitle && (
|
|
228
228
|
<div className="flex items-center gap-1 w-full">
|
|
229
229
|
{tab.subtitleIcon && <span className={cn('shrink-0 inline-flex', isActive ? subIconC : 'text-neutral-500')} style={{ width: 10, height: 10 }}>{tab.subtitleIcon}</span>}
|
|
230
|
-
<span className={cn('truncate text-
|
|
230
|
+
<span className={cn('truncate text-sm', isActive ? subC : 'text-neutral-500')}>{tab.subtitle}</span>
|
|
231
231
|
</div>
|
|
232
232
|
)}
|
|
233
233
|
|
|
@@ -272,12 +272,12 @@ export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, cla
|
|
|
272
272
|
<div className={cn('flex flex-col gap-0.5 px-2.5 py-1.5 rounded-t-lg border-t-2 shadow-xl bg-neutral-800', ghostBorder)}>
|
|
273
273
|
<div className="flex items-center gap-1.5">
|
|
274
274
|
{t.icon && <span className={cn('shrink-0 inline-flex', ghostIconC)} style={{ width: 14, height: 14 }}>{t.icon}</span>}
|
|
275
|
-
<span className={cn('text-
|
|
275
|
+
<span className={cn('text-md font-medium', ghostTitleC)}>{t.title}</span>
|
|
276
276
|
</div>
|
|
277
277
|
{t.subtitle && (
|
|
278
278
|
<div className="flex items-center gap-1">
|
|
279
279
|
{t.subtitleIcon && <span className={cn('shrink-0 inline-flex', ghostSubIconC)} style={{ width: 10, height: 10 }}>{t.subtitleIcon}</span>}
|
|
280
|
-
<span className={cn('text-
|
|
280
|
+
<span className={cn('text-sm', ghostSubC)}>{t.subtitle}</span>
|
|
281
281
|
</div>
|
|
282
282
|
)}
|
|
283
283
|
</div>
|