Files
Stirling-PDF/frontend/editor/src/portal/components/AppShell.tsx
T
EthanHealy01andReece Browne c91d63f215 New design, part one: shared branding, nav surfaces and theme tokens (#7163)
## Overview

First part of the move over to the new designs. This lays the groundwork
(shared brand components, button/nav styling, theme tokens) and applies
it across the editor and the processor. Later parts will build on top of
it.

## What's changed

**Branding**
- Shared `Logo` and `BrandMark` components used everywhere, so the mark
and wordmark are identical across the editor, processor, auth pages and
the chat FAB.
- The sidebar logo doubles as the editor to processor switcher, morphing
into a chevron on hover. It only appears for users who can actually
reach the processor.

**Navigation and layout**
- Both sidebars restructured onto the floating nav surface treatment,
with rounded panels sitting on the app canvas.
- The editor file sidebar is now three sections (controls, PDF Library,
settings) and the workbench top bar and tools panel match.
- Added a collapse toggle to both sidebars, with an animated expand and
collapse and a tidy icon rail when collapsed. The processor did not have
a desktop collapse before.

**Components**
- Buttons and action icons now share one styling system, so both react
to the same tokens.
- Secondary buttons in dark mode use a neutral fill and border instead
of inheriting the primary colour.
- Status badges default to a clean dot with no background, with a filled
pill as the alternative.
- Metric strips gained a row layout with an optional leading icon.

**Theme**
- Colour tokens consolidated. Literal colours live only in the palette
file, everything else references the semantic `--c-*` tokens.
- `saas-theme.css` removed and the parts that were genuinely needed
moved into the shared theme, so all builds get them.
- The colour linter enforces this across the app and runs in CI.

## Notes

- Nothing functional should change here, it is styling plus the sidebar
collapse feature.
- Main has been merged in. The Sources and billing pages picked up
changes from main during that merge and are worth a look alongside the
new styling.

---------

Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2026-08-03 14:59:28 +00:00

91 lines
2.8 KiB
TypeScript

import { useEffect, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { ActionIcon } from "@app/ui";
import { Sidebar } from "@portal/components/Sidebar";
import { useUI } from "@portal/contexts/UIContext";
import { MenuIcon, SearchIcon } from "@portal/components/icons";
import { Logo } from "@app/ui/Logo";
import "@portal/components/AppShell.css";
/**
* Compact header shown only under the mobile breakpoint (CSS-hidden on
* desktop): hamburger opens the sidebar drawer, search opens the palette
* (there's no ⌘K on a phone).
*/
function MobileTopbar() {
const { t } = useTranslation();
const { mobileNavOpen, toggleMobileNav, openSearch } = useUI();
return (
<header className="portal-shell__topbar">
<ActionIcon
variant="tertiary"
size="lg"
aria-label={t("portal.shell.topbar.openNav")}
aria-expanded={mobileNavOpen}
onClick={toggleMobileNav}
>
<MenuIcon size={20} />
</ActionIcon>
<Logo
variant="iconAndText"
iconHeight="1.6rem"
textHeight="1.3rem"
className="portal-shell__topbar-wordmark"
/>
<ActionIcon
variant="tertiary"
size="lg"
aria-label={t("portal.shell.topbar.search")}
onClick={openSearch}
>
<SearchIcon size={19} />
</ActionIcon>
</header>
);
}
/**
* Two-column layout: fixed-width sidebar on the left, a scrolling main column on
* the right. Under the mobile breakpoint the sidebar becomes an off-canvas
* drawer behind a scrim, opened from the topbar hamburger. The Sidebar reads
* its state from context, so this shell stays prop-free.
*/
export function AppShell({ children }: { children: ReactNode }) {
const { mobileNavOpen, closeMobileNav } = useUI();
const { pathname } = useLocation();
// Navigating (tap on a nav row, back button, deep link) always dismisses the
// drawer. Depends on pathname only: the close fn's identity changes with any
// UI state, and re-running on that would instantly close a just-opened drawer.
useEffect(() => {
closeMobileNav();
}, [pathname]);
useEffect(() => {
if (!mobileNavOpen) return;
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") closeMobileNav();
}
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [mobileNavOpen, closeMobileNav]);
return (
<div className="portal-shell">
<Sidebar />
{mobileNavOpen && (
<div
className="portal-shell__scrim"
onClick={closeMobileNav}
aria-hidden
/>
)}
<div className="portal-shell__main">
<MobileTopbar />
<main className="portal-shell__view">{children}</main>
</div>
</div>
);
}