|
| 1 | +export type GestureRecordingState = "idle" | "recording" | "preview"; |
| 2 | + |
| 3 | +interface GestureRecordIconProps { |
| 4 | + recording: boolean; |
| 5 | +} |
| 6 | + |
| 7 | +function GestureRecordIcon({ recording }: GestureRecordIconProps) { |
| 8 | + return ( |
| 9 | + <svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"> |
| 10 | + {recording ? ( |
| 11 | + <rect x="1" y="1" width="8" height="8" rx="1" fill="currentColor" /> |
| 12 | + ) : ( |
| 13 | + <circle cx="5" cy="5" r="4.5" fill="currentColor" /> |
| 14 | + )} |
| 15 | + </svg> |
| 16 | + ); |
| 17 | +} |
| 18 | + |
| 19 | +interface GestureRecordPanelButtonProps { |
| 20 | + recordingState?: GestureRecordingState; |
| 21 | + recordingDuration?: number; |
| 22 | + onToggleRecording: () => void; |
| 23 | +} |
| 24 | + |
| 25 | +export function GestureRecordPanelButton({ |
| 26 | + recordingState, |
| 27 | + recordingDuration, |
| 28 | + onToggleRecording, |
| 29 | +}: GestureRecordPanelButtonProps) { |
| 30 | + const recording = recordingState === "recording"; |
| 31 | + |
| 32 | + return ( |
| 33 | + <div className="px-4 pb-3"> |
| 34 | + <button |
| 35 | + type="button" |
| 36 | + onMouseDown={(e) => e.preventDefault()} |
| 37 | + onClick={onToggleRecording} |
| 38 | + className={`w-full flex items-center justify-center gap-2 rounded-lg py-2 text-[11px] font-medium transition-colors ${ |
| 39 | + recording |
| 40 | + ? "bg-red-500/15 text-red-400 border border-red-500/30 animate-pulse" |
| 41 | + : "bg-panel-input text-panel-text-2 hover:bg-panel-hover border border-panel-border" |
| 42 | + }`} |
| 43 | + > |
| 44 | + <GestureRecordIcon recording={recording} /> |
| 45 | + {recording |
| 46 | + ? `Stop recording ${(recordingDuration ?? 0).toFixed(1)}s — press R` |
| 47 | + : "Record gesture (R) — move pointer to capture motion"} |
| 48 | + </button> |
| 49 | + </div> |
| 50 | + ); |
| 51 | +} |
| 52 | + |
| 53 | +interface GestureRecordBadgeProps { |
| 54 | + rect: { left: number; top: number; width: number; height: number }; |
| 55 | + recordingState?: GestureRecordingState; |
| 56 | + onToggleRecording: () => void; |
| 57 | +} |
| 58 | + |
| 59 | +export function GestureRecordBadge({ |
| 60 | + rect, |
| 61 | + recordingState, |
| 62 | + onToggleRecording, |
| 63 | +}: GestureRecordBadgeProps) { |
| 64 | + const recording = recordingState === "recording"; |
| 65 | + const label = recording ? "Stop gesture recording (R)" : "Record gesture (R)"; |
| 66 | + |
| 67 | + return ( |
| 68 | + <button |
| 69 | + type="button" |
| 70 | + aria-label={label} |
| 71 | + title={label} |
| 72 | + className={`pointer-events-auto absolute z-20 flex h-7 w-7 items-center justify-center rounded-full border shadow-lg transition-colors ${ |
| 73 | + recording |
| 74 | + ? "border-red-400/60 bg-red-500 text-white animate-pulse" |
| 75 | + : "border-studio-accent/60 bg-neutral-950 text-studio-accent hover:bg-neutral-900" |
| 76 | + }`} |
| 77 | + style={{ |
| 78 | + left: Math.max(0, rect.left + rect.width + 8), |
| 79 | + top: Math.max(0, rect.top - 4), |
| 80 | + }} |
| 81 | + onPointerDown={(event) => { |
| 82 | + event.preventDefault(); |
| 83 | + event.stopPropagation(); |
| 84 | + }} |
| 85 | + onMouseDown={(event) => { |
| 86 | + event.preventDefault(); |
| 87 | + event.stopPropagation(); |
| 88 | + }} |
| 89 | + onClick={(event) => { |
| 90 | + event.preventDefault(); |
| 91 | + event.stopPropagation(); |
| 92 | + onToggleRecording(); |
| 93 | + }} |
| 94 | + > |
| 95 | + <GestureRecordIcon recording={recording} /> |
| 96 | + </button> |
| 97 | + ); |
| 98 | +} |
0 commit comments