@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,210 @@
1
+ /**
2
+ * FileTypeTabbedPromptEditor — Flat sidebar wrapper for TabbedPromptEditor
3
+ *
4
+ * Part of: Sections > Prompt Editor
5
+ *
6
+ * Adds a resizable left sidebar listing file types. Selecting a file type
7
+ * swaps the prompts displayed in the embedded TabbedPromptEditor.
8
+ *
9
+ * Used for: Verifier Prompts where each file type has its own prompt set.
10
+ *
11
+ * Layout: [resizable sidebar | TabbedPromptEditor]
12
+ *
13
+ * Usage:
14
+ * <FileTypeTabbedPromptEditor
15
+ * prompts={{ skills: { claude: '...', gemini: '...' } }}
16
+ * onPromptChange={(fileType, tool, value) => save(fileType, tool, value)}
17
+ * fileTypes={[{ id: 'skills', name: 'Skills', icon: <Zap /> }]}
18
+ * tools={[{ id: 'claude', name: 'Claude Code' }]}
19
+ * />
20
+ */
21
+
22
+ import { useState, useRef, useCallback } from 'react'
23
+ import { FileCode, GripVertical, Crosshair } from 'lucide-react'
24
+ import { TabbedPromptEditor } from './tabbed-prompt-editor.tsx'
25
+ import type { ToolTab, PromptPlaceholder, FileTypeOption } from './types.ts'
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Constants
29
+ // ---------------------------------------------------------------------------
30
+
31
+ const MIN_SIDEBAR_WIDTH = 160
32
+ const MAX_SIDEBAR_WIDTH = 320
33
+ const DEFAULT_SIDEBAR_WIDTH = 200
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Props
37
+ // ---------------------------------------------------------------------------
38
+
39
+ export interface FileTypeTabbedPromptEditorProps {
40
+ /** Nested prompts: fileType -> tool -> prompt */
41
+ prompts: Record<string, Record<string, string>>
42
+ /** Called when a prompt changes */
43
+ onPromptChange: (fileType: string, tool: string, value: string) => void
44
+ /** Available file types for the sidebar */
45
+ fileTypes: FileTypeOption[]
46
+ /** Tool tabs to display */
47
+ tools: ToolTab[]
48
+ /** Default prompts for reset: fileType -> tool -> prompt */
49
+ defaultPrompts?: Record<string, Record<string, string>>
50
+ /** Variables per file type */
51
+ variables?: Record<string, PromptPlaceholder[]>
52
+ /** Called when reset is triggered */
53
+ onReset?: (fileType: string, tool: string) => void
54
+ /** Called when save is triggered */
55
+ onSave?: (fileType: string, tool: string, content: string) => void
56
+ /** Custom label for sidebar header (default: "Target") */
57
+ selectorLabel?: string
58
+ /** Custom sublabel for sidebar header (default: "Select target file type") */
59
+ selectorSublabel?: string
60
+ /** When true, validates "## Verification Checklist" section */
61
+ validateChecklist?: boolean
62
+ className?: string
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Component
67
+ // ---------------------------------------------------------------------------
68
+
69
+ export function FileTypeTabbedPromptEditor({
70
+ prompts,
71
+ onPromptChange,
72
+ fileTypes,
73
+ tools,
74
+ defaultPrompts,
75
+ variables,
76
+ onReset,
77
+ onSave,
78
+ selectorLabel = 'Target',
79
+ selectorSublabel = 'Select target file type',
80
+ validateChecklist = false,
81
+ className = '',
82
+ }: FileTypeTabbedPromptEditorProps) {
83
+ const [selectedFileType, setSelectedFileType] = useState(fileTypes[0]?.id ?? '')
84
+ const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH)
85
+ const isDraggingRef = useRef(false)
86
+
87
+ // Derive prompts for current file type
88
+ const currentPrompts = prompts[selectedFileType] ?? Object.fromEntries(tools.map((t) => [t.id, '']))
89
+ const currentDefaultPrompts = defaultPrompts?.[selectedFileType]
90
+ const currentVariables = variables?.[selectedFileType]
91
+
92
+ // Forward prompt change with file type context
93
+ const handlePromptChange = useCallback(
94
+ (tool: string, value: string) => {
95
+ onPromptChange(selectedFileType, tool, value)
96
+ },
97
+ [selectedFileType, onPromptChange],
98
+ )
99
+
100
+ const handleReset = onReset
101
+ ? (tool: string) => onReset(selectedFileType, tool)
102
+ : undefined
103
+
104
+ const handleSave = onSave
105
+ ? (tool: string, content: string) => onSave(selectedFileType, tool, content)
106
+ : undefined
107
+
108
+ // Sidebar resize via pointer events
109
+ const handleSidebarPointerDown = useCallback((e: React.PointerEvent) => {
110
+ e.preventDefault()
111
+ e.stopPropagation()
112
+ isDraggingRef.current = true
113
+ const target = e.currentTarget as HTMLElement
114
+ target.setPointerCapture(e.pointerId)
115
+
116
+ const startX = e.clientX
117
+ const startWidth = sidebarWidth
118
+
119
+ const handlePointerMove = (moveEvent: PointerEvent) => {
120
+ if (!isDraggingRef.current) return
121
+ const deltaX = moveEvent.clientX - startX
122
+ const newWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, startWidth + deltaX))
123
+ setSidebarWidth(newWidth)
124
+ }
125
+
126
+ const handlePointerUp = () => {
127
+ isDraggingRef.current = false
128
+ target.removeEventListener('pointermove', handlePointerMove)
129
+ target.removeEventListener('pointerup', handlePointerUp)
130
+ }
131
+
132
+ target.addEventListener('pointermove', handlePointerMove)
133
+ target.addEventListener('pointerup', handlePointerUp)
134
+ }, [sidebarWidth])
135
+
136
+ return (
137
+ <div className={`flex w-full max-w-full bg-[#181825] border border-[#313244] rounded-lg overflow-hidden ${className}`}>
138
+ {/* Left Sidebar — File Type Selector */}
139
+ <div
140
+ className="relative shrink-0 bg-[#11111b] overflow-hidden flex flex-col"
141
+ style={{ width: sidebarWidth, minWidth: MIN_SIDEBAR_WIDTH, maxWidth: MAX_SIDEBAR_WIDTH }}
142
+ >
143
+ {/* Header */}
144
+ <div className="h-[52px] px-3 flex items-center border-b border-[#313244] shrink-0">
145
+ <div className="flex items-center gap-2">
146
+ <Crosshair className="w-4 h-4 text-neutral-500 shrink-0" />
147
+ <div className="min-w-0">
148
+ <div className="text-xs font-medium text-neutral-400">{selectorLabel}</div>
149
+ <div className="text-xs text-neutral-600">{selectorSublabel}</div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+
154
+ {/* File Type List */}
155
+ <div className="flex-1 overflow-y-auto">
156
+ {fileTypes.map((ft) => {
157
+ const isSelected = ft.id === selectedFileType
158
+ return (
159
+ <button
160
+ key={ft.id}
161
+ onClick={() => setSelectedFileType(ft.id)}
162
+ className={`w-full min-h-[44px] py-2 flex items-center gap-2.5 px-3 text-left transition-colors ${
163
+ isSelected
164
+ ? 'bg-[#313244]/50 border-l-2 border-[#89b4fa]'
165
+ : 'hover:bg-[#1e1e2e] border-l-2 border-transparent'
166
+ }`}
167
+ >
168
+ <div className={`flex-shrink-0 ${isSelected ? 'text-[#89b4fa]' : 'text-[#6c7086]'}`}>
169
+ {ft.icon ?? <FileCode className="w-4 h-4" />}
170
+ </div>
171
+ <div className="min-w-0 flex-1">
172
+ <div className={`text-sm font-medium truncate ${isSelected ? 'text-[#cdd6f4]' : 'text-[#a6adc8]'}`}>
173
+ {ft.name}
174
+ </div>
175
+ {ft.description && (
176
+ <div className="text-xs text-[#6c7086] mt-0.5 leading-relaxed">
177
+ {ft.description}
178
+ </div>
179
+ )}
180
+ </div>
181
+ </button>
182
+ )
183
+ })}
184
+ </div>
185
+
186
+ {/* Resize handle on right edge */}
187
+ <div
188
+ onPointerDown={handleSidebarPointerDown}
189
+ className="absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-[#89b4fa]/30 transition-colors z-10 flex items-center justify-center group"
190
+ >
191
+ <GripVertical className="w-3 h-3 text-[#45475a] group-hover:text-[#6c7086] opacity-0 group-hover:opacity-100 transition-opacity" />
192
+ </div>
193
+ </div>
194
+
195
+ {/* Main Content — TabbedPromptEditor */}
196
+ <div className="flex-1 min-w-0 w-0 overflow-hidden border-l border-[#313244]">
197
+ <TabbedPromptEditor
198
+ prompts={currentPrompts}
199
+ onPromptChange={handlePromptChange}
200
+ tools={tools}
201
+ defaultPrompts={currentDefaultPrompts}
202
+ variables={currentVariables}
203
+ onReset={handleReset}
204
+ onSave={handleSave}
205
+ validateChecklist={validateChecklist}
206
+ />
207
+ </div>
208
+ </div>
209
+ )
210
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Prompt Editor — Section barrel export
3
+ *
4
+ * Part of: Sections > Prompt Editor
5
+ *
6
+ * This section provides a 3-tier prompt editing system with Monaco editor,
7
+ * AI tool tabs, template variable support, and resizable sidebars.
8
+ *
9
+ * ╔═══════════════════════════════════════════════════════════════════════╗
10
+ * ║ ARCHITECTURE OVERVIEW ║
11
+ * ╠═══════════════════════════════════════════════════════════════════════╣
12
+ * ║ ║
13
+ * ║ The system has three tiers, each adding a sidebar layer: ║
14
+ * ║ ║
15
+ * ║ 1. TabbedPromptEditor (core) ║
16
+ * ║ [tool tabs | Monaco editor | variables sidebar] ║
17
+ * ║ Base editor with AI tool tabs and template variable support. ║
18
+ * ║ ║
19
+ * ║ 2. FileTypeTabbedPromptEditor ║
20
+ * ║ [file type sidebar | TabbedPromptEditor] ║
21
+ * ║ Adds a flat sidebar of file types. Each file type gets its own ║
22
+ * ║ set of per-tool prompts. ║
23
+ * ║ ║
24
+ * ║ 3. SimulatorPromptEditor ║
25
+ * ║ [scenario/step tree sidebar | TabbedPromptEditor] ║
26
+ * ║ Adds a hierarchical sidebar with expandable scenarios and child ║
27
+ * ║ steps. Each scenario+step gets its own per-tool prompts. ║
28
+ * ║ ║
29
+ * ╠═══════════════════════════════════════════════════════════════════════╣
30
+ * ║ QUICK START ║
31
+ * ╠═══════════════════════════════════════════════════════════════════════╣
32
+ * ║ ║
33
+ * ║ 1. Standalone TabbedPromptEditor: ║
34
+ * ║ ║
35
+ * ║ import { TabbedPromptEditor } from '@toolr/ui-design' ║
36
+ * ║ ║
37
+ * ║ <TabbedPromptEditor ║
38
+ * ║ prompts={{ claude: '...', gemini: '...' }} ║
39
+ * ║ onPromptChange={(tool, value) => save(tool, value)} ║
40
+ * ║ tools={[ ║
41
+ * ║ { id: 'claude', name: 'Claude Code' }, ║
42
+ * ║ { id: 'gemini', name: 'Gemini CLI' }, ║
43
+ * ║ ]} ║
44
+ * ║ variables={[{ name: 'FILE_PATH', description: 'Path' }]} ║
45
+ * ║ standalone ║
46
+ * ║ /> ║
47
+ * ║ ║
48
+ * ║ 2. FileTypeTabbedPromptEditor (for verifier prompts): ║
49
+ * ║ ║
50
+ * ║ import { FileTypeTabbedPromptEditor } from '@toolr/ui-design' ║
51
+ * ║ ║
52
+ * ║ <FileTypeTabbedPromptEditor ║
53
+ * ║ prompts={{ skills: { claude: '...' }, commands: { claude: '.' }}}║
54
+ * ║ onPromptChange={(fileType, tool, value) => ...} ║
55
+ * ║ fileTypes={[ ║
56
+ * ║ { id: 'skills', name: 'Skills' }, ║
57
+ * ║ { id: 'commands', name: 'Commands' }, ║
58
+ * ║ ]} ║
59
+ * ║ tools={[{ id: 'claude', name: 'Claude' }]} ║
60
+ * ║ /> ║
61
+ * ║ ║
62
+ * ║ 3. SimulatorPromptEditor (for simulator prompts): ║
63
+ * ║ ║
64
+ * ║ import { SimulatorPromptEditor } from '@toolr/ui-design' ║
65
+ * ║ ║
66
+ * ║ <SimulatorPromptEditor ║
67
+ * ║ prompts={{ clean: { main: { claude: '...' } } }} ║
68
+ * ║ onPromptChange={(scenario, step, tool, value) => ...} ║
69
+ * ║ scenarios={[{ ║
70
+ * ║ id: 'clean', name: 'Clean Build', ║
71
+ * ║ steps: [{ id: 'main', name: 'Main Step' }], ║
72
+ * ║ }]} ║
73
+ * ║ tools={[{ id: 'claude', name: 'Claude' }]} ║
74
+ * ║ /> ║
75
+ * ║ ║
76
+ * ╠═══════════════════════════════════════════════════════════════════════╣
77
+ * ║ IN CONFIGR ║
78
+ * ╠═══════════════════════════════════════════════════════════════════════╣
79
+ * ║ ║
80
+ * ║ - Verifier Prompts use FileTypeTabbedPromptEditor ║
81
+ * ║ (file types = skills, commands, project-memory, etc.) ║
82
+ * ║ - Simulator Prompts use SimulatorPromptEditor ║
83
+ * ║ (scenarios = clean, migrate, etc. with steps per scenario) ║
84
+ * ║ - Both variants embed TabbedPromptEditor for the actual editing ║
85
+ * ║ ║
86
+ * ╚═══════════════════════════════════════════════════════════════════════╝
87
+ */
88
+
89
+ // Types
90
+ export type {
91
+ AiToolKey,
92
+ ToolTab,
93
+ PromptPlaceholder,
94
+ PromptSnapshot,
95
+ PromptEditorApi,
96
+ FileTypeOption,
97
+ ScenarioOption,
98
+ } from './types.ts'
99
+
100
+ // Hook
101
+ export {
102
+ usePromptEditor,
103
+ type UsePromptEditorOptions,
104
+ type UsePromptEditorReturn,
105
+ } from './use-prompt-editor.ts'
106
+
107
+ // Components
108
+ export {
109
+ TabbedPromptEditor,
110
+ type TabbedPromptEditorProps,
111
+ } from './tabbed-prompt-editor.tsx'
112
+
113
+ export {
114
+ FileTypeTabbedPromptEditor,
115
+ type FileTypeTabbedPromptEditorProps,
116
+ } from './file-type-tabbed-prompt-editor.tsx'
117
+
118
+ export {
119
+ SimulatorPromptEditor,
120
+ type SimulatorPromptEditorProps,
121
+ } from './simulator-prompt-editor.tsx'
@@ -0,0 +1,276 @@
1
+ /**
2
+ * SimulatorPromptEditor — Hierarchical tree sidebar wrapper for TabbedPromptEditor
3
+ *
4
+ * Part of: Sections > Prompt Editor
5
+ *
6
+ * Adds a resizable left sidebar with expandable scenarios and child steps.
7
+ * Selecting a step swaps the prompts displayed in the embedded TabbedPromptEditor.
8
+ *
9
+ * Used for: Simulator Prompts where each scenario/step combination has its own prompt set.
10
+ *
11
+ * Layout: [resizable tree sidebar | TabbedPromptEditor]
12
+ *
13
+ * Usage:
14
+ * <SimulatorPromptEditor
15
+ * prompts={{ clean: { main: { claude: '...' } } }}
16
+ * onPromptChange={(scenario, step, tool, value) => save(scenario, step, tool, value)}
17
+ * scenarios={[{ id: 'clean', name: 'Clean Build', steps: [{ id: 'main', name: 'Main Step' }] }]}
18
+ * tools={[{ id: 'claude', name: 'Claude Code' }]}
19
+ * />
20
+ */
21
+
22
+ import { useState, useRef, useCallback, useMemo } from 'react'
23
+ import { ChevronDown, ChevronRight, GripVertical, Crosshair } from 'lucide-react'
24
+ import { TabbedPromptEditor } from './tabbed-prompt-editor.tsx'
25
+ import type { ToolTab, PromptPlaceholder, ScenarioOption } from './types.ts'
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Constants
29
+ // ---------------------------------------------------------------------------
30
+
31
+ const MIN_SIDEBAR_WIDTH = 180
32
+ const MAX_SIDEBAR_WIDTH = 360
33
+ const DEFAULT_SIDEBAR_WIDTH = 220
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Props
37
+ // ---------------------------------------------------------------------------
38
+
39
+ export interface SimulatorPromptEditorProps {
40
+ /** Nested prompts: scenario -> step -> tool -> prompt */
41
+ prompts: Record<string, Record<string, Record<string, string>>>
42
+ /** Called when a prompt changes */
43
+ onPromptChange: (scenario: string, step: string, tool: string, value: string) => void
44
+ /** Available scenarios with their steps */
45
+ scenarios: ScenarioOption[]
46
+ /** Tool tabs to display */
47
+ tools: ToolTab[]
48
+ /** Default prompts for reset: scenario -> step -> tool -> prompt */
49
+ defaultPrompts?: Record<string, Record<string, Record<string, string>>>
50
+ /** Variables per scenario/step (key format: "scenario:step") */
51
+ variables?: Record<string, PromptPlaceholder[]>
52
+ /** Called when reset is triggered */
53
+ onReset?: (scenario: string, step: string, tool: string) => void
54
+ /** Called when save is triggered */
55
+ onSave?: (scenario: string, step: string, tool: string, content: string) => void
56
+ /** When true, validates "## Verification Checklist" section */
57
+ validateChecklist?: boolean
58
+ className?: string
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Component
63
+ // ---------------------------------------------------------------------------
64
+
65
+ export function SimulatorPromptEditor({
66
+ prompts,
67
+ onPromptChange,
68
+ scenarios,
69
+ tools,
70
+ defaultPrompts,
71
+ variables,
72
+ onReset,
73
+ onSave,
74
+ validateChecklist = false,
75
+ className = '',
76
+ }: SimulatorPromptEditorProps) {
77
+ const defaultScenarioId = scenarios[0]?.id ?? ''
78
+ const defaultStepId = scenarios[0]?.steps[0]?.id ?? ''
79
+
80
+ const [selectedScenario, setSelectedScenario] = useState(defaultScenarioId)
81
+ const [selectedStep, setSelectedStep] = useState(defaultStepId)
82
+ const [expandedScenarios, setExpandedScenarios] = useState<Set<string>>(
83
+ new Set([defaultScenarioId]),
84
+ )
85
+ const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH)
86
+ const isDraggingRef = useRef(false)
87
+
88
+ // Ensure selected scenario is always expanded
89
+ const effectiveExpanded = useMemo(() => {
90
+ if (selectedScenario && !expandedScenarios.has(selectedScenario)) {
91
+ return new Set([...expandedScenarios, selectedScenario])
92
+ }
93
+ return expandedScenarios
94
+ }, [expandedScenarios, selectedScenario])
95
+
96
+ // Derive prompts for current selection
97
+ const currentPrompts = prompts[selectedScenario]?.[selectedStep]
98
+ ?? Object.fromEntries(tools.map((t) => [t.id, '']))
99
+ const currentDefaultPrompts = defaultPrompts?.[selectedScenario]?.[selectedStep]
100
+ const variableKey = `${selectedScenario}:${selectedStep}`
101
+ const currentVariables = variables?.[variableKey]
102
+
103
+ // Forward prompt change with scenario/step context
104
+ const handlePromptChange = useCallback(
105
+ (tool: string, value: string) => {
106
+ onPromptChange(selectedScenario, selectedStep, tool, value)
107
+ },
108
+ [selectedScenario, selectedStep, onPromptChange],
109
+ )
110
+
111
+ const handleReset = onReset
112
+ ? (tool: string) => onReset(selectedScenario, selectedStep, tool)
113
+ : undefined
114
+
115
+ const handleSave = onSave
116
+ ? (tool: string, content: string) => onSave(selectedScenario, selectedStep, tool, content)
117
+ : undefined
118
+
119
+ // Toggle scenario expansion
120
+ const toggleScenario = (scenarioId: string) => {
121
+ setExpandedScenarios((prev: Set<string>) => {
122
+ const next = new Set(prev)
123
+ if (next.has(scenarioId)) {
124
+ next.delete(scenarioId)
125
+ } else {
126
+ next.add(scenarioId)
127
+ }
128
+ return next
129
+ })
130
+ }
131
+
132
+ // Select a step (also expands parent scenario)
133
+ const selectStep = (scenarioId: string, stepId: string) => {
134
+ setSelectedScenario(scenarioId)
135
+ setSelectedStep(stepId)
136
+ if (!effectiveExpanded.has(scenarioId)) {
137
+ setExpandedScenarios((prev: Set<string>) => new Set([...prev, scenarioId]))
138
+ }
139
+ }
140
+
141
+ // Sidebar resize via pointer events
142
+ const handleSidebarPointerDown = useCallback((e: React.PointerEvent) => {
143
+ e.preventDefault()
144
+ e.stopPropagation()
145
+ isDraggingRef.current = true
146
+ const target = e.currentTarget as HTMLElement
147
+ target.setPointerCapture(e.pointerId)
148
+
149
+ const startX = e.clientX
150
+ const startWidth = sidebarWidth
151
+
152
+ const handlePointerMove = (moveEvent: PointerEvent) => {
153
+ if (!isDraggingRef.current) return
154
+ const deltaX = moveEvent.clientX - startX
155
+ const newWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, startWidth + deltaX))
156
+ setSidebarWidth(newWidth)
157
+ }
158
+
159
+ const handlePointerUp = () => {
160
+ isDraggingRef.current = false
161
+ target.removeEventListener('pointermove', handlePointerMove)
162
+ target.removeEventListener('pointerup', handlePointerUp)
163
+ }
164
+
165
+ target.addEventListener('pointermove', handlePointerMove)
166
+ target.addEventListener('pointerup', handlePointerUp)
167
+ }, [sidebarWidth])
168
+
169
+ return (
170
+ <div className={`flex w-full max-w-full bg-[#181825] border border-[#313244] rounded-lg overflow-hidden ${className}`}>
171
+ {/* Left Sidebar — Tree Selector */}
172
+ <div
173
+ className="relative shrink-0 bg-[#11111b] overflow-hidden flex flex-col"
174
+ style={{ width: sidebarWidth, minWidth: MIN_SIDEBAR_WIDTH, maxWidth: MAX_SIDEBAR_WIDTH }}
175
+ >
176
+ {/* Header */}
177
+ <div className="h-[52px] px-3 flex items-center border-b border-[#313244] shrink-0">
178
+ <div className="flex items-center gap-2">
179
+ <Crosshair className="w-4 h-4 text-neutral-500 shrink-0" />
180
+ <div className="min-w-0">
181
+ <div className="text-xs font-medium text-neutral-400">Scenario</div>
182
+ <div className="text-xs text-neutral-600">Select scenario and step</div>
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ {/* Tree View */}
188
+ <div className="flex-1 overflow-y-auto">
189
+ {scenarios.map((scenario) => {
190
+ const isExpanded = effectiveExpanded.has(scenario.id)
191
+ const isScenarioActive = selectedScenario === scenario.id
192
+
193
+ return (
194
+ <div key={scenario.id}>
195
+ {/* Scenario Header */}
196
+ <button
197
+ onClick={() => toggleScenario(scenario.id)}
198
+ className={`w-full min-h-[44px] py-2 flex items-start gap-2 px-3 text-left transition-colors hover:bg-[#1e1e2e] ${
199
+ isScenarioActive ? 'bg-[#1e1e2e]/50' : ''
200
+ }`}
201
+ >
202
+ <div className="flex-shrink-0 mt-0.5 text-[#6c7086]">
203
+ {isExpanded ? (
204
+ <ChevronDown className="w-4 h-4" />
205
+ ) : (
206
+ <ChevronRight className="w-4 h-4" />
207
+ )}
208
+ </div>
209
+ <div className="min-w-0 flex-1">
210
+ <div className={`text-sm font-medium ${isScenarioActive ? 'text-[#cdd6f4]' : 'text-[#a6adc8]'}`}>
211
+ {scenario.name}
212
+ </div>
213
+ {scenario.description && (
214
+ <div className="text-xs text-[#6c7086] mt-0.5 leading-relaxed">
215
+ {scenario.description}
216
+ </div>
217
+ )}
218
+ </div>
219
+ </button>
220
+
221
+ {/* Steps (children) */}
222
+ {isExpanded && (
223
+ <div className="pb-1">
224
+ {scenario.steps.map((step) => {
225
+ const isStepSelected = selectedScenario === scenario.id && selectedStep === step.id
226
+ return (
227
+ <button
228
+ key={step.id}
229
+ onClick={() => selectStep(scenario.id, step.id)}
230
+ className={`w-full h-[32px] flex items-center gap-2 pl-9 pr-3 text-left transition-colors ${
231
+ isStepSelected
232
+ ? 'bg-[#313244]/50 border-l-2 border-[#89b4fa]'
233
+ : 'hover:bg-[#1e1e2e] border-l-2 border-transparent'
234
+ }`}
235
+ >
236
+ <div className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${
237
+ isStepSelected ? 'bg-[#89b4fa]' : 'bg-[#45475a]'
238
+ }`} />
239
+ <span className={`text-xs ${isStepSelected ? 'text-[#cdd6f4]' : 'text-[#a6adc8]'}`}>
240
+ {step.name}
241
+ </span>
242
+ </button>
243
+ )
244
+ })}
245
+ </div>
246
+ )}
247
+ </div>
248
+ )
249
+ })}
250
+ </div>
251
+
252
+ {/* Resize handle on right edge */}
253
+ <div
254
+ onPointerDown={handleSidebarPointerDown}
255
+ className="absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-[#89b4fa]/30 transition-colors z-10 flex items-center justify-center group"
256
+ >
257
+ <GripVertical className="w-3 h-3 text-[#45475a] group-hover:text-[#6c7086] opacity-0 group-hover:opacity-100 transition-opacity" />
258
+ </div>
259
+ </div>
260
+
261
+ {/* Main Content — TabbedPromptEditor */}
262
+ <div className="flex-1 min-w-0 w-0 overflow-hidden border-l border-[#313244]">
263
+ <TabbedPromptEditor
264
+ prompts={currentPrompts}
265
+ onPromptChange={handlePromptChange}
266
+ tools={tools}
267
+ defaultPrompts={currentDefaultPrompts}
268
+ variables={currentVariables}
269
+ onReset={handleReset}
270
+ onSave={handleSave}
271
+ validateChecklist={validateChecklist}
272
+ />
273
+ </div>
274
+ </div>
275
+ )
276
+ }