@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,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
|
+
}
|