feat: PWA support + mobile-responsive layout + QR code to open Atlas on phone

Made-with: Cursor
This commit is contained in:
2026-03-02 20:56:20 -08:00
parent 585343968e
commit 3896eb671c
6 changed files with 181 additions and 9 deletions

View File

@@ -7,6 +7,57 @@ import { AtlasChat } from "@/components/AtlasChat";
import { OrchestratorChat } from "@/components/OrchestratorChat";
import { Loader2 } from "lucide-react";
function MobileQRButton({ projectId, workspace }: { projectId: string; workspace: string }) {
const [show, setShow] = useState(false);
const url = typeof window !== "undefined"
? `${window.location.origin}/${workspace}/project/${projectId}/overview`
: "";
const qrSrc = `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(url)}&bgcolor=f6f4f0&color=1a1a1a&margin=2`;
return (
<div style={{ position: "relative" }}>
<button
onClick={() => setShow(s => !s)}
title="Open on your phone"
style={{
display: "flex", alignItems: "center", gap: 6,
padding: "6px 12px", borderRadius: 7,
background: "none", border: "1px solid #e0dcd4",
fontSize: "0.72rem", fontFamily: "Outfit, sans-serif",
color: "#8a8478", cursor: "pointer",
transition: "border-color 0.12s",
}}
onMouseEnter={e => (e.currentTarget.style.borderColor = "#b5b0a6")}
onMouseLeave={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
>
📱 Open on phone
</button>
{show && (
<div style={{
position: "absolute", top: "calc(100% + 8px)", right: 0,
background: "#fff", borderRadius: 12,
border: "1px solid #e8e4dc",
boxShadow: "0 8px 24px #1a1a1a12",
padding: "16px", zIndex: 50,
display: "flex", flexDirection: "column", alignItems: "center", gap: 10,
minWidth: 220,
}}>
<img src={qrSrc} alt="QR code" width={180} height={180} style={{ borderRadius: 8 }} />
<p style={{ fontSize: "0.72rem", color: "#8a8478", textAlign: "center", margin: 0, fontFamily: "Outfit, sans-serif" }}>
Scan to open Atlas on your phone
</p>
<p style={{ fontSize: "0.65rem", color: "#b5b0a6", textAlign: "center", margin: 0, fontFamily: "IBM Plex Mono, monospace", wordBreak: "break-all" }}>
{url}
</p>
<button onClick={() => setShow(false)} style={{ fontSize: "0.68rem", color: "#a09a90", background: "none", border: "none", cursor: "pointer" }}>
Close
</button>
</div>
)}
</div>
);
}
interface Project {
id: string;
productName: string;
@@ -16,6 +67,7 @@ interface Project {
export default function ProjectOverviewPage() {
const params = useParams();
const projectId = params.projectId as string;
const workspace = params.workspace as string;
const { status: authStatus } = useSession();
const [project, setProject] = useState<Project | null>(null);
const [loading, setLoading] = useState(true);
@@ -50,6 +102,15 @@ export default function ProjectOverviewPage() {
return (
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
{/* Desktop-only: Open on phone button */}
<style>{`@media (max-width: 768px) { .vibn-phone-btn { display: none !important; } }`}</style>
<div className="vibn-phone-btn" style={{
position: "absolute", top: 14, right: 248,
zIndex: 20,
}}>
<MobileQRButton projectId={projectId} workspace={workspace} />
</div>
{(!project.stage || project.stage === "discovery") ? (
<AtlasChat
projectId={projectId}

View File

@@ -24,8 +24,18 @@ const ibmPlexMono = IBM_Plex_Mono({
});
export const metadata: Metadata = {
title: "VIBN - AI-Powered Development Platform",
description: "Track, manage, and deploy your AI-coded projects with ease",
title: "VIBN — Build with Atlas",
description: "Chat with Atlas to define your product, then let AI build it.",
manifest: "/manifest.json",
appleWebApp: {
capable: true,
statusBarStyle: "black-translucent",
title: "VIBN",
},
other: {
"mobile-web-app-capable": "yes",
"msapplication-TileColor": "#1a1a1a",
},
};
export default function RootLayout({
@@ -35,6 +45,12 @@ export default function RootLayout({
}>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#1a1a1a" />
<link rel="apple-touch-icon" href="/vibn-logo-circle.png" />
<link rel="manifest" href="/manifest.json" />
</head>
<body
className={`${outfit.variable} ${newsreader.variable} ${ibmPlexMono.variable} antialiased`}
>
@@ -42,6 +58,13 @@ export default function RootLayout({
{children}
<Toaster />
</Providers>
<script dangerouslySetInnerHTML={{ __html: `
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(() => {});
});
}
`}} />
</body>
</html>
);