c9f3aba8b8ce
rebuild SyncPairing UI as show/enter modes; add code/input styles, recent-pair toast, and vault-lock teardown
- time
- 2026-05-01 02:45 UTC · 6 months ago
- machine
- koh2
- parent
- 76230f557838
↓ peach-browser-c9f3aba8.tar.gzatlas at this save
files changed3 files
src/popup/App.tsx+20 −0src/popup/styles/popup.css+79 −102src/popup/views/SyncPairing.tsx+235 −122src/popup/App.tsx+20 −0
@@ -167,6 +167,7 @@167167 } | null>(null);168168 const [backExitNotice, setBackExitNotice] = createSignal(false);169169 const [incomingPair, setIncomingPair] = createSignal<IncomingPairingRequest | null>(null);170+ const [recentPairToast, setRecentPairToast] = createSignal<string | null>(null);170171 const [incomingPairResponding, setIncomingPairResponding] = createSignal(false);171172 const [incomingPairTick, setIncomingPairTick] = createSignal(0);172173 const incomingPairSecondsLeft = () => {@@ -461,6 +462,7 @@461462 462463 await refreshPendingPasskey();463464 await refreshIncomingPairing();465+ await consumeRecentPairing();464466 465467 // Track tab changes so smart sort stays current466468 if (!Capacitor.isNativePlatform()) {@@ -704,6 +706,18 @@704706 setIncomingPair(data);705707 }706708 709+ // Drains a single one-shot record left by the background when 6-digit710+ // pairing succeeded while the popup was closed (or the SyncPairing view was711+ // navigated away from before it could show its own success card).712+ async function consumeRecentPairing() {713+ const res = await sendMessage({ type: "GET_RECENT_PAIRING" });714+ const data = (res as { ok: boolean; data?: { deviceName?: string } | null }).data;715+ if (data?.deviceName) {716+ setRecentPairToast(data.deviceName);717+ window.setTimeout(() => setRecentPairToast(null), 4000);718+ }719+ }720+707721 async function respondIncomingPairing(accept: boolean) {708722 if (incomingPairResponding()) return;709723 setIncomingPairResponding(true);@@ -1121,6 +1135,12 @@11211135 </button>11221136 </div>11231137 </header>1138+1139+ <Show when={recentPairToast()}>1140+ <div class="peach-pair-toast" role="status" aria-live="polite">1141+ Paired with <strong>{recentPairToast()}</strong>1142+ </div>1143+ </Show>11241144 11251145 <Show when={incomingPair() !== null}>11261146 <div class="peach-pair-request-banner" role="dialog" aria-label="Pairing request">src/popup/styles/popup.css+79 −102
@@ -4815,50 +4815,10 @@48154815 text-align: center;48164816 }48174817 4818-.peach-pairing-discover {4819- justify-content: flex-start;4820- align-items: stretch;4821- text-align: left;4822- gap: 12px;4818+.peach-pairing-code-screen {4819+ gap: 18px;48234820 }48244821 4825-.peach-pairing-discover .peach-pairing-headline {4826- text-align: center;4827- margin-bottom: 4px;4828-}4829-4830-.peach-pairing-peer-card {4831- display: flex;4832- align-items: center;4833- justify-content: space-between;4834- gap: 12px;4835- padding: 10px 12px;4836- border: 1px solid var(--peach-t3);4837- border-radius: 10px;4838- background: var(--peach-bg-card);4839-}4840-4841-.peach-pairing-peer-info {4842- display: flex;4843- flex-direction: column;4844- gap: 2px;4845- flex: 1;4846- min-width: 0;4847-}4848-4849-.peach-pairing-peer-name {4850- font-size: 13px;4851- color: var(--peach-t0);4852- white-space: nowrap;4853- overflow: hidden;4854- text-overflow: ellipsis;4855-}4856-4857-.peach-pairing-peer-meta {4858- font-size: 11px;4859- color: var(--peach-t2);4860-}4861-48624822 .peach-pairing-headline {48634823 font-family: var(--font-display);48644824 font-style: italic;@@ -4881,81 +4841,82 @@48814841 text-align: center;48824842 }48834843 4884-.peach-pairing-code-display {4885- display: flex;4886- gap: 6px;4887-}4888-4889-.peach-pairing-code-digit {4890- display: flex;4891- align-items: center;4892- justify-content: center;4893- width: 36px;4894- height: 48px;4895- background: var(--peach-s2);4896- border: 1px solid var(--peach-t3);4897- border-radius: 6px;4898- font-family: var(--font-mono);4899- font-size: 22px;4844+/* 6-digit pairing code — both display (.peach-pairing-code) and entry4845+ (.peach-pairing-code-input) share IBM Plex Mono / 36px / wide tracking4846+ so the two modes feel like the same surface. */4847+.peach-pairing-code {4848+ font-family: "IBM Plex Mono", monospace;4849+ font-size: 36px;4850+ letter-spacing: 0.25em;49004851 color: var(--peach-t0);4901- letter-spacing: 0;4852+ text-align: center;4853+ padding: 24px 0;4854+ text-shadow:4855+ 0 0 24px color-mix(in srgb, var(--peach-accent) 22%, transparent),4856+ 0 0 48px color-mix(in srgb, var(--peach-accent) 12%, transparent);4857+ background: transparent;4858+ border: 0;4859+ cursor: default;4860+ user-select: text;49024861 }49034862 4904-.peach-pairing-countdown-ring {4905- position: relative;4906- display: inline-flex;4907- align-items: center;4908- justify-content: center;4863+.peach-pairing-code--loading,4864+.peach-pairing-code--expired {4865+ font-size: 18px;4866+ letter-spacing: 0.04em;4867+ color: var(--peach-t2);4868+ cursor: pointer;4869+ text-shadow: none;49094870 }49104871 4911-.peach-pairing-countdown-num {4912- position: absolute;4913- font-family: var(--font-mono);4914- font-size: 14px;4915- color: var(--peach-t1);4872+.peach-pairing-code-input {4873+ font-family: "IBM Plex Mono", monospace;4874+ font-size: 36px;4875+ letter-spacing: 0.25em;4876+ color: var(--peach-t0);4877+ background: transparent;4878+ border: 0;4879+ border-bottom: 1px solid var(--peach-t3);4880+ outline: none;4881+ text-align: center;4882+ padding: 18px 0;4883+ width: 220px;4884+ caret-color: var(--peach-accent);4885+ transition: border-color 160ms ease;49164886 }49174887 4918-.peach-pairing-ring-track {4919- fill: none;4920- stroke: var(--peach-t3);4921- stroke-width: 2;4888+.peach-pairing-code-input::placeholder {4889+ color: var(--peach-t3);49224890 }49234891 4924-.peach-pairing-ring-progress {4925- fill: none;4926- stroke: var(--peach-accent);4927- stroke-width: 2;4928- stroke-linecap: round;4929- transform: rotate(-90deg);4930- transform-origin: 50% 50%;4931- transition: stroke-dashoffset 0.5s linear;4892+.peach-pairing-code-input:focus {4893+ border-bottom-color: var(--peach-accent);49324894 }49334895 4934-.peach-pairing-code-inputs {4935- display: flex;4936- gap: 8px;4896+.peach-pairing-toggle {4897+ background: transparent;4898+ border: 0;4899+ color: var(--peach-t2);4900+ font-size: 12px;4901+ padding: 8px 4px;4902+ cursor: pointer;4903+ transition: color 120ms ease;49374904 }49384905 4939-.peach-pairing-code-box {4940- width: 36px;4941- height: 48px;4942- text-align: center;4943- background: var(--peach-s2);4944- border: 1px solid var(--peach-t3);4945- border-radius: 6px;4946- font-family: var(--font-mono);4947- font-size: 20px;4948- color: var(--peach-t0);4949- outline: none;4950- transition: border-color 120ms ease;4906+.peach-pairing-toggle:hover:not(:disabled) {4907+ color: var(--peach-t1);49514908 }49524909 4953-.peach-pairing-code-box:focus {4954- border-color: var(--peach-accent);4910+.peach-pairing-toggle:disabled {4911+ opacity: 0.5;4912+ cursor: default;49554913 }49564914 4957-.peach-pairing-code-box.has-error {4958- border-color: rgba(220, 80, 60, 0.45);4915+.peach-pairing-hint {4916+ font-size: 11px;4917+ color: var(--peach-t2);4918+ margin: 0;4919+ text-align: center;49594920 }49604921 49614922 .peach-pairing-status {@@ -4973,8 +4934,8 @@49734934 color: var(--peach-breach);49744935 }49754936 4976-.peach-pairing-error-msg {4977- color: var(--peach-t1);4937+.peach-pairing-error--muted {4938+ color: var(--peach-t2);49784939 }49794940 49804941 .peach-pairing-spinner {@@ -6201,6 +6162,22 @@62016162 border-bottom: 1px solid color-mix(in srgb, var(--peach-accent) 32%, transparent);62026163 font-family: "IBM Plex Mono", monospace;62036164 flex-shrink: 0;6165+}6166+6167+.peach-pair-toast {6168+ padding: 8px 12px;6169+ font-family: "IBM Plex Mono", monospace;6170+ font-size: 12px;6171+ color: var(--peach-t1);6172+ background: color-mix(in srgb, var(--peach-accent) 10%, var(--peach-bg-card));6173+ border-bottom: 1px solid color-mix(in srgb, var(--peach-accent) 22%, transparent);6174+ flex-shrink: 0;6175+ text-align: center;6176+}6177+6178+.peach-pair-toast strong {6179+ color: var(--peach-t0);6180+ font-weight: 500;62046181 }62056182 62066183 .peach-pair-request-header {src/popup/views/SyncPairing.tsx+235 −122
@@ -1,90 +1,188 @@1-import { createSignal, For, onCleanup, onMount, Show } from "solid-js";1+import { createSignal, onCleanup, onMount, Show } from "solid-js";22 import { sendMessage } from "../App";3-import type { DiscoveredPeer, PairedDevice, PairingStep } from "../../shared/sync-types";3+import type { PairedDevice } from "../../shared/sync-types";44 55 interface SyncPairingProps {66 onClose: () => void;77 onPaired: (device: PairedDevice) => void;88 }99 10+type Mode = "show" | "enter";11+1012 function Spinner() {1113 return <span class="peach-pairing-spinner" />;1214 }1315 16+function formatCode(code: string): string {17+ if (code.length <= 3) return code;18+ return `${code.slice(0, 3)} ${code.slice(3)}`;19+}20+1421 export function SyncPairingView(props: SyncPairingProps) {15- const [step, setStep] = createSignal<PairingStep>({ stage: "idle" });22+ const [mode, setMode] = createSignal<Mode>("show");23+ const [code, setCode] = createSignal<string | null>(null);24+ const [expiresAt, setExpiresAt] = createSignal<number | null>(null);25+ const [showError, setShowError] = createSignal<string | null>(null);1626 17- const [discoveredPeers, setDiscoveredPeers] = createSignal<DiscoveredPeer[]>([]);18- const [discoveryActive, setDiscoveryActive] = createSignal(false);19- const [discoveryError, setDiscoveryError] = createSignal<string | null>(null);20- const [pairingPeerId, setPairingPeerId] = createSignal<string | null>(null);27+ const [enteredCode, setEnteredCode] = createSignal("");28+ const [connecting, setConnecting] = createSignal(false);29+ const [enterError, setEnterError] = createSignal<string | null>(null);2130 22- onMount(() => {23- void startDiscovery();24- });31+ const [pairedDevice, setPairedDevice] = createSignal<PairedDevice | null>(null);2532 26- onCleanup(() => {27- if (discoveryActive()) void sendMessage({ type: "STOP_PEER_DISCOVERY" });28- });33+ let expiryTimer: ReturnType<typeof setTimeout> | null = null;2934 35+ function clearExpiryTimer() {36+ if (expiryTimer !== null) {37+ clearTimeout(expiryTimer);38+ expiryTimer = null;39+ }40+ }41+3042 function handleRuntimeMessage(msg: unknown) {31- const m = msg as { type?: string; peer?: DiscoveredPeer; deviceId?: string };32- if (m?.type === "SYNC_PEER_DISCOVERED" && m.peer) {33- const peer = m.peer;34- setDiscoveredPeers((prev) => {35- if (prev.some((p) => p.deviceId === peer.deviceId)) return prev;36- return [...prev, peer];37- });43+ const m = msg as { type?: string; payload?: { deviceName?: string } };44+ if (m?.type === "PAIRING_COMPLETED" && m.payload?.deviceName) {45+ // Background completed an auto-accept while we were on the show screen.46+ const device: PairedDevice = {47+ deviceId: m.payload.deviceName,48+ displayName: m.payload.deviceName,49+ x25519PublicKey: "",50+ pairedAt: Date.now(),51+ };52+ clearExpiryTimer();53+ setCode(null);54+ setExpiresAt(null);55+ setPairedDevice(device);56+ props.onPaired(device);3857 }39- if (m?.type === "SYNC_PEER_LOST" && m.deviceId) {40- const id = m.deviceId;41- setDiscoveredPeers((prev) => prev.filter((p) => p.deviceId !== id));58+ }59+60+ async function startShow() {61+ setShowError(null);62+ setCode(null);63+ setExpiresAt(null);64+ clearExpiryTimer();65+ const res = await sendMessage({ type: "START_PAIRING_SERVER" });66+ const response = res as {67+ ok: boolean;68+ error?: string;69+ data?: { token: string; expiresAt: number };70+ };71+ if (!response.ok || !response.data) {72+ setShowError(73+ response.error === "pro_required"74+ ? "Sync requires Peach Pro."75+ : "Couldn't start the pairing server.",76+ );77+ return;4278 }79+ setCode(response.data.token);80+ setExpiresAt(response.data.expiresAt);81+ const remaining = Math.max(0, response.data.expiresAt - Date.now());82+ expiryTimer = setTimeout(() => {83+ setCode(null);84+ setExpiresAt(null);85+ setShowError("Code expired — tap to refresh.");86+ }, remaining);4387 }4488 45- async function startDiscovery() {46- chrome.runtime.onMessage.addListener(handleRuntimeMessage);47- onCleanup(() => chrome.runtime.onMessage.removeListener(handleRuntimeMessage));89+ async function stopShow() {90+ clearExpiryTimer();91+ setCode(null);92+ setExpiresAt(null);93+ await sendMessage({ type: "STOP_PAIRING_SERVER" });94+ }4895 49- const res = await sendMessage({ type: "START_PEER_DISCOVERY" });50- const response = res as { ok: boolean; error?: string };51- if (response.ok) {52- setDiscoveryActive(true);53- setDiscoveryError(null);96+ async function refreshCode() {97+ await stopShow();98+ await startShow();99+ }100+101+ async function switchTo(target: Mode) {102+ if (mode() === target) return;103+ if (target === "enter") {104+ await stopShow();105+ setEnterError(null);106+ setEnteredCode("");107+ setMode("enter");54108 } else {55- setDiscoveryError(response.error ?? "discovery_failed");109+ setEnterError(null);110+ setMode("show");111+ await startShow();56112 }57113 }58114 59- async function handlePairWithPeer(peer: DiscoveredPeer) {60- setPairingPeerId(peer.deviceId);61- setStep({ stage: "connecting", peerName: peer.deviceName });115+ function onEnterInput(e: InputEvent) {116+ const target = e.currentTarget as HTMLInputElement;117+ const raw = target.value.replace(/[^0-9]/g, "").slice(0, 6);118+ setEnteredCode(formatCode(raw));119+ if (enterError()) setEnterError(null);120+ }121+122+ function digitsEntered(): string {123+ return enteredCode().replace(/\D/g, "");124+ }125+126+ async function connect() {127+ const token = digitsEntered();128+ if (token.length !== 6) return;129+ setConnecting(true);130+ setEnterError(null);62131 const res = await sendMessage({63- type: "INITIATE_PAIR_WITH_PEER",64- payload: { peer },132+ type: "CONNECT_WITH_CODE",133+ payload: { token },65134 });66- const response = res as { ok: boolean; error?: string };67- setPairingPeerId(null);135+ setConnecting(false);136+ const response = res as { ok: boolean; error?: string; data?: { deviceName?: string } };68137 if (!response.ok) {69- setStep({ stage: "error", message: response.error ?? "pair_failed" });138+ const msg =139+ response.error === "no peer responded to pairing code" ||140+ (response.error ?? "").includes("resolve failed")141+ ? "Couldn't find that device. Make sure both are on the same WiFi."142+ : response.error === "pair_rejected"143+ ? "The other device declined."144+ : response.error === "invalid_code"145+ ? "Enter all six digits."146+ : "Couldn't connect. Try again.";147+ setEnterError(msg);70148 return;71149 }72- const paired: PairedDevice = {73- deviceId: peer.deviceId,74- displayName: peer.deviceName,75- x25519PublicKey: peer.x25519PublicKey,150+ const name = response.data?.deviceName ?? "Peach";151+ const device: PairedDevice = {152+ deviceId: name,153+ displayName: name,154+ x25519PublicKey: "",76155 pairedAt: Date.now(),77156 };78- setStep({ stage: "done", device: paired });79- props.onPaired(paired);157+ setPairedDevice(device);158+ props.onPaired(device);80159 }81160 161+ onMount(() => {162+ chrome.runtime.onMessage.addListener(handleRuntimeMessage);163+ void startShow();164+ });165+166+ onCleanup(() => {167+ chrome.runtime.onMessage.removeListener(handleRuntimeMessage);168+ clearExpiryTimer();169+ // Pairing server keeps running in the background per spec — the user170+ // showed the code, so a peer can still complete pairing after the popup171+ // closes. The background clears it on success or vault-lock.172+ });173+82174 return (83175 <div class="peach-overlay-view ov-enter-right">84176 <div class="peach-overlay-header">85177 <button86178 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"87- onClick={props.onClose}179+ onClick={() => {180+ // Closing the popup is fine; if user explicitly hits back, also181+ // tear down the active pairing server so they don't leave a token182+ // active without realising.183+ void sendMessage({ type: "STOP_PAIRING_SERVER" });184+ props.onClose();185+ }}88186 aria-label="Close"89187 >90188 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">@@ -96,97 +194,112 @@96194 </div>97195 98196 <div class="peach-pairing-body">99- <Show when={step().stage === "idle"}>100- <div class="peach-pairing-content peach-pairing-discover">101- <p class="peach-pairing-headline">Add a device</p>102- <Show when={discoveryError()}>103- <p class="peach-pairing-error">104- {discoveryError() === "pro_required"105- ? "Sync requires Peach Pro."106- : "Native sync host is not installed yet."}197+ <Show when={pairedDevice()}>198+ {(device) => (199+ <div class="peach-pairing-content">200+ <div class="peach-pairing-success-icon">201+ <svg width="32" height="32" viewBox="0 0 32 32" fill="none">202+ <circle cx="16" cy="16" r="14" stroke="var(--peach-health-ok)" stroke-width="1.5" />203+ <path d="M10 16L14 20L22 12" stroke="var(--peach-health-ok)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />204+ </svg>205+ </div>206+ <p class="peach-pairing-headline">Paired!</p>207+ <p class="peach-pairing-subline peach-pairing-subline--center">208+ {device().displayName}107209 </p>108- </Show>109- <Show when={!discoveryError() && discoveredPeers().length === 0}>110- <p class="peach-pairing-status">111- <Spinner />112- Searching for Peach on your local network…113- </p>114- </Show>115- <For each={discoveredPeers()}>116- {(peer) => (117- <div class="peach-pairing-peer-card">118- <div class="peach-pairing-peer-info">119- <span class="peach-pairing-peer-name">{peer.deviceName}</span>120- <span class="peach-pairing-peer-meta">121- {peer.alreadyPaired ? "Already paired ✓" : "Found on network"}122- </span>123- </div>124- <Show when={!peer.alreadyPaired}>125- <button126- class="peach-btn peach-btn--accent peach-btn--sm"127- onClick={() => void handlePairWithPeer(peer)}128- disabled={pairingPeerId() !== null}129- >130- {pairingPeerId() === peer.deviceId ? "…" : "Pair →"}131- </button>132- </Show>210+ <button211+ class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"212+ onClick={props.onClose}213+ >214+ Done215+ </button>216+ </div>217+ )}218+ </Show>219+220+ <Show when={!pairedDevice() && mode() === "show"}>221+ <div class="peach-pairing-content peach-pairing-code-screen">222+ <p class="peach-pairing-headline">Add a device</p>223+224+ <Show when={code()} fallback={225+ <Show when={!showError()} fallback={226+ <button227+ class="peach-pairing-code peach-pairing-code--expired"228+ onClick={() => void refreshCode()}229+ >230+ refresh231+ </button>232+ }>233+ <div class="peach-pairing-code peach-pairing-code--loading">234+ <Spinner />133235 </div>236+ </Show>237+ }>238+ {(c) => (239+ <div class="peach-pairing-code" aria-label="pairing code">240+ {formatCode(c())}241+ </div>134242 )}135- </For>243+ </Show>244+136245 <p class="peach-pairing-subline peach-pairing-subline--center">137- Open Peach on the other device. Both devices need to be on the138- same WiFi network.246+ Open Peach on your other device, tap247+ <br />248+ Add a device, and enter this code.139249 </p>140- </div>141- </Show>250+ <p class="peach-pairing-hint">Both devices must be on the same WiFi.</p>142251 143- <Show when={step().stage === "connecting" || step().stage === "verifying"}>144- <div class="peach-pairing-content">145- <Spinner />146- <p class="peach-pairing-status">147- {step().stage === "connecting" ? "Connecting…" : "Verifying…"}148- </p>252+ <Show when={showError()}>253+ <p class="peach-pairing-error peach-pairing-error--muted">{showError()}</p>254+ </Show>255+256+ <button257+ class="peach-pairing-toggle"258+ onClick={() => void switchTo("enter")}259+ >260+ I have a code to enter →261+ </button>149262 </div>150263 </Show>151264 152- <Show when={step().stage === "done"}>153- <div class="peach-pairing-content">154- <div class="peach-pairing-success-icon">155- <svg width="32" height="32" viewBox="0 0 32 32" fill="none">156- <circle cx="16" cy="16" r="14" stroke="var(--peach-health-ok)" stroke-width="1.5" />157- <path d="M10 16L14 20L22 12" stroke="var(--peach-health-ok)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />158- </svg>159- </div>160- <p class="peach-pairing-headline">Paired!</p>161- <p class="peach-pairing-subline peach-pairing-subline--center">162- {(step() as { stage: "done"; device: PairedDevice }).device.displayName}163- </p>265+ <Show when={!pairedDevice() && mode() === "enter"}>266+ <div class="peach-pairing-content peach-pairing-code-screen">267+ <p class="peach-pairing-headline">Add a device</p>268+269+ <input270+ class="peach-pairing-code-input"271+ type="text"272+ inputmode="numeric"273+ autocomplete="off"274+ autocorrect="off"275+ spellcheck={false}276+ placeholder="000 000"277+ value={enteredCode()}278+ onInput={onEnterInput}279+ disabled={connecting()}280+ aria-label="enter pairing code"281+ />282+164283 <button165284 class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"166- onClick={props.onClose}285+ onClick={() => void connect()}286+ disabled={digitsEntered().length !== 6 || connecting()}167287 >168- Done288+ {connecting() ? "Connecting…" : "Connect →"}169289 </button>170- </div>171- </Show>172290 173- <Show when={step().stage === "error"}>174- <div class="peach-pairing-content">175- <div class="peach-pairing-error-icon">176- <svg width="32" height="32" viewBox="0 0 32 32" fill="none">177- <circle cx="16" cy="16" r="14" stroke="var(--peach-breach)" stroke-width="1.5" />178- <path d="M11 11L21 21M21 11L11 21" stroke="var(--peach-breach)" stroke-width="1.5" stroke-linecap="round" />179- </svg>180- </div>181- <p class="peach-pairing-headline">Pairing failed</p>182- <p class="peach-pairing-subline peach-pairing-subline--center peach-pairing-error-msg">183- {(step() as { stage: "error"; message: string }).message}184- </p>291+ <p class="peach-pairing-hint">Both devices must be on the same WiFi.</p>292+293+ <Show when={enterError()}>294+ <p class="peach-pairing-error peach-pairing-error--muted">{enterError()}</p>295+ </Show>296+185297 <button186- class="peach-btn peach-btn--ghost peach-btn--md"187- onClick={() => setStep({ stage: "idle" })}298+ class="peach-pairing-toggle"299+ onClick={() => void switchTo("show")}300+ disabled={connecting()}188301 >189- Try again302+ ← Show my code instead190303 </button>191304 </div>192305 </Show>