This repository has been archived on 2026-06-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
master-ai/vibn-frontend/components/project/project-icon-rail.tsx

256 lines
6.4 KiB
TypeScript

"use client";
/**
* Horizontal project nav bar — sits in the unified top row beside chat.
*
* Primary section icons sit on the trailing (right) side; Settings stays
* at the far right. The parent row owns the bottom border.
*/
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
Eye,
Code2,
ClipboardList,
Palette,
Globe,
Database,
Settings,
CreditCard,
PlaneTakeoff,
} from "lucide-react";
interface Props {
workspace: string;
projectId: string;
}
interface RailItem {
segment: string;
label: string;
Icon: typeof Eye;
aliases?: string[];
}
const PRIMARY_ITEMS: RailItem[] = [
{ segment: "preview", label: "Preview", Icon: Eye },
{ segment: "plan", label: "Plan", Icon: ClipboardList },
{ segment: "market", label: "Market", Icon: PlaneTakeoff },
{ segment: "product", label: "Code", Icon: Code2, aliases: ["code"] },
{ segment: "hosting", label: "Hosting", Icon: Globe },
{ segment: "infrastructure", label: "Infra", Icon: Database },
{ segment: "billing", label: "Billing", Icon: CreditCard },
];
export function ProjectIconRail({ workspace, projectId }: Props) {
const pathname = usePathname() ?? "";
const projectBase = `/${workspace}/project/${projectId}`;
const isPreviewActive =
pathname === `${projectBase}/preview` ||
pathname.startsWith(`${projectBase}/preview/`);
const isActive = (item: RailItem) => {
const segments = [item.segment, ...(item.aliases ?? [])];
return segments.some(
(s) =>
pathname === `${projectBase}/${s}` ||
pathname.startsWith(`${projectBase}/${s}/`),
);
};
return (
<nav style={bar} aria-label="Project sections">
{/* Dynamic Left Content Area (e.g. Preview Device Toggles) */}
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
{isPreviewActive && <PreviewDeviceToggles />}
</div>
<div style={{ flex: 1, minWidth: 0 }} aria-hidden />
<div style={primaryGroup}>
{PRIMARY_ITEMS.map((item) => (
<RailLink
key={item.segment}
href={`${projectBase}/${item.segment}`}
label={item.label}
Icon={item.Icon}
active={isActive(item)}
/>
))}
</div>
<RailLink
href={`${projectBase}/settings`}
label="Settings"
Icon={Settings}
active={
pathname === `${projectBase}/settings` ||
pathname.startsWith(`${projectBase}/settings/`)
}
/>
</nav>
);
}
import { Monitor, Smartphone, RotateCw } from "lucide-react";
import { usePreviewToolbarStore } from "./preview-toolbar/preview-toolbar-state";
function PreviewDeviceToggles() {
const deviceMode = usePreviewToolbarStore((s) => s.deviceMode);
const setDeviceMode = usePreviewToolbarStore((s) => s.setDeviceMode);
const triggerRefresh = usePreviewToolbarStore((s) => s.triggerRefresh);
return (
<div
style={{
display: "flex",
gap: 4,
background: "#f1ebe3",
padding: 4,
borderRadius: 8,
border: "1px solid #e8e4dc",
}}
>
<button
onClick={triggerRefresh}
title="Reload Preview"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 26,
borderRadius: 6,
border: "none",
cursor: "pointer",
transition: "all 0.15s",
background: "transparent",
color: "#8c8580",
marginRight: 4,
}}
onMouseEnter={(e) => (e.currentTarget.style.color = "#1a1a1a")}
onMouseLeave={(e) => (e.currentTarget.style.color = "#8c8580")}
>
<RotateCw size={14} />
</button>
<div
style={{
width: 1,
height: 16,
background: "#d9d2c5",
alignSelf: "center",
marginRight: 4,
}}
/>
<button
onClick={() => setDeviceMode("desktop")}
style={{
display: "flex",
alignItems: "center",
gap: 6,
padding: "4px 10px",
borderRadius: 6,
fontSize: "0.75rem",
fontWeight: 500,
border: "none",
cursor: "pointer",
transition: "all 0.15s",
background: deviceMode === "desktop" ? "#ffffff" : "transparent",
color: deviceMode === "desktop" ? "#1a1a1a" : "#8c8580",
boxShadow:
deviceMode === "desktop" ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
}}
>
<Monitor size={14} />
Desktop
</button>
<button
onClick={() => setDeviceMode("mobile")}
style={{
display: "flex",
alignItems: "center",
gap: 6,
padding: "4px 10px",
borderRadius: 6,
fontSize: "0.75rem",
fontWeight: 500,
border: "none",
cursor: "pointer",
transition: "all 0.15s",
background: deviceMode === "mobile" ? "#ffffff" : "transparent",
color: deviceMode === "mobile" ? "#1a1a1a" : "#8c8580",
boxShadow:
deviceMode === "mobile" ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
}}
>
<Smartphone size={14} />
Mobile
</button>
</div>
);
}
function RailLink({
href,
label,
Icon,
active,
}: {
href: string;
label: string;
Icon: typeof Eye;
active: boolean;
}) {
return (
<Link
href={href}
title={label}
aria-label={label}
aria-current={active ? "page" : undefined}
style={{
...linkBase,
background: active ? "#f6f2ec" : "transparent",
color: active ? "#1a1a1a" : "#6b665e",
borderColor: active ? "#d9d2c5" : "transparent",
}}
>
<Icon size={16} />
</Link>
);
}
const bar: React.CSSProperties = {
display: "flex",
flexDirection: "row",
alignItems: "center",
flex: 1,
minWidth: 0,
height: "100%",
padding: "0 12px",
gap: 6,
boxSizing: "border-box",
background: "#faf8f5",
fontFamily: '"Outfit", "Inter", ui-sans-serif, sans-serif',
};
const primaryGroup: React.CSSProperties = {
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 4,
};
const linkBase: React.CSSProperties = {
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 36,
height: 36,
borderRadius: 6,
border: "1px solid",
textDecoration: "none",
transition: "background 0.15s, color 0.15s, border-color 0.15s",
flexShrink: 0,
};