123456789101112131415import { Component, Show, createEffect, createSignal } from "solid-js";16import type { PairingProgress } from "../../shared/sync-types";1718export interface ChirpPairingFlowProps {19 progress: PairingProgress;20 21 onRetry: () => void;22 23 onCancel: () => void;24 25 onDismiss: () => void;26}2728const PAIRED_DISMISS_MS = 2500;2930export const ChirpPairingFlow: Component<ChirpPairingFlowProps> = (props) => {31 const [dismissArmed, setDismissArmed] = createSignal(false);3233 createEffect(() => {34 if (props.progress.phase === "paired" && !dismissArmed()) {35 setDismissArmed(true);36 const t = setTimeout(() => props.onDismiss(), PAIRED_DISMISS_MS);37 38 void t;39 }40 });4142 return (43 <div class="peach-chirp-pair" data-phase={props.progress.phase}44 onClick={() => props.progress.phase === "paired" && props.onDismiss()}>45 <div class="peach-chirp-stage">46 <DesktopBird phase={props.progress.phase} />47 <SoundWaves phase={props.progress.phase} />48 <MobileBird phase={props.progress.phase} />49 <ConnectionDot phase={props.progress.phase} />50 </div>5152 <PairingCopy progress={props.progress} />5354 <Show when={canCancel(props.progress.phase)}>55 <button class="peach-btn peach-btn-ghost peach-chirp-cancel" onClick={() => props.onCancel()}>56 Cancel57 </button>58 </Show>59 <Show when={isTerminalError(props.progress.phase)}>60 <button class="peach-btn peach-btn-primary peach-chirp-retry" onClick={() => props.onRetry()}>61 Try again62 </button>63 </Show>64 <Show when={props.progress.phase === "mic_denied"}>65 <p class="peach-chirp-mic-help">66 Update your browser's site settings to allow Peach to access the microphone, then try again.67 </p>68 </Show>69 </div>70 );71};7273function canCancel(phase: PairingProgress["phase"]): boolean {74 return phase === "listening" || phase === "chirping" || phase === "handshake";75}7677function isTerminalError(phase: PairingProgress["phase"]): boolean {78 return (79 phase === "timeout" ||80 phase === "handshake_failed" ||81 phase === "version_mismatch" ||82 phase === "mic_denied"83 );84}8586const PairingCopy: Component<{ progress: PairingProgress }> = (props) => {87 return (88 <div class="peach-chirp-copy">89 <h2 class="peach-chirp-headline">{headline(props.progress)}</h2>90 <p class="peach-chirp-sub">{subtext(props.progress)}</p>91 </div>92 );93};9495function headline(p: PairingProgress): string {96 switch (p.phase) {97 case "idle":98 case "listening":99 case "chirping":100 return "Hold your phone near your laptop";101 case "handshake":102 return "Pairing…";103 case "paired":104 return "Paired!";105 case "timeout":106 return "Couldn’t find your phone";107 case "mic_denied":108 return "Microphone access needed";109 case "version_mismatch":110 return "Update Peach on both devices";111 case "handshake_failed":112 return "Something went wrong";113 }114}115116function subtext(p: PairingProgress): string {117 switch (p.phase) {118 case "idle":119 case "listening":120 case "chirping":121 return "Peach will pair automatically when your phone is close";122 case "handshake":123 return "Hold still — exchanging keys";124 case "paired":125 return p.peerDeviceName ? `Connected to ${p.peerDeviceName}` : "Connected";126 case "timeout":127 return "Make sure Peach is open on your phone and try again";128 case "mic_denied":129 return "Peach uses your microphone to pair. No audio is recorded or stored.";130 case "version_mismatch":131 return "The other device is running a different protocol version";132 case "handshake_failed":133 return p.errorMessage ?? "Please try again";134 }135}136137138139140141142143const DesktopBird: Component<{ phase: PairingProgress["phase"] }> = (props) => {144 return (145 <svg class="peach-chirp-bird peach-chirp-bird-desktop"146 width="92" height="92" viewBox="0 0 92 92" data-phase={props.phase}>147 <ellipse cx="46" cy="58" rx="28" ry="22" class="peach-chirp-bird-body" />148 <circle cx="58" cy="42" r="14" class="peach-chirp-bird-head" />149 <circle cx="62" cy="40" r="2.2" class="peach-chirp-bird-eye" />150 <path d="M 70 42 L 78 42 L 72 47 Z" class="peach-chirp-bird-beak" />151 <path d="M 30 48 Q 22 56 30 64" class="peach-chirp-bird-wing" fill="none" />152 </svg>153 );154};155156const MobileBird: Component<{ phase: PairingProgress["phase"] }> = (props) => {157 return (158 <svg class="peach-chirp-bird peach-chirp-bird-mobile"159 width="74" height="74" viewBox="0 0 74 74" data-phase={props.phase}>160 <ellipse cx="36" cy="48" rx="22" ry="18" class="peach-chirp-bird-body" />161 <circle cx="26" cy="34" r="11" class="peach-chirp-bird-head" />162 <circle cx="22" cy="32" r="1.8" class="peach-chirp-bird-eye" />163 <path d="M 14 34 L 7 34 L 12 38 Z" class="peach-chirp-bird-beak" />164 <path d="M 50 40 Q 58 48 50 56" class="peach-chirp-bird-wing" fill="none" />165 </svg>166 );167};168169const SoundWaves: Component<{ phase: PairingProgress["phase"] }> = (props) => {170 return (171 <svg class="peach-chirp-waves" width="320" height="120" viewBox="0 0 320 120" data-phase={props.phase}>172 <path class="peach-chirp-wave peach-chirp-wave-out" d="M 80 60 Q 160 20 240 60" fill="none" />173 <path class="peach-chirp-wave peach-chirp-wave-in" d="M 240 60 Q 160 100 80 60" fill="none" />174 </svg>175 );176};177178const ConnectionDot: Component<{ phase: PairingProgress["phase"] }> = (props) => {179 return (180 <div class="peach-chirp-dot" data-phase={props.phase}>181 <span class="peach-chirp-dot-core" />182 <span class="peach-chirp-dot-halo" />183 </div>184 );185};