Files
vibn-agent-runner/vibn-frontend/components/project/project-icon-rail.tsx

428 lines
11 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { usePathname, useParams } from "next/navigation";
import {
Monitor,
Tablet,
Smartphone,
RotateCw,
ExternalLink,
ChevronDown,
} from "lucide-react";
import { usePreviewToolbarStore } from "./preview-toolbar/preview-toolbar-state";
import { useAnatomy } from "@/components/project/use-anatomy";
interface Props {
workspace: string;
projectId: string;
actions?: React.ReactNode;
}
export function ProjectIconRail({ workspace, projectId, actions }: Props) {
const pathname = usePathname() ?? "";
const projectBase = `/${workspace}/project/${projectId}`;
const isPreviewActive =
pathname === `${projectBase}/preview` ||
pathname.startsWith(`${projectBase}/preview/`);
return (
<nav style={bar} aria-label="Project sections">
<div style={primaryGroup}>
<Link
href={`${projectBase}/preview`}
style={{
padding: "4px 12px",
fontSize: "0.75rem",
fontWeight: 500,
borderRadius: 6,
display: "flex",
alignItems: "center",
height: 24, // Explicitly match device toggles height
textDecoration: "none",
background: isPreviewActive ? "#ffffff" : "transparent",
color: isPreviewActive ? "#18181b" : "#71717a",
boxShadow: isPreviewActive ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
transition: "all 0.15s ease",
}}
>
Preview
</Link>
<Link
href={`${projectBase}/plan`}
style={{
padding: "4px 12px",
fontSize: "0.75rem",
fontWeight: 500,
borderRadius: 6,
display: "flex",
alignItems: "center",
height: 24, // Explicitly match device toggles height
textDecoration: "none",
background: !isPreviewActive ? "#ffffff" : "transparent",
color: !isPreviewActive ? "#18181b" : "#71717a",
boxShadow: !isPreviewActive ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
transition: "all 0.15s ease",
}}
>
Dashboard
</Link>
</div>
<div
style={{
display: "flex",
alignItems: "center",
flex: 1,
minWidth: 0,
marginLeft: 12,
}}
>
{isPreviewActive && <PreviewDeviceToggles />}
</div>
<div
style={{
display: "flex",
alignItems: "center",
flexShrink: 0,
marginLeft: 12,
}}
>
{actions}
</div>
</nav>
);
}
function PreviewDeviceToggles() {
const deviceMode = usePreviewToolbarStore((s) => s.deviceMode);
const setDeviceMode = usePreviewToolbarStore((s) => s.setDeviceMode);
const triggerRefresh = usePreviewToolbarStore((s) => s.triggerRefresh);
const currentPath = usePreviewToolbarStore((s) => s.currentPath);
const setCurrentPath = usePreviewToolbarStore((s) => s.setCurrentPath);
const selectedPort = usePreviewToolbarStore((s) => s.selectedPort);
const setSelectedPort = usePreviewToolbarStore((s) => s.setSelectedPort);
const params = useParams();
const projectId = params?.projectId as string;
const { anatomy } = useAnatomy(projectId);
const [now, setNow] = useState<number>(() => Date.now());
useEffect(() => {
const id = setInterval(() => setNow(Date.now()), 10000);
return () => clearInterval(id);
}, []);
const previews = anatomy?.hosting.previews ?? [];
const validPreviews = previews
.filter(
(p) =>
p.state === "running" ||
(p.state === "starting" &&
now - new Date(p.startedAt).getTime() < 15 * 60 * 1000),
)
.sort((a, b) => a.port - b.port); // sort ports ascending
const activePreview = validPreviews.find((p) => p.port === selectedPort);
const running =
activePreview?.state === "running" ? activePreview : undefined;
const displayUrl = running?.url ?? "";
return (
<div
style={{
display: "flex",
alignItems: "center",
gap: 6,
background: "#fafafa",
border: "1px solid #e4e4e7",
padding: "4px 8px",
borderRadius: 8,
height: 34,
flex: 1,
minWidth: 0, // critical for input overflow to work
color: "#71717a",
fontSize: "0.75rem",
}}
>
<button
onClick={triggerRefresh}
title="Reload Preview"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 24,
height: 24,
borderRadius: 4,
border: "none",
cursor: "pointer",
transition: "all 0.15s",
background: "transparent",
color: "#71717a",
flexShrink: 0,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "#e4e4e7";
e.currentTarget.style.color = "#18181b";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "transparent";
e.currentTarget.style.color = "#71717a";
}}
>
<RotateCw size={12} />
</button>
<div
style={{
width: 1,
height: 14,
background: "#e4e4e7",
margin: "0 2px",
flexShrink: 0,
}}
/>
{/* Server Selection Dropdown */}
<div
style={{
position: "relative",
display: "flex",
alignItems: "center",
flexShrink: 0,
}}
>
<select
value={selectedPort}
onChange={(e) => setSelectedPort(Number(e.target.value))}
style={{
background: "transparent",
border: "none",
outline: "none",
color: "#18181b",
fontSize: "0.75rem",
fontWeight: 500,
cursor: "pointer",
appearance: "none",
paddingRight: 16,
paddingLeft: 4,
}}
>
{validPreviews.map((p) => (
<option key={p.id} value={p.port}>
{p.name && p.name !== `port-${p.port}`
? p.name
: `Port ${p.port}`}
</option>
))}
{validPreviews.length === 0 && (
<option value={3000}>No running server</option>
)}
</select>
<ChevronDown
size={12}
style={{
color: "#71717a",
position: "absolute",
right: 2,
pointerEvents: "none",
}}
/>
</div>
<div
style={{
width: 1,
height: 14,
background: "#e4e4e7",
margin: "0 4px",
flexShrink: 0,
}}
/>
<div
style={{
flex: 1,
minWidth: 0,
position: "relative",
display: "flex",
alignItems: "center",
}}
>
<select
value={currentPath}
onChange={(e) => setCurrentPath(e.target.value)}
style={{
background: "transparent",
border: "none",
outline: "none",
width: "100%",
color: "#18181b",
fontSize: "0.75rem",
textOverflow: "ellipsis",
fontFamily: "var(--font-mono), monospace",
paddingRight: 16,
appearance: "none",
cursor: "pointer",
}}
>
<option value="/">/</option>
<option value="/dashboard">/dashboard</option>
<option value="/login">/login</option>
<option value="/signup">/signup</option>
<option value="/about">/about</option>
<option value="/contact">/contact</option>
<option value="/pricing">/pricing</option>
<option value="/settings">/settings</option>
</select>
<ChevronDown
size={12}
style={{
color: "#71717a",
position: "absolute",
right: 4,
pointerEvents: "none",
}}
/>
</div>
<div
style={{
width: 1,
height: 14,
background: "#e4e4e7",
margin: "0 4px",
flexShrink: 0,
}}
/>
{/* Device Toggles Inside the Address Bar */}
<div
style={{
display: "flex",
gap: 2,
alignItems: "center",
marginRight: 4,
}}
>
<button
onClick={() => setDeviceMode("desktop")}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 24,
height: 24,
borderRadius: 4,
border: "none",
cursor: "pointer",
transition: "all 0.15s",
background: deviceMode === "desktop" ? "#e4e4e7" : "transparent",
color: deviceMode === "desktop" ? "#18181b" : "#71717a",
}}
title="Desktop view"
>
<Monitor size={12} />
</button>
<button
onClick={() => setDeviceMode("tablet")}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 24,
height: 24,
borderRadius: 4,
border: "none",
cursor: "pointer",
transition: "all 0.15s",
background: deviceMode === "tablet" ? "#e4e4e7" : "transparent",
color: deviceMode === "tablet" ? "#18181b" : "#71717a",
}}
title="Tablet view"
>
<Tablet size={12} />
</button>
<button
onClick={() => setDeviceMode("mobile")}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 24,
height: 24,
borderRadius: 4,
border: "none",
cursor: "pointer",
transition: "all 0.15s",
background: deviceMode === "mobile" ? "#e4e4e7" : "transparent",
color: deviceMode === "mobile" ? "#18181b" : "#71717a",
}}
title="Mobile view"
>
<Smartphone size={12} />
</button>
</div>
<div
style={{
width: 1,
height: 14,
background: "#e4e4e7",
margin: "0 4px",
flexShrink: 0,
}}
/>
{displayUrl ? (
<a
href={displayUrl}
target="_blank"
rel="noreferrer"
title="Open preview in new tab"
style={{
color: "#71717a",
display: "flex",
padding: "4px",
flexShrink: 0,
}}
>
<ExternalLink size={14} />
</a>
) : (
<div style={{ padding: "4px", opacity: 0.3, flexShrink: 0 }}>
<ExternalLink size={14} />
</div>
)}
</div>
);
}
const bar: React.CSSProperties = {
display: "flex",
flexDirection: "row",
alignItems: "center",
flex: 1,
minWidth: 0,
height: "100%", // Inherit height from parent
padding: "0 16px",
gap: 12,
boxSizing: "border-box",
background: "#faf8f5",
fontFamily: '"Outfit", "Inter", ui-sans-serif, sans-serif',
};
const primaryGroup: React.CSSProperties = {
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 2,
background: "#f4f4f5",
padding: 4,
borderRadius: 8,
border: "1px solid #e4e4e7",
};