@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,224 @@
|
|
|
1
|
+
import { type ReactNode, useState, useRef, useEffect, useCallback } from 'react'
|
|
2
|
+
import { ChevronsUpDown, ChevronsDownUp } from 'lucide-react'
|
|
3
|
+
import { IconButton } from './icon-button.tsx'
|
|
4
|
+
import { Label, type LabelProps } from './label.tsx'
|
|
5
|
+
import { AiToolIcon, AI_TOOL_NAMES, type AiToolKey } from '../lib/ai-tools.tsx'
|
|
6
|
+
import { FileStructureSection, type FileStructureSectionProps } from './file-structure-section.tsx'
|
|
7
|
+
import type { LucideIcon } from 'lucide-react'
|
|
8
|
+
|
|
9
|
+
export interface RegistryDetailProps {
|
|
10
|
+
// Header
|
|
11
|
+
icon: LucideIcon
|
|
12
|
+
iconColor: string
|
|
13
|
+
title: string
|
|
14
|
+
titleExtra?: ReactNode
|
|
15
|
+
subtitle: ReactNode
|
|
16
|
+
|
|
17
|
+
// Action button area
|
|
18
|
+
actionButton?: ReactNode
|
|
19
|
+
|
|
20
|
+
// Labels (rendered in title row after title)
|
|
21
|
+
labels?: LabelProps[]
|
|
22
|
+
|
|
23
|
+
// Standard sections (rendered in order before children)
|
|
24
|
+
description?: string
|
|
25
|
+
longDescription?: string
|
|
26
|
+
integration?: boolean
|
|
27
|
+
compatibleTools?: string[]
|
|
28
|
+
|
|
29
|
+
// File structure (rendered when files + onFetchContent are provided)
|
|
30
|
+
files?: FileStructureSectionProps['files']
|
|
31
|
+
rootName?: string
|
|
32
|
+
onFetchContent?: FileStructureSectionProps['onFetchContent']
|
|
33
|
+
|
|
34
|
+
// Above header (e.g. badge row)
|
|
35
|
+
aboveHeader?: ReactNode
|
|
36
|
+
|
|
37
|
+
// Max width class
|
|
38
|
+
maxWidth?: string
|
|
39
|
+
|
|
40
|
+
// Custom body sections
|
|
41
|
+
children?: ReactNode
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const MARKDOWN_CLASSES = 'text-sm text-neutral-400 leading-relaxed [&_strong]:text-neutral-200 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:bg-neutral-700/40 [&_code]:border [&_code]:border-neutral-500/40 [&_code]:text-neutral-200 [&_code]:font-mono [&_code]:text-xs [&_h1]:text-lg [&_h1]:font-semibold [&_h1]:text-neutral-200 [&_h1]:mb-2 [&_h2]:text-base [&_h2]:font-semibold [&_h2]:text-neutral-200 [&_h2]:mb-2 [&_h3]:text-sm [&_h3]:font-medium [&_h3]:text-neutral-200 [&_h3]:mb-1 [&_ul]:list-disc [&_ul]:pl-4 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:mb-1 [&_p]:mb-2 [&_pre]:bg-neutral-900 [&_pre]:rounded [&_pre]:p-3 [&_pre]:overflow-x-auto [&_pre]:text-xs'
|
|
45
|
+
|
|
46
|
+
// ── CollapsibleSection ────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const COLLAPSED_MAX_HEIGHT = 240
|
|
49
|
+
|
|
50
|
+
function CollapsibleTextSection({ children, header }: { children: string; header?: string }) {
|
|
51
|
+
const contentRef = useRef<HTMLDivElement>(null)
|
|
52
|
+
const [overflows, setOverflows] = useState(false)
|
|
53
|
+
const [expanded, setExpanded] = useState(false)
|
|
54
|
+
|
|
55
|
+
const measure = useCallback(() => {
|
|
56
|
+
const el = contentRef.current
|
|
57
|
+
if (!el) return
|
|
58
|
+
setOverflows(el.scrollHeight > COLLAPSED_MAX_HEIGHT + 8)
|
|
59
|
+
}, [])
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
measure()
|
|
63
|
+
}, [children, measure])
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const el = contentRef.current
|
|
67
|
+
if (!el) return
|
|
68
|
+
const observer = new ResizeObserver(measure)
|
|
69
|
+
observer.observe(el)
|
|
70
|
+
return () => observer.disconnect()
|
|
71
|
+
}, [measure])
|
|
72
|
+
|
|
73
|
+
const showCollapsed = overflows && !expanded
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
{(header || overflows) && (
|
|
78
|
+
<div className="flex items-center justify-between mb-2">
|
|
79
|
+
{header && (
|
|
80
|
+
<h3 className="text-xs font-medium text-neutral-500 uppercase tracking-wider">{header}</h3>
|
|
81
|
+
)}
|
|
82
|
+
{overflows && (
|
|
83
|
+
<IconButton
|
|
84
|
+
icon={expanded
|
|
85
|
+
? <ChevronsDownUp className="w-3.5 h-3.5" />
|
|
86
|
+
: <ChevronsUpDown className="w-3.5 h-3.5" />}
|
|
87
|
+
onClick={() => setExpanded(!expanded)}
|
|
88
|
+
size="sm"
|
|
89
|
+
tooltip={{ description: expanded ? 'Show less content' : 'Show full content' }}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
<div className="relative">
|
|
96
|
+
<div
|
|
97
|
+
ref={contentRef}
|
|
98
|
+
className={MARKDOWN_CLASSES}
|
|
99
|
+
style={showCollapsed ? { maxHeight: COLLAPSED_MAX_HEIGHT, overflow: 'hidden' } : undefined}
|
|
100
|
+
>
|
|
101
|
+
<p>{children}</p>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{showCollapsed && (
|
|
105
|
+
<div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-t from-neutral-800 to-transparent pointer-events-none" />
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── CompatibleWithSection ─────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
function CompatibleWithSection({ tools }: { tools: string[] }) {
|
|
115
|
+
if (tools.length === 0) return null
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div>
|
|
119
|
+
<h3 className="text-xs font-medium text-neutral-500 uppercase tracking-wider mb-3">Compatible with</h3>
|
|
120
|
+
<div className="flex items-start gap-3">
|
|
121
|
+
{tools.map((tool) => (
|
|
122
|
+
<div key={tool} className="flex flex-col items-center gap-1">
|
|
123
|
+
<AiToolIcon tool={tool} size={18} />
|
|
124
|
+
<span className="text-[10px] text-neutral-400">{AI_TOOL_NAMES[tool as AiToolKey] ?? tool}</span>
|
|
125
|
+
</div>
|
|
126
|
+
))}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Main Component ────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
export function RegistryDetail({
|
|
135
|
+
icon: Icon,
|
|
136
|
+
iconColor,
|
|
137
|
+
title,
|
|
138
|
+
titleExtra,
|
|
139
|
+
subtitle,
|
|
140
|
+
actionButton,
|
|
141
|
+
labels,
|
|
142
|
+
description,
|
|
143
|
+
longDescription,
|
|
144
|
+
integration,
|
|
145
|
+
compatibleTools,
|
|
146
|
+
files,
|
|
147
|
+
rootName,
|
|
148
|
+
onFetchContent,
|
|
149
|
+
aboveHeader,
|
|
150
|
+
maxWidth = 'max-w-[1440px]',
|
|
151
|
+
children,
|
|
152
|
+
}: RegistryDetailProps) {
|
|
153
|
+
return (
|
|
154
|
+
<div className="h-full overflow-y-auto p-6">
|
|
155
|
+
<div className={`${maxWidth} mx-auto space-y-6`}>
|
|
156
|
+
{/* Above header badges */}
|
|
157
|
+
{aboveHeader}
|
|
158
|
+
{/* Header row */}
|
|
159
|
+
<div className="flex items-start justify-between gap-4">
|
|
160
|
+
<div className="min-w-0">
|
|
161
|
+
<div className="flex items-center gap-3 flex-wrap">
|
|
162
|
+
<Icon className={`w-6 h-6 shrink-0 ${iconColor}`} />
|
|
163
|
+
<h2 className="text-xl font-semibold text-neutral-200">{title}</h2>
|
|
164
|
+
{labels && labels.length > 0 && labels.map((labelProps, i) => (
|
|
165
|
+
<Label key={i} size="lg" {...labelProps} />
|
|
166
|
+
))}
|
|
167
|
+
{titleExtra}
|
|
168
|
+
</div>
|
|
169
|
+
<div className="mt-1">{subtitle}</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{actionButton && (
|
|
173
|
+
<div className="shrink-0">
|
|
174
|
+
{actionButton}
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Description */}
|
|
180
|
+
{description && (
|
|
181
|
+
<div>
|
|
182
|
+
<h3 className="text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">Description</h3>
|
|
183
|
+
<p className="text-sm text-neutral-400 leading-relaxed">{description}</p>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{/* Long description (TL;DR) */}
|
|
188
|
+
{longDescription && (
|
|
189
|
+
<CollapsibleTextSection header="TL;DR">{longDescription}</CollapsibleTextSection>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
{/* Integration explanation */}
|
|
193
|
+
{integration && (
|
|
194
|
+
<CollapsibleTextSection header="Info">
|
|
195
|
+
{[
|
|
196
|
+
'This plugin contains no source files \u2014 only a README with setup instructions for an external tool you need to install on your machine (e.g. via brew or npm).',
|
|
197
|
+
'',
|
|
198
|
+
'Why install it then? Installing the plugin adds it to enabledPlugins in your settings. That entry is the signal the AI tool needs \u2014 without it, the AI tool has no idea the external tool exists, even if it\'s already on your machine. With it enabled, the AI tool will automatically find and use the external tool for code intelligence (go-to-definition, type checking, etc.).',
|
|
199
|
+
'',
|
|
200
|
+
'In short: the README tells you what to install locally, and the plugin setting tells the AI tool to use it.',
|
|
201
|
+
].join('\n')}
|
|
202
|
+
</CollapsibleTextSection>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
{/* Compatible with */}
|
|
206
|
+
{compatibleTools && compatibleTools.length > 0 && (
|
|
207
|
+
<CompatibleWithSection tools={compatibleTools} />
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{/* File structure */}
|
|
211
|
+
{files && onFetchContent && rootName && (
|
|
212
|
+
<FileStructureSection
|
|
213
|
+
files={files}
|
|
214
|
+
rootName={rootName}
|
|
215
|
+
onFetchContent={onFetchContent}
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
{/* Custom content */}
|
|
220
|
+
{children}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react'
|
|
2
|
+
import Editor from '@monaco-editor/react'
|
|
3
|
+
import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
|
|
4
|
+
|
|
5
|
+
const variantClasses = {
|
|
6
|
+
filled: 'bg-neutral-800 border text-neutral-200 placeholder-neutral-500',
|
|
7
|
+
outline: 'bg-transparent border text-neutral-200 placeholder-neutral-500',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ResizableTextareaBaseProps {
|
|
11
|
+
wrapperClassName?: string
|
|
12
|
+
resizable?: boolean
|
|
13
|
+
variant?: 'filled' | 'outline'
|
|
14
|
+
color?: FormColor
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ResizableTextareaFieldProps
|
|
18
|
+
extends ResizableTextareaBaseProps,
|
|
19
|
+
Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'color'> {
|
|
20
|
+
mode?: never
|
|
21
|
+
language?: never
|
|
22
|
+
children?: never
|
|
23
|
+
minHeight?: never
|
|
24
|
+
onHeightChange?: never
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ResizableTextareaCodeProps extends ResizableTextareaBaseProps {
|
|
28
|
+
mode: 'code'
|
|
29
|
+
language?: string
|
|
30
|
+
value?: string
|
|
31
|
+
onChange?: (value: string) => void
|
|
32
|
+
readOnly?: boolean
|
|
33
|
+
minHeight?: number
|
|
34
|
+
onHeightChange?: (height: number) => void
|
|
35
|
+
children?: never
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ResizableTextareaChildrenProps extends ResizableTextareaBaseProps {
|
|
39
|
+
mode?: never
|
|
40
|
+
language?: never
|
|
41
|
+
children: React.ReactNode
|
|
42
|
+
minHeight?: number
|
|
43
|
+
onHeightChange?: (height: number) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ResizableTextareaProps =
|
|
47
|
+
| ResizableTextareaFieldProps
|
|
48
|
+
| ResizableTextareaCodeProps
|
|
49
|
+
| ResizableTextareaChildrenProps
|
|
50
|
+
|
|
51
|
+
function isCodeMode(props: ResizableTextareaProps): props is ResizableTextareaCodeProps {
|
|
52
|
+
return 'mode' in props && props.mode === 'code'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isChildrenVariant(props: ResizableTextareaProps): props is ResizableTextareaChildrenProps {
|
|
56
|
+
return 'children' in props && props.children !== undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ResizeSvg = () => (
|
|
60
|
+
<svg className="w-2.5 h-2.5 text-neutral-600 pointer-events-none" viewBox="0 0 10 10">
|
|
61
|
+
<line x1="9" y1="1" x2="1" y2="9" stroke="currentColor" strokeWidth="1" />
|
|
62
|
+
<line x1="9" y1="4.5" x2="4.5" y2="9" stroke="currentColor" strokeWidth="1" />
|
|
63
|
+
</svg>
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
export function ResizableTextarea(props: ResizableTextareaProps) {
|
|
67
|
+
if (isCodeMode(props)) {
|
|
68
|
+
return <ResizableCode {...props} />
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (isChildrenVariant(props)) {
|
|
72
|
+
return <ResizableChildren {...props} />
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { wrapperClassName, resizable = true, variant, color, ...rest } = props
|
|
76
|
+
return <ResizableField wrapperClassName={wrapperClassName} resizable={resizable} variant={variant} color={color} {...rest} />
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Shared resize hook
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
function useResize(minHeight: number, onHeightChange?: (height: number) => void) {
|
|
84
|
+
const [height, setHeight] = useState<number | null>(null)
|
|
85
|
+
const dragRef = useRef<{ startY: number; startH: number } | null>(null)
|
|
86
|
+
|
|
87
|
+
const onResizeStart = useCallback(
|
|
88
|
+
(e: React.MouseEvent) => {
|
|
89
|
+
e.preventDefault()
|
|
90
|
+
const wrapper = (e.target as HTMLElement).closest('[data-resizable-wrapper]') as HTMLElement
|
|
91
|
+
if (!wrapper) return
|
|
92
|
+
const startH = wrapper.offsetHeight
|
|
93
|
+
dragRef.current = { startY: e.clientY, startH }
|
|
94
|
+
const onMove = (ev: MouseEvent) => {
|
|
95
|
+
if (!dragRef.current) return
|
|
96
|
+
const newH = Math.max(minHeight, dragRef.current.startH + ev.clientY - dragRef.current.startY)
|
|
97
|
+
setHeight(newH)
|
|
98
|
+
onHeightChange?.(newH)
|
|
99
|
+
}
|
|
100
|
+
const onUp = () => {
|
|
101
|
+
dragRef.current = null
|
|
102
|
+
document.removeEventListener('mousemove', onMove)
|
|
103
|
+
document.removeEventListener('mouseup', onUp)
|
|
104
|
+
}
|
|
105
|
+
document.addEventListener('mousemove', onMove)
|
|
106
|
+
document.addEventListener('mouseup', onUp)
|
|
107
|
+
},
|
|
108
|
+
[minHeight, onHeightChange],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return { height, onResizeStart }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Code variant — Monaco editor with resize handle
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
const MONACO_THEME = 'resizable-textarea-dark'
|
|
119
|
+
let themeRegistered = false
|
|
120
|
+
|
|
121
|
+
const codeWrapperClasses = {
|
|
122
|
+
filled: 'bg-neutral-800 border rounded-lg overflow-hidden',
|
|
123
|
+
outline: 'bg-transparent border rounded-lg overflow-hidden',
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function ResizableCode({
|
|
127
|
+
value = '',
|
|
128
|
+
onChange,
|
|
129
|
+
language = 'plaintext',
|
|
130
|
+
readOnly = false,
|
|
131
|
+
variant = 'outline',
|
|
132
|
+
color = 'blue',
|
|
133
|
+
resizable = true,
|
|
134
|
+
wrapperClassName,
|
|
135
|
+
minHeight = 80,
|
|
136
|
+
onHeightChange,
|
|
137
|
+
}: ResizableTextareaCodeProps) {
|
|
138
|
+
const { height, onResizeStart } = useResize(minHeight, onHeightChange)
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
className={`relative ${codeWrapperClasses[variant]} ${FORM_COLORS[color].border} ${wrapperClassName ?? ''}`}
|
|
143
|
+
data-resizable-wrapper
|
|
144
|
+
style={{ height: height ?? minHeight }}
|
|
145
|
+
>
|
|
146
|
+
<Editor
|
|
147
|
+
height="100%"
|
|
148
|
+
language={language}
|
|
149
|
+
value={value}
|
|
150
|
+
onChange={(v) => onChange?.(v ?? '')}
|
|
151
|
+
theme={MONACO_THEME}
|
|
152
|
+
options={{
|
|
153
|
+
minimap: { enabled: false },
|
|
154
|
+
fontSize: 13,
|
|
155
|
+
lineNumbers: 'off',
|
|
156
|
+
wordWrap: 'on',
|
|
157
|
+
scrollBeyondLastLine: false,
|
|
158
|
+
automaticLayout: true,
|
|
159
|
+
folding: false,
|
|
160
|
+
lineDecorationsWidth: 8,
|
|
161
|
+
renderLineHighlight: 'none',
|
|
162
|
+
scrollbar: { vertical: 'auto', horizontal: 'hidden', verticalScrollbarSize: 8 },
|
|
163
|
+
contextmenu: false,
|
|
164
|
+
tabSize: 2,
|
|
165
|
+
padding: { top: 8, bottom: 8 },
|
|
166
|
+
readOnly,
|
|
167
|
+
overviewRulerLanes: 0,
|
|
168
|
+
hideCursorInOverviewRuler: true,
|
|
169
|
+
overviewRulerBorder: false,
|
|
170
|
+
glyphMargin: false,
|
|
171
|
+
}}
|
|
172
|
+
beforeMount={(monaco) => {
|
|
173
|
+
if (!themeRegistered) {
|
|
174
|
+
monaco.editor.defineTheme(MONACO_THEME, {
|
|
175
|
+
base: 'vs-dark',
|
|
176
|
+
inherit: true,
|
|
177
|
+
rules: [],
|
|
178
|
+
colors: {
|
|
179
|
+
'editor.background': variant === 'filled' ? '#1f2937' : '#00000000',
|
|
180
|
+
'editorGutter.background': variant === 'filled' ? '#1f2937' : '#00000000',
|
|
181
|
+
'editor.lineHighlightBackground': '#00000000',
|
|
182
|
+
'editor.lineHighlightBorder': '#00000000',
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
themeRegistered = true
|
|
186
|
+
}
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
189
|
+
{resizable && (
|
|
190
|
+
<div
|
|
191
|
+
className="absolute bottom-[4px] right-[3px] w-4 h-3 cursor-row-resize flex items-end justify-end z-10"
|
|
192
|
+
onMouseDown={onResizeStart}
|
|
193
|
+
>
|
|
194
|
+
<ResizeSvg />
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Children variant — wraps any element with resize handle
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
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
|
+
function ResizableChildren({
|
|
211
|
+
children,
|
|
212
|
+
wrapperClassName,
|
|
213
|
+
resizable = true,
|
|
214
|
+
variant = 'outline',
|
|
215
|
+
color = 'blue',
|
|
216
|
+
minHeight = 40,
|
|
217
|
+
onHeightChange,
|
|
218
|
+
}: ResizableTextareaChildrenProps) {
|
|
219
|
+
const { height, onResizeStart } = useResize(minHeight, onHeightChange)
|
|
220
|
+
|
|
221
|
+
if (!resizable) {
|
|
222
|
+
return <>{children}</>
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div
|
|
227
|
+
className={`relative ${childrenVariantClasses[variant]} ${FORM_COLORS[color].border} ${wrapperClassName ?? ''}`}
|
|
228
|
+
data-resizable-wrapper
|
|
229
|
+
style={height != null ? { height } : undefined}
|
|
230
|
+
>
|
|
231
|
+
{children}
|
|
232
|
+
<div
|
|
233
|
+
className="absolute bottom-[4px] right-[3px] w-4 h-3 cursor-row-resize flex items-end justify-end"
|
|
234
|
+
onMouseDown={onResizeStart}
|
|
235
|
+
>
|
|
236
|
+
<ResizeSvg />
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Field variant — original textarea with resize handle
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
function ResizableField({
|
|
247
|
+
wrapperClassName,
|
|
248
|
+
resizable = true,
|
|
249
|
+
variant = 'outline',
|
|
250
|
+
color = 'blue',
|
|
251
|
+
...props
|
|
252
|
+
}: ResizableTextareaBaseProps & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'color'>) {
|
|
253
|
+
const taRef = useRef<HTMLTextAreaElement>(null)
|
|
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
|
+
}, [])
|
|
272
|
+
|
|
273
|
+
const className = `w-full rounded-lg focus:outline-none transition-colors ${variantClasses[variant]} ${FORM_COLORS[color].border} ${FORM_COLORS[color].focus} ${props.className ?? ''}`
|
|
274
|
+
|
|
275
|
+
if (!resizable) {
|
|
276
|
+
return <textarea {...props} className={className} style={{ resize: 'none', ...props.style }} />
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<div className={`relative ${wrapperClassName ?? ''}`}>
|
|
281
|
+
<textarea ref={taRef} {...props} className={className} style={{ resize: 'none', ...props.style }} />
|
|
282
|
+
<div
|
|
283
|
+
className="absolute bottom-[8px] right-[3px] w-4 h-3 cursor-row-resize flex items-end justify-end"
|
|
284
|
+
onMouseDown={onResizeStart}
|
|
285
|
+
>
|
|
286
|
+
<ResizeSvg />
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Home, Folder, Lock, Eye } from 'lucide-react'
|
|
2
|
+
import { Label, type LabelColor } from './label.tsx'
|
|
3
|
+
|
|
4
|
+
export type ScopeType = 'user' | 'project' | 'local' | 'read-only'
|
|
5
|
+
|
|
6
|
+
interface ScopeBadgeProps {
|
|
7
|
+
scope: ScopeType
|
|
8
|
+
size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const scopeConfig: Record<
|
|
12
|
+
ScopeType,
|
|
13
|
+
{
|
|
14
|
+
icon: typeof Home
|
|
15
|
+
label: string
|
|
16
|
+
color: LabelColor
|
|
17
|
+
title: string
|
|
18
|
+
description: string
|
|
19
|
+
}
|
|
20
|
+
> = {
|
|
21
|
+
user: {
|
|
22
|
+
icon: Home,
|
|
23
|
+
label: 'user',
|
|
24
|
+
color: 'emerald',
|
|
25
|
+
title: 'User-level',
|
|
26
|
+
description: 'From ~/.claude/ - Available to all projects',
|
|
27
|
+
},
|
|
28
|
+
project: {
|
|
29
|
+
icon: Folder,
|
|
30
|
+
label: 'project',
|
|
31
|
+
color: 'blue',
|
|
32
|
+
title: 'Project-level',
|
|
33
|
+
description: 'From .claude/ - Shared via version control',
|
|
34
|
+
},
|
|
35
|
+
local: {
|
|
36
|
+
icon: Lock,
|
|
37
|
+
label: 'local',
|
|
38
|
+
color: 'pink',
|
|
39
|
+
title: 'Local',
|
|
40
|
+
description: 'Personal to this project only, gitignored',
|
|
41
|
+
},
|
|
42
|
+
'read-only': {
|
|
43
|
+
icon: Eye,
|
|
44
|
+
label: 'read-only',
|
|
45
|
+
color: 'amber',
|
|
46
|
+
title: 'Read-Only',
|
|
47
|
+
description: 'This file cannot be edited',
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ScopeBadge({ scope, size = 'xs' }: ScopeBadgeProps) {
|
|
52
|
+
const config = scopeConfig[scope]
|
|
53
|
+
const Icon = config.icon
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Label
|
|
57
|
+
text={config.label}
|
|
58
|
+
color={config.color}
|
|
59
|
+
IconComponent={Icon}
|
|
60
|
+
tooltip={{
|
|
61
|
+
title: config.title,
|
|
62
|
+
description: config.description,
|
|
63
|
+
}}
|
|
64
|
+
size={size}
|
|
65
|
+
/>
|
|
66
|
+
)
|
|
67
|
+
}
|