256 lines
6.4 KiB
TypeScript
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,
|
|
};
|