@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.
Files changed (112) hide show
  1. package/README.md +63 -0
  2. package/components/content/info-panel-primitives.tsx +297 -0
  3. package/components/diagrams/diagram-utils.tsx +908 -0
  4. package/components/hooks/use-click-outside.ts +27 -0
  5. package/components/hooks/use-dropdown-max-height.ts +20 -0
  6. package/components/hooks/use-navigation-history.ts +94 -0
  7. package/components/lib/ai-tools.tsx +44 -0
  8. package/components/lib/cn.ts +6 -0
  9. package/components/lib/form-colors.ts +32 -0
  10. package/components/lib/theme-engine.ts +97 -0
  11. package/components/lib/toolr-brand.tsx +31 -0
  12. package/components/sections/ai-tools-paths/index.ts +37 -0
  13. package/components/sections/ai-tools-paths/tools-paths-panel.tsx +212 -0
  14. package/components/sections/ai-tools-paths/types.ts +111 -0
  15. package/components/sections/ai-tools-paths/use-tools-paths.ts +159 -0
  16. package/components/sections/captured-issues/captured-issues-panel.tsx +214 -0
  17. package/components/sections/captured-issues/index.ts +38 -0
  18. package/components/sections/captured-issues/types.ts +113 -0
  19. package/components/sections/captured-issues/use-captured-issues.ts +111 -0
  20. package/components/sections/golden-snapshots/file-diff-viewer.tsx +420 -0
  21. package/components/sections/golden-snapshots/golden-sync-panel.tsx +223 -0
  22. package/components/sections/golden-snapshots/index.ts +145 -0
  23. package/components/sections/golden-snapshots/snapshot-manager.tsx +200 -0
  24. package/components/sections/golden-snapshots/status-overview.tsx +305 -0
  25. package/components/sections/golden-snapshots/types.ts +288 -0
  26. package/components/sections/golden-snapshots/use-golden-sync.ts +477 -0
  27. package/components/sections/golden-snapshots/version-manager.tsx +186 -0
  28. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +210 -0
  29. package/components/sections/prompt-editor/index.ts +121 -0
  30. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +276 -0
  31. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +514 -0
  32. package/components/sections/prompt-editor/types.ts +101 -0
  33. package/components/sections/prompt-editor/use-prompt-editor.ts +131 -0
  34. package/components/sections/report-bug/error-logger.ts +392 -0
  35. package/components/sections/report-bug/index.ts +59 -0
  36. package/components/sections/report-bug/issue-reporter-api.ts +83 -0
  37. package/components/sections/report-bug/report-bug-form.tsx +282 -0
  38. package/components/sections/report-bug/screenshot-uploader.tsx +228 -0
  39. package/components/sections/report-bug/use-report-bug.ts +170 -0
  40. package/components/sections/snapshot-browser/index.ts +53 -0
  41. package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +147 -0
  42. package/components/sections/snapshot-browser/snapshot-tree.tsx +451 -0
  43. package/components/sections/snapshot-browser/types.ts +106 -0
  44. package/components/sections/snapshot-browser/use-snapshot-browser.ts +125 -0
  45. package/components/sections/snippets-editor/index.ts +31 -0
  46. package/components/sections/snippets-editor/snippets-editor.tsx +381 -0
  47. package/components/sections/snippets-editor/types.ts +48 -0
  48. package/components/sections/snippets-editor/use-snippets-editor.ts +217 -0
  49. package/components/ui/action-dialog.tsx +309 -0
  50. package/components/ui/ai-action-button.tsx +137 -0
  51. package/components/ui/ai-execution-action-buttons.tsx +106 -0
  52. package/components/ui/badge.tsx +67 -0
  53. package/components/ui/bottom-panel-header.tsx +240 -0
  54. package/components/ui/breadcrumb.tsx +168 -0
  55. package/components/ui/checkbox.tsx +102 -0
  56. package/components/ui/collapsible-section.tsx +100 -0
  57. package/components/ui/confirm-badge.tsx +71 -0
  58. package/components/ui/detail-section.tsx +67 -0
  59. package/components/ui/detail-view-wrapper.tsx +55 -0
  60. package/components/ui/editor-placeholder-card.tsx +197 -0
  61. package/components/ui/editor-toolbar.tsx +123 -0
  62. package/components/ui/execution-details-panel.tsx +93 -0
  63. package/components/ui/extension-list-card.tsx +105 -0
  64. package/components/ui/file-structure-section.tsx +373 -0
  65. package/components/ui/file-tree.tsx +171 -0
  66. package/components/ui/files-panel.tsx +251 -0
  67. package/components/ui/filter-dropdown.tsx +173 -0
  68. package/components/ui/form-actions.tsx +127 -0
  69. package/components/ui/frontmatter-form-header.tsx +80 -0
  70. package/components/ui/icon-button.tsx +388 -0
  71. package/components/ui/input.tsx +211 -0
  72. package/components/ui/label.tsx +159 -0
  73. package/components/ui/layout-tab-bar.tsx +289 -0
  74. package/components/ui/modal.tsx +194 -0
  75. package/components/ui/nav-card.tsx +81 -0
  76. package/components/ui/navigation-bar.tsx +285 -0
  77. package/components/ui/number-input.tsx +165 -0
  78. package/components/ui/registry-browser.tsx +261 -0
  79. package/components/ui/registry-card.tsx +710 -0
  80. package/components/ui/registry-detail.tsx +224 -0
  81. package/components/ui/resizable-textarea.tsx +290 -0
  82. package/components/ui/scope-badge.tsx +67 -0
  83. package/components/ui/segmented-toggle.tsx +133 -0
  84. package/components/ui/select.tsx +172 -0
  85. package/components/ui/selection-grid.tsx +313 -0
  86. package/components/ui/setting-row.tsx +97 -0
  87. package/components/ui/snapshot-card.tsx +107 -0
  88. package/components/ui/snippets-panel.tsx +161 -0
  89. package/components/ui/sort-dropdown.tsx +109 -0
  90. package/components/ui/status-card.tsx +96 -0
  91. package/components/ui/tab-bar.tsx +340 -0
  92. package/components/ui/toggle.tsx +142 -0
  93. package/components/ui/tooltip.tsx +326 -0
  94. package/dist/content.d.ts +110 -0
  95. package/dist/content.js +195 -0
  96. package/dist/diagrams.d.ts +371 -0
  97. package/dist/diagrams.js +702 -0
  98. package/dist/index.d.ts +2714 -0
  99. package/dist/index.js +11220 -0
  100. package/dist/preset.d.ts +24 -0
  101. package/dist/preset.js +17 -0
  102. package/dist/tokens/tokens/primitives.css +45 -0
  103. package/dist/tokens/tokens/semantic.css +46 -0
  104. package/dist/tokens/tokens/theme.css +11 -0
  105. package/dist/tokens/tokens/tokens.json +65 -0
  106. package/index.ts +123 -0
  107. package/package.json +63 -0
  108. package/tailwind-preset.ts +22 -0
  109. package/tokens/primitives.css +45 -0
  110. package/tokens/semantic.css +46 -0
  111. package/tokens/theme.css +11 -0
  112. 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
+ }