website: add /cloud design partners page and shared shell
- New /cloud page with signup form (email, name, company, role, message) posting to a Discord webhook via a TanStack Start server function on the Cloudflare Worker. Webhook URL read from DISCORD_WEBHOOK_URL secret. - Extract SiteShell, SiteFooter, FAQItem from landing-page. - Migrate /privacy, /download, /blog, /changelog, /cloud to SiteShell so they share header + footer with consistent width. - Add /cloud to header and footer nav. - Make /docs sidebar full-width; bump footer text from xs to sm. - Add Cloudflare Workers types and ignore .dev.vars.
This commit is contained in:
@@ -13,6 +13,8 @@ out/
|
||||
.env.local
|
||||
.env.test.local
|
||||
.env*.local
|
||||
.dev.vars
|
||||
**/.dev.vars
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { env } from "cloudflare:workers";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
|
||||
export interface CloudSignupInput {
|
||||
email: string;
|
||||
name?: string;
|
||||
company?: string;
|
||||
role?: string;
|
||||
message: string;
|
||||
honeypot?: string;
|
||||
}
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
function asString(v: unknown, max: number): string {
|
||||
if (typeof v !== "string") throw new Error("expected string");
|
||||
const trimmed = v.trim();
|
||||
if (trimmed.length > max) throw new Error("field too long");
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function validate(raw: unknown): CloudSignupInput {
|
||||
if (typeof raw !== "object" || raw === null) throw new Error("invalid input");
|
||||
const r = raw as Record<string, unknown>;
|
||||
|
||||
const email = asString(r.email, 320);
|
||||
if (!EMAIL_RE.test(email)) throw new Error("invalid email");
|
||||
|
||||
const message = asString(r.message, 4000);
|
||||
if (message.length === 0) throw new Error("message required");
|
||||
|
||||
const name = r.name === undefined || r.name === "" ? undefined : asString(r.name, 200);
|
||||
const company =
|
||||
r.company === undefined || r.company === "" ? undefined : asString(r.company, 200);
|
||||
const role = r.role === undefined || r.role === "" ? undefined : asString(r.role, 200);
|
||||
const honeypot = typeof r.honeypot === "string" ? r.honeypot : "";
|
||||
|
||||
return { email, name, company, role, message, honeypot };
|
||||
}
|
||||
|
||||
function truncate(s: string, max: number): string {
|
||||
return s.length <= max ? s : `${s.slice(0, max - 1)}…`;
|
||||
}
|
||||
|
||||
function buildEmbed(input: CloudSignupInput) {
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [
|
||||
{ name: "Email", value: truncate(input.email, 1024), inline: true },
|
||||
];
|
||||
if (input.name) {
|
||||
fields.push({ name: "Name", value: truncate(input.name, 1024), inline: true });
|
||||
}
|
||||
if (input.company) {
|
||||
fields.push({ name: "Company", value: truncate(input.company, 1024), inline: true });
|
||||
}
|
||||
if (input.role) {
|
||||
fields.push({ name: "Role", value: truncate(input.role, 1024), inline: true });
|
||||
}
|
||||
fields.push({ name: "Message", value: truncate(input.message, 1024) });
|
||||
|
||||
return {
|
||||
title: "Paseo Cloud signup",
|
||||
color: 0x5865f2,
|
||||
fields,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export const submitCloudSignup = createServerFn({ method: "POST" })
|
||||
.inputValidator(validate)
|
||||
.handler(async ({ data }) => {
|
||||
if (data.honeypot) return { ok: true };
|
||||
|
||||
const url = (env as { DISCORD_WEBHOOK_URL?: string }).DISCORD_WEBHOOK_URL;
|
||||
if (!url) throw new Error("webhook not configured");
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ embeds: [buildEmbed(data)] }),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`webhook ${res.status}`);
|
||||
return { ok: true };
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function FAQItem({ question, children }: { question: string; children: ReactNode }) {
|
||||
return (
|
||||
<details className="group">
|
||||
<summary className="font-medium text-sm cursor-pointer list-none flex items-start gap-2 -ml-4">
|
||||
<span className="font-mono text-white/40 flex-shrink-0 group-open:hidden">+</span>
|
||||
<span className="font-mono text-white/40 flex-shrink-0 hidden group-open:inline">−</span>
|
||||
{question}
|
||||
</summary>
|
||||
<div className="text-sm text-muted-foreground space-y-2 mt-2 prose">{children}</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@@ -48,6 +48,8 @@ import { useRelease } from "~/routes/__root";
|
||||
import { Mic } from "lucide-react";
|
||||
import { HeroMockup } from "~/components/hero-mockup";
|
||||
import { ClaudeIcon } from "~/components/mockup";
|
||||
import { FAQItem } from "~/components/faq-item";
|
||||
import { SiteFooter } from "~/components/site-footer";
|
||||
import { SiteHeader } from "~/components/site-header";
|
||||
import "~/styles.css";
|
||||
|
||||
@@ -97,126 +99,7 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
|
||||
<SponsorCTA />
|
||||
</div>
|
||||
</main>
|
||||
<footer className="p-6 md:p-20 md:pt-0 max-w-5xl mx-auto">
|
||||
<div className="border-t border-white/10 pt-8 pb-4 grid grid-cols-2 sm:grid-cols-4 gap-8 text-xs">
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/60 font-medium">Product</p>
|
||||
<div className="space-y-2">
|
||||
<a
|
||||
href="/blog"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="/docs"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<a
|
||||
href="/changelog"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Changelog
|
||||
</a>
|
||||
<a
|
||||
href="/docs/cli"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
CLI
|
||||
</a>
|
||||
<a
|
||||
href="/privacy"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Privacy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/60 font-medium">Agents</p>
|
||||
<div className="space-y-2">
|
||||
<a
|
||||
href="/claude-code"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Claude Code
|
||||
</a>
|
||||
<a
|
||||
href="/codex"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Codex
|
||||
</a>
|
||||
<a
|
||||
href="/opencode"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
OpenCode
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/60 font-medium">Community</p>
|
||||
<div className="space-y-2">
|
||||
<a
|
||||
href="https://discord.gg/jz8T2uahpH"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Discord
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/getpaseo/paseo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/60 font-medium">Download</p>
|
||||
<div className="space-y-2">
|
||||
<a
|
||||
href={appStoreUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
App Store
|
||||
</a>
|
||||
<a
|
||||
href={playStoreUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Google Play
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/getpaseo/paseo/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Desktop
|
||||
</a>
|
||||
<a
|
||||
href={webAppUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Web App
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<SiteFooter />
|
||||
</div>
|
||||
</CursorFieldProvider>
|
||||
);
|
||||
@@ -1685,16 +1568,3 @@ function SponsorCTA() {
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function FAQItem({ question, children }: { question: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<details className="group">
|
||||
<summary className="font-medium text-sm cursor-pointer list-none flex items-start gap-2 -ml-4">
|
||||
<span className="font-mono text-white/40 flex-shrink-0 group-open:hidden">+</span>
|
||||
<span className="font-mono text-white/40 flex-shrink-0 hidden group-open:inline">−</span>
|
||||
{question}
|
||||
</summary>
|
||||
<div className="text-sm text-muted-foreground space-y-2 mt-2 prose">{children}</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { appStoreUrl, playStoreUrl, webAppUrl } from "~/downloads";
|
||||
|
||||
interface SiteFooterProps {
|
||||
width?: "default" | "prose";
|
||||
}
|
||||
|
||||
export function SiteFooter({ width = "default" }: SiteFooterProps) {
|
||||
const widthClasses =
|
||||
width === "prose" ? "max-w-prose p-6 md:p-12 md:pt-0" : "max-w-5xl p-6 md:p-20 md:pt-0";
|
||||
return (
|
||||
<footer className={`${widthClasses} mx-auto`}>
|
||||
<div className="border-t border-white/10 pt-8 pb-4 grid grid-cols-2 sm:grid-cols-4 gap-8 text-sm">
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/60 font-medium">Product</p>
|
||||
<div className="space-y-2">
|
||||
<a href="/blog" className="block text-white/40 hover:text-white/60 transition-colors">
|
||||
Blog
|
||||
</a>
|
||||
<a href="/docs" className="block text-white/40 hover:text-white/60 transition-colors">
|
||||
Docs
|
||||
</a>
|
||||
<a
|
||||
href="/changelog"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Changelog
|
||||
</a>
|
||||
<a href="/cloud" className="block text-white/40 hover:text-white/60 transition-colors">
|
||||
Cloud
|
||||
</a>
|
||||
<a
|
||||
href="/docs/cli"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
CLI
|
||||
</a>
|
||||
<a
|
||||
href="/privacy"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Privacy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/60 font-medium">Agents</p>
|
||||
<div className="space-y-2">
|
||||
<a
|
||||
href="/claude-code"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Claude Code
|
||||
</a>
|
||||
<a href="/codex" className="block text-white/40 hover:text-white/60 transition-colors">
|
||||
Codex
|
||||
</a>
|
||||
<a
|
||||
href="/opencode"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
OpenCode
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/60 font-medium">Community</p>
|
||||
<div className="space-y-2">
|
||||
<a
|
||||
href="https://discord.gg/jz8T2uahpH"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Discord
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/getpaseo/paseo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/60 font-medium">Download</p>
|
||||
<div className="space-y-2">
|
||||
<a
|
||||
href={appStoreUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
App Store
|
||||
</a>
|
||||
<a
|
||||
href={playStoreUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Google Play
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/getpaseo/paseo/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Desktop
|
||||
</a>
|
||||
<a
|
||||
href={webAppUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Web App
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,12 @@ export function SiteHeader() {
|
||||
>
|
||||
Changelog
|
||||
</a>
|
||||
<a
|
||||
href="/cloud"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Cloud
|
||||
</a>
|
||||
<a
|
||||
href="/download"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { SiteFooter } from "~/components/site-footer";
|
||||
import { SiteHeader } from "~/components/site-header";
|
||||
|
||||
interface SiteShellProps {
|
||||
children: ReactNode;
|
||||
width?: "default" | "prose";
|
||||
}
|
||||
|
||||
export function SiteShell({ children, width = "default" }: SiteShellProps) {
|
||||
const mainClasses =
|
||||
width === "prose" ? "max-w-prose p-6 md:p-12 mx-auto" : "max-w-5xl p-6 md:p-20 mx-auto";
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<main className={mainClasses}>
|
||||
<div className="mb-12">
|
||||
<SiteHeader />
|
||||
</div>
|
||||
{children}
|
||||
</main>
|
||||
<SiteFooter width={width} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { Route as OpencodeRouteImport } from "./routes/opencode";
|
||||
import { Route as DownloadRouteImport } from "./routes/download";
|
||||
import { Route as DocsRouteImport } from "./routes/docs";
|
||||
import { Route as CodexRouteImport } from "./routes/codex";
|
||||
import { Route as CloudRouteImport } from "./routes/cloud";
|
||||
import { Route as ClaudeCodeRouteImport } from "./routes/claude-code";
|
||||
import { Route as ChangelogRouteImport } from "./routes/changelog";
|
||||
import { Route as BlogRouteImport } from "./routes/blog";
|
||||
@@ -48,6 +49,11 @@ const CodexRoute = CodexRouteImport.update({
|
||||
path: "/codex",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const CloudRoute = CloudRouteImport.update({
|
||||
id: "/cloud",
|
||||
path: "/cloud",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const ClaudeCodeRoute = ClaudeCodeRouteImport.update({
|
||||
id: "/claude-code",
|
||||
path: "/claude-code",
|
||||
@@ -94,6 +100,7 @@ export interface FileRoutesByFullPath {
|
||||
"/blog": typeof BlogRouteWithChildren;
|
||||
"/changelog": typeof ChangelogRoute;
|
||||
"/claude-code": typeof ClaudeCodeRoute;
|
||||
"/cloud": typeof CloudRoute;
|
||||
"/codex": typeof CodexRoute;
|
||||
"/docs": typeof DocsRouteWithChildren;
|
||||
"/download": typeof DownloadRoute;
|
||||
@@ -108,6 +115,7 @@ export interface FileRoutesByTo {
|
||||
"/": typeof IndexRoute;
|
||||
"/changelog": typeof ChangelogRoute;
|
||||
"/claude-code": typeof ClaudeCodeRoute;
|
||||
"/cloud": typeof CloudRoute;
|
||||
"/codex": typeof CodexRoute;
|
||||
"/download": typeof DownloadRoute;
|
||||
"/opencode": typeof OpencodeRoute;
|
||||
@@ -123,6 +131,7 @@ export interface FileRoutesById {
|
||||
"/blog": typeof BlogRouteWithChildren;
|
||||
"/changelog": typeof ChangelogRoute;
|
||||
"/claude-code": typeof ClaudeCodeRoute;
|
||||
"/cloud": typeof CloudRoute;
|
||||
"/codex": typeof CodexRoute;
|
||||
"/docs": typeof DocsRouteWithChildren;
|
||||
"/download": typeof DownloadRoute;
|
||||
@@ -140,6 +149,7 @@ export interface FileRouteTypes {
|
||||
| "/blog"
|
||||
| "/changelog"
|
||||
| "/claude-code"
|
||||
| "/cloud"
|
||||
| "/codex"
|
||||
| "/docs"
|
||||
| "/download"
|
||||
@@ -154,6 +164,7 @@ export interface FileRouteTypes {
|
||||
| "/"
|
||||
| "/changelog"
|
||||
| "/claude-code"
|
||||
| "/cloud"
|
||||
| "/codex"
|
||||
| "/download"
|
||||
| "/opencode"
|
||||
@@ -168,6 +179,7 @@ export interface FileRouteTypes {
|
||||
| "/blog"
|
||||
| "/changelog"
|
||||
| "/claude-code"
|
||||
| "/cloud"
|
||||
| "/codex"
|
||||
| "/docs"
|
||||
| "/download"
|
||||
@@ -184,6 +196,7 @@ export interface RootRouteChildren {
|
||||
BlogRoute: typeof BlogRouteWithChildren;
|
||||
ChangelogRoute: typeof ChangelogRoute;
|
||||
ClaudeCodeRoute: typeof ClaudeCodeRoute;
|
||||
CloudRoute: typeof CloudRoute;
|
||||
CodexRoute: typeof CodexRoute;
|
||||
DocsRoute: typeof DocsRouteWithChildren;
|
||||
DownloadRoute: typeof DownloadRoute;
|
||||
@@ -228,6 +241,13 @@ declare module "@tanstack/react-router" {
|
||||
preLoaderRoute: typeof CodexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/cloud": {
|
||||
id: "/cloud";
|
||||
path: "/cloud";
|
||||
fullPath: "/cloud";
|
||||
preLoaderRoute: typeof CloudRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/claude-code": {
|
||||
id: "/claude-code";
|
||||
path: "/claude-code";
|
||||
@@ -316,6 +336,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
BlogRoute: BlogRouteWithChildren,
|
||||
ChangelogRoute: ChangelogRoute,
|
||||
ClaudeCodeRoute: ClaudeCodeRoute,
|
||||
CloudRoute: CloudRoute,
|
||||
CodexRoute: CodexRoute,
|
||||
DocsRoute: DocsRouteWithChildren,
|
||||
DownloadRoute: DownloadRoute,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
||||
import { SiteHeader } from "~/components/site-header";
|
||||
import { SiteShell } from "~/components/site-shell";
|
||||
|
||||
export const Route = createFileRoute("/blog")({
|
||||
component: BlogLayout,
|
||||
@@ -7,13 +7,8 @@ export const Route = createFileRoute("/blog")({
|
||||
|
||||
function BlogLayout() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="max-w-prose mx-auto p-6 md:p-12">
|
||||
<div className="mb-8">
|
||||
<SiteHeader />
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
<SiteShell>
|
||||
<Outlet />
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import changelogMarkdown from "../../../../CHANGELOG.md?raw";
|
||||
import { SiteShell } from "~/components/site-shell";
|
||||
import { pageMeta } from "~/meta";
|
||||
import { SiteHeader } from "~/components/site-header";
|
||||
|
||||
export const Route = createFileRoute("/changelog")({
|
||||
head: () => ({
|
||||
@@ -16,15 +16,10 @@ export const Route = createFileRoute("/changelog")({
|
||||
|
||||
function Changelog() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="max-w-4xl mx-auto p-6 md:p-12">
|
||||
<div className="mb-8">
|
||||
<SiteHeader />
|
||||
</div>
|
||||
<article className="changelog-markdown rounded-xl border border-border bg-card/40 p-6 md:p-8">
|
||||
<ReactMarkdown>{changelogMarkdown}</ReactMarkdown>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<SiteShell>
|
||||
<article className="changelog-markdown rounded-xl border border-border bg-card/40 p-6 md:p-8">
|
||||
<ReactMarkdown>{changelogMarkdown}</ReactMarkdown>
|
||||
</article>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useCallback, useState, type FormEvent } from "react";
|
||||
import { submitCloudSignup, type CloudSignupInput } from "~/cloud-signup";
|
||||
import { FAQItem } from "~/components/faq-item";
|
||||
import { SiteShell } from "~/components/site-shell";
|
||||
import { pageMeta } from "~/meta";
|
||||
|
||||
export const Route = createFileRoute("/cloud")({
|
||||
head: () => ({
|
||||
meta: pageMeta(
|
||||
"Paseo Cloud - Design Partners",
|
||||
"Paseo across machines, with a team, or inside a company. Looking for design partners.",
|
||||
),
|
||||
}),
|
||||
component: Cloud,
|
||||
});
|
||||
|
||||
const INPUT_CLASS =
|
||||
"block w-full rounded-md bg-white/5 border border-white/10 px-3.5 py-2.5 text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-white/30 transition-colors";
|
||||
|
||||
type Status = "idle" | "submitting" | "success" | "error";
|
||||
|
||||
function Cloud() {
|
||||
return (
|
||||
<SiteShell>
|
||||
<h1 className="text-3xl font-medium mb-3">Paseo Cloud</h1>
|
||||
<p className="text-white/70 leading-relaxed mb-10">
|
||||
For using Paseo across machines, with a team, or inside a company. Looking for design
|
||||
partners.
|
||||
</p>
|
||||
|
||||
<div className="space-y-20">
|
||||
<SignupForm />
|
||||
<FaqSection />
|
||||
</div>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
|
||||
function SignupForm() {
|
||||
const [status, setStatus] = useState<Status>("idle");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = useCallback(async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setStatus("submitting");
|
||||
setError(null);
|
||||
|
||||
const form = new FormData(event.currentTarget);
|
||||
const data: CloudSignupInput = {
|
||||
email: String(form.get("email") ?? ""),
|
||||
name: form.get("name") ? String(form.get("name")) : undefined,
|
||||
company: form.get("company") ? String(form.get("company")) : undefined,
|
||||
role: form.get("role") ? String(form.get("role")) : undefined,
|
||||
message: String(form.get("message") ?? ""),
|
||||
honeypot: form.get("website") ? String(form.get("website")) : "",
|
||||
};
|
||||
|
||||
try {
|
||||
await submitCloudSignup({ data });
|
||||
setStatus("success");
|
||||
} catch (err) {
|
||||
setStatus("error");
|
||||
setError(err instanceof Error ? err.message : "Something went wrong.");
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (status === "success") {
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
<p className="text-white/70">
|
||||
Got it. I'll be in touch. If you don't hear back within a week, ping me on{" "}
|
||||
<a
|
||||
href="https://discord.gg/jz8T2uahpH"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-white/80"
|
||||
>
|
||||
Discord
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const submitting = status === "submitting";
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6" noValidate>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<Field label="Email" required>
|
||||
<input type="email" name="email" required autoComplete="email" className={INPUT_CLASS} />
|
||||
</Field>
|
||||
<Field label="Name">
|
||||
<input type="text" name="name" autoComplete="name" className={INPUT_CLASS} />
|
||||
</Field>
|
||||
<Field label="Company">
|
||||
<input type="text" name="company" autoComplete="organization" className={INPUT_CLASS} />
|
||||
</Field>
|
||||
<Field label="Role">
|
||||
<input
|
||||
type="text"
|
||||
name="role"
|
||||
autoComplete="organization-title"
|
||||
className={INPUT_CLASS}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field label="Message" required>
|
||||
<textarea
|
||||
name="message"
|
||||
required
|
||||
rows={5}
|
||||
placeholder="A bit about you and what you'd want from Paseo Cloud."
|
||||
className={`${INPUT_CLASS} resize-y`}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="website"
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
aria-hidden="true"
|
||||
className="absolute left-[-9999px] w-px h-px opacity-0"
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-red-400">
|
||||
{error === "webhook not configured"
|
||||
? "The form isn't wired up yet. Try Discord for now."
|
||||
: "Something went wrong. Try again or DM me on Discord."}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="rounded-md bg-white text-black px-4 py-2 text-sm font-medium hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{submitting ? "Sending…" : "Send"}
|
||||
</button>
|
||||
<p className="text-sm text-white/50">
|
||||
Or{" "}
|
||||
<a
|
||||
href="https://discord.gg/jz8T2uahpH"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-white/80"
|
||||
>
|
||||
DM me on Discord
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
required,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
required?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<label className="block">
|
||||
<span className="block text-sm font-medium text-white/80 pb-2">
|
||||
{label}
|
||||
{required && <span className="text-white/40"> *</span>}
|
||||
</span>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function FaqSection() {
|
||||
return (
|
||||
<section className="space-y-6">
|
||||
<h2 className="text-3xl font-medium">FAQ</h2>
|
||||
<div className="space-y-6">
|
||||
<FAQItem question="What is Paseo Cloud?">
|
||||
An optional layer on top of Paseo for running daemons across machines, syncing config
|
||||
between them, and using Paseo with a team or company. Think shared runners, permissions,
|
||||
audit, managed daemons, and org controls.
|
||||
</FAQItem>
|
||||
<FAQItem question="Will Paseo stay free and open source?">
|
||||
Yes. The whole stack stays free and open source: app, daemon, CLI, protocols, and the
|
||||
Cloud control plane. Managed Cloud is the optional paid layer for people who don't
|
||||
want to host it themselves.
|
||||
</FAQItem>
|
||||
<FAQItem question="Self-hosted or managed?">
|
||||
Both. The control plane will live in the Paseo monorepo so you can host it yourself.
|
||||
Managed is for people and teams who don't want to.
|
||||
</FAQItem>
|
||||
<FAQItem question="Does my code go through Paseo?">
|
||||
No. Daemons run on your machines and talk to agent providers directly. Cloud handles
|
||||
registration, config sync, permissions, and orchestration. Code and model traffic stay on
|
||||
your machines.
|
||||
</FAQItem>
|
||||
<FAQItem question="When will it be available?">
|
||||
Early access for design partners now. No public date. The app and daemon come first.
|
||||
</FAQItem>
|
||||
<FAQItem question="How does pricing work?">
|
||||
Not set yet. Design partners help shape it.
|
||||
</FAQItem>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -39,14 +39,14 @@ function DocsLayout() {
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
<div className="flex">
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden md:block w-56 shrink-0 border-r border-border p-6 sticky top-0 h-screen">
|
||||
<Link to="/" className="flex items-center gap-3 mb-8">
|
||||
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
|
||||
<span className="text-lg font-medium">Paseo</span>
|
||||
</Link>
|
||||
<nav className="space-y-1">
|
||||
<nav className="space-y-1 -ml-3">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { CodeBlock } from "~/components/code-block";
|
||||
import { SiteShell } from "~/components/site-shell";
|
||||
import { pageMeta } from "~/meta";
|
||||
import {
|
||||
downloadUrls,
|
||||
@@ -32,232 +33,176 @@ function Download() {
|
||||
const urls = downloadUrls(release);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="max-w-3xl mx-auto p-6 md:p-12">
|
||||
<header className="flex items-center justify-between gap-4 mb-12">
|
||||
<Link to="/" className="flex items-center gap-3">
|
||||
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
|
||||
<span className="text-lg font-medium">Paseo</span>
|
||||
</Link>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
to="/docs"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
<Link
|
||||
to="/changelog"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Changelog
|
||||
</Link>
|
||||
<a
|
||||
href="https://discord.gg/jz8T2uahpH"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Discord"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
|
||||
>
|
||||
<svg
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/getpaseo/paseo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="GitHub"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M12 0C5.37 0 0 5.484 0 12.252c0 5.418 3.438 10.013 8.205 11.637.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.738-4.042-1.61-4.042-1.61-.546-1.403-1.333-1.776-1.333-1.776-1.089-.756.084-.741.084-.741 1.205.087 1.838 1.262 1.838 1.262 1.07 1.87 2.809 1.33 3.495 1.017.108-.79.417-1.33.76-1.636-2.665-.31-5.467-1.35-5.467-6.005 0-1.327.465-2.413 1.235-3.262-.124-.31-.535-1.556.117-3.243 0 0 1.008-.33 3.3 1.248a11.2 11.2 0 0 1 3.003-.404c1.02.005 2.045.138 3.003.404 2.29-1.578 3.297-1.248 3.297-1.248.653 1.687.242 2.933.118 3.243.77.85 1.233 1.935 1.233 3.262 0 4.667-2.807 5.692-5.48 5.995.43.38.823 1.133.823 2.285 0 1.65-.015 2.98-.015 3.386 0 .315.218.694.825.576C20.565 22.26 24 17.667 24 12.252 24 5.484 18.627 0 12 0z" />
|
||||
</svg>
|
||||
</a>
|
||||
<SiteShell>
|
||||
<h1 className="text-3xl md:text-4xl font-semibold tracking-tight mb-2">Download</h1>
|
||||
<p className="text-muted-foreground mb-10">v{version}</p>
|
||||
|
||||
{/* Desktop */}
|
||||
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
|
||||
<div className="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Desktop</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Recommended, bundles everything you need
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<MonitorIcon className="h-5 w-5 text-muted-foreground mt-1.5" />
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl md:text-4xl font-semibold tracking-tight mb-2">Download</h1>
|
||||
<p className="text-muted-foreground mb-10">v{version}</p>
|
||||
|
||||
{/* Desktop */}
|
||||
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
|
||||
<div className="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Desktop</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">Recommended, bundles the server</p>
|
||||
<div className="divide-y divide-border">
|
||||
{/* macOS */}
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<AppleIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">macOS</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DownloadPill href={urls.macAppleSilicon} label="Apple Silicon" />
|
||||
<DownloadPill href={urls.macIntel} label="Intel" />
|
||||
</div>
|
||||
<MonitorIcon className="h-5 w-5 text-muted-foreground mt-1.5" />
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-border">
|
||||
{/* macOS */}
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<AppleIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">macOS</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DownloadPill href={urls.macAppleSilicon} label="Apple Silicon" />
|
||||
<DownloadPill href={urls.macIntel} label="Intel" />
|
||||
</div>
|
||||
{/* Homebrew */}
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<TerminalIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">Homebrew</span>
|
||||
</div>
|
||||
|
||||
{/* Homebrew */}
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<TerminalIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">Homebrew</span>
|
||||
</div>
|
||||
<CodeBlock size="sm">brew install --cask paseo</CodeBlock>
|
||||
</div>
|
||||
|
||||
{/* Windows */}
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<WindowsIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">Windows</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill
|
||||
href={urls.windowsExeX64}
|
||||
label={urls.windowsExeArm64 ? "Intel / x64" : "Download"}
|
||||
/>
|
||||
{urls.windowsExeArm64 && <DownloadPill href={urls.windowsExeArm64} label="ARM64" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Linux */}
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<LinuxIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">Linux</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill href={urls.linuxAppImage} label="AppImage" />
|
||||
<DownloadPill href={urls.linuxDeb} label="DEB" />
|
||||
<DownloadPill href={urls.linuxRpm} label="RPM" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Mobile */}
|
||||
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-2xl font-semibold">Mobile</h2>
|
||||
<PhoneIcon className="h-5 w-5 text-muted-foreground" />
|
||||
<CodeBlock size="sm">brew install --cask paseo</CodeBlock>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-border">
|
||||
{/* Android */}
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<AndroidIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">Android</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill href={playStoreUrl} label="Play Store" external />
|
||||
<DownloadPill href={urls.androidApk} label="APK" />
|
||||
</div>
|
||||
{/* Windows */}
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<WindowsIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">Windows</span>
|
||||
</div>
|
||||
|
||||
{/* iOS */}
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<AppleIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">iOS</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill href={appStoreUrl} label="App Store" external />
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill
|
||||
href={urls.windowsExeX64}
|
||||
label={urls.windowsExeArm64 ? "Intel / x64" : "Download"}
|
||||
/>
|
||||
{urls.windowsExeArm64 && <DownloadPill href={urls.windowsExeArm64} label="ARM64" />}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Web */}
|
||||
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
|
||||
<div className="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Web</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Connect to a server from any browser
|
||||
</p>
|
||||
{/* Linux */}
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<LinuxIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">Linux</span>
|
||||
</div>
|
||||
<GlobeIcon className="h-5 w-5 text-muted-foreground mt-1.5" />
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-border">
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<GlobeIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">Web App</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill href={webAppUrl} label="Open" external />
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill href={urls.linuxAppImage} label="AppImage" />
|
||||
<DownloadPill href={urls.linuxDeb} label="DEB" />
|
||||
<DownloadPill href={urls.linuxRpm} label="RPM" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Server */}
|
||||
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8">
|
||||
<div className="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Server</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Run the Paseo server anywhere, connect from any client
|
||||
</p>
|
||||
{/* Mobile */}
|
||||
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-2xl font-semibold">Mobile</h2>
|
||||
<PhoneIcon className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-border">
|
||||
{/* Android */}
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<AndroidIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">Android</span>
|
||||
</div>
|
||||
<TerminalIcon className="h-5 w-5 text-muted-foreground mt-1.5" />
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-border">
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<TerminalIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">npm</span>
|
||||
</div>
|
||||
<CodeBlock size="sm">npm install -g @getpaseo/cli && paseo</CodeBlock>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<TerminalIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">Nix</span>
|
||||
</div>
|
||||
<CodeBlock size="sm">nix run github:getpaseo/paseo</CodeBlock>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill href={playStoreUrl} label="Play Store" external />
|
||||
<DownloadPill href={urls.androidApk} label="APK" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p className="text-center text-xs text-muted-foreground mt-8">
|
||||
All releases are available on{" "}
|
||||
<a
|
||||
href="https://github.com/getpaseo/paseo/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-foreground transition-colors"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* iOS */}
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<AppleIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">iOS</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill href={appStoreUrl} label="App Store" external />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Web */}
|
||||
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
|
||||
<div className="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Web</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Connect to a server from any browser
|
||||
</p>
|
||||
</div>
|
||||
<GlobeIcon className="h-5 w-5 text-muted-foreground mt-1.5" />
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-border">
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<GlobeIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">Web App</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill href={webAppUrl} label="Open" external />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Server */}
|
||||
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8">
|
||||
<div className="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Server</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Run the Paseo server anywhere, connect from any client
|
||||
</p>
|
||||
</div>
|
||||
<TerminalIcon className="h-5 w-5 text-muted-foreground mt-1.5" />
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-border">
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<TerminalIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">npm</span>
|
||||
</div>
|
||||
<CodeBlock size="sm">npm install -g @getpaseo/cli && paseo</CodeBlock>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<TerminalIcon className="h-5 w-5 text-foreground" />
|
||||
<span className="font-medium">Nix</span>
|
||||
</div>
|
||||
<CodeBlock size="sm">nix run github:getpaseo/paseo</CodeBlock>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p className="text-center text-xs text-muted-foreground mt-8">
|
||||
All releases are available on{" "}
|
||||
<a
|
||||
href="https://github.com/getpaseo/paseo/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-foreground transition-colors"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { SiteShell } from "~/components/site-shell";
|
||||
import { pageMeta } from "~/meta";
|
||||
|
||||
export const Route = createFileRoute("/privacy")({
|
||||
@@ -13,83 +14,81 @@ export const Route = createFileRoute("/privacy")({
|
||||
|
||||
function Privacy() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="max-w-3xl mx-auto p-6 md:p-12">
|
||||
<h1 className="text-3xl font-medium mb-8">Privacy Policy</h1>
|
||||
<SiteShell>
|
||||
<h1 className="text-3xl font-medium mb-8">Privacy Policy</h1>
|
||||
|
||||
<div className="space-y-6 text-white/70 leading-relaxed">
|
||||
<div className="space-y-6 text-white/70 leading-relaxed">
|
||||
<p>
|
||||
Paseo is a self-hosted tool for managing coding agents. Your code and data stay on your
|
||||
machine.
|
||||
</p>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">What we collect</h2>
|
||||
<p>Nothing. Paseo runs on your machine and doesn't send us any data.</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">The relay server</h2>
|
||||
<p>
|
||||
Paseo is a self-hosted tool for managing coding agents. Your code and data stay on your
|
||||
machine.
|
||||
If you use the optional encrypted relay to connect your phone to your daemon, the relay
|
||||
sees:
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-4">
|
||||
<li>IP addresses and connection timing</li>
|
||||
<li>Message sizes</li>
|
||||
<li>Session IDs</li>
|
||||
</ul>
|
||||
<p>
|
||||
All messages between your phone and daemon are end-to-end encrypted with AES-256-GCM.
|
||||
The relay cannot read your messages, see your code, or decrypt your traffic.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">What we collect</h2>
|
||||
<p>Nothing. Paseo runs on your machine and doesn't send us any data.</p>
|
||||
</section>
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">Analytics and tracking</h2>
|
||||
<p>
|
||||
We don't use analytics, tracking pixels, cookies, or ads. The app doesn't
|
||||
phone home.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">The relay server</h2>
|
||||
<p>
|
||||
If you use the optional encrypted relay to connect your phone to your daemon, the
|
||||
relay sees:
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-4">
|
||||
<li>IP addresses and connection timing</li>
|
||||
<li>Message sizes</li>
|
||||
<li>Session IDs</li>
|
||||
</ul>
|
||||
<p>
|
||||
All messages between your phone and daemon are end-to-end encrypted with AES-256-GCM.
|
||||
The relay cannot read your messages, see your code, or decrypt your traffic.
|
||||
</p>
|
||||
</section>
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">Third-party services</h2>
|
||||
<p>
|
||||
Paseo wraps agent providers like Claude Code, Codex, and OpenCode. Those tools
|
||||
communicate with their own APIs (Anthropic, OpenAI, etc.) using your credentials. Paseo
|
||||
doesn't manage or intercept those API calls.
|
||||
</p>
|
||||
<p>
|
||||
If you use voice features with cloud providers (OpenAI speech), your voice data is sent
|
||||
to those services according to their privacy policies.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">Analytics and tracking</h2>
|
||||
<p>
|
||||
We don't use analytics, tracking pixels, cookies, or ads. The app doesn't
|
||||
phone home.
|
||||
</p>
|
||||
</section>
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">We don't sell your data</h2>
|
||||
<p>We don't have your data to sell. Paseo is self-hosted and local-first.</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">Third-party services</h2>
|
||||
<p>
|
||||
Paseo wraps agent providers like Claude Code, Codex, and OpenCode. Those tools
|
||||
communicate with their own APIs (Anthropic, OpenAI, etc.) using your credentials.
|
||||
Paseo doesn't manage or intercept those API calls.
|
||||
</p>
|
||||
<p>
|
||||
If you use voice features with cloud providers (OpenAI speech), your voice data is
|
||||
sent to those services according to their privacy policies.
|
||||
</p>
|
||||
</section>
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">Questions</h2>
|
||||
<p>
|
||||
If you have questions about privacy, open an issue on{" "}
|
||||
<a
|
||||
href="https://github.com/getpaseo/paseo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-white/90"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">We don't sell your data</h2>
|
||||
<p>We don't have your data to sell. Paseo is self-hosted and local-first.</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">Questions</h2>
|
||||
<p>
|
||||
If you have questions about privacy, open an issue on{" "}
|
||||
<a
|
||||
href="https://github.com/getpaseo/paseo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-white/90"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p className="text-sm text-white/50 pt-6">Last updated: February 2025</p>
|
||||
</div>
|
||||
<p className="text-sm text-white/50 pt-6">Last updated: February 2025</p>
|
||||
</div>
|
||||
</div>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"types": ["@cloudflare/workers-types"],
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ const sitemapPages = [
|
||||
"/",
|
||||
"/changelog",
|
||||
"/claude-code",
|
||||
"/cloud",
|
||||
"/codex",
|
||||
"/download",
|
||||
"/opencode",
|
||||
|
||||
Reference in New Issue
Block a user