Portal: full mock-driven surfaces, demonolithed components, backend-ready mocks (#6686)

This commit is contained in:
Reece Browne
2026-06-16 12:20:35 +01:00
committed by GitHub
parent 6716398ccb
commit 96accea984
146 changed files with 12221 additions and 286 deletions
+64 -30
View File
@@ -1,11 +1,16 @@
import { NavItem } from "@shared/components";
import { Dropdown, NavItem } from "@shared/components";
import { useView, type ViewId } from "@portal/contexts/ViewContext";
import { useTier } from "@portal/contexts/TierContext";
import { useTheme } from "@portal/contexts/ThemeContext";
import { useUI } from "@portal/contexts/UIContext";
import { useAsync } from "@portal/hooks/useAsync";
import { fetchHomeKpis, type KpiEntry } from "@portal/api/home";
import wordmarkLight from "@shared/assets/stirling-pdf-logo-light.svg";
import wordmarkDark from "@shared/assets/stirling-pdf-logo-dark.svg";
import markLight from "@shared/assets/stirling-mark-light.svg";
import markDark from "@shared/assets/stirling-mark-dark.svg";
import {
HomeIcon,
EditorIcon,
SourcesIcon,
PipelinesIcon,
DocumentsIcon,
@@ -17,6 +22,12 @@ import {
} from "@portal/components/icons";
import "@portal/components/Sidebar.css";
// TEMP app switcher. The editor is a separate Vite app — no shared shell yet
// (see PORTAL_INTEGRATION_PLAN.md), so switching is a hard navigation: the
// editor's own dev server in dev, the site root in prod. A standalone portal
// deploy will later gate this behind a configured editor URL.
const EDITOR_URL = import.meta.env.DEV ? "http://localhost:5180/" : "/";
interface NavEntry {
id: ViewId;
label: string;
@@ -28,7 +39,6 @@ const GROUP_PRIMARY: NavEntry[] = [
];
const GROUP_OPERATIONAL: NavEntry[] = [
{ id: "editor", label: "Editor", icon: <EditorIcon /> },
{ id: "sources", label: "Sources", icon: <SourcesIcon /> },
{ id: "pipelines", label: "Pipelines", icon: <PipelinesIcon /> },
{ id: "documents", label: "Documents", icon: <DocumentsIcon /> },
@@ -44,22 +54,6 @@ const GROUP_PLATFORM: NavEntry[] = [
{ id: "docs", label: "Developer Docs", icon: <DocsIcon /> },
];
function StirlingMark() {
// The Stirling brand mark — two stacked parallelograms in the brand blues.
return (
<svg
width="22"
height="22"
viewBox="0 0 22 22"
aria-hidden
role="presentation"
>
<path d="M5 4 L20 4 L17 11 L2 11 Z" fill="var(--color-blue)" />
<path d="M5 11 L17 11 L14 18 L2 18 Z" fill="var(--color-blue-border)" />
</svg>
);
}
function UsageFooter() {
const { tier } = useTier();
// Read the same endpoint Home's KPI strip uses so the doc count here can't
@@ -117,6 +111,8 @@ function UsageFooter() {
export function Sidebar() {
const { activeView, setActiveView } = useView();
const { theme } = useTheme();
const { openSettings } = useUI();
function renderGroup(entries: NavEntry[]) {
return entries.map((entry) => (
@@ -134,15 +130,54 @@ export function Sidebar() {
return (
<aside className="portal-sidebar" aria-label="Primary navigation">
<div className="portal-sidebar__logo">
<StirlingMark />
<span className="portal-sidebar__wordmark">Stirling</span>
<button
type="button"
className="portal-sidebar__workspace"
aria-label="Switch workspace"
>
<ChevronDownIcon size={14} />
</button>
<span className="portal-sidebar__brand">
<img
className="portal-sidebar__brand-logo"
src={theme === "dark" ? wordmarkDark : wordmarkLight}
alt="Stirling PDF"
/>
<span className="portal-sidebar__logo-suffix">portal</span>
</span>
<Dropdown.Root align="end" className="portal-sidebar__app-switch">
<Dropdown.Trigger>
<button
type="button"
className="portal-sidebar__app-switch-btn"
aria-label="Switch app"
>
<ChevronDownIcon size={14} />
</button>
</Dropdown.Trigger>
<Dropdown.Menu width="11rem">
<Dropdown.Item
active
leading={
<img
className="portal-sidebar__app-icon"
src={theme === "dark" ? markDark : markLight}
alt=""
/>
}
>
Portal
</Dropdown.Item>
<Dropdown.Item
onSelect={() => {
window.location.href = EDITOR_URL;
}}
leading={
<img
className="portal-sidebar__app-icon"
src={theme === "dark" ? markDark : markLight}
alt=""
/>
}
>
Editor
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown.Root>
</div>
<nav className="portal-sidebar__nav">
@@ -164,8 +199,7 @@ export function Sidebar() {
id="settings"
label="Settings"
icon={<SettingsIcon />}
isActive={activeView === "settings"}
onClick={(id) => setActiveView(id as ViewId)}
onClick={() => openSettings()}
/>
<UsageFooter />
</div>