Add Homebrew Cask install to downloads page, fix APK URL
- Extract CommandDialog shared component from landing page's ServerInstallButton
- Add Homebrew pill to macOS section on download page that opens install dialog
- Add ESC key support to close the command dialog
- Fix APK download URL (paseo-v${version}-android.apk)
This commit is contained in:
@@ -0,0 +1,122 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
|
||||||
|
interface CommandDialogProps {
|
||||||
|
trigger: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
command: string;
|
||||||
|
footnote?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommandDialog({
|
||||||
|
trigger,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
command,
|
||||||
|
footnote,
|
||||||
|
}: CommandDialogProps) {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
function handleClickOutside(e: MouseEvent) {
|
||||||
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape") setOpen(false);
|
||||||
|
}
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative" ref={ref}>
|
||||||
|
<button onClick={() => setOpen(!open)}>{trigger}</button>
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||||
|
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-md rounded-xl border border-white/20 bg-background p-6 space-y-4"
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-base font-medium text-white">{title}</p>
|
||||||
|
{description && (
|
||||||
|
<p className="text-sm text-muted-foreground">{description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<CodeBlock>{command}</CodeBlock>
|
||||||
|
{footnote && (
|
||||||
|
<p className="text-xs text-white/30">{footnote}</p>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CodeBlock({ children }: { children: string }) {
|
||||||
|
const [copied, setCopied] = React.useState(false);
|
||||||
|
|
||||||
|
function handleCopy() {
|
||||||
|
navigator.clipboard.writeText(children);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-black/30 backdrop-blur-sm rounded-lg p-3 md:p-4 font-mono text-sm flex items-center justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground select-none">$ </span>
|
||||||
|
<span className="text-foreground">{children}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="text-muted-foreground hover:text-foreground transition-colors p-1"
|
||||||
|
title="Copy to clipboard"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
>
|
||||||
|
<path d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
>
|
||||||
|
<path d="M216,28H88A20,20,0,0,0,68,48V76H40A20,20,0,0,0,20,96V216a20,20,0,0,0,20,20H168a20,20,0,0,0,20-20V188h28a20,20,0,0,0,20-20V48A20,20,0,0,0,216,28ZM164,212H44V100H164Zm48-48H188V96a20,20,0,0,0-20-20H92V52H212Z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { CursorFieldProvider } from "~/components/butterfly";
|
import { CursorFieldProvider } from "~/components/butterfly";
|
||||||
|
import { CommandDialog } from "~/components/command-dialog";
|
||||||
import {
|
import {
|
||||||
appStoreUrl,
|
appStoreUrl,
|
||||||
playStoreUrl,
|
playStoreUrl,
|
||||||
@@ -507,63 +508,23 @@ function DownloadButton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ServerInstallButton() {
|
function ServerInstallButton() {
|
||||||
const [open, setOpen] = React.useState(false);
|
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
function handleClickOutside(e: MouseEvent) {
|
|
||||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" ref={ref}>
|
<CommandDialog
|
||||||
<button
|
trigger={
|
||||||
onClick={() => setOpen(!open)}
|
<span className="inline-flex items-center justify-center rounded-lg border border-white/20 px-3 py-2 text-white hover:bg-white/10 transition-colors">
|
||||||
className="inline-flex items-center justify-center rounded-lg border border-white/20 px-3 py-2 text-white hover:bg-white/10 transition-colors"
|
<TerminalIcon className="h-5 w-5" />
|
||||||
aria-label="Install on a server"
|
</span>
|
||||||
>
|
}
|
||||||
<TerminalIcon className="h-5 w-5" />
|
title="Run agents on a remote machine"
|
||||||
</button>
|
description="For headless machines you want to connect to from the Paseo apps. The desktop app already includes a built-in daemon."
|
||||||
<AnimatePresence>
|
command="npm install -g @getpaseo/cli && paseo"
|
||||||
{open && (
|
footnote={
|
||||||
<>
|
<>
|
||||||
<motion.div
|
Requires Node.js 18+. Run <span className="font-mono text-white/40">paseo</span> to
|
||||||
initial={{ opacity: 0 }}
|
start the daemon.
|
||||||
animate={{ opacity: 1 }}
|
</>
|
||||||
exit={{ opacity: 0 }}
|
}
|
||||||
transition={{ duration: 0.2 }}
|
/>
|
||||||
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95 }}
|
|
||||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
||||||
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-md rounded-xl border border-white/20 bg-background p-6 space-y-4"
|
|
||||||
>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-base font-medium text-white">Run agents on a remote machine</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
For headless machines you want to connect to from the Paseo apps. The desktop app
|
|
||||||
already includes a built-in daemon.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<CodeBlock>npm install -g @getpaseo/cli && paseo</CodeBlock>
|
|
||||||
<p className="text-xs text-white/30">
|
|
||||||
Requires Node.js 18+. Run <span className="font-mono text-white/40">paseo</span> to
|
|
||||||
start the daemon.
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,52 +632,6 @@ function Step({ number, children }: { number: number; children: React.ReactNode
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CodeBlock({ children }: { children: React.ReactNode }) {
|
|
||||||
const [copied, setCopied] = React.useState(false);
|
|
||||||
const text = typeof children === "string" ? children : "";
|
|
||||||
|
|
||||||
function handleCopy() {
|
|
||||||
navigator.clipboard.writeText(text);
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-black/30 backdrop-blur-sm rounded-lg p-3 md:p-4 font-mono text-sm flex items-center justify-between gap-2">
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground select-none">$ </span>
|
|
||||||
<span className="text-foreground">{children}</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleCopy}
|
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors p-1"
|
|
||||||
title="Copy to clipboard"
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 256 256"
|
|
||||||
>
|
|
||||||
<path d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 256 256"
|
|
||||||
>
|
|
||||||
<path d="M216,28H88A20,20,0,0,0,68,48V76H40A20,20,0,0,0,20,96V216a20,20,0,0,0,20,20H168a20,20,0,0,0,20-20V188h28a20,20,0,0,0,20-20V48A20,20,0,0,0,216,28ZM164,212H44V100H164Zm48-48H188V96a20,20,0,0,0-20-20H92V52H212Z" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bashKeywords = new Set([
|
const bashKeywords = new Set([
|
||||||
"while",
|
"while",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
import { Link, createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { CommandDialog } from "~/components/command-dialog";
|
||||||
import { pageMeta } from "~/meta";
|
import { pageMeta } from "~/meta";
|
||||||
import {
|
import {
|
||||||
desktopVersion,
|
desktopVersion,
|
||||||
@@ -109,9 +110,18 @@ function Download() {
|
|||||||
<AppleIcon className="h-5 w-5 text-foreground" />
|
<AppleIcon className="h-5 w-5 text-foreground" />
|
||||||
<span className="font-medium">macOS</span>
|
<span className="font-medium">macOS</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<DownloadPill href={macAppleSiliconDownloadUrl} label="Apple Silicon" />
|
<DownloadPill href={macAppleSiliconDownloadUrl} label="Apple Silicon" />
|
||||||
<DownloadPill href={macIntelDownloadUrl} label="Intel" />
|
<DownloadPill href={macIntelDownloadUrl} label="Intel" />
|
||||||
|
<CommandDialog
|
||||||
|
trigger={
|
||||||
|
<span className="inline-flex items-center justify-center rounded-full bg-foreground px-4 py-1.5 text-sm font-medium text-background hover:bg-foreground/85 transition-colors">
|
||||||
|
Homebrew
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
title="Install via Homebrew"
|
||||||
|
command="brew install --cask paseo"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -168,7 +178,7 @@ function Download() {
|
|||||||
external
|
external
|
||||||
/>
|
/>
|
||||||
<DownloadPill
|
<DownloadPill
|
||||||
href={`${releaseBase}/paseo-${desktopVersion}.apk`}
|
href={`${releaseBase}/paseo-v${desktopVersion}-android.apk`}
|
||||||
label="APK"
|
label="APK"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user