mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
chore(lint): clear website warnings to zero
Resolves all 61 oxlint warnings in packages/website/ by hoisting inline callbacks/JSX, extracting sub-components to satisfy jsx-max-depth, using stable data-derived keys, adding explicit button types, flattening nested ternaries, and removing unused code.
This commit is contained in:
@@ -46,9 +46,14 @@ export function CommandDialog({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleToggle = React.useCallback(() => setOpen((prev) => !prev), []);
|
||||
const handleClose = React.useCallback(() => setOpen(false), []);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={ref}>
|
||||
<button onClick={() => setOpen(!open)}>{trigger}</button>
|
||||
<button type="button" onClick={handleToggle}>
|
||||
{trigger}
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<>
|
||||
@@ -58,7 +63,7 @@ export function CommandDialog({
|
||||
exit={OVERLAY_EXIT}
|
||||
transition={OVERLAY_TRANSITION}
|
||||
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => setOpen(false)}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<motion.div
|
||||
initial={PANEL_INITIAL}
|
||||
@@ -84,11 +89,11 @@ export function CommandDialog({
|
||||
function CodeBlock({ children }: { children: string }) {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
function handleCopy() {
|
||||
const handleCopy = React.useCallback(() => {
|
||||
navigator.clipboard.writeText(children);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
}, [children]);
|
||||
|
||||
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">
|
||||
@@ -97,6 +102,7 @@ function CodeBlock({ children }: { children: string }) {
|
||||
<span className="text-foreground">{children}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors p-1"
|
||||
title="Copy to clipboard"
|
||||
|
||||
@@ -316,36 +316,23 @@ const DIFF_LINES: DiffLine[] = [
|
||||
|
||||
// ── Implementation agent chat data ──────────────────────
|
||||
|
||||
const IMPLEMENT_CHAT: ChatItem[] = [
|
||||
{ type: "text", text: "Starting implementation of the returns dashboard.", bold: true },
|
||||
{ type: "tool", label: "Edit file", summary: "src/pages/dashboard.tsx", status: "done" },
|
||||
{ type: "tool", label: "Edit file", summary: "src/components/return-table.tsx", status: "done" },
|
||||
{ type: "tool", label: "Edit file", summary: "src/components/filter-bar.tsx", status: "done" },
|
||||
{ type: "tool", label: "Edit file", summary: "src/components/status-chart.tsx", status: "done" },
|
||||
{ type: "tool", label: "Run command", summary: "npm run typecheck", status: "done" },
|
||||
{ type: "text", text: "Typecheck passes. Adding API route and data fetching." },
|
||||
{ type: "tool", label: "Edit file", summary: "src/api/returns.ts", status: "done" },
|
||||
{ type: "tool", label: "Edit file", summary: "src/hooks/use-returns.ts", status: "done" },
|
||||
{ type: "tool", label: "Run command", summary: "npm run typecheck", status: "running" },
|
||||
];
|
||||
|
||||
// ── Terminal log lines ─────────────────────────────────
|
||||
|
||||
const TERMINAL_LINES = [
|
||||
{ text: "$ npm run dev", cls: "text-mock-fg" },
|
||||
{ text: "", cls: "" },
|
||||
{ text: "> acme-returns@0.1.0 dev", cls: "text-mock-fg-muted" },
|
||||
{ text: "> next dev --turbopack", cls: "text-mock-fg-muted" },
|
||||
{ text: "", cls: "" },
|
||||
{ text: " ▲ Next.js 15.3.1 (Turbopack)", cls: "text-mock-fg" },
|
||||
{ text: " - Local: http://localhost:3000", cls: "text-mock-fg-muted" },
|
||||
{ text: "", cls: "" },
|
||||
{ text: " ✓ Starting...", cls: "text-mock-green" },
|
||||
{ text: " ✓ Ready in 1.2s", cls: "text-mock-green" },
|
||||
{ text: " ○ Compiling /dashboard ...", cls: "text-mock-fg-muted" },
|
||||
{ text: " ✓ Compiled /dashboard in 340ms", cls: "text-mock-green" },
|
||||
{ text: " ○ Compiling /api/returns ...", cls: "text-mock-fg-muted" },
|
||||
{ text: " ✓ Compiled /api/returns in 120ms", cls: "text-mock-green" },
|
||||
{ id: "cmd", text: "$ npm run dev", cls: "text-mock-fg" },
|
||||
{ id: "blank1", text: "", cls: "" },
|
||||
{ id: "pkg", text: "> acme-returns@0.1.0 dev", cls: "text-mock-fg-muted" },
|
||||
{ id: "pkg-cmd", text: "> next dev --turbopack", cls: "text-mock-fg-muted" },
|
||||
{ id: "blank2", text: "", cls: "" },
|
||||
{ id: "next-ver", text: " ▲ Next.js 15.3.1 (Turbopack)", cls: "text-mock-fg" },
|
||||
{ id: "next-local", text: " - Local: http://localhost:3000", cls: "text-mock-fg-muted" },
|
||||
{ id: "blank3", text: "", cls: "" },
|
||||
{ id: "starting", text: " ✓ Starting...", cls: "text-mock-green" },
|
||||
{ id: "ready", text: " ✓ Ready in 1.2s", cls: "text-mock-green" },
|
||||
{ id: "compiling-dashboard", text: " ○ Compiling /dashboard ...", cls: "text-mock-fg-muted" },
|
||||
{ id: "compiled-dashboard", text: " ✓ Compiled /dashboard in 340ms", cls: "text-mock-green" },
|
||||
{ id: "compiling-api", text: " ○ Compiling /api/returns ...", cls: "text-mock-fg-muted" },
|
||||
{ id: "compiled-api", text: " ✓ Compiled /api/returns in 120ms", cls: "text-mock-green" },
|
||||
];
|
||||
|
||||
// ── Sub-components ──────────────────────────────────────
|
||||
@@ -471,13 +458,112 @@ function Composer({
|
||||
);
|
||||
}
|
||||
|
||||
function prStateLabel(state: "open" | "merged" | "closed"): string {
|
||||
if (state === "open") return "Open";
|
||||
if (state === "merged") return "Merged";
|
||||
return "Closed";
|
||||
}
|
||||
|
||||
function WorkspaceIcon({ workspace }: { workspace: SidebarWorkspace }) {
|
||||
if (workspace.status === "syncing") return <SyncedLoader size={11} />;
|
||||
const icon =
|
||||
workspace.kind === "worktree" ? (
|
||||
<FolderGit2 size={14} className="text-mock-fg-muted" />
|
||||
) : (
|
||||
<Monitor size={14} className="text-mock-fg-muted" />
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{icon}
|
||||
{workspace.status !== "idle" && (
|
||||
<div className="absolute bottom-0 right-0">
|
||||
<WorkspaceStatusDot status={workspace.status} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarWorkspaceRow({ workspace }: { workspace: SidebarWorkspace }) {
|
||||
return (
|
||||
<div className={`mb-1 mx-1.5 rounded-lg ${workspace.selected ? "bg-mock-surface1" : ""}`}>
|
||||
<div className="flex items-center gap-2 min-h-[28px] py-1 pl-[22px] pr-1">
|
||||
<div className="relative w-[14px] h-4 flex-shrink-0 flex items-center justify-center">
|
||||
<WorkspaceIcon workspace={workspace} />
|
||||
</div>
|
||||
<span className="text-[12px] text-mock-fg/[0.76] font-normal truncate flex-1 min-w-0 leading-[1.4]">
|
||||
{workspace.name}
|
||||
</span>
|
||||
{workspace.diffStat && (
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<span className="text-[10px] text-mock-green-400 font-normal leading-none">
|
||||
+{workspace.diffStat.additions}
|
||||
</span>
|
||||
<span className="text-[10px] text-mock-red font-normal leading-none">
|
||||
-{workspace.diffStat.deletions}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{workspace.pr && (
|
||||
<div className="flex items-center gap-1 pl-[42px] pr-2 pb-0.5">
|
||||
<GitPullRequest size={11} className="text-mock-fg-muted" />
|
||||
<span className="text-[10px] text-mock-fg-muted leading-none truncate">
|
||||
#{workspace.pr.number} · {prStateLabel(workspace.pr.state)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function diffLineBg(type: DiffLineType): string {
|
||||
if (type === "add") return "bg-mock-diff-add";
|
||||
if (type === "remove") return "bg-mock-diff-remove";
|
||||
return "bg-mock-surface1";
|
||||
}
|
||||
|
||||
function diffLineNumCls(type: DiffLineType): string {
|
||||
if (type === "add") return "text-mock-green-400";
|
||||
if (type === "remove") return "text-mock-red";
|
||||
return "text-mock-fg-muted";
|
||||
}
|
||||
|
||||
function DiffLineRow({ line }: { line: DiffLine }) {
|
||||
const lineBg = diffLineBg(line.type);
|
||||
const lineNumCls = diffLineNumCls(line.type);
|
||||
return (
|
||||
<div className={`flex items-stretch ${lineBg}`}>
|
||||
<div className="w-8 border-r border-mock-border flex-shrink-0 flex items-center justify-end">
|
||||
<code className={`text-[10px] font-mono ${lineNumCls} select-none pr-2 py-[1px]`}>
|
||||
{line.ln ?? ""}
|
||||
</code>
|
||||
</div>
|
||||
<code className="text-[10px] font-mono text-mock-fg pl-3 pr-3 py-[1px] whitespace-pre flex-1 min-w-0">
|
||||
{line.tokens?.map((tok) => (
|
||||
<span key={`${tok.cls}:${tok.text}`} className={tok.cls}>
|
||||
{tok.text}
|
||||
</span>
|
||||
))}
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function chatItemKey(item: ChatItem): string {
|
||||
if (item.type === "user") return `user-${item.text}`;
|
||||
if (item.type === "tool") return `tool-${item.label}-${item.summary ?? ""}`;
|
||||
return `text-${item.text}`;
|
||||
}
|
||||
|
||||
function ChatMessages({ items }: { items: ChatItem[] }) {
|
||||
return (
|
||||
<div className="flex-1 overflow-hidden px-2 py-2">
|
||||
{items.map((item, i) => {
|
||||
{items.map((item) => {
|
||||
const key = chatItemKey(item);
|
||||
if (item.type === "user") {
|
||||
return (
|
||||
<div key={i} className="flex justify-end py-1">
|
||||
<div key={key} className="flex justify-end py-1">
|
||||
<div
|
||||
className="bg-mock-surface2 px-2.5 py-1.5 max-w-[85%] leading-none"
|
||||
style={USER_BUBBLE_STYLE}
|
||||
@@ -491,7 +577,7 @@ function ChatMessages({ items }: { items: ChatItem[] }) {
|
||||
const isDone = item.status === "done";
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
key={key}
|
||||
className="flex items-center rounded-lg px-2 py-[1px] -mx-[6px] border border-transparent"
|
||||
>
|
||||
<div className="w-[22px] h-[22px] rounded-full flex items-center justify-center flex-shrink-0 mr-1">
|
||||
@@ -517,7 +603,7 @@ function ChatMessages({ items }: { items: ChatItem[] }) {
|
||||
}
|
||||
return (
|
||||
<p
|
||||
key={i}
|
||||
key={key}
|
||||
className={`text-[11px] px-2 py-[1px] text-mock-fg leading-[1.5] ${item.bold ? "font-medium" : ""}`}
|
||||
>
|
||||
{item.text}
|
||||
@@ -537,21 +623,12 @@ function ChatArea() {
|
||||
);
|
||||
}
|
||||
|
||||
function ImplementPane() {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 min-w-0 min-h-0 bg-mock-surface0">
|
||||
<ChatMessages items={IMPLEMENT_CHAT} />
|
||||
<Composer provider="codex" model="gpt-5.4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TerminalPane() {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 min-w-0 min-h-0 bg-mock-surface0">
|
||||
<div className="flex-1 overflow-hidden px-3 py-2">
|
||||
{TERMINAL_LINES.map((line, i) => (
|
||||
<div key={i} className="leading-[1.3]">
|
||||
{TERMINAL_LINES.map((line) => (
|
||||
<div key={line.id} className="leading-[1.3]">
|
||||
<code className={`text-[10px] font-mono ${line.cls} whitespace-pre`}>{line.text}</code>
|
||||
</div>
|
||||
))}
|
||||
@@ -609,37 +686,9 @@ function ExplorerSidebar() {
|
||||
|
||||
{/* Diff lines */}
|
||||
<div className="overflow-hidden bg-mock-surface1">
|
||||
{DIFF_LINES.map((line, i) => {
|
||||
const isAdd = line.type === "add";
|
||||
const isRemove = line.type === "remove";
|
||||
const lineBg = isAdd
|
||||
? "bg-mock-diff-add"
|
||||
: isRemove
|
||||
? "bg-mock-diff-remove"
|
||||
: "bg-mock-surface1";
|
||||
const lineNumCls = isAdd
|
||||
? "text-mock-green-400"
|
||||
: isRemove
|
||||
? "text-mock-red"
|
||||
: "text-mock-fg-muted";
|
||||
|
||||
return (
|
||||
<div key={i} className={`flex items-stretch ${lineBg}`}>
|
||||
<div className="w-8 border-r border-mock-border flex-shrink-0 flex items-center justify-end">
|
||||
<code className={`text-[10px] font-mono ${lineNumCls} select-none pr-2 py-[1px]`}>
|
||||
{line.ln ?? ""}
|
||||
</code>
|
||||
</div>
|
||||
<code className="text-[10px] font-mono text-mock-fg pl-3 pr-3 py-[1px] whitespace-pre flex-1 min-w-0">
|
||||
{line.tokens?.map((tok, j) => (
|
||||
<span key={j} className={tok.cls}>
|
||||
{tok.text}
|
||||
</span>
|
||||
))}
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{DIFF_LINES.map((line) => (
|
||||
<DiffLineRow key={`${line.type}-${line.ln ?? "blank"}`} line={line} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Collapsed file rows */}
|
||||
@@ -684,6 +733,7 @@ function ExplorerSidebar() {
|
||||
const DOT_SEQUENCE = [0, 1, 3, 5, 4, 2] as const;
|
||||
const SYNCED_LOADER_DURATION_MS = 950;
|
||||
const DOT_COUNT = 6;
|
||||
const DOT_INDICES = Array.from({ length: DOT_COUNT }, (_, i) => i);
|
||||
const STEP_MS = SYNCED_LOADER_DURATION_MS / DOT_COUNT; // 158.333…ms per step
|
||||
|
||||
interface SyncedLoaderDotProps {
|
||||
@@ -743,8 +793,13 @@ function SyncedLoader({ size = 11 }: { size?: number }) {
|
||||
return (
|
||||
<div style={outerStyle}>
|
||||
<div style={innerStyle}>
|
||||
{Array.from({ length: DOT_COUNT }).map((_, dotIndex) => (
|
||||
<SyncedLoaderDot key={dotIndex} dotIndex={dotIndex} dotSize={dotSize} gap={gap} />
|
||||
{DOT_INDICES.map((dotIndex) => (
|
||||
<SyncedLoaderDot
|
||||
key={`dot-${dotIndex}`}
|
||||
dotIndex={dotIndex}
|
||||
dotSize={dotSize}
|
||||
gap={gap}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -792,57 +847,7 @@ function Sidebar() {
|
||||
|
||||
{/* Workspace rows — indented one level */}
|
||||
{project.workspaces.map((workspace) => (
|
||||
<div
|
||||
key={workspace.name}
|
||||
className={`mb-1 mx-1.5 rounded-lg ${workspace.selected ? "bg-mock-surface1" : ""}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-h-[28px] py-1 pl-[22px] pr-1">
|
||||
<div className="relative w-[14px] h-4 flex-shrink-0 flex items-center justify-center">
|
||||
{workspace.status === "syncing" ? (
|
||||
<SyncedLoader size={11} />
|
||||
) : (
|
||||
<>
|
||||
{workspace.kind === "worktree" ? (
|
||||
<FolderGit2 size={14} className="text-mock-fg-muted" />
|
||||
) : (
|
||||
<Monitor size={14} className="text-mock-fg-muted" />
|
||||
)}
|
||||
{workspace.status !== "idle" && (
|
||||
<div className="absolute bottom-0 right-0">
|
||||
<WorkspaceStatusDot status={workspace.status} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[12px] text-mock-fg/[0.76] font-normal truncate flex-1 min-w-0 leading-[1.4]">
|
||||
{workspace.name}
|
||||
</span>
|
||||
{workspace.diffStat && (
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<span className="text-[10px] text-mock-green-400 font-normal leading-none">
|
||||
+{workspace.diffStat.additions}
|
||||
</span>
|
||||
<span className="text-[10px] text-mock-red font-normal leading-none">
|
||||
-{workspace.diffStat.deletions}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{workspace.pr && (
|
||||
<div className="flex items-center gap-1 pl-[42px] pr-2 pb-0.5">
|
||||
<GitPullRequest size={11} className="text-mock-fg-muted" />
|
||||
<span className="text-[10px] text-mock-fg-muted leading-none truncate">
|
||||
#{workspace.pr.number} ·{" "}
|
||||
{workspace.pr.state === "open"
|
||||
? "Open"
|
||||
: workspace.pr.state === "merged"
|
||||
? "Merged"
|
||||
: "Closed"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SidebarWorkspaceRow key={workspace.name} workspace={workspace} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
@@ -867,6 +872,75 @@ function Sidebar() {
|
||||
);
|
||||
}
|
||||
|
||||
const TERMINAL_TABS: TabDef[] = [
|
||||
{ name: "npm run dev", provider: "terminal", done: false, active: true },
|
||||
];
|
||||
|
||||
function TitleBarLeft() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<PanelLeft size={14} className="text-mock-fg-muted" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<span className="text-[13px] font-light text-mock-fg truncate flex-shrink-0">main</span>
|
||||
<span className="text-[13px] text-mock-fg-muted truncate flex-shrink min-w-0">
|
||||
acme/returns-app
|
||||
</span>
|
||||
<div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Ellipsis size={14} className="text-mock-fg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TitleBarRight() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 ml-auto flex-shrink-0">
|
||||
<div className="flex items-stretch rounded-md border border-mock-border-accent overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-3 py-1">
|
||||
<GitCommitHorizontal size={12} className="text-mock-fg-muted flex-shrink-0" />
|
||||
<span className="text-[11px] text-mock-fg font-normal">Commit</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-7 border-l border-mock-border-accent">
|
||||
<ChevronDown size={12} className="text-mock-fg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1 rounded-md">
|
||||
<SourceControlIcon size={14} className="text-mock-fg-muted" />
|
||||
<span className="text-[11px] font-normal text-mock-green-400">+247</span>
|
||||
<span className="text-[11px] font-normal text-mock-red">-15</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DesktopSplitPanes() {
|
||||
return (
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{/* Left pane: all agent tabs + chat */}
|
||||
<div className="flex flex-col flex-1 min-w-0 min-h-0">
|
||||
<motion.div {...fade(D.tabs)}>
|
||||
<PaneTabBar tabs={TABS} focused />
|
||||
</motion.div>
|
||||
<motion.div {...fade(D.chat)} className="flex-1 flex min-h-0">
|
||||
<ChatArea />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Resize handle */}
|
||||
<div className="w-px bg-mock-border flex-shrink-0" />
|
||||
|
||||
{/* Right pane: terminal only */}
|
||||
<motion.div {...fade(D.chat)} className="flex flex-col flex-1 min-w-0 min-h-0">
|
||||
<PaneTabBar tabs={TERMINAL_TABS} />
|
||||
<TerminalPane />
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Desktop Mockup ──────────────────────────────────────
|
||||
|
||||
function DesktopMockup() {
|
||||
@@ -908,64 +982,11 @@ function DesktopMockup() {
|
||||
{...fade(D.titleBar)}
|
||||
className="flex items-center h-10 px-2 bg-mock-surface0 border-b border-mock-border flex-shrink-0"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<PanelLeft size={14} className="text-mock-fg-muted" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<span className="text-[13px] font-light text-mock-fg truncate flex-shrink-0">
|
||||
main
|
||||
</span>
|
||||
<span className="text-[13px] text-mock-fg-muted truncate flex-shrink min-w-0">
|
||||
acme/returns-app
|
||||
</span>
|
||||
<div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Ellipsis size={14} className="text-mock-fg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-auto flex-shrink-0">
|
||||
<div className="flex items-stretch rounded-md border border-mock-border-accent overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-3 py-1">
|
||||
<GitCommitHorizontal size={12} className="text-mock-fg-muted flex-shrink-0" />
|
||||
<span className="text-[11px] text-mock-fg font-normal">Commit</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-7 border-l border-mock-border-accent">
|
||||
<ChevronDown size={12} className="text-mock-fg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1 rounded-md">
|
||||
<SourceControlIcon size={14} className="text-mock-fg-muted" />
|
||||
<span className="text-[11px] font-normal text-mock-green-400">+247</span>
|
||||
<span className="text-[11px] font-normal text-mock-red">-15</span>
|
||||
</div>
|
||||
</div>
|
||||
<TitleBarLeft />
|
||||
<TitleBarRight />
|
||||
</motion.div>
|
||||
|
||||
{/* Split panes */}
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{/* Left pane: all agent tabs + chat */}
|
||||
<div className="flex flex-col flex-1 min-w-0 min-h-0">
|
||||
<motion.div {...fade(D.tabs)}>
|
||||
<PaneTabBar tabs={TABS} focused />
|
||||
</motion.div>
|
||||
<motion.div {...fade(D.chat)} className="flex-1 flex min-h-0">
|
||||
<ChatArea />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Resize handle */}
|
||||
<div className="w-px bg-mock-border flex-shrink-0" />
|
||||
|
||||
{/* Right pane: terminal only */}
|
||||
<motion.div {...fade(D.chat)} className="flex flex-col flex-1 min-w-0 min-h-0">
|
||||
<PaneTabBar
|
||||
tabs={[{ name: "npm run dev", provider: "terminal", done: false, active: true }]}
|
||||
/>
|
||||
<TerminalPane />
|
||||
</motion.div>
|
||||
</div>
|
||||
<DesktopSplitPanes />
|
||||
</div>
|
||||
|
||||
{/* Explorer sidebar — full height, pushes center column */}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
// reference and doesn't trigger jsx-no-new-object-as-prop.
|
||||
const FADE_IN_UP = { opacity: 0, y: 20 };
|
||||
const FADE_IN = { opacity: 1, y: 0 };
|
||||
const FADE_IN_UP_SMALL = { opacity: 0, y: 16 };
|
||||
const FADE_IN_UP_TINY = { opacity: 0, y: -10 };
|
||||
const FADE_IN_UP_XL = { opacity: 0, y: 30 };
|
||||
const FADE_IN_UP_40 = { opacity: 0, y: 40 };
|
||||
@@ -29,7 +28,6 @@ const EASE_OUT_015: Transition = { duration: 0.15, ease: "easeOut" };
|
||||
const DURATION_05: Transition = { duration: 0.5 };
|
||||
|
||||
const VIEWPORT_60 = { once: true, margin: "-60px" };
|
||||
const VIEWPORT_40 = { once: true, margin: "-40px" };
|
||||
|
||||
const SVG_OVERFLOW_VISIBLE_STYLE = { overflow: "visible" as const };
|
||||
const PHONE_PERSPECTIVE_STYLE = { minHeight: 480, perspective: 1200 };
|
||||
@@ -255,14 +253,22 @@ function Hero({ title, subtitle }: { title: React.ReactNode; subtitle: string })
|
||||
);
|
||||
}
|
||||
|
||||
const CLAUDE_CODE_BADGE_ICON = <ClaudeCodeIcon className="h-6 w-6" />;
|
||||
const CODEX_BADGE_ICON = <CodexIcon className="h-6 w-6" />;
|
||||
const OPENCODE_BADGE_ICON = <OpenCodeIcon className="h-6 w-6" />;
|
||||
const COPILOT_BADGE_ICON = <CopilotIcon className="h-6 w-6" />;
|
||||
const PI_BADGE_ICON = <PiIcon className="h-6 w-6" />;
|
||||
|
||||
function AgentBadge({ name, icon }: { name: string; icon: React.ReactNode }) {
|
||||
const [hovered, setHovered] = React.useState(false);
|
||||
const handleMouseEnter = React.useCallback(() => setHovered(true), []);
|
||||
const handleMouseLeave = React.useCallback(() => setHovered(false), []);
|
||||
|
||||
return (
|
||||
<span
|
||||
className="relative inline-flex items-center justify-center rounded-full p-1.5 text-white/60"
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{icon}
|
||||
<AnimatePresence>
|
||||
@@ -308,22 +314,6 @@ function FeatureSection({
|
||||
);
|
||||
}
|
||||
|
||||
function PrinciplesSection() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={FADE_IN_UP_SMALL}
|
||||
whileInView={FADE_IN}
|
||||
viewport={VIEWPORT_40}
|
||||
transition={EASE_OUT_05}
|
||||
className="py-32 px-6 md:px-20 max-w-3xl mx-auto text-center"
|
||||
>
|
||||
<p className="text-2xl md:text-4xl font-medium text-white/90">
|
||||
Here's what's under the hood.
|
||||
</p>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function MultiProviderSection() {
|
||||
const providers = [
|
||||
{ name: "Claude Code", icon: <ClaudeIcon size={28} /> },
|
||||
@@ -444,6 +434,19 @@ function SelfHostedDiagram() {
|
||||
const clientRefs = React.useRef<(HTMLDivElement | null)[]>([]);
|
||||
const hostRefs = React.useRef<(HTMLDivElement | null)[]>([]);
|
||||
const centerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const setClientRef = React.useCallback(
|
||||
(index: number) => (el: HTMLDivElement | null) => {
|
||||
clientRefs.current[index] = el;
|
||||
},
|
||||
[],
|
||||
);
|
||||
const setHostRef = React.useCallback(
|
||||
(index: number) => (el: HTMLDivElement | null) => {
|
||||
hostRefs.current[index] = el;
|
||||
},
|
||||
[],
|
||||
);
|
||||
const [paths, setPaths] = React.useState<{ left: string[]; right: string[] }>({
|
||||
left: [],
|
||||
right: [],
|
||||
@@ -546,10 +549,10 @@ function SelfHostedDiagram() {
|
||||
style={SVG_OVERFLOW_VISIBLE_STYLE}
|
||||
>
|
||||
{[...paths.left, ...paths.right].map(
|
||||
(d, i) =>
|
||||
(d) =>
|
||||
d && (
|
||||
<path
|
||||
key={i}
|
||||
key={d}
|
||||
d={d}
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.25)"
|
||||
@@ -565,9 +568,7 @@ function SelfHostedDiagram() {
|
||||
{clients.map((c, i) => (
|
||||
<div
|
||||
key={c.name}
|
||||
ref={(el) => {
|
||||
clientRefs.current[i] = el;
|
||||
}}
|
||||
ref={setClientRef(i)}
|
||||
className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4 backdrop-blur-sm"
|
||||
>
|
||||
<span className="text-white/80">{c.icon}</span>
|
||||
@@ -597,9 +598,7 @@ function SelfHostedDiagram() {
|
||||
{hosts.map((h, i) => (
|
||||
<div
|
||||
key={h}
|
||||
ref={(el) => {
|
||||
hostRefs.current[i] = el;
|
||||
}}
|
||||
ref={setHostRef(i)}
|
||||
className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4 backdrop-blur-sm"
|
||||
>
|
||||
<span className="text-white/80">
|
||||
@@ -757,12 +756,14 @@ function VoiceBar({ index, barCount }: VoiceBarProps) {
|
||||
return <div className="w-[3px] rounded-full bg-white/30" style={style} />;
|
||||
}
|
||||
|
||||
const VOICE_BAR_COUNT = 48;
|
||||
const VOICE_BAR_INDICES = Array.from({ length: VOICE_BAR_COUNT }, (_, i) => i);
|
||||
|
||||
function VoiceWaveform() {
|
||||
const barCount = 48;
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-[3px] h-16">
|
||||
{Array.from({ length: barCount }).map((_, i) => (
|
||||
<VoiceBar key={i} index={i} barCount={barCount} />
|
||||
{VOICE_BAR_INDICES.map((i) => (
|
||||
<VoiceBar key={`voice-bar-${i}`} index={i} barCount={VOICE_BAR_COUNT} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -874,6 +875,25 @@ function useVoiceConversation() {
|
||||
return { dictationWordIndex, responseWordIndex, showResponse };
|
||||
}
|
||||
|
||||
function makeWordKey(words: string[], i: number): string {
|
||||
const word = words[i];
|
||||
let occurrence = 0;
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (words[j] === word) occurrence++;
|
||||
}
|
||||
return `${word}#${occurrence}`;
|
||||
}
|
||||
|
||||
function WordSpan({ word, confirmed }: { word: string; confirmed: boolean }) {
|
||||
return (
|
||||
<span
|
||||
className={`transition-colors duration-300 ${confirmed ? "text-white/90" : "text-white/40"}`}
|
||||
>
|
||||
{word}{" "}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function StreamingWords({
|
||||
words,
|
||||
wordIndex,
|
||||
@@ -894,14 +914,7 @@ function StreamingWords({
|
||||
{words.map((word, i) => {
|
||||
if (i >= wordIndex) return null;
|
||||
const confirmed = i < wordIndex - confirmLag;
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className={`transition-colors duration-300 ${confirmed ? "text-white/90" : "text-white/40"}`}
|
||||
>
|
||||
{word}{" "}
|
||||
</span>
|
||||
);
|
||||
return <WordSpan key={makeWordKey(words, i)} word={word} confirmed={confirmed} />;
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
@@ -1005,11 +1018,11 @@ function GetStarted() {
|
||||
<div className="flex items-center gap-2 pt-6">
|
||||
<span className="text-xs text-white/40">Supports</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<AgentBadge name="Claude Code" icon={<ClaudeCodeIcon className="h-6 w-6" />} />
|
||||
<AgentBadge name="Codex" icon={<CodexIcon className="h-6 w-6" />} />
|
||||
<AgentBadge name="OpenCode" icon={<OpenCodeIcon className="h-6 w-6" />} />
|
||||
<AgentBadge name="Copilot" icon={<CopilotIcon className="h-6 w-6" />} />
|
||||
<AgentBadge name="Pi" icon={<PiIcon className="h-6 w-6" />} />
|
||||
<AgentBadge name="Claude Code" icon={CLAUDE_CODE_BADGE_ICON} />
|
||||
<AgentBadge name="Codex" icon={CODEX_BADGE_ICON} />
|
||||
<AgentBadge name="OpenCode" icon={OPENCODE_BADGE_ICON} />
|
||||
<AgentBadge name="Copilot" icon={COPILOT_BADGE_ICON} />
|
||||
<AgentBadge name="Pi" icon={PI_BADGE_ICON} />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -1035,23 +1048,27 @@ function DownloadButton() {
|
||||
);
|
||||
}
|
||||
|
||||
const SERVER_INSTALL_TRIGGER = (
|
||||
<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">
|
||||
<TerminalIcon className="h-5 w-5" />
|
||||
</span>
|
||||
);
|
||||
|
||||
const SERVER_INSTALL_FOOTNOTE = (
|
||||
<>
|
||||
Requires Node.js 18+. Run <span className="font-mono text-white/40">paseo</span> to start the
|
||||
daemon.
|
||||
</>
|
||||
);
|
||||
|
||||
function ServerInstallButton() {
|
||||
return (
|
||||
<CommandDialog
|
||||
trigger={
|
||||
<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">
|
||||
<TerminalIcon className="h-5 w-5" />
|
||||
</span>
|
||||
}
|
||||
trigger={SERVER_INSTALL_TRIGGER}
|
||||
title="Run agents on a remote machine"
|
||||
description="For headless machines you want to connect to from the Paseo apps. The desktop app already includes a built-in daemon."
|
||||
command="npm install -g @getpaseo/cli && paseo"
|
||||
footnote={
|
||||
<>
|
||||
Requires Node.js 18+. Run <span className="font-mono text-white/40">paseo</span> to start
|
||||
the daemon.
|
||||
</>
|
||||
}
|
||||
footnote={SERVER_INSTALL_FOOTNOTE}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1141,61 +1158,6 @@ function PiIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
);
|
||||
}
|
||||
|
||||
function AppStoreIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 960 960"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="M342.277 86.6927C463.326 84.6952 587.87 65.619 705.523 104.97C830.467 143.522 874.012 278.153 872.814 397.105C873.713 481.299 874.012 566.193 858.931 649.19C834.262 804.895 746.172 873.01 590.666 874.608C422.377 880.301 172.489 908.965 104.474 711.012C76.5092 599.452 86.6964 481.1 88.1946 366.843C98.9811 200.75 163.301 90.2882 342.277 86.6927ZM715.411 596.156C758.856 591.362 754.362 524.645 710.816 524.545C610.542 525.244 639.605 550.513 594.462 456.83C577.383 418.778 540.529 337.279 496.085 396.006C479.206 431.062 516.359 464.121 528.844 495.382C569.892 560.6 606.647 628.515 648.494 693.334C667.77 724.495 716.509 696.73 697.333 663.372C685.048 642.298 677.258 619.726 665.773 598.253C682.452 597.854 698.831 598.053 715.411 596.156Z" />
|
||||
<path
|
||||
d="M697.234 663.371C716.41 696.729 667.671 724.494 648.395 693.333C606.548 628.614 569.794 560.699 528.745 495.381C516.161 464.219 479.107 431.161 495.986 396.005C540.43 337.178 577.384 418.776 594.363 456.829C639.506 550.512 610.443 525.243 710.717 524.544C754.263 524.644 758.757 591.361 715.312 596.155C698.732 598.052 682.453 597.852 665.674 598.252C677.159 619.725 684.95 642.297 697.234 663.371Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M474.312 257.679C486.597 230.913 517.059 198.453 545.224 224.92C564.3 242.298 551.316 269.465 538.332 287.242C489.194 363.747 450.242 445.844 405.598 524.845C445.448 528.341 485.598 525.844 525.149 532.835C564.1 539.827 558.907 597.455 519.256 598.353C442.153 601.35 365.049 595.457 287.845 599.652C260.28 597.554 225.024 612.336 203.751 589.065C161.104 516.456 275.761 527.442 317.608 524.546C343.776 499.377 356.659 456.93 377.833 425.769C395.311 394.608 412.39 363.147 429.868 331.986C432.964 322.199 418.982 314.109 415.486 305.12C349.169 230.713 442.153 172.885 474.312 257.679Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M265.471 626.12C284.647 595.758 329.491 609.042 330.39 643.199C325.296 664.872 313.511 684.647 298.53 701.027C275.758 724.997 235.009 703.124 242.5 670.864C246.195 654.485 256.882 640.302 265.471 626.12Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ChevronDownIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function Step({ number, children }: { number: number; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-white/20 flex items-center justify-center text-xs font-medium">
|
||||
{number}
|
||||
</span>
|
||||
<div className="space-y-2 flex-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const bashKeywords = new Set([
|
||||
"while",
|
||||
"do",
|
||||
@@ -1210,167 +1172,134 @@ const bashKeywords = new Set([
|
||||
]);
|
||||
const bashCommands = new Set(["paseo", "echo", "jq"]);
|
||||
|
||||
function tokenizeBashComment(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
const end = code.indexOf("\n", i);
|
||||
const comment = end === -1 ? code.slice(i) : code.slice(i, end);
|
||||
return {
|
||||
node: <span className="text-white/30 italic">{comment}</span>,
|
||||
len: comment.length,
|
||||
};
|
||||
}
|
||||
|
||||
function tokenizeBashDoubleQuoted(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
let j = i + 1;
|
||||
while (j < code.length && code[j] !== '"') {
|
||||
if (code[j] === "\\") j++;
|
||||
j++;
|
||||
}
|
||||
const str = code.slice(i, j + 1);
|
||||
return { node: <span className="text-green-400/80">{str}</span>, len: str.length };
|
||||
}
|
||||
|
||||
function tokenizeBashSingleQuoted(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
let j = i + 1;
|
||||
while (j < code.length && code[j] !== "'") j++;
|
||||
const str = code.slice(i, j + 1);
|
||||
return { node: <span className="text-green-400/80">{str}</span>, len: str.length };
|
||||
}
|
||||
|
||||
function tokenizeBashDollar(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
if (code[i + 1] === "(") {
|
||||
return { node: <span className="text-amber-300/70">$(</span>, len: 2 };
|
||||
}
|
||||
let j = i + 1;
|
||||
while (j < code.length && /\w/.test(code[j])) j++;
|
||||
return {
|
||||
node: <span className="text-amber-300/70">{code.slice(i, j)}</span>,
|
||||
len: j - i,
|
||||
};
|
||||
}
|
||||
|
||||
function tokenizeBashFlag(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
let j = i;
|
||||
if (code[j + 1] === "-") j++;
|
||||
j++;
|
||||
while (j < code.length && /[\w-]/.test(code[j])) j++;
|
||||
return {
|
||||
node: <span className="text-sky-300/70">{code.slice(i, j)}</span>,
|
||||
len: j - i,
|
||||
};
|
||||
}
|
||||
|
||||
function tokenizeBashWord(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
let j = i;
|
||||
while (j < code.length && /\w/.test(code[j])) j++;
|
||||
const word = code.slice(i, j);
|
||||
const len = j - i;
|
||||
if (bashKeywords.has(word)) {
|
||||
return { node: <span className="text-purple-400">{word}</span>, len };
|
||||
}
|
||||
if (bashCommands.has(word)) {
|
||||
return { node: <span className="text-white">{word}</span>, len };
|
||||
}
|
||||
return { node: word, len };
|
||||
}
|
||||
|
||||
function isBashFlagStart(code: string, i: number): boolean {
|
||||
return (
|
||||
code[i] === "-" &&
|
||||
(i === 0 || /\s/.test(code[i - 1])) &&
|
||||
i + 1 < code.length &&
|
||||
/[\w-]/.test(code[i + 1])
|
||||
);
|
||||
}
|
||||
|
||||
function isBashCommentStart(code: string, i: number): boolean {
|
||||
return code[i] === "#" && (i === 0 || /[\s(]/.test(code[i - 1]));
|
||||
}
|
||||
|
||||
function tokenizeBashChar(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
const c = code[i];
|
||||
if (c === "|" || (c === "&" && code[i + 1] === "&")) {
|
||||
const op = c === "|" ? "|" : "&&";
|
||||
return { node: <span className="text-white/40">{op}</span>, len: op.length };
|
||||
}
|
||||
if (c === "\\") return { node: <span className="text-white/40">\</span>, len: 1 };
|
||||
if (c === ")") return { node: <span className="text-amber-300/70">)</span>, len: 1 };
|
||||
return { node: c, len: 1 };
|
||||
}
|
||||
|
||||
function nextBashToken(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
if (isBashCommentStart(code, i)) return tokenizeBashComment(code, i);
|
||||
if (code[i] === '"') return tokenizeBashDoubleQuoted(code, i);
|
||||
if (code[i] === "'") return tokenizeBashSingleQuoted(code, i);
|
||||
if (code[i] === "$") return tokenizeBashDollar(code, i);
|
||||
if (isBashFlagStart(code, i)) return tokenizeBashFlag(code, i);
|
||||
if (/[a-zA-Z_]/.test(code[i])) return tokenizeBashWord(code, i);
|
||||
return tokenizeBashChar(code, i);
|
||||
}
|
||||
|
||||
function highlightBash(code: string): React.ReactNode {
|
||||
const tokens: React.ReactNode[] = [];
|
||||
let i = 0;
|
||||
let key = 0;
|
||||
|
||||
while (i < code.length) {
|
||||
if (code[i] === "#" && (i === 0 || /[\s(]/.test(code[i - 1]))) {
|
||||
const end = code.indexOf("\n", i);
|
||||
const comment = end === -1 ? code.slice(i) : code.slice(i, end);
|
||||
tokens.push(
|
||||
<span key={key++} className="text-white/30 italic">
|
||||
{comment}
|
||||
</span>,
|
||||
);
|
||||
i += comment.length;
|
||||
continue;
|
||||
const { node, len } = nextBashToken(code, i);
|
||||
if (React.isValidElement(node)) {
|
||||
tokens.push(React.cloneElement(node, { key: key++ }));
|
||||
} else {
|
||||
tokens.push(node);
|
||||
}
|
||||
|
||||
if (code[i] === '"') {
|
||||
let j = i + 1;
|
||||
while (j < code.length && code[j] !== '"') {
|
||||
if (code[j] === "\\") j++;
|
||||
j++;
|
||||
}
|
||||
const str = code.slice(i, j + 1);
|
||||
tokens.push(
|
||||
<span key={key++} className="text-green-400/80">
|
||||
{str}
|
||||
</span>,
|
||||
);
|
||||
i = j + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code[i] === "'") {
|
||||
let j = i + 1;
|
||||
while (j < code.length && code[j] !== "'") j++;
|
||||
const str = code.slice(i, j + 1);
|
||||
tokens.push(
|
||||
<span key={key++} className="text-green-400/80">
|
||||
{str}
|
||||
</span>,
|
||||
);
|
||||
i = j + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code[i] === "$") {
|
||||
if (code[i + 1] === "(") {
|
||||
tokens.push(
|
||||
<span key={key++} className="text-amber-300/70">
|
||||
$(
|
||||
</span>,
|
||||
);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
let j = i + 1;
|
||||
while (j < code.length && /\w/.test(code[j])) j++;
|
||||
tokens.push(
|
||||
<span key={key++} className="text-amber-300/70">
|
||||
{code.slice(i, j)}
|
||||
</span>,
|
||||
);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
code[i] === "-" &&
|
||||
(i === 0 || /\s/.test(code[i - 1])) &&
|
||||
i + 1 < code.length &&
|
||||
/[\w-]/.test(code[i + 1])
|
||||
) {
|
||||
let j = i;
|
||||
if (code[j + 1] === "-") j++;
|
||||
j++;
|
||||
while (j < code.length && /[\w-]/.test(code[j])) j++;
|
||||
tokens.push(
|
||||
<span key={key++} className="text-sky-300/70">
|
||||
{code.slice(i, j)}
|
||||
</span>,
|
||||
);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/[a-zA-Z_]/.test(code[i])) {
|
||||
let j = i;
|
||||
while (j < code.length && /\w/.test(code[j])) j++;
|
||||
const word = code.slice(i, j);
|
||||
if (bashKeywords.has(word)) {
|
||||
tokens.push(
|
||||
<span key={key++} className="text-purple-400">
|
||||
{word}
|
||||
</span>,
|
||||
);
|
||||
} else if (bashCommands.has(word)) {
|
||||
tokens.push(
|
||||
<span key={key++} className="text-white">
|
||||
{word}
|
||||
</span>,
|
||||
);
|
||||
} else {
|
||||
tokens.push(word);
|
||||
key++;
|
||||
}
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code[i] === "|" || (code[i] === "&" && code[i + 1] === "&")) {
|
||||
const op = code[i] === "|" ? "|" : "&&";
|
||||
tokens.push(
|
||||
<span key={key++} className="text-white/40">
|
||||
{op}
|
||||
</span>,
|
||||
);
|
||||
i += op.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code[i] === "\\") {
|
||||
tokens.push(
|
||||
<span key={key++} className="text-white/40">
|
||||
\
|
||||
</span>,
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code[i] === ")") {
|
||||
tokens.push(
|
||||
<span key={key++} className="text-amber-300/70">
|
||||
)
|
||||
</span>,
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
tokens.push(code[i]);
|
||||
i++;
|
||||
i += len;
|
||||
}
|
||||
|
||||
return <>{tokens}</>;
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function CLICodeBlock({ children }: { children: string }) {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
function handleCopy() {
|
||||
const handleCopy = React.useCallback(() => {
|
||||
navigator.clipboard.writeText(children);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
}, [children]);
|
||||
|
||||
return (
|
||||
<div className="relative bg-white/5 rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="absolute top-3 right-3 text-white/30 hover:text-white/70 transition-colors p-1"
|
||||
title="Copy to clipboard"
|
||||
@@ -1559,6 +1488,33 @@ function PhoneShowcase() {
|
||||
);
|
||||
}
|
||||
|
||||
function CLITabButton({
|
||||
title,
|
||||
index,
|
||||
active,
|
||||
onSelect,
|
||||
}: {
|
||||
title: string;
|
||||
index: number;
|
||||
active: boolean;
|
||||
onSelect: (i: number) => void;
|
||||
}) {
|
||||
const handleClick = React.useCallback(() => onSelect(index), [onSelect, index]);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className={`text-xs px-3 py-1.5 rounded-full border transition-colors ${
|
||||
active
|
||||
? "border-white/40 text-white bg-white/10"
|
||||
: "border-white/15 text-white/50 hover:text-white/80 hover:border-white/30"
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function CLISection() {
|
||||
const [activeIndex, setActiveIndex] = React.useState(0);
|
||||
const active = cliExamples[activeIndex];
|
||||
@@ -1570,17 +1526,13 @@ function CLISection() {
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{cliExamples.map((example, i) => (
|
||||
<button
|
||||
<CLITabButton
|
||||
key={example.title}
|
||||
onClick={() => setActiveIndex(i)}
|
||||
className={`text-xs px-3 py-1.5 rounded-full border transition-colors ${
|
||||
i === activeIndex
|
||||
? "border-white/40 text-white bg-white/10"
|
||||
: "border-white/15 text-white/50 hover:text-white/80 hover:border-white/30"
|
||||
}`}
|
||||
>
|
||||
{example.title}
|
||||
</button>
|
||||
title={example.title}
|
||||
index={i}
|
||||
active={i === activeIndex}
|
||||
onSelect={setActiveIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -26,6 +26,12 @@ export const Route = createFileRoute("/download")({
|
||||
component: Download,
|
||||
});
|
||||
|
||||
const homebrewTrigger = (
|
||||
<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>
|
||||
);
|
||||
|
||||
function Download() {
|
||||
const { version } = useRelease();
|
||||
const urls = downloadUrls(version);
|
||||
@@ -110,11 +116,7 @@ function Download() {
|
||||
<DownloadPill href={urls.macAppleSilicon} label="Apple Silicon" />
|
||||
<DownloadPill href={urls.macIntel} 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>
|
||||
}
|
||||
trigger={homebrewTrigger}
|
||||
title="Install via Homebrew"
|
||||
command="brew install --cask paseo"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user