@thehoneyjar/sigil-hud 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/LICENSE.md +660 -0
- package/README.md +146 -0
- package/dist/index.d.ts +911 -0
- package/dist/index.js +3079 -0
- package/package.json +68 -0
- package/src/components/DataSourceIndicator.tsx +185 -0
- package/src/components/DiagnosticsPanel.tsx +444 -0
- package/src/components/FeedbackPrompt.tsx +348 -0
- package/src/components/HudPanel.tsx +179 -0
- package/src/components/HudTrigger.tsx +81 -0
- package/src/components/IssueList.tsx +228 -0
- package/src/components/LensPanel.tsx +286 -0
- package/src/components/ObservationCaptureModal.tsx +502 -0
- package/src/components/PhysicsAnalysis.tsx +273 -0
- package/src/components/SimulationPanel.tsx +173 -0
- package/src/components/StateComparison.tsx +238 -0
- package/src/hooks/useDataSource.ts +324 -0
- package/src/hooks/useKeyboardShortcuts.ts +125 -0
- package/src/hooks/useObservationCapture.ts +154 -0
- package/src/hooks/useSignalCapture.ts +138 -0
- package/src/index.ts +112 -0
- package/src/providers/HudProvider.tsx +115 -0
- package/src/store.ts +60 -0
- package/src/styles/theme.ts +256 -0
- package/src/types.ts +276 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Prompt Component
|
|
3
|
+
*
|
|
4
|
+
* Prompts users for feedback after diagnostics or crafting.
|
|
5
|
+
* Emits MODIFY signals and links to observations.
|
|
6
|
+
* Implements TASK-204 requirements.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useCallback } from 'react'
|
|
10
|
+
import { useSignalCapture } from '../hooks/useSignalCapture'
|
|
11
|
+
import { useObservationCapture } from '../hooks/useObservationCapture'
|
|
12
|
+
import type { Signal, Observation } from '../types'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Feedback option
|
|
16
|
+
*/
|
|
17
|
+
interface FeedbackOption {
|
|
18
|
+
id: string
|
|
19
|
+
label: string
|
|
20
|
+
description: string
|
|
21
|
+
signal: 'ACCEPT' | 'MODIFY' | 'REJECT'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default feedback options
|
|
26
|
+
*/
|
|
27
|
+
const defaultOptions: FeedbackOption[] = [
|
|
28
|
+
{
|
|
29
|
+
id: 'looks-good',
|
|
30
|
+
label: 'Looks good',
|
|
31
|
+
description: 'The physics feel right for this use case',
|
|
32
|
+
signal: 'ACCEPT',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'timing-off',
|
|
36
|
+
label: 'Timing feels off',
|
|
37
|
+
description: 'The duration should be faster or slower',
|
|
38
|
+
signal: 'MODIFY',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'animation-wrong',
|
|
42
|
+
label: 'Animation wrong',
|
|
43
|
+
description: 'The easing or motion feels incorrect',
|
|
44
|
+
signal: 'MODIFY',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'needs-confirmation',
|
|
48
|
+
label: 'Needs confirmation',
|
|
49
|
+
description: 'Should have (or shouldn\'t have) a confirmation step',
|
|
50
|
+
signal: 'MODIFY',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'other',
|
|
54
|
+
label: 'Something else',
|
|
55
|
+
description: 'Describe what feels wrong',
|
|
56
|
+
signal: 'MODIFY',
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Props for FeedbackPrompt
|
|
62
|
+
*/
|
|
63
|
+
export interface FeedbackPromptProps {
|
|
64
|
+
/** Component name being reviewed */
|
|
65
|
+
componentName: string
|
|
66
|
+
/** Detected effect type */
|
|
67
|
+
effect: string
|
|
68
|
+
/** Current physics values */
|
|
69
|
+
physics?: {
|
|
70
|
+
behavioral?: { sync: string; timing: string; confirmation: string }
|
|
71
|
+
animation?: { easing: string; duration: string }
|
|
72
|
+
material?: { surface: string; shadow: string; radius: string }
|
|
73
|
+
}
|
|
74
|
+
/** Custom feedback options */
|
|
75
|
+
options?: FeedbackOption[]
|
|
76
|
+
/** Callback when feedback is submitted */
|
|
77
|
+
onFeedback?: (signal: Signal, observation?: Observation) => void
|
|
78
|
+
/** Callback to close the prompt */
|
|
79
|
+
onClose?: () => void
|
|
80
|
+
/** Whether to show the prompt */
|
|
81
|
+
visible?: boolean
|
|
82
|
+
/** Custom class name */
|
|
83
|
+
className?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Feedback Prompt Component
|
|
88
|
+
*/
|
|
89
|
+
export function FeedbackPrompt({
|
|
90
|
+
componentName,
|
|
91
|
+
effect,
|
|
92
|
+
physics,
|
|
93
|
+
options = defaultOptions,
|
|
94
|
+
onFeedback,
|
|
95
|
+
onClose,
|
|
96
|
+
visible = true,
|
|
97
|
+
className = '',
|
|
98
|
+
}: FeedbackPromptProps) {
|
|
99
|
+
const [selectedOption, setSelectedOption] = useState<FeedbackOption | null>(null)
|
|
100
|
+
const [customFeedback, setCustomFeedback] = useState('')
|
|
101
|
+
const [showDetailInput, setShowDetailInput] = useState(false)
|
|
102
|
+
|
|
103
|
+
const { accept, modify, reject } = useSignalCapture({ enabled: true })
|
|
104
|
+
const { captureInsight, captureIssue } = useObservationCapture({ enabled: true })
|
|
105
|
+
|
|
106
|
+
// Handle option selection
|
|
107
|
+
const handleSelectOption = useCallback((option: FeedbackOption) => {
|
|
108
|
+
setSelectedOption(option)
|
|
109
|
+
|
|
110
|
+
if (option.signal === 'ACCEPT') {
|
|
111
|
+
// Immediate accept - no detail needed
|
|
112
|
+
handleSubmit(option, '')
|
|
113
|
+
} else if (option.id === 'other') {
|
|
114
|
+
// Show detail input for custom feedback
|
|
115
|
+
setShowDetailInput(true)
|
|
116
|
+
} else {
|
|
117
|
+
// Show detail input for MODIFY signals
|
|
118
|
+
setShowDetailInput(true)
|
|
119
|
+
}
|
|
120
|
+
}, [])
|
|
121
|
+
|
|
122
|
+
// Handle submit
|
|
123
|
+
const handleSubmit = useCallback(
|
|
124
|
+
async (option: FeedbackOption, detail: string) => {
|
|
125
|
+
let observation: Observation | undefined
|
|
126
|
+
|
|
127
|
+
if (option.signal === 'ACCEPT') {
|
|
128
|
+
await accept(componentName, effect, 'diagnose')
|
|
129
|
+
} else if (option.signal === 'MODIFY') {
|
|
130
|
+
// Capture an observation for the modification
|
|
131
|
+
const content = detail || option.description
|
|
132
|
+
observation = await captureInsight(content, {
|
|
133
|
+
component: componentName,
|
|
134
|
+
effect,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Emit MODIFY signal
|
|
138
|
+
await modify(
|
|
139
|
+
componentName,
|
|
140
|
+
effect,
|
|
141
|
+
{ from: 'current', to: content },
|
|
142
|
+
{ inference: `User feedback: ${option.label}` }
|
|
143
|
+
)
|
|
144
|
+
} else if (option.signal === 'REJECT') {
|
|
145
|
+
// Capture an issue observation
|
|
146
|
+
observation = await captureIssue(detail || option.description, {
|
|
147
|
+
component: componentName,
|
|
148
|
+
effect,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
await reject(componentName, effect, detail || option.description)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Notify callback
|
|
155
|
+
const signal: Signal = {
|
|
156
|
+
timestamp: new Date().toISOString(),
|
|
157
|
+
signal: option.signal,
|
|
158
|
+
source: 'hud',
|
|
159
|
+
component: {
|
|
160
|
+
name: componentName,
|
|
161
|
+
effect,
|
|
162
|
+
craft_type: 'diagnose',
|
|
163
|
+
},
|
|
164
|
+
physics,
|
|
165
|
+
hud_context: {
|
|
166
|
+
panel_visible: true,
|
|
167
|
+
diagnostics_shown: true,
|
|
168
|
+
observation_linked: observation?.id,
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
onFeedback?.(signal, observation)
|
|
173
|
+
|
|
174
|
+
// Reset and close
|
|
175
|
+
setSelectedOption(null)
|
|
176
|
+
setCustomFeedback('')
|
|
177
|
+
setShowDetailInput(false)
|
|
178
|
+
onClose?.()
|
|
179
|
+
},
|
|
180
|
+
[componentName, effect, physics, accept, modify, reject, captureInsight, captureIssue, onFeedback, onClose]
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
// Handle cancel
|
|
184
|
+
const handleCancel = useCallback(() => {
|
|
185
|
+
setSelectedOption(null)
|
|
186
|
+
setCustomFeedback('')
|
|
187
|
+
setShowDetailInput(false)
|
|
188
|
+
onClose?.()
|
|
189
|
+
}, [onClose])
|
|
190
|
+
|
|
191
|
+
if (!visible) return null
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div className={className} style={styles.container}>
|
|
195
|
+
{/* Prompt */}
|
|
196
|
+
<div style={styles.promptRow}>
|
|
197
|
+
<span style={styles.promptText}>Does this feel right for your user?</span>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Options */}
|
|
201
|
+
{!showDetailInput && (
|
|
202
|
+
<div style={styles.optionsGrid}>
|
|
203
|
+
{options.map((option) => (
|
|
204
|
+
<button
|
|
205
|
+
key={option.id}
|
|
206
|
+
onClick={() => handleSelectOption(option)}
|
|
207
|
+
style={{
|
|
208
|
+
...styles.optionButton,
|
|
209
|
+
borderColor:
|
|
210
|
+
option.signal === 'ACCEPT'
|
|
211
|
+
? 'rgba(34, 197, 94, 0.3)'
|
|
212
|
+
: option.signal === 'REJECT'
|
|
213
|
+
? 'rgba(239, 68, 68, 0.3)'
|
|
214
|
+
: 'rgba(255, 255, 255, 0.1)',
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
<span style={styles.optionLabel}>{option.label}</span>
|
|
218
|
+
</button>
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
{/* Detail Input */}
|
|
224
|
+
{showDetailInput && selectedOption && (
|
|
225
|
+
<div style={styles.detailSection}>
|
|
226
|
+
<p style={styles.detailPrompt}>
|
|
227
|
+
{selectedOption.id === 'other'
|
|
228
|
+
? 'What feels wrong?'
|
|
229
|
+
: `Describe what would feel better:`}
|
|
230
|
+
</p>
|
|
231
|
+
<textarea
|
|
232
|
+
value={customFeedback}
|
|
233
|
+
onChange={(e) => setCustomFeedback(e.target.value)}
|
|
234
|
+
placeholder={
|
|
235
|
+
selectedOption.id === 'timing-off'
|
|
236
|
+
? 'e.g., "Should be faster, around 500ms..."'
|
|
237
|
+
: selectedOption.id === 'animation-wrong'
|
|
238
|
+
? 'e.g., "Should use spring instead of ease-out..."'
|
|
239
|
+
: 'Describe the issue or desired feel...'
|
|
240
|
+
}
|
|
241
|
+
rows={2}
|
|
242
|
+
style={styles.detailTextarea}
|
|
243
|
+
autoFocus
|
|
244
|
+
/>
|
|
245
|
+
<div style={styles.detailActions}>
|
|
246
|
+
<button onClick={handleCancel} style={styles.cancelButton}>
|
|
247
|
+
Cancel
|
|
248
|
+
</button>
|
|
249
|
+
<button
|
|
250
|
+
onClick={() => handleSubmit(selectedOption, customFeedback)}
|
|
251
|
+
disabled={selectedOption.id === 'other' && !customFeedback.trim()}
|
|
252
|
+
style={{
|
|
253
|
+
...styles.submitButton,
|
|
254
|
+
opacity: selectedOption.id === 'other' && !customFeedback.trim() ? 0.5 : 1,
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
Submit Feedback
|
|
258
|
+
</button>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Styles
|
|
268
|
+
*/
|
|
269
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
270
|
+
container: {
|
|
271
|
+
padding: '12px',
|
|
272
|
+
backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
|
273
|
+
borderRadius: '8px',
|
|
274
|
+
border: '1px solid rgba(255, 255, 255, 0.05)',
|
|
275
|
+
},
|
|
276
|
+
promptRow: {
|
|
277
|
+
marginBottom: '12px',
|
|
278
|
+
},
|
|
279
|
+
promptText: {
|
|
280
|
+
fontSize: '11px',
|
|
281
|
+
fontWeight: 600,
|
|
282
|
+
color: '#888',
|
|
283
|
+
textTransform: 'uppercase',
|
|
284
|
+
letterSpacing: '0.5px',
|
|
285
|
+
},
|
|
286
|
+
optionsGrid: {
|
|
287
|
+
display: 'grid',
|
|
288
|
+
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
289
|
+
gap: '6px',
|
|
290
|
+
},
|
|
291
|
+
optionButton: {
|
|
292
|
+
padding: '8px 10px',
|
|
293
|
+
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
|
294
|
+
border: '1px solid',
|
|
295
|
+
borderRadius: '4px',
|
|
296
|
+
textAlign: 'left',
|
|
297
|
+
cursor: 'pointer',
|
|
298
|
+
transition: 'all 0.15s ease-out',
|
|
299
|
+
},
|
|
300
|
+
optionLabel: {
|
|
301
|
+
fontSize: '11px',
|
|
302
|
+
color: '#fff',
|
|
303
|
+
},
|
|
304
|
+
detailSection: {
|
|
305
|
+
display: 'flex',
|
|
306
|
+
flexDirection: 'column',
|
|
307
|
+
gap: '8px',
|
|
308
|
+
},
|
|
309
|
+
detailPrompt: {
|
|
310
|
+
margin: 0,
|
|
311
|
+
fontSize: '12px',
|
|
312
|
+
color: '#888',
|
|
313
|
+
},
|
|
314
|
+
detailTextarea: {
|
|
315
|
+
width: '100%',
|
|
316
|
+
padding: '8px',
|
|
317
|
+
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
|
318
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
319
|
+
borderRadius: '4px',
|
|
320
|
+
color: '#fff',
|
|
321
|
+
fontSize: '11px',
|
|
322
|
+
fontFamily: 'inherit',
|
|
323
|
+
resize: 'vertical',
|
|
324
|
+
},
|
|
325
|
+
detailActions: {
|
|
326
|
+
display: 'flex',
|
|
327
|
+
justifyContent: 'flex-end',
|
|
328
|
+
gap: '8px',
|
|
329
|
+
},
|
|
330
|
+
cancelButton: {
|
|
331
|
+
padding: '6px 12px',
|
|
332
|
+
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
|
333
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
334
|
+
borderRadius: '4px',
|
|
335
|
+
color: '#888',
|
|
336
|
+
fontSize: '11px',
|
|
337
|
+
cursor: 'pointer',
|
|
338
|
+
},
|
|
339
|
+
submitButton: {
|
|
340
|
+
padding: '6px 12px',
|
|
341
|
+
backgroundColor: 'rgba(59, 130, 246, 0.2)',
|
|
342
|
+
border: '1px solid rgba(59, 130, 246, 0.3)',
|
|
343
|
+
borderRadius: '4px',
|
|
344
|
+
color: '#3b82f6',
|
|
345
|
+
fontSize: '11px',
|
|
346
|
+
cursor: 'pointer',
|
|
347
|
+
},
|
|
348
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HUD Panel Component
|
|
3
|
+
*
|
|
4
|
+
* Main panel container for the HUD.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ReactNode } from 'react'
|
|
8
|
+
import { useHud } from '../providers/HudProvider'
|
|
9
|
+
import type { HudPanelType } from '../types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Props for HudPanel
|
|
13
|
+
*/
|
|
14
|
+
export interface HudPanelProps {
|
|
15
|
+
/** Panel content */
|
|
16
|
+
children?: ReactNode
|
|
17
|
+
/** Custom class name */
|
|
18
|
+
className?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Panel tab configuration
|
|
23
|
+
*/
|
|
24
|
+
interface PanelTab {
|
|
25
|
+
id: HudPanelType
|
|
26
|
+
label: string
|
|
27
|
+
available: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Main HUD panel component
|
|
32
|
+
*/
|
|
33
|
+
export function HudPanel({ children, className = '' }: HudPanelProps) {
|
|
34
|
+
const {
|
|
35
|
+
isOpen,
|
|
36
|
+
isMinimized,
|
|
37
|
+
activePanel,
|
|
38
|
+
position,
|
|
39
|
+
setActivePanel,
|
|
40
|
+
toggleMinimized,
|
|
41
|
+
close,
|
|
42
|
+
lensService,
|
|
43
|
+
forkService,
|
|
44
|
+
simulationService,
|
|
45
|
+
diagnosticsService,
|
|
46
|
+
} = useHud()
|
|
47
|
+
|
|
48
|
+
if (!isOpen) return null
|
|
49
|
+
|
|
50
|
+
// Determine which tabs are available based on services
|
|
51
|
+
const tabs: PanelTab[] = [
|
|
52
|
+
{ id: 'lens', label: 'Lens', available: lensService !== null },
|
|
53
|
+
{ id: 'simulation', label: 'Simulation', available: simulationService !== null },
|
|
54
|
+
{ id: 'diagnostics', label: 'Diagnostics', available: diagnosticsService !== null },
|
|
55
|
+
{ id: 'state', label: 'State', available: forkService !== null },
|
|
56
|
+
{ id: 'signals', label: 'Signals', available: true },
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
const availableTabs = tabs.filter((t) => t.available)
|
|
60
|
+
|
|
61
|
+
// Position classes
|
|
62
|
+
const positionClasses: Record<typeof position, string> = {
|
|
63
|
+
'bottom-right': 'bottom-4 right-4',
|
|
64
|
+
'bottom-left': 'bottom-4 left-4',
|
|
65
|
+
'top-right': 'top-4 right-4',
|
|
66
|
+
'top-left': 'top-4 left-4',
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
className={`fixed ${positionClasses[position]} z-50 ${className}`}
|
|
72
|
+
style={{
|
|
73
|
+
width: isMinimized ? '200px' : '400px',
|
|
74
|
+
maxHeight: isMinimized ? '40px' : '600px',
|
|
75
|
+
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
76
|
+
borderRadius: '8px',
|
|
77
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
78
|
+
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.5)',
|
|
79
|
+
fontFamily: 'ui-monospace, monospace',
|
|
80
|
+
fontSize: '12px',
|
|
81
|
+
color: '#fff',
|
|
82
|
+
overflow: 'hidden',
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{/* Header */}
|
|
86
|
+
<div
|
|
87
|
+
style={{
|
|
88
|
+
display: 'flex',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
justifyContent: 'space-between',
|
|
91
|
+
padding: '8px 12px',
|
|
92
|
+
borderBottom: isMinimized ? 'none' : '1px solid rgba(255, 255, 255, 0.1)',
|
|
93
|
+
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<span style={{ fontWeight: 600, color: '#10b981' }}>◆ Sigil HUD</span>
|
|
97
|
+
<div style={{ display: 'flex', gap: '8px' }}>
|
|
98
|
+
<button
|
|
99
|
+
onClick={toggleMinimized}
|
|
100
|
+
style={{
|
|
101
|
+
background: 'none',
|
|
102
|
+
border: 'none',
|
|
103
|
+
color: '#666',
|
|
104
|
+
cursor: 'pointer',
|
|
105
|
+
fontSize: '14px',
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{isMinimized ? '◻' : '–'}
|
|
109
|
+
</button>
|
|
110
|
+
<button
|
|
111
|
+
onClick={close}
|
|
112
|
+
style={{
|
|
113
|
+
background: 'none',
|
|
114
|
+
border: 'none',
|
|
115
|
+
color: '#666',
|
|
116
|
+
cursor: 'pointer',
|
|
117
|
+
fontSize: '14px',
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
×
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{!isMinimized && (
|
|
126
|
+
<>
|
|
127
|
+
{/* Tabs */}
|
|
128
|
+
<div
|
|
129
|
+
style={{
|
|
130
|
+
display: 'flex',
|
|
131
|
+
gap: '2px',
|
|
132
|
+
padding: '4px',
|
|
133
|
+
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{availableTabs.map((tab) => (
|
|
137
|
+
<button
|
|
138
|
+
key={tab.id}
|
|
139
|
+
onClick={() => setActivePanel(tab.id)}
|
|
140
|
+
style={{
|
|
141
|
+
flex: 1,
|
|
142
|
+
padding: '6px 8px',
|
|
143
|
+
background:
|
|
144
|
+
activePanel === tab.id
|
|
145
|
+
? 'rgba(16, 185, 129, 0.2)'
|
|
146
|
+
: 'transparent',
|
|
147
|
+
border: 'none',
|
|
148
|
+
borderRadius: '4px',
|
|
149
|
+
color: activePanel === tab.id ? '#10b981' : '#888',
|
|
150
|
+
cursor: 'pointer',
|
|
151
|
+
fontSize: '11px',
|
|
152
|
+
fontWeight: 500,
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
{tab.label}
|
|
156
|
+
</button>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* Content */}
|
|
161
|
+
<div
|
|
162
|
+
style={{
|
|
163
|
+
padding: '12px',
|
|
164
|
+
overflowY: 'auto',
|
|
165
|
+
maxHeight: '500px',
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
{children}
|
|
169
|
+
{!activePanel && (
|
|
170
|
+
<div style={{ color: '#666', textAlign: 'center', padding: '20px' }}>
|
|
171
|
+
Select a panel to get started
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
</>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HUD Trigger Component
|
|
3
|
+
*
|
|
4
|
+
* Floating button to open the HUD.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ReactNode } from 'react'
|
|
8
|
+
import { useHud } from '../providers/HudProvider'
|
|
9
|
+
import type { HudPosition } from '../types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Props for HudTrigger
|
|
13
|
+
*/
|
|
14
|
+
export interface HudTriggerProps {
|
|
15
|
+
/** Custom class name */
|
|
16
|
+
className?: string
|
|
17
|
+
/** Custom children (replaces default icon) */
|
|
18
|
+
children?: ReactNode
|
|
19
|
+
/** Override position (defaults to HUD position) */
|
|
20
|
+
position?: HudPosition
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Floating trigger button to open the HUD
|
|
25
|
+
*/
|
|
26
|
+
export function HudTrigger({
|
|
27
|
+
className = '',
|
|
28
|
+
children,
|
|
29
|
+
position: overridePosition,
|
|
30
|
+
}: HudTriggerProps) {
|
|
31
|
+
const { isOpen, toggle, position: hudPosition } = useHud()
|
|
32
|
+
|
|
33
|
+
const position = overridePosition ?? hudPosition
|
|
34
|
+
|
|
35
|
+
// Position classes
|
|
36
|
+
const positionClasses: Record<typeof position, string> = {
|
|
37
|
+
'bottom-right': 'bottom-4 right-4',
|
|
38
|
+
'bottom-left': 'bottom-4 left-4',
|
|
39
|
+
'top-right': 'top-4 right-4',
|
|
40
|
+
'top-left': 'top-4 left-4',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Don't show trigger when HUD is open
|
|
44
|
+
if (isOpen) return null
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
onClick={toggle}
|
|
49
|
+
className={`fixed ${positionClasses[position]} z-50 ${className}`}
|
|
50
|
+
style={{
|
|
51
|
+
width: '44px',
|
|
52
|
+
height: '44px',
|
|
53
|
+
borderRadius: '50%',
|
|
54
|
+
backgroundColor: 'rgba(16, 185, 129, 0.9)',
|
|
55
|
+
border: '2px solid rgba(16, 185, 129, 0.3)',
|
|
56
|
+
boxShadow: '0 4px 12px rgba(16, 185, 129, 0.3)',
|
|
57
|
+
cursor: 'pointer',
|
|
58
|
+
display: 'flex',
|
|
59
|
+
alignItems: 'center',
|
|
60
|
+
justifyContent: 'center',
|
|
61
|
+
transition: 'transform 0.2s, box-shadow 0.2s',
|
|
62
|
+
}}
|
|
63
|
+
onMouseEnter={(e) => {
|
|
64
|
+
e.currentTarget.style.transform = 'scale(1.1)'
|
|
65
|
+
e.currentTarget.style.boxShadow = '0 6px 16px rgba(16, 185, 129, 0.4)'
|
|
66
|
+
}}
|
|
67
|
+
onMouseLeave={(e) => {
|
|
68
|
+
e.currentTarget.style.transform = 'scale(1)'
|
|
69
|
+
e.currentTarget.style.boxShadow = '0 4px 12px rgba(16, 185, 129, 0.3)'
|
|
70
|
+
}}
|
|
71
|
+
aria-label="Open Sigil HUD"
|
|
72
|
+
title="Sigil HUD (⌘⇧D)"
|
|
73
|
+
>
|
|
74
|
+
{children ?? (
|
|
75
|
+
<span style={{ color: '#fff', fontSize: '18px', fontWeight: 600 }}>
|
|
76
|
+
◆
|
|
77
|
+
</span>
|
|
78
|
+
)}
|
|
79
|
+
</button>
|
|
80
|
+
)
|
|
81
|
+
}
|