diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml
index 2cfc42475c..fe0b70a4eb 100644
--- a/frontend/editor/public/locales/en-US/translation.toml
+++ b/frontend/editor/public/locales/en-US/translation.toml
@@ -7182,26 +7182,6 @@ sources = "Sources"
usage = "Usage & Billing"
users = "Users"
-[portal.notifications]
-markAllRead = "Mark all read"
-title = "Notifications"
-viewAll = "View all"
-
-[portal.notifications.ariaLabel]
-none = "Notifications, no unread"
-unread_one = "Notifications, {{count}} unread"
-unread_other = "Notifications, {{count}} unread"
-
-[portal.notifications.count]
-allRead = "all read"
-loading = "loading"
-new_one = "{{count}} new"
-new_other = "{{count}} new"
-
-[portal.notifications.empty]
-description = "No new notifications."
-title = "You're all caught up"
-
[portal.pipelines]
subtitle = "Every automated document pipeline on the backend: an ordered chain of operations over a set of sources, run on a trigger. Click a row for its steps and sources."
title = "Pipelines"
@@ -7882,26 +7862,11 @@ admin = "Admin"
[portal.settings.sections]
account-link = "Account link"
-[portal.shell.header]
-accountFallback = "Account"
-accountMenu = "Account menu"
-darkMode = "Dark mode"
-lightMode = "Light mode"
-search = "Search"
-searchPlaceholder = "Search…"
-signOut = "Sign out"
-switchToDark = "Switch to dark theme"
-switchToLight = "Switch to light theme"
-
[portal.shell.sidebar]
appEditor = "Editor"
appProcessor = "Processor"
brandSuffix = "Stirling Processor"
-docsCount = "{{docs}} docs"
linkAccount = "Link Stirling account"
-planEditor = "Editor plan"
-planEnterprise = "Enterprise plan"
-planProcessor = "Processor plan"
primaryNav = "Primary navigation"
switchApp = "Switch app"
diff --git a/frontend/editor/src/portal/components/AppShell.tsx b/frontend/editor/src/portal/components/AppShell.tsx
index 606f021c0b..b3744d36bf 100644
--- a/frontend/editor/src/portal/components/AppShell.tsx
+++ b/frontend/editor/src/portal/components/AppShell.tsx
@@ -1,19 +1,17 @@
import type { ReactNode } from "react";
import { Sidebar } from "@portal/components/Sidebar";
-import { Header } from "@portal/components/Header";
import "@portal/components/AppShell.css";
/**
- * Two-column layout: fixed-width sidebar on the left, sticky header + scrolling
- * main column on the right. The Sidebar and Header read their state from
- * context, so this shell stays prop-free.
+ * Two-column layout: fixed-width sidebar on the left, a scrolling main column on
+ * the right. The Sidebar reads its state from context, so this shell stays
+ * prop-free.
*/
export function AppShell({ children }: { children: ReactNode }) {
return (
diff --git a/frontend/editor/src/portal/components/Header.css b/frontend/editor/src/portal/components/Header.css
deleted file mode 100644
index d1c4ede717..0000000000
--- a/frontend/editor/src/portal/components/Header.css
+++ /dev/null
@@ -1,191 +0,0 @@
-.portal-header {
- height: 3.25rem;
- display: flex;
- align-items: center;
- gap: 0.75rem;
- padding: 0 1rem;
- background: var(--color-header-bg);
- border-bottom: 1px solid var(--color-header-border);
- position: sticky;
- top: 0;
- z-index: 10;
-}
-
-.portal-header__left {
- flex: 0 0 auto;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- min-width: 12.5rem;
-}
-
-.portal-header__breadcrumb {
- font-size: 0.8125rem;
- font-weight: 500;
- color: var(--color-header-text);
-}
-
-/* Search trigger */
-.portal-header__search {
- flex: 0 1 20rem;
- margin: 0 auto;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.375rem 0.625rem;
- background: var(--color-search-bg);
- border: 1px solid var(--color-search-border);
- border-radius: var(--radius-md);
- color: var(--color-search-text);
- font-size: 0.75rem;
- transition:
- border-color var(--motion-fast),
- background var(--motion-fast);
-}
-.portal-header__search:hover {
- border-color: var(--color-search-border-hover);
-}
-
-/* The icon / placeholder / ⌘K live inside the shared Button's label, so the
- label must fill the button for the placeholder's flex:1 to push ⌘K to the
- far right (otherwise the label is content-width and everything left-packs). */
-.portal-header__search .mantine-Button-inner {
- width: 100%;
-}
-.portal-header__search .mantine-Button-label {
- flex: 1;
- gap: 0.5rem;
-}
-
-.portal-header__search-placeholder {
- flex: 1 1 auto;
- text-align: left;
-}
-
-.portal-header__search-kbd {
- font-family: var(--font-mono);
- font-size: 0.6875rem;
- padding: 0.0625rem 0.25rem;
- border-radius: var(--radius-xs);
- background: var(--color-bg-muted);
- color: var(--color-text-4);
-}
-
-/* Right cluster */
-.portal-header__right {
- flex: 0 0 auto;
- display: flex;
- align-items: center;
- gap: 0.375rem;
-}
-
-.portal-header__icon-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 2rem;
- height: 2rem;
- border-radius: var(--radius-md);
- color: var(--color-bell-stroke, var(--color-text-3));
- position: relative;
- transition:
- background var(--motion-fast),
- color var(--motion-fast);
-}
-.portal-header__icon-btn:hover {
- background: var(--color-bg-hover);
- color: var(--color-text-1);
-}
-
-.portal-header__bell-dot {
- position: absolute;
- top: 0.375rem;
- right: 0.4375rem;
- width: 0.4375rem;
- height: 0.4375rem;
- border-radius: 50%;
- background: var(--color-red);
- border: 2px solid var(--color-header-bg);
- box-sizing: content-box;
-}
-
-/* Tier switcher */
-.portal-header__tier {
- position: relative;
-}
-
-.portal-header__tier-btn {
- --sui-btn-py: 0.3125rem;
- padding: 0.3125rem 0.625rem;
- font-size: 0.75rem;
- color: var(--color-text-2);
- background: var(--color-surface);
- border: 1px solid var(--color-border);
- border-radius: var(--radius-md);
- transition:
- border-color var(--motion-fast),
- background var(--motion-fast);
-}
-.portal-header__tier-btn:hover {
- background: var(--color-bg-hover);
- border-color: var(--color-border-hover);
-}
-
-.portal-header__tier-dot {
- width: 0.4375rem;
- height: 0.4375rem;
- border-radius: 50%;
-}
-
-.portal-header__tier-label {
- font-weight: 500;
-}
-
-.portal-header__tier-menu {
- position: absolute;
- top: calc(100% + 0.25rem);
- right: 0;
- min-width: 12rem;
- margin: 0;
- padding: 0.25rem;
- list-style: none;
- background: var(--color-dropdown-bg);
- border: 1px solid var(--color-dropdown-border);
- border-radius: var(--radius-md);
- box-shadow: var(--shadow-lg);
- z-index: 20;
- animation: fadeInUp var(--motion-enter) both;
-}
-
-.portal-header__tier-option {
- width: 100%;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.4375rem 0.625rem;
- font-size: 0.8125rem;
- color: var(--color-text-2);
- border-radius: var(--radius-sm);
- text-align: left;
-}
-.portal-header__tier-option:hover {
- background: var(--color-dropdown-hover);
-}
-.portal-header__tier-option.is-active {
- background: var(--color-nav-active);
- color: var(--color-nav-active-text);
-}
-
-/* Avatar */
-.portal-header__avatar {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 2rem;
- height: 2rem;
- border-radius: 50%;
- background: var(--grad-blue-btn);
- color: #fff;
- font-size: 0.8125rem;
- font-weight: 600;
-}
diff --git a/frontend/editor/src/portal/components/Header.stories.tsx b/frontend/editor/src/portal/components/Header.stories.tsx
deleted file mode 100644
index 25f2b9c2b5..0000000000
--- a/frontend/editor/src/portal/components/Header.stories.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { Header } from "@portal/components/Header";
-
-const meta: Meta = {
- title: "Portal/Shell/Header",
- component: Header,
- parameters: { layout: "fullscreen" },
- decorators: [
- (S) => (
-
-
-
- Page body would render below the header.
-
-
- ),
- ],
-};
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
-
-export const FreeTier: Story = { globals: { tier: "free" } };
-export const ProTier: Story = { globals: { tier: "pro" } };
-export const EnterpriseTier: Story = { globals: { tier: "enterprise" } };
diff --git a/frontend/editor/src/portal/components/Header.tsx b/frontend/editor/src/portal/components/Header.tsx
deleted file mode 100644
index 317ef6040c..0000000000
--- a/frontend/editor/src/portal/components/Header.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import { useTranslation } from "react-i18next";
-import { ActionIcon, Avatar, Button, Dropdown } from "@app/ui";
-import { useAuth } from "@app/auth";
-import { useTheme } from "@portal/contexts/ThemeContext";
-import { useTier, TIER_INFO, type Tier } from "@portal/contexts/TierContext";
-import { useView } from "@portal/contexts/ViewContext";
-import { useUI } from "@portal/contexts/UIContext";
-import {
- SearchIcon,
- SunIcon,
- MoonIcon,
- ChevronDownIcon,
-} from "@portal/components/icons";
-import { NotificationsDropdown } from "@portal/components/NotificationsDropdown";
-import "@portal/components/Header.css";
-
-function ThemeToggle() {
- const { theme, toggle } = useTheme();
- const { t } = useTranslation();
- return (
-
- {theme === "light" ? : }
-
- );
-}
-
-function TierSwitcher() {
- const { tier, setTier, isDerived } = useTier();
- const info = TIER_INFO[tier];
- // In the app the tier is derived from the real link/wallet state and can't
- // be switched by hand; the dropdown only renders where the tier is pinned
- // (Storybook / demo surfaces — see TierProvider's initialTier).
- if (isDerived) return null;
- return (
-
-
-
- }
- rightSection={}
- >
- {info.label}
-
-
-
- {(Object.keys(TIER_INFO) as Tier[]).map((id) => (
- setTier(id)}
- leading={
-
- }
- >
- {TIER_INFO[id].label}
-
- ))}
-
-
- );
-}
-
-function UserMenu() {
- const { t } = useTranslation();
- const { displayName, signOut } = useAuth();
- const name =
- displayName ?? t("portal.shell.header.accountFallback", "Account");
- return (
-
-
-
-
-
-
-
- {name}
- void signOut()}>
- {t("portal.shell.header.signOut", "Sign out")}
-
-
-
- );
-}
-
-export function Header() {
- const { activeView } = useView();
- const { openSearch } = useUI();
- const { t } = useTranslation();
- return (
-
- );
-}
diff --git a/frontend/editor/src/portal/components/NotificationsDropdown.stories.tsx b/frontend/editor/src/portal/components/NotificationsDropdown.stories.tsx
deleted file mode 100644
index 1470f5e447..0000000000
--- a/frontend/editor/src/portal/components/NotificationsDropdown.stories.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { http, HttpResponse, delay } from "msw";
-import { NotificationsDropdown } from "@portal/components/NotificationsDropdown";
-import { NOTIFICATIONS } from "@portal/mocks/notifications";
-
-const meta: Meta = {
- title: "Portal/Header/NotificationsDropdown",
- component: NotificationsDropdown,
- parameters: { layout: "centered" },
- decorators: [
- (S) => (
-
-
-
- ),
- ],
-};
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
-
-export const Empty: Story = {
- parameters: {
- msw: {
- handlers: [http.get("/v1/notifications", () => HttpResponse.json([]))],
- },
- },
-};
-
-export const Loading: Story = {
- parameters: {
- msw: {
- handlers: [
- http.get("/v1/notifications", async () => {
- await delay("infinite");
- return HttpResponse.json([]);
- }),
- ],
- },
- },
-};
-
-export const HighVolume: Story = {
- parameters: {
- msw: {
- handlers: [
- http.get("/v1/notifications", () => {
- const items = Array.from({ length: 24 }, (_, i) => ({
- ...NOTIFICATIONS[i % NOTIFICATIONS.length],
- id: `n${i + 1}`,
- }));
- return HttpResponse.json(items);
- }),
- ],
- },
- },
-};
-
-export const NetworkError: Story = {
- parameters: {
- msw: {
- handlers: [
- http.get("/v1/notifications", () =>
- HttpResponse.json({ error: "Service unavailable" }, { status: 503 }),
- ),
- ],
- },
- },
-};
diff --git a/frontend/editor/src/portal/components/NotificationsDropdown.tsx b/frontend/editor/src/portal/components/NotificationsDropdown.tsx
deleted file mode 100644
index 1401837e53..0000000000
--- a/frontend/editor/src/portal/components/NotificationsDropdown.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import { useState } from "react";
-import { useTranslation } from "react-i18next";
-import { ActionIcon, Button, Dropdown, EmptyState, Skeleton } from "@app/ui";
-import { BellIcon } from "@portal/components/icons";
-import { useAsync, useSectionFlags } from "@portal/hooks/useAsync";
-import {
- fetchNotifications,
- markAllNotificationsRead,
- type Notification,
- type NotificationCategory,
-} from "@portal/api/notifications";
-import "@portal/components/NotificationsDropdown.css";
-
-const CATEGORY_COLOUR: Record = {
- pipeline: "var(--color-blue)",
- deploy: "var(--color-green)",
- billing: "var(--color-amber)",
- audit: "var(--color-purple)",
- agent: "var(--color-cat-insurance)",
- doc: "var(--color-cat-healthcare)",
-};
-
-export function NotificationsDropdown() {
- const { t } = useTranslation();
- const state = useAsync(() => fetchNotifications(), []);
- const { data: items } = state;
- const { isLoading } = useSectionFlags(state);
-
- // Optimistically clear on "mark all read"; revert if the request fails.
- const [cleared, setCleared] = useState(false);
- const visible = cleared ? [] : (items ?? []);
-
- async function onMarkAllRead() {
- setCleared(true);
- try {
- await markAllNotificationsRead();
- } catch {
- setCleared(false);
- }
- }
-
- const isEmpty = !isLoading && visible.length === 0;
- const hasUnread = visible.length > 0;
-
- return (
-
-
-
-
- {hasUnread && (
-
- )}
-
-
-
-
-
- {t("portal.notifications.title")}
-
- {hasUnread ? (
-
- {t("portal.notifications.count.new", { count: visible.length })}
-
- ) : isLoading ? (
-
- {t("portal.notifications.count.loading")}
-
- ) : (
-
- {t("portal.notifications.count.allRead")}
-
- )}
-
- {isLoading && (
-
-
-
-
-
-
- )}
- {isEmpty && (
-
- )}
- {!isLoading && !isEmpty && (
-
- {visible.map((item) => (
- -
-
-
-
{item.title}
-
{item.description}
-
{item.time}
-
-
- ))}
-
- )}
-
-
-
-
-
-
- );
-}
diff --git a/frontend/editor/src/portal/components/PortalChrome.tsx b/frontend/editor/src/portal/components/PortalChrome.tsx
index 49172784fc..1262c84b8e 100644
--- a/frontend/editor/src/portal/components/PortalChrome.tsx
+++ b/frontend/editor/src/portal/components/PortalChrome.tsx
@@ -4,7 +4,6 @@ import { ToolRegistryProvider } from "@app/contexts/ToolRegistryProvider";
import { ErrorBoundary } from "@portal/components/ErrorBoundary";
import { useUI } from "@portal/contexts/UIContext";
import { AppShell } from "@portal/components/AppShell";
-import { AssistantMount } from "@portal/components/AssistantMount";
import { SearchModal } from "@portal/components/SearchModal";
import { PortalSettingsHost } from "@portal/components/PortalSettingsHost";
import { ViewRouter } from "@portal/ViewRouter";
@@ -66,7 +65,6 @@ export function PortalChrome() {
-
>
diff --git a/frontend/editor/src/portal/components/Sidebar.css b/frontend/editor/src/portal/components/Sidebar.css
index 62d6226151..256b0708dd 100644
--- a/frontend/editor/src/portal/components/Sidebar.css
+++ b/frontend/editor/src/portal/components/Sidebar.css
@@ -77,37 +77,3 @@
flex-direction: column;
gap: 0.5rem;
}
-
-.portal-sidebar__usage {
- padding: 0.5rem 0.625rem;
- font-size: 0.75rem;
- color: var(--color-usage-text);
-}
-
-.portal-sidebar__usage-line {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 0.5rem;
-}
-
-.portal-sidebar__usage-value {
- color: var(--color-usage-value);
- font-weight: 500;
-}
-
-.portal-sidebar__plan {
- display: inline-flex;
- align-items: center;
- gap: 0.375rem;
- color: var(--color-text-2);
- font-weight: 500;
-}
-
-.portal-sidebar__plan-dot {
- width: 0.4375rem;
- height: 0.4375rem;
- border-radius: 50%;
- background: var(--color-green);
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-green) 30%, transparent);
-}
diff --git a/frontend/editor/src/portal/components/Sidebar.tsx b/frontend/editor/src/portal/components/Sidebar.tsx
index 9bf7c8c373..356acc5349 100644
--- a/frontend/editor/src/portal/components/Sidebar.tsx
+++ b/frontend/editor/src/portal/components/Sidebar.tsx
@@ -3,12 +3,9 @@ import { AppSwitch } from "@app/components/shared/AppSwitch";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
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 { LinkAccountFooterItem } from "@portal/components/LinkAccountFooterItem";
-import { useAsync } from "@portal/hooks/useAsync";
-import { fetchFleetStats, type FleetStats } from "@portal/api/fleetStats";
import { EDITOR_URL, EDITOR_IS_SAME_APP } from "@portal/auth/editorUrl";
import markLight from "@app/assets/brand/modern-logo/StirlingPDFLogoNoTextLight.svg";
import markDark from "@app/assets/brand/modern-logo/StirlingPDFLogoNoTextDark.svg";
@@ -21,36 +18,6 @@ import {
} from "@portal/components/sidebarGroups";
import "@portal/components/Sidebar.css";
-function UsageFooter() {
- const { tier } = useTier();
- const { t } = useTranslation();
- // Real 30-day processed-PDF count from the fleet-usage endpoint (the same
- // source as Home's status strip, so the two can't drift). null → "—".
- const { data, loading } = useAsync(() => fetchFleetStats(), []);
- const docs = loading ? undefined : (data?.pdfsProcessed ?? undefined);
-
- const planLabel =
- tier === "free"
- ? t("portal.shell.sidebar.planEditor", "Editor plan")
- : tier === "pro"
- ? t("portal.shell.sidebar.planProcessor", "Processor plan")
- : t("portal.shell.sidebar.planEnterprise", "Enterprise plan");
-
- return (
-
-
-
-
- {planLabel}
-
-
- {docs != null ? t("portal.shell.sidebar.docsCount", { docs }) : "—"}
-
-
-
- );
-}
-
export function Sidebar() {
const { activeView, setActiveView } = useView();
const { theme } = useTheme();
@@ -135,7 +102,6 @@ export function Sidebar() {
icon={}
onClick={() => openSettings()}
/>
-
);