import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { ArrowReloadHorizontalIcon, Globe02Icon, LinkSquare02Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { openUrl } from "@tauri-apps/plugin-opener"; import { forwardRef, useEffect, useImperativeHandle, useRef, useState, } from "react"; type PortPreset = { port: number; label: string; hint: string; }; // Curated dev-server ports. Ordered by frontend frequency, then backend. const PORT_PRESETS: readonly PortPreset[] = [ { port: 5173, label: "Vite", hint: "vite, sveltekit" }, { port: 5174, label: "Vite (alt)", hint: "second vite instance" }, { port: 3000, label: "Next.js", hint: "next, express, rails" }, { port: 3001, label: "Next.js (alt)", hint: "second next instance" }, { port: 4173, label: "Vite preview", hint: "vite preview" }, { port: 4200, label: "Angular", hint: "angular cli" }, { port: 4321, label: "Astro", hint: "astro" }, { port: 5500, label: "Live Server", hint: "vscode live server" }, { port: 6006, label: "Storybook", hint: "storybook" }, { port: 8080, label: "Webpack", hint: "webpack, vue cli" }, { port: 8081, label: "Metro", hint: "react native metro" }, { port: 8000, label: "Django / FastAPI", hint: "django, fastapi" }, { port: 8888, label: "Jupyter", hint: "jupyter notebook" }, { port: 5000, label: "Flask", hint: "flask" }, { port: 7860, label: "Gradio", hint: "gradio" }, { port: 11434, label: "Ollama", hint: "ollama api" }, ]; export type PreviewAddressBarHandle = { focus: () => void; }; type Props = { url: string; onSubmit: (url: string) => void; onReload: () => void; }; export const PreviewAddressBar = forwardRef( function PreviewAddressBar({ url, onSubmit, onReload }, ref) { const [draft, setDraft] = useState(url); const inputRef = useRef(null); // Keep draft in sync when the parent updates the URL externally // (AI tool, detected localhost chip, etc.). useEffect(() => { setDraft(url); }, [url]); useImperativeHandle( ref, () => ({ focus: () => { const el = inputRef.current; if (!el) return; el.focus(); el.select(); }, }), [], ); const [notice, setNotice] = useState(null); const [checkingPort, setCheckingPort] = useState(null); const submit = () => { const next = normalizeUrl(draft); if (!next) { setNotice("Enter a URL or pick a port preset."); return; } setNotice(null); if (next !== url) onSubmit(next); else onReload(); }; const tryPort = async (port: number) => { setNotice(null); setCheckingPort(port); const url = `http://localhost:${port}`; const ok = await probeUrl(url); setCheckingPort(null); if (!ok) { setNotice(`No server listening on :${port}.`); return; } setDraft(url); onSubmit(url); }; return (
{PORT_PRESETS.map((p) => ( { e.preventDefault(); void tryPort(p.port); }} > {p.label} {checkingPort === p.port ? "checking…" : `:${p.port}`} ))}
setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); submit(); } else if (e.key === "Escape") { e.preventDefault(); setDraft(url); inputRef.current?.blur(); } }} />
{notice ? (
{notice}
) : null}
); }, ); async function probeUrl(url: string): Promise { try { await fetch(url, { method: "GET", mode: "no-cors", cache: "no-store", signal: AbortSignal.timeout(900), }); return true; } catch { return false; } } function normalizeUrl(raw: string): string | null { const trimmed = raw.trim(); if (!trimmed) return null; if (/^https?:\/\//i.test(trimmed)) return trimmed; if (/^localhost(:|\/|$)/i.test(trimmed)) return `http://${trimmed}`; if (/^\d{1,3}(\.\d{1,3}){3}(:|\/|$)/.test(trimmed)) return `http://${trimmed}`; if (/^[\w.-]+\.[a-z]{2,}/i.test(trimmed)) return `https://${trimmed}`; return trimmed; }