design(preview): replace mock visual tools with wide address bar showing active dev URL

This commit is contained in:
2026-06-12 11:12:09 -07:00
parent 9b13320253
commit 7305c2a57c
3 changed files with 111 additions and 158 deletions

View File

@@ -7,6 +7,7 @@ import { ReactNode } from "react";
import { Toaster } from "sonner";
import { ChatPanel } from "@/components/vibn-chat/chat-panel";
import { ProjectStreamHandler } from "@/components/project/project-stream-handler";
import { DashboardSidebar } from "@/components/project/dashboard-sidebar";
export default async function ProjectShell({
children,
@@ -21,7 +22,14 @@ export default async function ProjectShell({
<>
<ProjectStreamHandler projectId={projectId} />
<div style={pageWrap}>
<ChatPanel structural artifactSlot={children} />
<ChatPanel
structural
artifactSlot={
<DashboardSidebar workspace={workspace} projectId={projectId}>
{children}
</DashboardSidebar>
}
/>
</div>
<Toaster position="top-center" />
</>

View File

@@ -1,101 +1,89 @@
"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";
import { usePathname, useParams } from "next/navigation";
import { Monitor, Smartphone, RotateCw, ExternalLink } 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;
}
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) {
export function ProjectIconRail({ workspace, projectId, actions }: 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 }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
flex: 1,
minWidth: 0,
}}
>
{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)}
/>
))}
<Link
href={`${projectBase}/preview`}
style={{
padding: "6px 14px",
fontSize: "0.8rem",
fontWeight: 500,
borderRadius: 6,
textDecoration: "none",
background: isPreviewActive ? "#fff" : "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: "6px 14px",
fontSize: "0.8rem",
fontWeight: 500,
borderRadius: 6,
textDecoration: "none",
background: !isPreviewActive ? "#fff" : "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",
flex: 1,
justifyContent: "flex-end",
alignItems: "center",
minWidth: 0,
}}
>
{actions}
</div>
<RailLink
href={`${projectBase}/settings`}
label="Settings"
Icon={Settings}
active={
pathname === `${projectBase}/settings` ||
pathname.startsWith(`${projectBase}/settings/`)
}
/>
</nav>
);
}
import { Monitor, Smartphone, RotateCw, ExternalLink } from "lucide-react";
import { usePreviewToolbarStore } from "./preview-toolbar/preview-toolbar-state";
import { useAnatomy } from "@/components/project/use-anatomy";
import { useParams } from "next/navigation";
function PreviewDeviceToggles() {
const deviceMode = usePreviewToolbarStore((s) => s.deviceMode);
const setDeviceMode = usePreviewToolbarStore((s) => s.setDeviceMode);
@@ -110,7 +98,7 @@ function PreviewDeviceToggles() {
const displayUrl = running?.url ?? "";
return (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, width: "100%" }}>
<div
style={{
display: "flex",
@@ -248,45 +236,6 @@ function PreviewDeviceToggles() {
);
}
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,
// Active item shows its label so the user always knows which mode
// they're in; inactive items stay icon-only (with tooltip) to save
// space. This makes the editor's modes explicit instead of a row of
// unlabeled glyphs.
width: active ? "auto" : 36,
padding: active ? "0 12px" : 0,
gap: active ? 6 : 0,
fontSize: "0.8rem",
fontWeight: 600,
background: active ? "#f6f2ec" : "transparent",
color: active ? "#1a1a1a" : "#6b665e",
borderColor: active ? "#d9d2c5" : "transparent",
}}
>
<Icon size={16} />
{active && <span>{label}</span>}
</Link>
);
}
const bar: React.CSSProperties = {
display: "flex",
flexDirection: "row",
@@ -294,10 +243,11 @@ const bar: React.CSSProperties = {
flex: 1,
minWidth: 0,
height: "100%",
padding: "0 12px",
gap: 6,
padding: "0 16px",
gap: 12,
boxSizing: "border-box",
background: "#faf8f5",
background: "#fafafa",
borderBottom: "1px solid #e4e4e7",
fontFamily: '"Outfit", "Inter", ui-sans-serif, sans-serif',
};
@@ -305,18 +255,9 @@ 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,
gap: 2,
background: "#f4f4f5",
padding: 4,
borderRadius: 8,
border: "1px solid #e4e4e7",
};

View File

@@ -2472,7 +2472,39 @@ export function ChatPanel({
alignItems: "stretch",
}}
>
<ProjectIconRail workspace={workspace} projectId={projectId} />
<ProjectIconRail
workspace={workspace}
projectId={projectId}
actions={
<button
type="button"
onClick={() => {
if (activeThread && !sending) {
sendMessage("ship it");
}
}}
disabled={!activeThread || sending}
style={{
background: "#18181b",
border: "none",
cursor:
activeThread && !sending ? "pointer" : "not-allowed",
padding: "6px 14px",
borderRadius: 6,
color: "#fff",
display: "flex",
alignItems: "center",
fontSize: "0.75rem",
fontWeight: 600,
opacity: activeThread && !sending ? 1 : 0.5,
transition: "opacity 0.15s ease",
}}
title="Ship to production"
>
Publish
</button>
}
/>
</div>
</div>
@@ -2724,34 +2756,6 @@ export function ChatPanel({
</button>
)}
<div style={{ display: "flex", gap: 4 }}>
{/* Top-Level Publish Button */}
<button
type="button"
onClick={() => {
if (activeThread && !sending) {
sendMessage("ship it");
}
}}
disabled={!activeThread || sending}
style={{
background: "#18181b",
border: "none",
cursor: activeThread && !sending ? "pointer" : "not-allowed",
padding: structural ? "4px 12px" : "5px 14px",
borderRadius: 6,
color: "#fff",
display: "flex",
alignItems: "center",
fontSize: "0.75rem",
fontWeight: 600,
opacity: activeThread && !sending ? 1 : 0.5,
transition: "opacity 0.15s ease",
}}
title="Ship to production"
>
Publish
</button>
<button
type="button"
onClick={newThread}