@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,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSnippetsEditor — Form state and CRUD hook for the snippets editor
|
|
3
|
+
*
|
|
4
|
+
* Part of: Sections > Snippets Editor
|
|
5
|
+
*
|
|
6
|
+
* Manages selected snippet, form data, edit/add mode, validation, search,
|
|
7
|
+
* and CRUD operations via the provided API.
|
|
8
|
+
*
|
|
9
|
+
* AI agent notes:
|
|
10
|
+
* - This hook is used internally by SnippetsEditor but can also be used
|
|
11
|
+
* standalone for custom UIs
|
|
12
|
+
* - Validation checks: required fields, SNIPPET_NAME_REGEX, duplicate names
|
|
13
|
+
* - The hook does NOT hold the snippets array — it receives it as a param
|
|
14
|
+
* so the consuming app owns the source of truth
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useState, useCallback, useMemo } from 'react'
|
|
18
|
+
import { SNIPPET_NAME_REGEX, type SnippetData, type SnippetsEditorApi } from './types.ts'
|
|
19
|
+
|
|
20
|
+
interface SnippetFormData {
|
|
21
|
+
name: string
|
|
22
|
+
description: string
|
|
23
|
+
value: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const EMPTY_FORM: SnippetFormData = { name: '', description: '', value: '' }
|
|
27
|
+
|
|
28
|
+
export interface UseSnippetsEditorOptions {
|
|
29
|
+
api: SnippetsEditorApi
|
|
30
|
+
snippets: SnippetData[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UseSnippetsEditorReturn {
|
|
34
|
+
// Selection
|
|
35
|
+
selectedName: string | null
|
|
36
|
+
selectSnippet: (name: string | null) => void
|
|
37
|
+
|
|
38
|
+
// Search
|
|
39
|
+
searchQuery: string
|
|
40
|
+
setSearchQuery: (query: string) => void
|
|
41
|
+
filteredSnippets: SnippetData[]
|
|
42
|
+
|
|
43
|
+
// Form
|
|
44
|
+
formData: SnippetFormData
|
|
45
|
+
setFormField: (field: keyof SnippetFormData, value: string) => void
|
|
46
|
+
formError: string | null
|
|
47
|
+
isEditing: boolean
|
|
48
|
+
isAdding: boolean
|
|
49
|
+
|
|
50
|
+
// Actions
|
|
51
|
+
startAdd: () => void
|
|
52
|
+
startEdit: (snippet: SnippetData) => void
|
|
53
|
+
cancelForm: () => void
|
|
54
|
+
save: () => Promise<void>
|
|
55
|
+
remove: (name: string) => Promise<void>
|
|
56
|
+
resetForm: () => void
|
|
57
|
+
|
|
58
|
+
// State
|
|
59
|
+
isSaving: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function useSnippetsEditor({ api, snippets }: UseSnippetsEditorOptions): UseSnippetsEditorReturn {
|
|
63
|
+
const [selectedName, setSelectedName] = useState<string | null>(null)
|
|
64
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
65
|
+
const [formData, setFormData] = useState<SnippetFormData>(EMPTY_FORM)
|
|
66
|
+
const [formError, setFormError] = useState<string | null>(null)
|
|
67
|
+
const [isAdding, setIsAdding] = useState(false)
|
|
68
|
+
const [editingOriginalName, setEditingOriginalName] = useState<string | null>(null)
|
|
69
|
+
const [isSaving, setIsSaving] = useState(false)
|
|
70
|
+
|
|
71
|
+
const isEditing = editingOriginalName !== null
|
|
72
|
+
|
|
73
|
+
const filteredSnippets = useMemo(() => {
|
|
74
|
+
if (!searchQuery.trim()) return snippets
|
|
75
|
+
const q = searchQuery.toLowerCase()
|
|
76
|
+
return snippets.filter(
|
|
77
|
+
(s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q),
|
|
78
|
+
)
|
|
79
|
+
}, [snippets, searchQuery])
|
|
80
|
+
|
|
81
|
+
const selectSnippet = useCallback((name: string | null) => {
|
|
82
|
+
setSelectedName(name)
|
|
83
|
+
setIsAdding(false)
|
|
84
|
+
setEditingOriginalName(null)
|
|
85
|
+
setFormError(null)
|
|
86
|
+
if (name) {
|
|
87
|
+
const snippet = snippets.find((s) => s.name === name)
|
|
88
|
+
if (snippet) {
|
|
89
|
+
setFormData({ name: snippet.name, description: snippet.description, value: snippet.value })
|
|
90
|
+
setEditingOriginalName(snippet.name)
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
setFormData(EMPTY_FORM)
|
|
94
|
+
}
|
|
95
|
+
}, [snippets])
|
|
96
|
+
|
|
97
|
+
const setFormField = useCallback((field: keyof SnippetFormData, value: string) => {
|
|
98
|
+
setFormData((prev: SnippetFormData) => ({ ...prev, [field]: field === 'name' ? value.toUpperCase() : value }))
|
|
99
|
+
setFormError(null)
|
|
100
|
+
}, [])
|
|
101
|
+
|
|
102
|
+
const startAdd = useCallback(() => {
|
|
103
|
+
setIsAdding(true)
|
|
104
|
+
setEditingOriginalName(null)
|
|
105
|
+
setSelectedName(null)
|
|
106
|
+
setFormData(EMPTY_FORM)
|
|
107
|
+
setFormError(null)
|
|
108
|
+
}, [])
|
|
109
|
+
|
|
110
|
+
const startEdit = useCallback((snippet: SnippetData) => {
|
|
111
|
+
setSelectedName(snippet.name)
|
|
112
|
+
setEditingOriginalName(snippet.name)
|
|
113
|
+
setIsAdding(false)
|
|
114
|
+
setFormData({ name: snippet.name, description: snippet.description, value: snippet.value })
|
|
115
|
+
setFormError(null)
|
|
116
|
+
}, [])
|
|
117
|
+
|
|
118
|
+
const cancelForm = useCallback(() => {
|
|
119
|
+
setIsAdding(false)
|
|
120
|
+
setEditingOriginalName(null)
|
|
121
|
+
setSelectedName(null)
|
|
122
|
+
setFormData(EMPTY_FORM)
|
|
123
|
+
setFormError(null)
|
|
124
|
+
}, [])
|
|
125
|
+
|
|
126
|
+
const resetForm = useCallback(() => {
|
|
127
|
+
if (editingOriginalName) {
|
|
128
|
+
const snippet = snippets.find((s) => s.name === editingOriginalName)
|
|
129
|
+
if (snippet) {
|
|
130
|
+
setFormData({ name: snippet.name, description: snippet.description, value: snippet.value })
|
|
131
|
+
setFormError(null)
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
setFormData(EMPTY_FORM)
|
|
135
|
+
setFormError(null)
|
|
136
|
+
}
|
|
137
|
+
}, [editingOriginalName, snippets])
|
|
138
|
+
|
|
139
|
+
const validate = useCallback((data: SnippetFormData): string | null => {
|
|
140
|
+
if (!data.name.trim()) return 'Snippet name is required'
|
|
141
|
+
if (!SNIPPET_NAME_REGEX.test(data.name)) return 'Name must be uppercase with underscores (e.g., MY_SNIPPET)'
|
|
142
|
+
const isDuplicate = snippets.some(
|
|
143
|
+
(s) => s.name.toUpperCase() === data.name.toUpperCase() && s.name !== editingOriginalName,
|
|
144
|
+
)
|
|
145
|
+
if (isDuplicate) return 'A snippet with this name already exists'
|
|
146
|
+
if (!data.description.trim()) return 'Description is required'
|
|
147
|
+
return null
|
|
148
|
+
}, [snippets, editingOriginalName])
|
|
149
|
+
|
|
150
|
+
const save = useCallback(async () => {
|
|
151
|
+
const error = validate(formData)
|
|
152
|
+
if (error) {
|
|
153
|
+
setFormError(error)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const snippet: SnippetData = {
|
|
158
|
+
name: formData.name.toUpperCase(),
|
|
159
|
+
description: formData.description.trim(),
|
|
160
|
+
value: formData.value,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setIsSaving(true)
|
|
164
|
+
try {
|
|
165
|
+
if (editingOriginalName) {
|
|
166
|
+
await api.updateSnippet(editingOriginalName, snippet)
|
|
167
|
+
} else {
|
|
168
|
+
await api.addSnippet(snippet)
|
|
169
|
+
}
|
|
170
|
+
setSelectedName(snippet.name)
|
|
171
|
+
setEditingOriginalName(snippet.name)
|
|
172
|
+
setIsAdding(false)
|
|
173
|
+
setFormError(null)
|
|
174
|
+
} catch (err) {
|
|
175
|
+
setFormError(`Failed to save: ${err}`)
|
|
176
|
+
} finally {
|
|
177
|
+
setIsSaving(false)
|
|
178
|
+
}
|
|
179
|
+
}, [formData, editingOriginalName, api, validate])
|
|
180
|
+
|
|
181
|
+
const remove = useCallback(async (name: string) => {
|
|
182
|
+
setIsSaving(true)
|
|
183
|
+
try {
|
|
184
|
+
await api.removeSnippet(name)
|
|
185
|
+
if (selectedName === name) {
|
|
186
|
+
setSelectedName(null)
|
|
187
|
+
setEditingOriginalName(null)
|
|
188
|
+
setFormData(EMPTY_FORM)
|
|
189
|
+
setFormError(null)
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
setFormError(`Failed to delete: ${err}`)
|
|
193
|
+
} finally {
|
|
194
|
+
setIsSaving(false)
|
|
195
|
+
}
|
|
196
|
+
}, [api, selectedName])
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
selectedName,
|
|
200
|
+
selectSnippet,
|
|
201
|
+
searchQuery,
|
|
202
|
+
setSearchQuery,
|
|
203
|
+
filteredSnippets,
|
|
204
|
+
formData,
|
|
205
|
+
setFormField,
|
|
206
|
+
formError,
|
|
207
|
+
isEditing,
|
|
208
|
+
isAdding,
|
|
209
|
+
startAdd,
|
|
210
|
+
startEdit,
|
|
211
|
+
cancelForm,
|
|
212
|
+
save,
|
|
213
|
+
remove,
|
|
214
|
+
resetForm,
|
|
215
|
+
isSaving,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActionDialog - Standard dialog for AI execution workflows
|
|
3
|
+
*
|
|
4
|
+
* Built-in sections:
|
|
5
|
+
* 1. SelectionGrid - tool/item selection (single/multi) when items provided
|
|
6
|
+
* 2. Scenarios - second SelectionGrid for scenario selection with "Select All"
|
|
7
|
+
* 3. children - additional content
|
|
8
|
+
* 4. ExecutionDetailsPanel - mandatory execution configuration display
|
|
9
|
+
*
|
|
10
|
+
* Header: icon, title, subtitle, optional settings button
|
|
11
|
+
* Footer: FormActions (status text, cancel/submit)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect } from 'react'
|
|
15
|
+
import { createPortal } from 'react-dom'
|
|
16
|
+
import {
|
|
17
|
+
ArrowLeft, ArrowRight, ArrowUp, ArrowDown,
|
|
18
|
+
ChevronLeft, ChevronRight, ChevronUp, ChevronDown,
|
|
19
|
+
Check, X, Plus, Minus, Pencil, Trash2, Copy, Save,
|
|
20
|
+
RefreshCw, RotateCcw, Undo2, Redo2,
|
|
21
|
+
Search, Filter, Download, Upload, ExternalLink, Link2,
|
|
22
|
+
Eye, EyeOff, Lock, Unlock, Settings, MoreHorizontal, MoreVertical,
|
|
23
|
+
Info, HelpCircle,
|
|
24
|
+
User, Users, Folder, File, Image, Code, Terminal,
|
|
25
|
+
Star, Heart, Bell, Bookmark, Tag, Pin, Mail, Send,
|
|
26
|
+
Globe, Database, Cloud,
|
|
27
|
+
Wand2, Shield, ShieldCheck, Zap, Sparkles,
|
|
28
|
+
Play, Pause, Square, StopCircle,
|
|
29
|
+
Menu, GripVertical, Maximize2, Minimize2,
|
|
30
|
+
Scan, Webhook, Bot, Puzzle, Plug,
|
|
31
|
+
} from 'lucide-react'
|
|
32
|
+
import type { LucideIcon } from 'lucide-react'
|
|
33
|
+
import { IconButton } from './icon-button.tsx'
|
|
34
|
+
import type { IconName } from './icon-button.tsx'
|
|
35
|
+
import { FormActions } from './form-actions.tsx'
|
|
36
|
+
import { SelectionGrid, type SelectionCardItem, type CodingToolPresetConfig } from './selection-grid.tsx'
|
|
37
|
+
import { ExecutionDetailsPanel, type ExecutionDetailRow } from './execution-details-panel.tsx'
|
|
38
|
+
import { cn } from '../lib/cn.ts'
|
|
39
|
+
|
|
40
|
+
const dialogIconMap: Record<string, LucideIcon> = {
|
|
41
|
+
'arrow-left': ArrowLeft, 'arrow-right': ArrowRight, 'arrow-up': ArrowUp, 'arrow-down': ArrowDown,
|
|
42
|
+
'chevron-left': ChevronLeft, 'chevron-right': ChevronRight, 'chevron-up': ChevronUp, 'chevron-down': ChevronDown,
|
|
43
|
+
'check': Check, 'x': X, 'plus': Plus, 'minus': Minus, 'pencil': Pencil, 'trash': Trash2, 'copy': Copy, 'save': Save,
|
|
44
|
+
'refresh': RefreshCw, 'rotate': RotateCcw, 'undo': Undo2, 'redo': Redo2,
|
|
45
|
+
'search': Search, 'filter': Filter, 'download': Download, 'upload': Upload, 'external-link': ExternalLink, 'link': Link2,
|
|
46
|
+
'eye': Eye, 'eye-off': EyeOff, 'lock': Lock, 'unlock': Unlock, 'settings': Settings, 'more-h': MoreHorizontal, 'more-v': MoreVertical,
|
|
47
|
+
'info': Info, 'help': HelpCircle,
|
|
48
|
+
'user': User, 'users': Users, 'folder': Folder, 'file': File, 'image': Image, 'code': Code, 'terminal': Terminal,
|
|
49
|
+
'star': Star, 'heart': Heart, 'bell': Bell, 'bookmark': Bookmark, 'tag': Tag, 'pin': Pin, 'mail': Mail, 'send': Send,
|
|
50
|
+
'globe': Globe, 'database': Database, 'cloud': Cloud,
|
|
51
|
+
'wand': Wand2, 'shield': Shield, 'shield-check': ShieldCheck, 'zap': Zap, 'sparkles': Sparkles,
|
|
52
|
+
'play': Play, 'pause': Pause, 'stop': Square, 'stop-circle': StopCircle, 'scan': Scan,
|
|
53
|
+
'menu': Menu, 'grip': GripVertical, 'maximize': Maximize2, 'minimize': Minimize2,
|
|
54
|
+
'webhook': Webhook, 'bot': Bot, 'puzzle': Puzzle, 'plug': Plug,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ActionDialogProps {
|
|
58
|
+
/** Dialog title */
|
|
59
|
+
title: string
|
|
60
|
+
/** Subtitle text next to title */
|
|
61
|
+
subtitle?: string
|
|
62
|
+
/** Icon displayed before title */
|
|
63
|
+
icon?: IconName
|
|
64
|
+
/** Icon color */
|
|
65
|
+
iconColor?: string
|
|
66
|
+
/** Settings gear button callback */
|
|
67
|
+
onSettings?: () => void
|
|
68
|
+
/** Cancel/close callback */
|
|
69
|
+
onCancel?: () => void
|
|
70
|
+
/** Submit/action callback */
|
|
71
|
+
onSubmit?: () => void
|
|
72
|
+
/** Submit button label (shown in tooltip) */
|
|
73
|
+
submitLabel?: string
|
|
74
|
+
/** Submit button icon */
|
|
75
|
+
submitIcon?: IconName
|
|
76
|
+
/** Submit button color variant */
|
|
77
|
+
submitColor?: string
|
|
78
|
+
/** Disable submit button */
|
|
79
|
+
submitDisabled?: boolean
|
|
80
|
+
/** Status text shown in footer left */
|
|
81
|
+
statusText?: string
|
|
82
|
+
|
|
83
|
+
// ── Selection section (optional) ─────────────────────────────────────────
|
|
84
|
+
/** Label above the selection grid (e.g. "AI Tool:") */
|
|
85
|
+
selectionLabel?: string
|
|
86
|
+
/** Custom selection items - renders SelectionGrid when provided */
|
|
87
|
+
items?: SelectionCardItem[]
|
|
88
|
+
/** Built-in AI tool presets. Ignored when `items` is provided. */
|
|
89
|
+
presets?: CodingToolPresetConfig[]
|
|
90
|
+
/** Currently selected item IDs */
|
|
91
|
+
selectedIds?: string[]
|
|
92
|
+
/** Selection change handler */
|
|
93
|
+
onSelect?: (ids: string[]) => void
|
|
94
|
+
/** Single or multiple selection */
|
|
95
|
+
selectionMode?: 'single' | 'multiple'
|
|
96
|
+
/** Grid (icon-top cards) or list (icon-left rows) */
|
|
97
|
+
selectionLayout?: 'grid' | 'list'
|
|
98
|
+
/** Grid column count (auto if not set) */
|
|
99
|
+
selectionColumns?: number
|
|
100
|
+
|
|
101
|
+
// ── Scenarios section (optional) ─────────────────────────────────────────
|
|
102
|
+
/** Label above scenarios (e.g. "Select scenarios to run:") */
|
|
103
|
+
scenarioLabel?: string
|
|
104
|
+
/** Scenario items - renders second SelectionGrid when provided */
|
|
105
|
+
scenarios?: SelectionCardItem[]
|
|
106
|
+
/** Currently selected scenario IDs */
|
|
107
|
+
selectedScenarioIds?: string[]
|
|
108
|
+
/** Scenario selection change handler */
|
|
109
|
+
onSelectScenarios?: (ids: string[]) => void
|
|
110
|
+
/** Scenario grid layout */
|
|
111
|
+
scenarioLayout?: 'grid' | 'list'
|
|
112
|
+
/** Scenario grid column count */
|
|
113
|
+
scenarioColumns?: number
|
|
114
|
+
|
|
115
|
+
// ── Execution details section (mandatory) ────────────────────────────────
|
|
116
|
+
/** Execution detail rows (Tool, Permissions, Output, CLI Flags, Changes) */
|
|
117
|
+
executionDetails: ExecutionDetailRow[]
|
|
118
|
+
/** Whether direct file edits are allowed */
|
|
119
|
+
allowDirectEdits?: boolean
|
|
120
|
+
/** Callback to toggle direct edits - shows the toggle when provided */
|
|
121
|
+
onAllowDirectEditsChange?: (value: boolean) => void
|
|
122
|
+
/** Warning message shown in the execution details section */
|
|
123
|
+
executionWarning?: string
|
|
124
|
+
|
|
125
|
+
// ── Additional content ───────────────────────────────────────────────────
|
|
126
|
+
/** Additional content rendered between scenarios and execution details */
|
|
127
|
+
children?: React.ReactNode
|
|
128
|
+
/** Additional className */
|
|
129
|
+
className?: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function ActionDialog({
|
|
133
|
+
title,
|
|
134
|
+
subtitle,
|
|
135
|
+
icon,
|
|
136
|
+
iconColor,
|
|
137
|
+
onSettings,
|
|
138
|
+
onCancel,
|
|
139
|
+
onSubmit,
|
|
140
|
+
submitLabel = 'Submit',
|
|
141
|
+
submitIcon = 'chevron-right',
|
|
142
|
+
submitColor = 'blue',
|
|
143
|
+
submitDisabled = false,
|
|
144
|
+
statusText,
|
|
145
|
+
selectionLabel,
|
|
146
|
+
items,
|
|
147
|
+
presets,
|
|
148
|
+
selectedIds,
|
|
149
|
+
onSelect,
|
|
150
|
+
selectionMode = 'multiple',
|
|
151
|
+
selectionLayout = 'grid',
|
|
152
|
+
selectionColumns,
|
|
153
|
+
scenarioLabel = 'Select scenarios to run:',
|
|
154
|
+
scenarios,
|
|
155
|
+
selectedScenarioIds,
|
|
156
|
+
onSelectScenarios,
|
|
157
|
+
scenarioLayout = 'list',
|
|
158
|
+
scenarioColumns = 2,
|
|
159
|
+
executionDetails,
|
|
160
|
+
allowDirectEdits,
|
|
161
|
+
onAllowDirectEditsChange,
|
|
162
|
+
executionWarning,
|
|
163
|
+
children,
|
|
164
|
+
className,
|
|
165
|
+
}: ActionDialogProps) {
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
168
|
+
if (e.key === 'Escape') onCancel?.()
|
|
169
|
+
}
|
|
170
|
+
document.addEventListener('keydown', handleEscape)
|
|
171
|
+
return () => document.removeEventListener('keydown', handleEscape)
|
|
172
|
+
}, [onCancel])
|
|
173
|
+
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
document.body.style.overflow = 'hidden'
|
|
176
|
+
return () => { document.body.style.overflow = '' }
|
|
177
|
+
}, [])
|
|
178
|
+
|
|
179
|
+
const Icon = icon ? dialogIconMap[icon] : null
|
|
180
|
+
const hasSelection = ((items && items.length > 0) || (presets && presets.length > 0)) && selectedIds && onSelect
|
|
181
|
+
const hasScenarios = scenarios && scenarios.length > 0 && selectedScenarioIds && onSelectScenarios
|
|
182
|
+
const hasExecutionDetails = executionDetails.length > 0 || onAllowDirectEditsChange || executionWarning
|
|
183
|
+
|
|
184
|
+
const allScenariosSelected = hasScenarios && selectedScenarioIds!.length === scenarios!.filter(s => !s.disabled).length
|
|
185
|
+
|
|
186
|
+
function handleSelectAllScenarios() {
|
|
187
|
+
if (!scenarios || !onSelectScenarios) return
|
|
188
|
+
if (allScenariosSelected) {
|
|
189
|
+
onSelectScenarios([])
|
|
190
|
+
} else {
|
|
191
|
+
onSelectScenarios(scenarios.filter(s => !s.disabled).map(s => s.id))
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return createPortal(
|
|
196
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
197
|
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onCancel} />
|
|
198
|
+
<div
|
|
199
|
+
className={cn(
|
|
200
|
+
'relative bg-neutral-950 border border-neutral-700 rounded-xl shadow-2xl w-full max-w-[800px] mx-4 flex flex-col',
|
|
201
|
+
'max-h-[80vh]',
|
|
202
|
+
className,
|
|
203
|
+
)}
|
|
204
|
+
>
|
|
205
|
+
{/* Header */}
|
|
206
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-neutral-700 flex-shrink-0">
|
|
207
|
+
{Icon && (
|
|
208
|
+
<Icon
|
|
209
|
+
className="w-4 h-4 flex-shrink-0"
|
|
210
|
+
style={iconColor ? { color: iconColor } : undefined}
|
|
211
|
+
/>
|
|
212
|
+
)}
|
|
213
|
+
<div className="flex flex-col">
|
|
214
|
+
<span className="text-sm font-semibold text-neutral-200">
|
|
215
|
+
{title}
|
|
216
|
+
</span>
|
|
217
|
+
{subtitle && (
|
|
218
|
+
<span className="text-xs text-neutral-500">{subtitle}</span>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
<div className="flex-1" />
|
|
222
|
+
{onSettings && (
|
|
223
|
+
<IconButton
|
|
224
|
+
icon="settings"
|
|
225
|
+
size="sm"
|
|
226
|
+
color="neutral"
|
|
227
|
+
onClick={onSettings}
|
|
228
|
+
tooltip={{ description: 'Open settings' }}
|
|
229
|
+
/>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{/* Content */}
|
|
234
|
+
<div className="px-4 py-4 overflow-y-auto flex-1 min-h-0 space-y-4">
|
|
235
|
+
{/* Built-in SelectionGrid for tools/items */}
|
|
236
|
+
{hasSelection && (
|
|
237
|
+
<div>
|
|
238
|
+
{selectionLabel && (
|
|
239
|
+
<div className="text-xs text-neutral-500 mb-2">{selectionLabel}</div>
|
|
240
|
+
)}
|
|
241
|
+
<SelectionGrid
|
|
242
|
+
items={items}
|
|
243
|
+
presets={!items ? presets : undefined}
|
|
244
|
+
selectedIds={selectedIds}
|
|
245
|
+
onSelect={onSelect}
|
|
246
|
+
mode={selectionMode}
|
|
247
|
+
layout={selectionLayout}
|
|
248
|
+
columns={selectionColumns}
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
|
|
253
|
+
{/* Built-in SelectionGrid for scenarios */}
|
|
254
|
+
{hasScenarios && (
|
|
255
|
+
<div>
|
|
256
|
+
<div className="flex items-center justify-between mb-2">
|
|
257
|
+
<span className="text-xs text-neutral-500">{scenarioLabel}</span>
|
|
258
|
+
<button
|
|
259
|
+
type="button"
|
|
260
|
+
onClick={handleSelectAllScenarios}
|
|
261
|
+
className="text-xs text-blue-400 hover:text-blue-300 transition-colors cursor-pointer"
|
|
262
|
+
>
|
|
263
|
+
{allScenariosSelected ? 'Deselect All' : 'Select All'}
|
|
264
|
+
</button>
|
|
265
|
+
</div>
|
|
266
|
+
<SelectionGrid
|
|
267
|
+
items={scenarios}
|
|
268
|
+
selectedIds={selectedScenarioIds}
|
|
269
|
+
onSelect={onSelectScenarios}
|
|
270
|
+
mode="multiple"
|
|
271
|
+
layout={scenarioLayout}
|
|
272
|
+
columns={scenarioColumns}
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
{/* Additional content */}
|
|
278
|
+
{children}
|
|
279
|
+
|
|
280
|
+
{/* Built-in ExecutionDetailsPanel */}
|
|
281
|
+
{hasExecutionDetails && (
|
|
282
|
+
<ExecutionDetailsPanel
|
|
283
|
+
details={executionDetails}
|
|
284
|
+
allowDirectEdits={allowDirectEdits}
|
|
285
|
+
onAllowDirectEditsChange={onAllowDirectEditsChange}
|
|
286
|
+
warningMessage={executionWarning}
|
|
287
|
+
/>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{/* Footer */}
|
|
292
|
+
<div className="flex-shrink-0">
|
|
293
|
+
<FormActions
|
|
294
|
+
padding="modal"
|
|
295
|
+
statusText={statusText}
|
|
296
|
+
onCancel={onCancel}
|
|
297
|
+
cancelTooltip="Close this dialog"
|
|
298
|
+
onConfirm={onSubmit}
|
|
299
|
+
confirmTooltip={submitLabel}
|
|
300
|
+
confirmIcon={submitIcon}
|
|
301
|
+
confirmColor={submitColor as 'blue'}
|
|
302
|
+
confirmDisabled={submitDisabled}
|
|
303
|
+
/>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
</div>,
|
|
307
|
+
document.body,
|
|
308
|
+
)
|
|
309
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { IconButton, type IconButtonProps, type IconName } from './icon-button.tsx'
|
|
3
|
+
|
|
4
|
+
export type AiActionStatus = 'idle' | 'running' | 'completed'
|
|
5
|
+
export type AiCompletionResult = 'success' | 'partial' | 'error'
|
|
6
|
+
|
|
7
|
+
export interface AiActionButtonProps {
|
|
8
|
+
/** Current execution status */
|
|
9
|
+
status?: AiActionStatus
|
|
10
|
+
/** Result when status is 'completed' */
|
|
11
|
+
completionResult?: AiCompletionResult
|
|
12
|
+
|
|
13
|
+
/** Icon shown in default state */
|
|
14
|
+
icon: IconName
|
|
15
|
+
/** Icon shown when running (defaults to 'loader') */
|
|
16
|
+
runningIcon?: IconName
|
|
17
|
+
/** Icon shown when completed successfully (defaults to 'check-circle') */
|
|
18
|
+
completedIcon?: IconName
|
|
19
|
+
|
|
20
|
+
/** Base tooltip (title/description modified based on state) */
|
|
21
|
+
tooltip: { title: string; description: string }
|
|
22
|
+
/** Tooltip title when running (defaults to "Show {title} Progress") */
|
|
23
|
+
runningTooltipTitle?: string
|
|
24
|
+
/** Tooltip title when completed (defaults to "View {title} Results") */
|
|
25
|
+
completedTooltipTitle?: string
|
|
26
|
+
|
|
27
|
+
onClick?: () => void
|
|
28
|
+
|
|
29
|
+
/** Force disabled state (in addition to automatic running state) */
|
|
30
|
+
disabled?: boolean
|
|
31
|
+
/** Custom reason when force-disabled */
|
|
32
|
+
disabledReason?: string
|
|
33
|
+
|
|
34
|
+
/** Color in default state */
|
|
35
|
+
color?: IconButtonProps['color']
|
|
36
|
+
/** Color when completed successfully (defaults to 'green') */
|
|
37
|
+
completedColor?: IconButtonProps['color']
|
|
38
|
+
|
|
39
|
+
size?: IconButtonProps['size']
|
|
40
|
+
className?: string
|
|
41
|
+
testId?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const COMPLETION_COLORS: Record<AiCompletionResult, IconButtonProps['color']> = {
|
|
45
|
+
success: 'green',
|
|
46
|
+
partial: 'amber',
|
|
47
|
+
error: 'red',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const COMPLETION_ICONS: Record<AiCompletionResult, IconName> = {
|
|
51
|
+
success: 'check-circle',
|
|
52
|
+
partial: 'alert-triangle',
|
|
53
|
+
error: 'x-circle',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function AiActionButton({
|
|
57
|
+
status = 'idle',
|
|
58
|
+
completionResult,
|
|
59
|
+
icon,
|
|
60
|
+
runningIcon,
|
|
61
|
+
completedIcon,
|
|
62
|
+
tooltip,
|
|
63
|
+
runningTooltipTitle,
|
|
64
|
+
completedTooltipTitle,
|
|
65
|
+
onClick,
|
|
66
|
+
disabled: forceDisabled = false,
|
|
67
|
+
disabledReason,
|
|
68
|
+
color = 'neutral',
|
|
69
|
+
completedColor = 'green',
|
|
70
|
+
size = 'sm',
|
|
71
|
+
className,
|
|
72
|
+
testId,
|
|
73
|
+
}: AiActionButtonProps) {
|
|
74
|
+
const isRunning = status === 'running'
|
|
75
|
+
const isCompleted = status === 'completed'
|
|
76
|
+
|
|
77
|
+
const resolvedIcon: IconName = useMemo(() => {
|
|
78
|
+
if (isRunning) return runningIcon ?? 'loader'
|
|
79
|
+
if (isCompleted && completionResult) {
|
|
80
|
+
return completionResult === 'success'
|
|
81
|
+
? (completedIcon ?? COMPLETION_ICONS[completionResult])
|
|
82
|
+
: COMPLETION_ICONS[completionResult]
|
|
83
|
+
}
|
|
84
|
+
return icon
|
|
85
|
+
}, [isRunning, isCompleted, completionResult, icon, runningIcon, completedIcon])
|
|
86
|
+
|
|
87
|
+
const resolvedColor = useMemo(() => {
|
|
88
|
+
if (isCompleted && completionResult) {
|
|
89
|
+
return completionResult === 'success'
|
|
90
|
+
? completedColor
|
|
91
|
+
: COMPLETION_COLORS[completionResult]
|
|
92
|
+
}
|
|
93
|
+
return color
|
|
94
|
+
}, [isCompleted, completionResult, completedColor, color])
|
|
95
|
+
|
|
96
|
+
const resolvedTooltip = useMemo(() => {
|
|
97
|
+
if (forceDisabled && disabledReason) {
|
|
98
|
+
return {
|
|
99
|
+
title: `${tooltip.title} Unavailable`,
|
|
100
|
+
description: disabledReason,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (isRunning) {
|
|
105
|
+
return {
|
|
106
|
+
title: runningTooltipTitle ?? `Show ${tooltip.title} Progress`,
|
|
107
|
+
description: 'Click to view current progress',
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (isCompleted) {
|
|
112
|
+
return {
|
|
113
|
+
title: completedTooltipTitle ?? `View ${tooltip.title} Results`,
|
|
114
|
+
description: 'Click to view completed results',
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return tooltip
|
|
119
|
+
}, [tooltip, forceDisabled, disabledReason, isRunning, isCompleted, runningTooltipTitle, completedTooltipTitle])
|
|
120
|
+
|
|
121
|
+
const isDisabled = forceDisabled
|
|
122
|
+
const blinkClass = isCompleted ? 'animate-pulse' : ''
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<IconButton
|
|
126
|
+
icon={resolvedIcon}
|
|
127
|
+
color={resolvedColor}
|
|
128
|
+
size={size}
|
|
129
|
+
disabled={isDisabled}
|
|
130
|
+
onClick={isDisabled ? () => {} : onClick}
|
|
131
|
+
tooltip={resolvedTooltip}
|
|
132
|
+
active={isRunning}
|
|
133
|
+
className={`${className ?? ''} ${blinkClass}`.trim() || undefined}
|
|
134
|
+
testId={testId}
|
|
135
|
+
/>
|
|
136
|
+
)
|
|
137
|
+
}
|