Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions frontend/src/app/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1544,7 +1544,7 @@ export default function ChatPage() {
};

return (
<div style={{ minHeight: "100dvh", background: "var(--bg)", color: "var(--text)", fontFamily: "system-ui, -apple-system, sans-serif" }}>
<div style={{ minHeight: "calc(100dvh - var(--pe-shell-h))", background: "var(--bg)", color: "var(--text)", fontFamily: "system-ui, -apple-system, sans-serif" }}>
<style>{`
[data-tip],[data-tip-left],[data-tip-right]{position:relative}
[data-tip]::after,[data-tip-left]::after,[data-tip-right]::after{
Expand Down Expand Up @@ -1580,10 +1580,10 @@ export default function ChatPage() {
}
`}</style>
{/* Body */}
<div style={{ display: "flex", margin: "0 auto", padding: "0", gap: "0", width: "100%", minHeight: "100dvh" }}>
<div style={{ display: "flex", margin: "0 auto", padding: "0", gap: "0", width: "100%", minHeight: "calc(100dvh - var(--pe-shell-h))" }}>
{!isEmbed && !sidebarOpen && (
/* Rail */
<div data-pe-sidebar style={{ width: "60px", flexShrink: 0, background: "var(--sidebar-bg)", borderRight: "1px solid var(--border)", display: "flex", flexDirection: "column", alignItems: "center", padding: "10px 0", position: "sticky", top: 0, height: "100dvh", boxSizing: "border-box" }}>
<div data-pe-sidebar style={{ width: "60px", flexShrink: 0, background: "var(--sidebar-bg)", borderRight: "1px solid var(--border)", display: "flex", flexDirection: "column", alignItems: "center", padding: "10px 0", position: "sticky", top: "var(--pe-shell-h)", height: "calc(100dvh - var(--pe-shell-h))", boxSizing: "border-box" }}>
<button onClick={() => setSidebarOpen(true)} data-tip-right="Open sidebar" aria-label="Open sidebar" style={{ background: "transparent", border: "none", cursor: "pointer", padding: "8px", borderRadius: "10px", display: "flex", color: "var(--text)", marginBottom: "4px" }}
onMouseEnter={(e) => (e.currentTarget as HTMLElement).style.background = "var(--surface-hover)"}
onMouseLeave={(e) => (e.currentTarget as HTMLElement).style.background = "transparent"}
Expand Down Expand Up @@ -1628,7 +1628,7 @@ export default function ChatPage() {
)}
{/* Sidebar */}
{!isEmbed && sidebarOpen && (
<div data-pe-sidebar style={{ width: "260px", flexShrink: 0, background: "var(--sidebar-bg)", borderRight: "1px solid var(--border)", padding: "12px 8px", position: "sticky", top: 0, height: "100dvh", alignSelf: "flex-start", display: "flex", flexDirection: "column", boxSizing: "border-box" }}>
<div data-pe-sidebar style={{ width: "260px", flexShrink: 0, background: "var(--sidebar-bg)", borderRight: "1px solid var(--border)", padding: "12px 8px", position: "sticky", top: "var(--pe-shell-h)", height: "calc(100dvh - var(--pe-shell-h))", alignSelf: "flex-start", display: "flex", flexDirection: "column", boxSizing: "border-box" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "4px", padding: "4px 4px 4px 10px", gap: "8px" }}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={`${APP_BASE_PATH}/policyengine-logo.svg`} alt="PolicyEngine" style={{ display: "block", width: "128px", height: "auto" }} />
Expand Down Expand Up @@ -1798,7 +1798,7 @@ export default function ChatPage() {
)}

{/* Chat area */}
<div data-pe-chat style={{ flex: 1, padding: "16px 24px max(24px, env(safe-area-inset-bottom))", minWidth: 0, minHeight: "100dvh", boxSizing: "border-box", display: "flex", flexDirection: "column", justifyContent: hasMessages ? "flex-start" : "center", alignItems: "stretch" }}>
<div data-pe-chat style={{ flex: 1, padding: "16px 24px max(24px, env(safe-area-inset-bottom))", minWidth: 0, minHeight: "calc(100dvh - var(--pe-shell-h))", boxSizing: "border-box", display: "flex", flexDirection: "column", justifyContent: hasMessages ? "flex-start" : "center", alignItems: "stretch" }}>
{!hasMessages && (
<div style={{ width: "100%", maxWidth: "760px", margin: "0 auto", textAlign: "center", marginBottom: "20px" }}>
<h1 style={{ fontSize: "30px", fontWeight: 500, color: "var(--text)", margin: 0, letterSpacing: "-0.01em" }}>What&apos;s on your mind today?</h1>
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata, Viewport } from "next";
import "@mantine/core/styles.css";
import { APP_BASE_PATH } from "@/utils/backend";
import SiteHeader from "@/components/SiteHeader";
import Providers from "./Providers";

export const metadata: Metadata = {
Expand All @@ -22,6 +23,10 @@ export const viewport: Viewport = {

const themeVars = `
:root {
/* Height of the PolicyEngine site shell. The chat's full-height panes
subtract this so the header does not push them below the fold.
SiteHeader sets it to 0px for embedded (?embed) copies. */
--pe-shell-h: 56px;
--bg: #ffffff;
--surface: #ffffff;
--surface-2: #f4f4f4;
Expand Down Expand Up @@ -88,6 +93,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
/>
</head>
<body style={{ margin: 0, fontFamily: "system-ui, -apple-system, sans-serif", background: "var(--bg)", color: "var(--text)" }}>
<SiteHeader />
<Providers>{children}</Providers>
</body>
</html>
Expand Down
110 changes: 110 additions & 0 deletions frontend/src/components/SiteHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"use client";

import { useEffect, useState } from "react";

/**
* The PolicyEngine site shell.
*
* /uk/chat is served on policyengine.org through a multizone rewrite, and the
* rewrite does not inject the parent app's header into this response — every
* zone child renders the shell itself. The app-zone-shell-audit in
* policyengine-app-v2 asserts that the top 140px of the page carries the
* PolicyEngine brand plus the Research, Model, API and Donate nav labels.
*
* Links are absolute so they resolve both here and on the standalone
* deployment, where /uk/* belongs to the chat rather than the website.
*/
const SITE = "https://policyengine.org";

const NAV_LINKS = [
{ label: "Research", href: `${SITE}/uk/research` },
{ label: "Model", href: `${SITE}/uk/model` },
{ label: "API", href: `${SITE}/uk/api` },
{ label: "About", href: `${SITE}/uk/team` },
];

export default function SiteHeader() {
// Embedded copies (?embed) are framed inside another page that already has
// its own chrome, so the shell would be a duplicate header.
const [isEmbed, setIsEmbed] = useState(false);

useEffect(() => {
const embedded = new URLSearchParams(window.location.search).has("embed");
setIsEmbed(embedded);
document.documentElement.style.setProperty(
"--pe-shell-h",
embedded ? "0px" : "",
);
}, []);

if (isEmbed) return null;

return (
<header
style={{
height: "var(--pe-shell-h)",
boxSizing: "border-box",
position: "sticky",
top: 0,
zIndex: 100,
background: "var(--bg)",
borderBottom: "1px solid var(--border)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "16px",
padding: "0 24px",
}}
>
<a
href={`${SITE}/uk`}
aria-label="PolicyEngine"
style={{
display: "flex",
alignItems: "center",
gap: "8px",
textDecoration: "none",
color: "var(--text)",
fontWeight: 700,
letterSpacing: "0.02em",
whiteSpace: "nowrap",
}}
>
PolicyEngine
</a>

<nav
aria-label="PolicyEngine navigation"
style={{
display: "flex",
alignItems: "center",
gap: "20px",
fontSize: "14px",
whiteSpace: "nowrap",
}}
>
{NAV_LINKS.map((link) => (
<a
key={link.label}
href={link.href}
style={{ color: "var(--text-2)", textDecoration: "none" }}
>
{link.label}
</a>
))}
<a
href={`${SITE}/uk/donate`}
style={{
color: "var(--text)",
textDecoration: "none",
border: "1px solid var(--border)",
borderRadius: "6px",
padding: "6px 14px",
}}
>
Donate
</a>
</nav>
</header>
);
}