@toolr/ui-design 0.1.0
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/README.md +63 -0
- package/components/content/info-panel-primitives.tsx +297 -0
- package/components/diagrams/diagram-utils.tsx +908 -0
- package/components/hooks/use-click-outside.ts +27 -0
- package/components/hooks/use-dropdown-max-height.ts +20 -0
- package/components/hooks/use-navigation-history.ts +94 -0
- package/components/lib/ai-tools.tsx +44 -0
- package/components/lib/cn.ts +6 -0
- package/components/lib/form-colors.ts +32 -0
- package/components/lib/theme-engine.ts +97 -0
- package/components/lib/toolr-brand.tsx +31 -0
- package/components/sections/ai-tools-paths/index.ts +37 -0
- package/components/sections/ai-tools-paths/tools-paths-panel.tsx +212 -0
- package/components/sections/ai-tools-paths/types.ts +111 -0
- package/components/sections/ai-tools-paths/use-tools-paths.ts +159 -0
- package/components/sections/captured-issues/captured-issues-panel.tsx +214 -0
- package/components/sections/captured-issues/index.ts +38 -0
- package/components/sections/captured-issues/types.ts +113 -0
- package/components/sections/captured-issues/use-captured-issues.ts +111 -0
- package/components/sections/golden-snapshots/file-diff-viewer.tsx +420 -0
- package/components/sections/golden-snapshots/golden-sync-panel.tsx +223 -0
- package/components/sections/golden-snapshots/index.ts +145 -0
- package/components/sections/golden-snapshots/snapshot-manager.tsx +200 -0
- package/components/sections/golden-snapshots/status-overview.tsx +305 -0
- package/components/sections/golden-snapshots/types.ts +288 -0
- package/components/sections/golden-snapshots/use-golden-sync.ts +477 -0
- package/components/sections/golden-snapshots/version-manager.tsx +186 -0
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +210 -0
- package/components/sections/prompt-editor/index.ts +121 -0
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +276 -0
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +514 -0
- package/components/sections/prompt-editor/types.ts +101 -0
- package/components/sections/prompt-editor/use-prompt-editor.ts +131 -0
- package/components/sections/report-bug/error-logger.ts +392 -0
- package/components/sections/report-bug/index.ts +59 -0
- package/components/sections/report-bug/issue-reporter-api.ts +83 -0
- package/components/sections/report-bug/report-bug-form.tsx +282 -0
- package/components/sections/report-bug/screenshot-uploader.tsx +228 -0
- package/components/sections/report-bug/use-report-bug.ts +170 -0
- package/components/sections/snapshot-browser/index.ts +53 -0
- package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +147 -0
- package/components/sections/snapshot-browser/snapshot-tree.tsx +451 -0
- package/components/sections/snapshot-browser/types.ts +106 -0
- package/components/sections/snapshot-browser/use-snapshot-browser.ts +125 -0
- package/components/sections/snippets-editor/index.ts +31 -0
- package/components/sections/snippets-editor/snippets-editor.tsx +381 -0
- package/components/sections/snippets-editor/types.ts +48 -0
- package/components/sections/snippets-editor/use-snippets-editor.ts +217 -0
- package/components/ui/action-dialog.tsx +309 -0
- package/components/ui/ai-action-button.tsx +137 -0
- package/components/ui/ai-execution-action-buttons.tsx +106 -0
- package/components/ui/badge.tsx +67 -0
- package/components/ui/bottom-panel-header.tsx +240 -0
- package/components/ui/breadcrumb.tsx +168 -0
- package/components/ui/checkbox.tsx +102 -0
- package/components/ui/collapsible-section.tsx +100 -0
- package/components/ui/confirm-badge.tsx +71 -0
- package/components/ui/detail-section.tsx +67 -0
- package/components/ui/detail-view-wrapper.tsx +55 -0
- package/components/ui/editor-placeholder-card.tsx +197 -0
- package/components/ui/editor-toolbar.tsx +123 -0
- package/components/ui/execution-details-panel.tsx +93 -0
- package/components/ui/extension-list-card.tsx +105 -0
- package/components/ui/file-structure-section.tsx +373 -0
- package/components/ui/file-tree.tsx +171 -0
- package/components/ui/files-panel.tsx +251 -0
- package/components/ui/filter-dropdown.tsx +173 -0
- package/components/ui/form-actions.tsx +127 -0
- package/components/ui/frontmatter-form-header.tsx +80 -0
- package/components/ui/icon-button.tsx +388 -0
- package/components/ui/input.tsx +211 -0
- package/components/ui/label.tsx +159 -0
- package/components/ui/layout-tab-bar.tsx +289 -0
- package/components/ui/modal.tsx +194 -0
- package/components/ui/nav-card.tsx +81 -0
- package/components/ui/navigation-bar.tsx +285 -0
- package/components/ui/number-input.tsx +165 -0
- package/components/ui/registry-browser.tsx +261 -0
- package/components/ui/registry-card.tsx +710 -0
- package/components/ui/registry-detail.tsx +224 -0
- package/components/ui/resizable-textarea.tsx +290 -0
- package/components/ui/scope-badge.tsx +67 -0
- package/components/ui/segmented-toggle.tsx +133 -0
- package/components/ui/select.tsx +172 -0
- package/components/ui/selection-grid.tsx +313 -0
- package/components/ui/setting-row.tsx +97 -0
- package/components/ui/snapshot-card.tsx +107 -0
- package/components/ui/snippets-panel.tsx +161 -0
- package/components/ui/sort-dropdown.tsx +109 -0
- package/components/ui/status-card.tsx +96 -0
- package/components/ui/tab-bar.tsx +340 -0
- package/components/ui/toggle.tsx +142 -0
- package/components/ui/tooltip.tsx +326 -0
- package/dist/content.d.ts +110 -0
- package/dist/content.js +195 -0
- package/dist/diagrams.d.ts +371 -0
- package/dist/diagrams.js +702 -0
- package/dist/index.d.ts +2714 -0
- package/dist/index.js +11220 -0
- package/dist/preset.d.ts +24 -0
- package/dist/preset.js +17 -0
- package/dist/tokens/tokens/primitives.css +45 -0
- package/dist/tokens/tokens/semantic.css +46 -0
- package/dist/tokens/tokens/theme.css +11 -0
- package/dist/tokens/tokens/tokens.json +65 -0
- package/index.ts +123 -0
- package/package.json +63 -0
- package/tailwind-preset.ts +22 -0
- package/tokens/primitives.css +45 -0
- package/tokens/semantic.css +46 -0
- package/tokens/theme.css +11 -0
- package/tokens/tokens.json +65 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SettingRow - A settings row with built-in control types
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - Settings pages - boolean toggles, dropdowns, text inputs
|
|
6
|
+
* - Configuration panels - key-value setting rows
|
|
7
|
+
*
|
|
8
|
+
* Control types:
|
|
9
|
+
* - toggle: renders a Toggle switch
|
|
10
|
+
* - select: renders a Select dropdown
|
|
11
|
+
* - input: renders a text Input
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Toggle, type ToggleColor, type ToggleSize, type ToggleVariant } from './toggle.tsx'
|
|
15
|
+
import { Select, type SelectOption } from './select.tsx'
|
|
16
|
+
import { Input } from './input.tsx'
|
|
17
|
+
|
|
18
|
+
interface SettingRowBase {
|
|
19
|
+
label: string
|
|
20
|
+
description?: string
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SettingRowToggle extends SettingRowBase {
|
|
26
|
+
type: 'toggle'
|
|
27
|
+
checked: boolean
|
|
28
|
+
onChange: (checked: boolean) => void
|
|
29
|
+
color?: ToggleColor
|
|
30
|
+
size?: ToggleSize
|
|
31
|
+
variant?: ToggleVariant
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface SettingRowSelect extends SettingRowBase {
|
|
35
|
+
type: 'select'
|
|
36
|
+
value: string
|
|
37
|
+
options: SelectOption[]
|
|
38
|
+
onChange: (value: string) => void
|
|
39
|
+
selectSize?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
|
|
40
|
+
selectVariant?: 'filled' | 'outline'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface SettingRowInput extends SettingRowBase {
|
|
44
|
+
type: 'input'
|
|
45
|
+
value: string
|
|
46
|
+
onChange: (value: string) => void
|
|
47
|
+
placeholder?: string
|
|
48
|
+
inputSize?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
|
|
49
|
+
inputVariant?: 'filled' | 'outline'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type SettingRowProps = SettingRowToggle | SettingRowSelect | SettingRowInput
|
|
53
|
+
|
|
54
|
+
export function SettingRow(props: SettingRowProps) {
|
|
55
|
+
const { label, description, disabled, className = '' } = props
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className={`flex items-start justify-between gap-4 ${className}`}>
|
|
59
|
+
<div>
|
|
60
|
+
<label className="text-neutral-200 leading-7">{label}</label>
|
|
61
|
+
{description && <p className="text-sm text-neutral-500">{description}</p>}
|
|
62
|
+
</div>
|
|
63
|
+
{props.type === 'toggle' && (
|
|
64
|
+
<Toggle
|
|
65
|
+
checked={props.checked}
|
|
66
|
+
onChange={props.onChange}
|
|
67
|
+
disabled={disabled}
|
|
68
|
+
color={props.color}
|
|
69
|
+
size={props.size}
|
|
70
|
+
variant={props.variant}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
{props.type === 'select' && (
|
|
74
|
+
<Select
|
|
75
|
+
value={props.value}
|
|
76
|
+
options={props.options}
|
|
77
|
+
onChange={props.onChange}
|
|
78
|
+
disabled={disabled}
|
|
79
|
+
size={props.selectSize ?? 'sm'}
|
|
80
|
+
variant={props.selectVariant ?? 'outline'}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
{props.type === 'input' && (
|
|
84
|
+
<div className="w-44 shrink-0">
|
|
85
|
+
<Input
|
|
86
|
+
value={props.value}
|
|
87
|
+
onChange={props.onChange}
|
|
88
|
+
disabled={disabled}
|
|
89
|
+
placeholder={props.placeholder}
|
|
90
|
+
size={props.inputSize ?? 'sm'}
|
|
91
|
+
variant={props.inputVariant ?? 'outline'}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Snapshot card with status stripe, stats grid, and sync/view actions. */
|
|
2
|
+
|
|
3
|
+
import { RefreshCw, Eye } from 'lucide-react'
|
|
4
|
+
import { cn } from '../lib/cn.ts'
|
|
5
|
+
import { Label, type LabelColor } from './label.tsx'
|
|
6
|
+
import type { IconName } from './icon-button.tsx'
|
|
7
|
+
|
|
8
|
+
type SnapshotStatus = 'synced' | 'pending' | 'conflict' | 'outdated'
|
|
9
|
+
|
|
10
|
+
export interface SnapshotCardProps {
|
|
11
|
+
title: string
|
|
12
|
+
timestamp?: string
|
|
13
|
+
status: SnapshotStatus
|
|
14
|
+
description?: string
|
|
15
|
+
stats?: { label: string; value: string | number }[]
|
|
16
|
+
onSync?: () => void
|
|
17
|
+
onView?: () => void
|
|
18
|
+
className?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const statusStripeColor: Record<SnapshotStatus, string> = {
|
|
22
|
+
synced: 'bg-green-400',
|
|
23
|
+
pending: 'bg-amber-400',
|
|
24
|
+
conflict: 'bg-red-400',
|
|
25
|
+
outdated: 'bg-neutral-500',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const statusLabelConfig: Record<SnapshotStatus, { text: string; color: LabelColor; icon: IconName; tooltip: string }> = {
|
|
29
|
+
synced: { text: 'Synced', color: 'green', icon: 'check-circle', tooltip: 'Snapshot is in sync' },
|
|
30
|
+
pending: { text: 'Pending', color: 'amber', icon: 'loader', tooltip: 'Snapshot is awaiting sync' },
|
|
31
|
+
conflict: { text: 'Conflict', color: 'red', icon: 'alert-triangle', tooltip: 'Snapshot has conflicts' },
|
|
32
|
+
outdated: { text: 'Outdated', color: 'neutral', icon: 'info', tooltip: 'Snapshot is outdated' },
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function SnapshotCard({
|
|
36
|
+
title,
|
|
37
|
+
timestamp,
|
|
38
|
+
status,
|
|
39
|
+
description,
|
|
40
|
+
stats,
|
|
41
|
+
onSync,
|
|
42
|
+
onView,
|
|
43
|
+
className,
|
|
44
|
+
}: SnapshotCardProps) {
|
|
45
|
+
return (
|
|
46
|
+
<div className={cn('rounded-lg border border-neutral-700 bg-neutral-800 overflow-hidden', className)}>
|
|
47
|
+
<div className={cn('h-1', statusStripeColor[status])} />
|
|
48
|
+
|
|
49
|
+
<div className="p-4">
|
|
50
|
+
<div className="flex items-start justify-between gap-2">
|
|
51
|
+
<h3 className="text-sm font-medium text-neutral-200 truncate">{title}</h3>
|
|
52
|
+
<Label
|
|
53
|
+
text={statusLabelConfig[status].text}
|
|
54
|
+
color={statusLabelConfig[status].color}
|
|
55
|
+
icon={statusLabelConfig[status].icon}
|
|
56
|
+
tooltip={{ description: statusLabelConfig[status].tooltip }}
|
|
57
|
+
size="xs"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{timestamp && (
|
|
62
|
+
<p className="mt-1 text-[10px] text-neutral-500">{timestamp}</p>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{description && (
|
|
66
|
+
<p className="mt-2 text-xs text-neutral-500 leading-relaxed line-clamp-2">{description}</p>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{stats && stats.length > 0 && (
|
|
70
|
+
<div className="mt-3 grid grid-cols-2 gap-x-4 gap-y-2">
|
|
71
|
+
{stats.map((stat) => (
|
|
72
|
+
<div key={stat.label}>
|
|
73
|
+
<p className="text-[10px] text-neutral-500">{stat.label}</p>
|
|
74
|
+
<p className="text-xs font-medium text-neutral-200">{stat.value}</p>
|
|
75
|
+
</div>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{(onSync || onView) && (
|
|
81
|
+
<div className="mt-3 pt-3 border-t border-neutral-700 flex items-center gap-2">
|
|
82
|
+
{onSync && (
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
onClick={onSync}
|
|
86
|
+
className="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-md bg-blue-400/15 text-blue-400 hover:bg-blue-400/25 transition-colors cursor-pointer"
|
|
87
|
+
>
|
|
88
|
+
<RefreshCw className={cn('w-3 h-3', status === 'pending' && 'animate-spin')} />
|
|
89
|
+
Sync
|
|
90
|
+
</button>
|
|
91
|
+
)}
|
|
92
|
+
{onView && (
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={onView}
|
|
96
|
+
className="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-md text-neutral-400 hover:bg-neutral-700 transition-colors cursor-pointer"
|
|
97
|
+
>
|
|
98
|
+
<Eye className="w-3 h-3" />
|
|
99
|
+
View
|
|
100
|
+
</button>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/** Searchable panel for browsing, copying, and inserting code snippets. */
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react'
|
|
4
|
+
import { Search } from 'lucide-react'
|
|
5
|
+
import { cn } from '../lib/cn.ts'
|
|
6
|
+
import { IconButton } from './icon-button.tsx'
|
|
7
|
+
|
|
8
|
+
export interface Snippet {
|
|
9
|
+
id: string
|
|
10
|
+
label: string
|
|
11
|
+
language?: string
|
|
12
|
+
description?: string
|
|
13
|
+
code: string
|
|
14
|
+
tags?: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SnippetsPanelProps {
|
|
18
|
+
snippets: Snippet[]
|
|
19
|
+
onInsert?: (id: string) => void
|
|
20
|
+
onCopy?: (id: string) => void
|
|
21
|
+
showSearch?: boolean
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const LANGUAGE_COLORS: Record<string, string> = {
|
|
26
|
+
typescript: '#3178c6',
|
|
27
|
+
javascript: '#f7df1e',
|
|
28
|
+
tsx: '#3178c6',
|
|
29
|
+
jsx: '#f7df1e',
|
|
30
|
+
css: '#264de4',
|
|
31
|
+
html: '#e34c26',
|
|
32
|
+
json: '#f9e2af',
|
|
33
|
+
python: '#3572a5',
|
|
34
|
+
rust: '#dea584',
|
|
35
|
+
go: '#00add8',
|
|
36
|
+
bash: 'blue-400',
|
|
37
|
+
shell: 'blue-400',
|
|
38
|
+
sql: '#e38c00',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function matchesSearch(snippet: Snippet, query: string): boolean {
|
|
42
|
+
if (snippet.label.toLowerCase().includes(query)) return true
|
|
43
|
+
if (snippet.description?.toLowerCase().includes(query)) return true
|
|
44
|
+
if (snippet.language?.toLowerCase().includes(query)) return true
|
|
45
|
+
if (snippet.tags?.some((t) => t.toLowerCase().includes(query))) return true
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function SnippetsPanel({
|
|
50
|
+
snippets,
|
|
51
|
+
onInsert,
|
|
52
|
+
onCopy,
|
|
53
|
+
showSearch = false,
|
|
54
|
+
className,
|
|
55
|
+
}: SnippetsPanelProps) {
|
|
56
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
57
|
+
|
|
58
|
+
const filteredSnippets = useMemo(
|
|
59
|
+
() => searchQuery ? snippets.filter((s) => matchesSearch(s, searchQuery.toLowerCase())) : snippets,
|
|
60
|
+
[snippets, searchQuery],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className={cn('flex flex-col bg-neutral-800 rounded-lg overflow-hidden', className)}>
|
|
65
|
+
<div className="flex items-center justify-between px-3 py-2 border-b border-neutral-700">
|
|
66
|
+
<span className="text-[11px] font-semibold uppercase tracking-wider text-neutral-500">Snippets</span>
|
|
67
|
+
<span className="text-[10px] text-neutral-500">{snippets.length} snippets</span>
|
|
68
|
+
</div>
|
|
69
|
+
{showSearch && (
|
|
70
|
+
<div className="px-2 py-2 border-b border-neutral-700">
|
|
71
|
+
<div className="flex items-center gap-1.5 px-2 py-1 bg-black border border-neutral-700 rounded text-xs">
|
|
72
|
+
<Search className="w-3 h-3 text-neutral-500 shrink-0" />
|
|
73
|
+
<input
|
|
74
|
+
type="text"
|
|
75
|
+
placeholder="Search snippets..."
|
|
76
|
+
value={searchQuery}
|
|
77
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
78
|
+
className="flex-1 bg-transparent text-neutral-200 placeholder-neutral-500 outline-none text-xs"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
<div className="flex-1 overflow-y-auto p-2 space-y-2">
|
|
84
|
+
{filteredSnippets.map((snippet: Snippet) => (
|
|
85
|
+
<SnippetCard
|
|
86
|
+
key={snippet.id}
|
|
87
|
+
snippet={snippet}
|
|
88
|
+
onInsert={onInsert}
|
|
89
|
+
onCopy={onCopy}
|
|
90
|
+
/>
|
|
91
|
+
))}
|
|
92
|
+
{filteredSnippets.length === 0 && (
|
|
93
|
+
<p className="text-[11px] text-neutral-500 text-center py-4">No snippets found</p>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface SnippetCardProps {
|
|
101
|
+
snippet: Snippet
|
|
102
|
+
onInsert?: (id: string) => void
|
|
103
|
+
onCopy?: (id: string) => void
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function SnippetCard({ snippet, onInsert, onCopy }: SnippetCardProps) {
|
|
107
|
+
const langColor = snippet.language ? LANGUAGE_COLORS[snippet.language.toLowerCase()] ?? '#6b7280' : '#6b7280'
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="rounded-md border border-neutral-700 bg-neutral-900 hover:border-neutral-600 transition-colors">
|
|
111
|
+
<div className="flex items-center justify-between gap-2 px-2.5 py-1.5">
|
|
112
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
113
|
+
<span className="text-xs font-medium text-neutral-200 truncate">{snippet.label}</span>
|
|
114
|
+
{snippet.language && (
|
|
115
|
+
<span
|
|
116
|
+
className="px-1.5 py-0.5 text-[9px] font-medium rounded shrink-0"
|
|
117
|
+
style={{ color: langColor, backgroundColor: `${langColor}20` }}
|
|
118
|
+
>
|
|
119
|
+
{snippet.language}
|
|
120
|
+
</span>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
<div className="flex items-center gap-0.5 shrink-0">
|
|
124
|
+
{onInsert && (
|
|
125
|
+
<IconButton
|
|
126
|
+
icon="arrow-down-to-line"
|
|
127
|
+
onClick={() => onInsert(snippet.id)}
|
|
128
|
+
size="xss"
|
|
129
|
+
tooltip={{ title: 'Insert snippet', description: 'Insert this snippet' }}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
{onCopy && (
|
|
133
|
+
<IconButton
|
|
134
|
+
icon="copy"
|
|
135
|
+
onClick={() => onCopy(snippet.id)}
|
|
136
|
+
size="xss"
|
|
137
|
+
tooltip={{ title: 'Copy snippet', description: 'Copy to clipboard' }}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
{snippet.description && (
|
|
143
|
+
<p className="px-2.5 pb-1.5 text-[11px] text-neutral-400 leading-relaxed">{snippet.description}</p>
|
|
144
|
+
)}
|
|
145
|
+
<div className="mx-2.5 mb-2 rounded bg-black border border-neutral-700 overflow-hidden">
|
|
146
|
+
<pre className="p-2 text-[11px] text-neutral-400 leading-relaxed overflow-x-auto max-h-24">
|
|
147
|
+
<code>{snippet.code}</code>
|
|
148
|
+
</pre>
|
|
149
|
+
</div>
|
|
150
|
+
{snippet.tags && snippet.tags.length > 0 && (
|
|
151
|
+
<div className="flex flex-wrap gap-1 px-2.5 pb-2">
|
|
152
|
+
{snippet.tags.map((tag) => (
|
|
153
|
+
<span key={tag} className="px-1.5 py-0.5 text-[9px] rounded bg-neutral-700 text-neutral-500">
|
|
154
|
+
{tag}
|
|
155
|
+
</span>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react'
|
|
2
|
+
import { ArrowUp, ArrowDown, ChevronDown, Check } from 'lucide-react'
|
|
3
|
+
import { useClickOutside } from '../hooks/use-click-outside.ts'
|
|
4
|
+
import { useDropdownMaxHeight } from '../hooks/use-dropdown-max-height.ts'
|
|
5
|
+
import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
|
|
6
|
+
|
|
7
|
+
const VARIANT_CLASSES = {
|
|
8
|
+
filled: { bg: 'bg-neutral-800', hoverBg: 'hover:bg-neutral-700' },
|
|
9
|
+
outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-800' },
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SortField {
|
|
13
|
+
value: string
|
|
14
|
+
label: string
|
|
15
|
+
/** Label suffix when ascending (default: "↑") */
|
|
16
|
+
ascLabel?: string
|
|
17
|
+
/** Label suffix when descending (default: "↓") */
|
|
18
|
+
descLabel?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SortDropdownProps {
|
|
22
|
+
field: string
|
|
23
|
+
ascending: boolean
|
|
24
|
+
onFieldChange: (field: string) => void
|
|
25
|
+
onToggleDirection: () => void
|
|
26
|
+
fields: SortField[]
|
|
27
|
+
variant?: 'filled' | 'outline'
|
|
28
|
+
color?: FormColor
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function SortDropdown({
|
|
32
|
+
field,
|
|
33
|
+
ascending,
|
|
34
|
+
onFieldChange,
|
|
35
|
+
onToggleDirection,
|
|
36
|
+
fields,
|
|
37
|
+
variant = 'outline',
|
|
38
|
+
color = 'blue',
|
|
39
|
+
}: SortDropdownProps) {
|
|
40
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
41
|
+
const [highlightIdx, setHighlightIdx] = useState(-1)
|
|
42
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
43
|
+
const menuRef = useDropdownMaxHeight<HTMLDivElement>(isOpen)
|
|
44
|
+
const v = VARIANT_CLASSES[variant]
|
|
45
|
+
|
|
46
|
+
useClickOutside(ref, isOpen, () => setIsOpen(false))
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (isOpen) setHighlightIdx(fields.findIndex((f) => f.value === field))
|
|
50
|
+
}, [isOpen, fields, field])
|
|
51
|
+
|
|
52
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
53
|
+
if (!isOpen) return
|
|
54
|
+
if (e.key === 'ArrowDown') {
|
|
55
|
+
e.preventDefault()
|
|
56
|
+
setHighlightIdx((i) => Math.min(i + 1, fields.length - 1))
|
|
57
|
+
} else if (e.key === 'ArrowUp') {
|
|
58
|
+
e.preventDefault()
|
|
59
|
+
setHighlightIdx((i) => Math.max(i - 1, 0))
|
|
60
|
+
} else if (e.key === 'Enter' && highlightIdx >= 0) {
|
|
61
|
+
e.preventDefault()
|
|
62
|
+
onFieldChange(fields[highlightIdx].value)
|
|
63
|
+
setIsOpen(false)
|
|
64
|
+
} else if (e.key === 'Escape') {
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
setIsOpen(false)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const current = fields.find((f) => f.value === field) ?? fields[0]
|
|
71
|
+
const DirIcon = ascending ? ArrowUp : ArrowDown
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="relative flex items-center" ref={ref} onKeyDown={handleKeyDown}>
|
|
75
|
+
<button
|
|
76
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
77
|
+
className={`flex items-center gap-1.5 h-7 px-2 rounded-md border ${v.bg} text-xs transition-colors cursor-pointer ${FORM_COLORS[color].border} text-neutral-200 ${FORM_COLORS[color].hover}`}
|
|
78
|
+
>
|
|
79
|
+
<span
|
|
80
|
+
className={`${FORM_COLORS[color].accent} hover:brightness-125 transition-colors`}
|
|
81
|
+
onClick={(e) => { e.stopPropagation(); onToggleDirection() }}
|
|
82
|
+
role="button"
|
|
83
|
+
>
|
|
84
|
+
<DirIcon className="w-3 h-3" />
|
|
85
|
+
</span>
|
|
86
|
+
<span className="whitespace-nowrap">{current.label}</span>
|
|
87
|
+
<ChevronDown className={`w-3 h-3 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
|
88
|
+
</button>
|
|
89
|
+
|
|
90
|
+
{isOpen && (
|
|
91
|
+
<div ref={menuRef} className={`absolute right-0 top-full z-50 mt-1 min-w-[140px] ${v.bg} border ${FORM_COLORS[color].border} rounded-lg shadow-xl overflow-hidden`}>
|
|
92
|
+
{fields.map((f, idx) => (
|
|
93
|
+
<button
|
|
94
|
+
key={f.value}
|
|
95
|
+
onClick={() => { onFieldChange(f.value); setIsOpen(false) }}
|
|
96
|
+
onPointerEnter={() => setHighlightIdx(idx)}
|
|
97
|
+
className={`w-full flex items-center gap-2 px-3 py-1.5 text-xs text-left transition-colors cursor-pointer ${
|
|
98
|
+
idx === highlightIdx ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : field === f.value ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
|
|
99
|
+
}`}
|
|
100
|
+
>
|
|
101
|
+
<Check className={`w-3 h-3 shrink-0 ${field === f.value ? FORM_COLORS[color].accent : 'invisible'}`} />
|
|
102
|
+
<span>{f.label}</span>
|
|
103
|
+
</button>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Status card displaying a list of labeled items with color-coded status indicators. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Settings, Shield, Database, Globe, Zap, Code, Terminal,
|
|
5
|
+
Star, Cloud, Bell, Heart, Sparkles, Bot, Plug,
|
|
6
|
+
} from 'lucide-react'
|
|
7
|
+
import type { LucideIcon } from 'lucide-react'
|
|
8
|
+
import type { IconName } from './icon-button.tsx'
|
|
9
|
+
import { cn } from '../lib/cn.ts'
|
|
10
|
+
|
|
11
|
+
const iconSubset: Partial<Record<IconName, LucideIcon>> = {
|
|
12
|
+
settings: Settings, shield: Shield, database: Database, globe: Globe,
|
|
13
|
+
zap: Zap, code: Code, terminal: Terminal, star: Star, cloud: Cloud,
|
|
14
|
+
bell: Bell, heart: Heart, sparkles: Sparkles, bot: Bot, plug: Plug,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type StatusType = 'success' | 'warning' | 'error' | 'info' | 'neutral'
|
|
18
|
+
|
|
19
|
+
export interface StatusItem {
|
|
20
|
+
label: string
|
|
21
|
+
value: string | number
|
|
22
|
+
status: StatusType
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StatusCardProps {
|
|
26
|
+
title: string
|
|
27
|
+
icon?: IconName
|
|
28
|
+
iconColor?: string
|
|
29
|
+
items: StatusItem[]
|
|
30
|
+
action?: { label: string; onClick: () => void }
|
|
31
|
+
className?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const statusDotColor: Record<StatusType, string> = {
|
|
35
|
+
success: 'bg-green-400',
|
|
36
|
+
warning: 'bg-amber-400',
|
|
37
|
+
error: 'bg-red-400',
|
|
38
|
+
info: 'bg-blue-400',
|
|
39
|
+
neutral: 'bg-neutral-500',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const statusValueColor: Record<StatusType, string> = {
|
|
43
|
+
success: 'text-green-400',
|
|
44
|
+
warning: 'text-amber-400',
|
|
45
|
+
error: 'text-red-400',
|
|
46
|
+
info: 'text-blue-400',
|
|
47
|
+
neutral: 'text-neutral-400',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function StatusCard({
|
|
51
|
+
title,
|
|
52
|
+
icon,
|
|
53
|
+
iconColor = 'blue-400',
|
|
54
|
+
items,
|
|
55
|
+
action,
|
|
56
|
+
className,
|
|
57
|
+
}: StatusCardProps) {
|
|
58
|
+
const Icon = icon ? iconSubset[icon] : undefined
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className={cn('rounded-lg border border-neutral-700 bg-neutral-800 overflow-hidden', className)}>
|
|
62
|
+
<div className="flex items-center gap-2.5 px-4 py-3 border-b border-neutral-700">
|
|
63
|
+
{Icon && (
|
|
64
|
+
<Icon className="w-4 h-4 shrink-0" style={{ color: iconColor }} />
|
|
65
|
+
)}
|
|
66
|
+
<h3 className="text-sm font-medium text-neutral-200">{title}</h3>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="divide-y divide-[#374151]/60">
|
|
70
|
+
{items.map((item) => (
|
|
71
|
+
<div key={item.label} className="flex items-center justify-between px-4 py-2.5">
|
|
72
|
+
<span className="text-xs text-neutral-400">{item.label}</span>
|
|
73
|
+
<div className="flex items-center gap-2">
|
|
74
|
+
<span className={cn('text-xs font-medium', statusValueColor[item.status])}>
|
|
75
|
+
{item.value}
|
|
76
|
+
</span>
|
|
77
|
+
<span className={cn('w-2 h-2 rounded-full shrink-0', statusDotColor[item.status])} />
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{action && (
|
|
84
|
+
<div className="px-4 py-2.5 border-t border-neutral-700">
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
onClick={action.onClick}
|
|
88
|
+
className="text-xs text-blue-400 hover:text-blue-300 transition-colors cursor-pointer"
|
|
89
|
+
>
|
|
90
|
+
{action.label}
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|