@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,514 @@
1
+ /**
2
+ * TabbedPromptEditor — Core Monaco editor with AI tool tabs
3
+ *
4
+ * Part of: Sections > Prompt Editor
5
+ *
6
+ * This is the base editor component. It provides:
7
+ * - AI tool tabs along the top (icon + name, active tab highlighted)
8
+ * - Monaco editor (markdown) with {{VARIABLE}} highlighting and autocomplete
9
+ * - Variables sidebar on the right (resizable, searchable, click-to-copy)
10
+ * - Editor toolbar with save/reset/dirty indicator
11
+ * - Cmd/Ctrl+S to save, validation for checklist section
12
+ *
13
+ * Usage:
14
+ * <TabbedPromptEditor
15
+ * prompts={{ claude: '...', gemini: '...' }}
16
+ * onPromptChange={(tool, value) => save(tool, value)}
17
+ * tools={[{ id: 'claude', name: 'Claude Code', icon: <AiToolIcon tool="claude" /> }]}
18
+ * variables={[{ name: 'FILE_PATH', description: 'Path to the file' }]}
19
+ * />
20
+ */
21
+
22
+ import { useState, useRef, useCallback, useEffect, useMemo } from 'react'
23
+ import Editor, { type Monaco } from '@monaco-editor/react'
24
+ import type { editor, languages } from 'monaco-editor'
25
+ import { Variable, Info, Search, X, AlertTriangle } from 'lucide-react'
26
+ import { IconButton } from '../../ui/icon-button.tsx'
27
+ import { Input } from '../../ui/input.tsx'
28
+ import { EditorToolbar } from '../../ui/editor-toolbar.tsx'
29
+ import { EditorPlaceholderCard } from '../../ui/editor-placeholder-card.tsx'
30
+ import type { ToolTab, PromptPlaceholder } from './types.ts'
31
+ import { AiToolIcon } from '../../lib/ai-tools.tsx'
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Constants
35
+ // ---------------------------------------------------------------------------
36
+
37
+ const THEME_NAME = 'prompt-editor-dark'
38
+ let themeRegistered = false
39
+
40
+ const MIN_SIDEBAR_WIDTH = 220
41
+ const MAX_SIDEBAR_WIDTH = 400
42
+ const DEFAULT_SIDEBAR_WIDTH = 280
43
+ const DEFAULT_EDITOR_HEIGHT = 400
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Props
47
+ // ---------------------------------------------------------------------------
48
+
49
+ export interface TabbedPromptEditorProps {
50
+ /** Prompt content keyed by tool id */
51
+ prompts: Record<string, string>
52
+ /** Called when a prompt is saved */
53
+ onPromptChange: (tool: string, value: string) => void
54
+ /** Tool tabs to display */
55
+ tools: ToolTab[]
56
+ /** Default/reset prompts keyed by tool id */
57
+ defaultPrompts?: Record<string, string>
58
+ /** Available template variables */
59
+ variables?: PromptPlaceholder[]
60
+ /** Called when reset is triggered */
61
+ onReset?: (tool: string) => void
62
+ /** Called when save is triggered */
63
+ onSave?: (tool: string, content: string) => void
64
+ /** When true, validates that prompt contains "## Verification Checklist" */
65
+ validateChecklist?: boolean
66
+ /** When true, adds border and rounding for standalone usage */
67
+ standalone?: boolean
68
+ className?: string
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Component
73
+ // ---------------------------------------------------------------------------
74
+
75
+ export function TabbedPromptEditor({
76
+ prompts,
77
+ onPromptChange,
78
+ tools,
79
+ defaultPrompts,
80
+ variables,
81
+ onReset,
82
+ onSave,
83
+ validateChecklist = false,
84
+ standalone = false,
85
+ className = '',
86
+ }: TabbedPromptEditorProps) {
87
+ const [activeTab, setActiveTab] = useState(tools[0]?.id ?? '')
88
+ const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH)
89
+ const [variableSearch, setVariableSearch] = useState('')
90
+ const [localContent, setLocalContent] = useState<Record<string, string>>(prompts)
91
+ const [isDirty, setIsDirty] = useState(false)
92
+
93
+ const isDraggingSidebarRef = useRef(false)
94
+ const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
95
+ const monacoRef = useRef<Monaco | null>(null)
96
+ const decorationsRef = useRef<string[]>([])
97
+ const completionProviderRef = useRef<{ dispose: () => void } | null>(null)
98
+
99
+ // Sync local content when prompts change externally
100
+ useEffect(() => {
101
+ setLocalContent(prompts)
102
+ setIsDirty(false)
103
+ }, [prompts])
104
+
105
+ // --- Decorations: highlight {{VARIABLE}} patterns ---
106
+
107
+ const updateDecorations = useCallback(() => {
108
+ if (!editorRef.current || !monacoRef.current) return
109
+ const model = editorRef.current.getModel()
110
+ if (!model) return
111
+
112
+ const content = model.getValue()
113
+ const matches: RegExpExecArray[] = Array.from(content.matchAll(/\{\{(\w+)\}\}/g) as IterableIterator<RegExpExecArray>)
114
+ const newDecorations: editor.IModelDeltaDecoration[] = []
115
+
116
+ for (const match of matches) {
117
+ if (match.index === undefined) continue
118
+ const startPos = model.getPositionAt(match.index)
119
+ const endPos = model.getPositionAt(match.index + match[0].length)
120
+
121
+ const varName = match[1] as string
122
+ const varInfo = variables?.find((v) => v.name === varName)
123
+ const hoverContent = varInfo
124
+ ? `**{{${varName}}}**\n\n${varInfo.description}${varInfo.example ? `\n\n*Value: ${varInfo.example}*` : ''}`
125
+ : `**Template Variable**: ${varName}`
126
+
127
+ newDecorations.push({
128
+ range: new monacoRef.current.Range(
129
+ startPos.lineNumber, startPos.column,
130
+ endPos.lineNumber, endPos.column,
131
+ ),
132
+ options: {
133
+ inlineClassName: 'template-variable-highlight',
134
+ hoverMessage: { value: hoverContent },
135
+ },
136
+ })
137
+ }
138
+
139
+ decorationsRef.current = editorRef.current.deltaDecorations(
140
+ decorationsRef.current,
141
+ newDecorations,
142
+ )
143
+ }, [variables])
144
+
145
+ // --- Completion provider: {{ trigger ---
146
+
147
+ const registerCompletionProvider = useCallback((monaco: Monaco) => {
148
+ if (completionProviderRef.current) {
149
+ completionProviderRef.current.dispose()
150
+ }
151
+ if (!variables || variables.length === 0) return
152
+
153
+ completionProviderRef.current = monaco.languages.registerCompletionItemProvider('markdown', {
154
+ triggerCharacters: ['{'],
155
+ provideCompletionItems: (model: editor.ITextModel, position: { lineNumber: number; column: number }) => {
156
+ const textUntilPosition = model.getValueInRange({
157
+ startLineNumber: position.lineNumber,
158
+ startColumn: Math.max(1, position.column - 2),
159
+ endLineNumber: position.lineNumber,
160
+ endColumn: position.column,
161
+ })
162
+
163
+ if (!textUntilPosition.endsWith('{{')) {
164
+ return { suggestions: [] }
165
+ }
166
+
167
+ const lineContent = model.getLineContent(position.lineNumber)
168
+ const textAfterCursor = lineContent.substring(position.column - 1)
169
+ const closingBracesMatch = textAfterCursor.match(/^(\}+)/)
170
+ const existingClosingBraces = closingBracesMatch ? closingBracesMatch[1].length : 0
171
+
172
+ const suggestions: languages.CompletionItem[] = variables.map((v) => ({
173
+ label: v.name,
174
+ kind: monaco.languages.CompletionItemKind.Variable,
175
+ detail: v.required ? '(required)' : undefined,
176
+ documentation: {
177
+ value: `${v.description}${v.example ? `\n\n*Value: ${v.example}*` : ''}`,
178
+ },
179
+ insertText: `${v.name}}}`,
180
+ range: {
181
+ startLineNumber: position.lineNumber,
182
+ startColumn: position.column,
183
+ endLineNumber: position.lineNumber,
184
+ endColumn: position.column + Math.min(existingClosingBraces, 2),
185
+ },
186
+ }))
187
+
188
+ return { suggestions }
189
+ },
190
+ })
191
+ }, [variables])
192
+
193
+ // --- Editor mount ---
194
+
195
+ const handleEditorDidMount = useCallback(
196
+ (editorInstance: editor.IStandaloneCodeEditor, monaco: Monaco) => {
197
+ editorRef.current = editorInstance
198
+ monacoRef.current = monaco
199
+
200
+ // Inject CSS for variable highlighting
201
+ const styleId = 'template-variable-styles'
202
+ if (!document.getElementById(styleId)) {
203
+ const style = document.createElement('style')
204
+ style.id = styleId
205
+ style.textContent = `
206
+ .template-variable-highlight {
207
+ background-color: rgba(180, 190, 254, 0.2);
208
+ border: 1px solid rgba(180, 190, 254, 0.4);
209
+ border-radius: 3px;
210
+ color: #b4befe !important;
211
+ font-weight: 500;
212
+ }
213
+ `
214
+ document.head.appendChild(style)
215
+ }
216
+
217
+ registerCompletionProvider(monaco)
218
+ updateDecorations()
219
+
220
+ editorInstance.onDidChangeModelContent(() => {
221
+ updateDecorations()
222
+ })
223
+ },
224
+ [updateDecorations, registerCompletionProvider],
225
+ )
226
+
227
+ // Re-register completion provider when variables change
228
+ useEffect(() => {
229
+ if (monacoRef.current) {
230
+ registerCompletionProvider(monacoRef.current)
231
+ }
232
+ return () => {
233
+ if (completionProviderRef.current) {
234
+ completionProviderRef.current.dispose()
235
+ }
236
+ }
237
+ }, [variables, registerCompletionProvider])
238
+
239
+ // --- Sidebar resize ---
240
+
241
+ const handleSidebarMouseDown = useCallback((e: React.PointerEvent) => {
242
+ e.preventDefault()
243
+ e.stopPropagation()
244
+ isDraggingSidebarRef.current = true
245
+ const target = e.currentTarget as HTMLElement
246
+ target.setPointerCapture(e.pointerId)
247
+
248
+ const startX = e.clientX
249
+ const startWidth = sidebarWidth
250
+
251
+ const handlePointerMove = (moveEvent: PointerEvent) => {
252
+ if (!isDraggingSidebarRef.current) return
253
+ const deltaX = startX - moveEvent.clientX
254
+ const newWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, startWidth + deltaX))
255
+ setSidebarWidth(newWidth)
256
+ }
257
+
258
+ const handlePointerUp = () => {
259
+ isDraggingSidebarRef.current = false
260
+ target.removeEventListener('pointermove', handlePointerMove)
261
+ target.removeEventListener('pointerup', handlePointerUp)
262
+ }
263
+
264
+ target.addEventListener('pointermove', handlePointerMove)
265
+ target.addEventListener('pointerup', handlePointerUp)
266
+ }, [sidebarWidth])
267
+
268
+ // --- Content change ---
269
+
270
+ const handleEditorChange = useCallback(
271
+ (value: string | undefined) => {
272
+ const newValue = value ?? ''
273
+ setLocalContent((prev: Record<string, string>) => {
274
+ const updated: Record<string, string> = { ...prev, [activeTab]: newValue }
275
+ const hasDiff = Object.keys(prompts).some(
276
+ (tool) => updated[tool] !== prompts[tool],
277
+ )
278
+ setIsDirty(hasDiff)
279
+ return updated
280
+ })
281
+ },
282
+ [activeTab, prompts],
283
+ )
284
+
285
+ // --- Save ---
286
+
287
+ const handleSave = useCallback(() => {
288
+ for (const tool of Object.keys(prompts)) {
289
+ if (localContent[tool] !== prompts[tool]) {
290
+ onPromptChange(tool, localContent[tool])
291
+ onSave?.(tool, localContent[tool])
292
+ }
293
+ }
294
+ setIsDirty(false)
295
+ }, [localContent, prompts, onPromptChange, onSave])
296
+
297
+ // Keyboard shortcut: Cmd/Ctrl + S
298
+ useEffect(() => {
299
+ const handleKeyDown = (e: KeyboardEvent) => {
300
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') {
301
+ e.preventDefault()
302
+ if (isDirty) handleSave()
303
+ }
304
+ }
305
+ window.addEventListener('keydown', handleKeyDown)
306
+ return () => window.removeEventListener('keydown', handleKeyDown)
307
+ }, [isDirty, handleSave])
308
+
309
+ // --- Reset ---
310
+
311
+ const handleReset = useCallback(() => {
312
+ onReset?.(activeTab)
313
+ }, [activeTab, onReset])
314
+
315
+ // --- Derived state ---
316
+
317
+ const currentPrompt = localContent[activeTab] ?? ''
318
+ const canReset = !!onReset && !!defaultPrompts && currentPrompt !== defaultPrompts[activeTab]
319
+ const hasVariables = variables && variables.length > 0
320
+
321
+ const checklistMissing = useMemo(() => {
322
+ if (!validateChecklist) return false
323
+ if (!currentPrompt || currentPrompt.trim().length === 0) return true
324
+ return !/#+\s*Verification Checklist/i.test(currentPrompt)
325
+ }, [validateChecklist, currentPrompt])
326
+
327
+ const filteredVariables = useMemo((): PromptPlaceholder[] => {
328
+ if (!variables) return []
329
+ if (!variableSearch.trim()) return variables
330
+ const search = variableSearch.toLowerCase()
331
+ return variables.filter(
332
+ (v) =>
333
+ v.name.toLowerCase().includes(search) ||
334
+ v.description.toLowerCase().includes(search) ||
335
+ (v.example && v.example.toLowerCase().includes(search)),
336
+ )
337
+ }, [variables, variableSearch])
338
+
339
+ return (
340
+ <div className={`flex w-full h-full bg-[#181825] overflow-hidden ${standalone ? 'border border-[#313244] rounded-lg' : ''} ${className}`}>
341
+ {/* Main content area */}
342
+ <div className="flex-1 min-w-0 w-0 flex flex-col overflow-hidden">
343
+ {/* Tool Tabs */}
344
+ <div className="relative flex h-[26px] bg-[#11111b] shrink-0">
345
+ {tools.map((tool) => {
346
+ const isActive = activeTab === tool.id
347
+ const activeColor = tool.activeColor ?? 'text-[#89b4fa]'
348
+
349
+ return (
350
+ <button
351
+ key={tool.id}
352
+ onClick={() => setActiveTab(tool.id)}
353
+ className={`flex-1 flex items-center justify-center gap-1.5 px-2 text-xs font-medium border-b-2 transition-colors ${
354
+ isActive
355
+ ? `border-current ${activeColor} bg-[#1e1e2e]`
356
+ : 'border-[#313244] text-[#6c7086] hover:text-[#a6adc8] hover:bg-[#1e1e2e]/50'
357
+ }`}
358
+ title={tool.name}
359
+ >
360
+ {tool.icon ?? <AiToolIcon tool={tool.id} size={14} />}
361
+ <span className="hidden sm:inline">{tool.shortName ?? tool.name}</span>
362
+ </button>
363
+ )
364
+ })}
365
+ </div>
366
+
367
+ {/* Checklist validation warning */}
368
+ {checklistMissing && (
369
+ <div className="flex items-start gap-2 px-3 py-2 bg-[#f38ba8]/10 border-b border-[#f38ba8]/30 shrink-0">
370
+ <AlertTriangle className="w-4 h-4 text-[#f38ba8] shrink-0 mt-0.5" />
371
+ <div className="text-xs text-[#f38ba8]">
372
+ <span className="font-medium">Missing "# Verification Checklist" section.</span>
373
+ {' '}When verification is enabled, only content under this heading will be sent to the AI for output validation.
374
+ </div>
375
+ </div>
376
+ )}
377
+
378
+ {/* Editor Toolbar */}
379
+ <EditorToolbar
380
+ isDirty={isDirty}
381
+ onSave={handleSave}
382
+ canReset={canReset}
383
+ onReset={handleReset}
384
+ resetTooltip={{
385
+ title: 'Reset Prompt',
386
+ description: `Restore default prompt for ${tools.find((t) => t.id === activeTab)?.name ?? activeTab}.`,
387
+ }}
388
+ />
389
+
390
+ {/* Monaco Editor */}
391
+ <div className="flex-1 min-h-0" style={{ minHeight: DEFAULT_EDITOR_HEIGHT }}>
392
+ <Editor
393
+ key={activeTab}
394
+ height="100%"
395
+ defaultLanguage="markdown"
396
+ theme={THEME_NAME}
397
+ value={currentPrompt}
398
+ onChange={handleEditorChange}
399
+ onMount={handleEditorDidMount}
400
+ beforeMount={(monaco: Monaco) => {
401
+ if (!themeRegistered) {
402
+ monaco.editor.defineTheme(THEME_NAME, {
403
+ base: 'vs-dark',
404
+ inherit: true,
405
+ rules: [],
406
+ colors: {
407
+ 'editor.background': '#181825',
408
+ 'editorGutter.background': '#181825',
409
+ 'minimap.background': '#181825',
410
+ 'scrollbar.shadow': '#00000000',
411
+ },
412
+ })
413
+ themeRegistered = true
414
+ }
415
+ }}
416
+ options={{
417
+ minimap: { enabled: false },
418
+ fontSize: 13,
419
+ lineNumbers: 'on',
420
+ wordWrap: 'on',
421
+ padding: { top: 8, bottom: 16 },
422
+ scrollBeyondLastLine: false,
423
+ automaticLayout: true,
424
+ quickSuggestions: { strings: true, other: true, comments: true },
425
+ }}
426
+ />
427
+ </div>
428
+ </div>
429
+
430
+ {/* Variables Sidebar */}
431
+ {hasVariables && (
432
+ <div
433
+ className="relative shrink-0 bg-[#11111b] overflow-hidden flex flex-col border-l border-[#313244]"
434
+ style={{ width: sidebarWidth, minWidth: MIN_SIDEBAR_WIDTH, maxWidth: MAX_SIDEBAR_WIDTH }}
435
+ >
436
+ {/* Resize handle on left edge */}
437
+ <div
438
+ onPointerDown={handleSidebarMouseDown}
439
+ className="absolute left-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-[#b4befe]/30 transition-colors z-10"
440
+ />
441
+
442
+ {/* Header */}
443
+ <div className="h-[52px] px-3 flex items-center justify-between border-b border-[#313244] shrink-0">
444
+ <div className="flex items-center gap-2">
445
+ <Variable className="w-4 h-4 text-neutral-500 shrink-0" />
446
+ <div className="min-w-0">
447
+ <div className="text-xs font-medium text-neutral-400">Placeholders</div>
448
+ <div className="text-xs text-neutral-600 truncate">Available placeholders</div>
449
+ </div>
450
+ </div>
451
+ <span className="px-1.5 py-0.5 bg-[#b4befe]/20 text-[#b4befe] rounded text-xs">
452
+ {variableSearch.trim() ? `${filteredVariables.length}/${variables!.length}` : variables!.length}
453
+ </span>
454
+ </div>
455
+
456
+ {/* Search */}
457
+ <div className="px-2.5 py-2 border-b border-[#313244] shrink-0">
458
+ <div className="relative">
459
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-[#6c7086]" />
460
+ <Input
461
+ value={variableSearch}
462
+ onChange={setVariableSearch}
463
+ placeholder="Search placeholders..."
464
+ size="sm"
465
+ className="pl-7 pr-7"
466
+ />
467
+ {variableSearch && (
468
+ <IconButton
469
+ icon={<X className="w-3 h-3" />}
470
+ onClick={() => setVariableSearch('')}
471
+ color="neutral"
472
+ size="xss"
473
+ className="absolute right-2 top-1/2 -translate-y-1/2"
474
+ tooltip={{ title: 'Clear', description: 'Clear search' }}
475
+ />
476
+ )}
477
+ </div>
478
+ </div>
479
+
480
+ {/* Autocomplete tip */}
481
+ <div className="px-3 py-2 border-b border-[#313244] shrink-0">
482
+ <div className="flex items-center gap-1.5 text-xs text-[#6c7086]">
483
+ <Info className="w-3 h-3 flex-shrink-0" />
484
+ <span>Type <code className="text-[#b4befe]">{'{{'}</code> in editor for autocomplete</span>
485
+ </div>
486
+ </div>
487
+
488
+ {/* Variable list */}
489
+ <div className="flex-1 overflow-y-auto p-2.5 space-y-2">
490
+ {filteredVariables.length > 0 ? (
491
+ filteredVariables.map((variable: PromptPlaceholder) => (
492
+ <div key={variable.name} className="bg-[#1e1e2e] border border-[#313244] rounded-lg">
493
+ <EditorPlaceholderCard
494
+ name={variable.name}
495
+ description={variable.description}
496
+ value={variable.example}
497
+ required={variable.required}
498
+ valueLabel={variable.valueLabel}
499
+ accentColor="purple"
500
+ showCopyPlaceholder
501
+ />
502
+ </div>
503
+ ))
504
+ ) : variableSearch.trim() ? (
505
+ <div className="text-center py-4 text-xs text-[#6c7086]">
506
+ No placeholders match "{variableSearch}"
507
+ </div>
508
+ ) : null}
509
+ </div>
510
+ </div>
511
+ )}
512
+ </div>
513
+ )
514
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Prompt Editor — Shared type definitions
3
+ *
4
+ * Part of: Sections > Prompt Editor
5
+ *
6
+ * These types define the shape of prompt data and the API contract
7
+ * between the UI layer and whatever backend stores prompts.
8
+ */
9
+
10
+ import type { ReactNode } from 'react'
11
+
12
+ // Re-export the AI tool key type from the shared lib
13
+ export type { AiToolKey } from '../../lib/ai-tools.tsx'
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Tool definition (generic — not tied to specific AI tools)
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /** A tool tab shown in the editor (e.g. Claude, Gemini, Copilot) */
20
+ export interface ToolTab {
21
+ id: string
22
+ name: string
23
+ icon?: ReactNode
24
+ /** Short name for narrow layouts */
25
+ shortName?: string
26
+ /** Tailwind text color class for the active state (e.g. "text-orange-400") */
27
+ activeColor?: string
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Prompt template variables
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /** A template variable available for insertion into prompts */
35
+ export interface PromptPlaceholder {
36
+ name: string
37
+ description: string
38
+ example?: string
39
+ required?: boolean
40
+ /** Label for the value display (default: "Value:") */
41
+ valueLabel?: string
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Prompt snapshots
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /** A saved snapshot of a prompt for version history */
49
+ export interface PromptSnapshot {
50
+ content: string
51
+ savedAt: string // ISO date string
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // API adapter interface
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /**
59
+ * PromptEditorApi — optional callback interface for persistence.
60
+ *
61
+ * When provided, the editor can save, reset, and manage snapshots.
62
+ * When omitted, the editor works in "controlled" mode where the
63
+ * parent manages all state via props.
64
+ */
65
+ export interface PromptEditorApi {
66
+ /** Save the current prompt */
67
+ savePrompt: (prompt: string) => Promise<void>
68
+ /** Get the default/reset value */
69
+ getDefaultPrompt?: () => Promise<string>
70
+ /** Save a snapshot */
71
+ saveSnapshot?: (content: string) => Promise<void>
72
+ /** Get saved snapshots */
73
+ getSnapshots?: () => Promise<PromptSnapshot[]>
74
+ /** Delete a snapshot by timestamp */
75
+ deleteSnapshot?: (savedAt: string) => Promise<void>
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // File type selector (for FileTypeTabbedPromptEditor)
80
+ // ---------------------------------------------------------------------------
81
+
82
+ /** A file type option in the sidebar selector */
83
+ export interface FileTypeOption {
84
+ id: string
85
+ name: string
86
+ description?: string
87
+ icon?: ReactNode
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Scenario/step tree (for SimulatorPromptEditor)
92
+ // ---------------------------------------------------------------------------
93
+
94
+ /** A scenario with child steps for the tree sidebar */
95
+ export interface ScenarioOption {
96
+ id: string
97
+ name: string
98
+ description?: string
99
+ icon?: ReactNode
100
+ steps: Array<{ id: string; name: string }>
101
+ }