mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Remove unused portal UI (#6949)
Removes some cluttered/unused UI from the portal: - Search bar in the header - The top bar entirely (breadcrumb, notification bell, plan switcher, user menu) - The plan/usage indicator in the sidebar footer - The floating assistant badge UI only. Where a component isn't deleted it's just no longer rendered, so anything here is easy to restore.
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="portal-shell">
|
||||
<Sidebar />
|
||||
<div className="portal-shell__main">
|
||||
<Header />
|
||||
<main className="portal-shell__view">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Header } from "@portal/components/Header";
|
||||
|
||||
const meta: Meta<typeof Header> = {
|
||||
title: "Portal/Shell/Header",
|
||||
component: Header,
|
||||
parameters: { layout: "fullscreen" },
|
||||
decorators: [
|
||||
(S) => (
|
||||
<div style={{ background: "var(--color-bg)" }}>
|
||||
<S />
|
||||
<div style={{ padding: "1.5rem", color: "var(--color-text-3)" }}>
|
||||
Page body would render below the header.
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Header>;
|
||||
|
||||
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" } };
|
||||
@@ -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 (
|
||||
<ActionIcon
|
||||
type="button"
|
||||
variant="quiet"
|
||||
className="portal-header__icon-btn"
|
||||
onClick={toggle}
|
||||
aria-label={
|
||||
theme === "light"
|
||||
? t("portal.shell.header.switchToDark")
|
||||
: t("portal.shell.header.switchToLight")
|
||||
}
|
||||
title={
|
||||
theme === "light"
|
||||
? t("portal.shell.header.darkMode")
|
||||
: t("portal.shell.header.lightMode")
|
||||
}
|
||||
>
|
||||
{theme === "light" ? <MoonIcon size={16} /> : <SunIcon size={16} />}
|
||||
</ActionIcon>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Dropdown.Root align="end">
|
||||
<Dropdown.Trigger>
|
||||
<Button
|
||||
type="button"
|
||||
variant="quiet"
|
||||
justify="start"
|
||||
className="portal-header__tier-btn"
|
||||
leftSection={
|
||||
<span
|
||||
className="portal-header__tier-dot"
|
||||
style={{ background: info.dotColor }}
|
||||
aria-hidden
|
||||
/>
|
||||
}
|
||||
rightSection={<ChevronDownIcon size={14} />}
|
||||
>
|
||||
<span className="portal-header__tier-label">{info.label}</span>
|
||||
</Button>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Menu width="12rem">
|
||||
{(Object.keys(TIER_INFO) as Tier[]).map((id) => (
|
||||
<Dropdown.Item
|
||||
key={id}
|
||||
active={tier === id}
|
||||
onSelect={() => setTier(id)}
|
||||
leading={
|
||||
<span
|
||||
className="portal-header__tier-dot"
|
||||
style={{ background: TIER_INFO[id].dotColor }}
|
||||
aria-hidden
|
||||
/>
|
||||
}
|
||||
>
|
||||
{TIER_INFO[id].label}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function UserMenu() {
|
||||
const { t } = useTranslation();
|
||||
const { displayName, signOut } = useAuth();
|
||||
const name =
|
||||
displayName ?? t("portal.shell.header.accountFallback", "Account");
|
||||
return (
|
||||
<Dropdown.Root align="end">
|
||||
<Dropdown.Trigger>
|
||||
<ActionIcon
|
||||
type="button"
|
||||
variant="quiet"
|
||||
className="portal-header__user"
|
||||
aria-label={t("portal.shell.header.accountMenu", "Account menu")}
|
||||
title={name}
|
||||
>
|
||||
<Avatar name={name} size="md" tone="blue" />
|
||||
</ActionIcon>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Menu width="12rem">
|
||||
<Dropdown.Item disabled>{name}</Dropdown.Item>
|
||||
<Dropdown.Item onSelect={() => void signOut()}>
|
||||
{t("portal.shell.header.signOut", "Sign out")}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export function Header() {
|
||||
const { activeView } = useView();
|
||||
const { openSearch } = useUI();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<header className="portal-header">
|
||||
<div className="portal-header__left">
|
||||
<span className="portal-header__breadcrumb">
|
||||
{t(`portal.nav.${activeView}`)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="quiet"
|
||||
className="portal-header__search"
|
||||
aria-label={t("portal.shell.header.search")}
|
||||
onClick={openSearch}
|
||||
>
|
||||
<SearchIcon size={14} />
|
||||
<span className="portal-header__search-placeholder">
|
||||
{t("portal.shell.header.searchPlaceholder")}
|
||||
</span>
|
||||
<span className="portal-header__search-kbd" aria-hidden>
|
||||
⌘K
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<div className="portal-header__right">
|
||||
<ThemeToggle />
|
||||
<NotificationsDropdown />
|
||||
<TierSwitcher />
|
||||
<UserMenu />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -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<typeof NotificationsDropdown> = {
|
||||
title: "Portal/Header/NotificationsDropdown",
|
||||
component: NotificationsDropdown,
|
||||
parameters: { layout: "centered" },
|
||||
decorators: [
|
||||
(S) => (
|
||||
<div
|
||||
style={{
|
||||
padding: "4rem 8rem",
|
||||
minHeight: "30rem",
|
||||
background: "var(--color-bg)",
|
||||
}}
|
||||
>
|
||||
<S />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof NotificationsDropdown>;
|
||||
|
||||
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 }),
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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<NotificationCategory, string> = {
|
||||
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<Notification[]>(() => 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 (
|
||||
<Dropdown.Root align="end">
|
||||
<Dropdown.Trigger>
|
||||
<ActionIcon
|
||||
type="button"
|
||||
variant="quiet"
|
||||
className="portal-header__icon-btn portal-header__icon-btn--badge"
|
||||
aria-label={
|
||||
hasUnread
|
||||
? t("portal.notifications.ariaLabel.unread", {
|
||||
count: visible.length,
|
||||
})
|
||||
: t("portal.notifications.ariaLabel.none")
|
||||
}
|
||||
>
|
||||
<BellIcon size={16} />
|
||||
{hasUnread && (
|
||||
<span className="portal-header__bell-dot" aria-hidden />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Menu width="22.5rem" className="portal-notif__menu">
|
||||
<div className="portal-notif__header">
|
||||
<span className="portal-notif__title">
|
||||
{t("portal.notifications.title")}
|
||||
</span>
|
||||
{hasUnread ? (
|
||||
<span className="portal-notif__count">
|
||||
{t("portal.notifications.count.new", { count: visible.length })}
|
||||
</span>
|
||||
) : isLoading ? (
|
||||
<span className="portal-notif__count portal-notif__count--quiet">
|
||||
{t("portal.notifications.count.loading")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="portal-notif__count portal-notif__count--quiet">
|
||||
{t("portal.notifications.count.allRead")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="portal-notif__loading">
|
||||
<Skeleton height="0.875rem" />
|
||||
<Skeleton height="0.6875rem" width="60%" />
|
||||
<Skeleton height="0.875rem" />
|
||||
<Skeleton height="0.6875rem" width="60%" />
|
||||
</div>
|
||||
)}
|
||||
{isEmpty && (
|
||||
<EmptyState
|
||||
size="compact"
|
||||
title={t("portal.notifications.empty.title")}
|
||||
description={t("portal.notifications.empty.description")}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && !isEmpty && (
|
||||
<ul className="portal-notif__list">
|
||||
{visible.map((item) => (
|
||||
<li key={item.id} className="portal-notif__item">
|
||||
<span
|
||||
className="portal-notif__dot"
|
||||
style={{ background: CATEGORY_COLOUR[item.category] }}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="portal-notif__body">
|
||||
<div className="portal-notif__item-title">{item.title}</div>
|
||||
<div className="portal-notif__desc">{item.description}</div>
|
||||
<div className="portal-notif__time">{item.time}</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<div className="portal-notif__footer">
|
||||
<Button
|
||||
type="button"
|
||||
variant="quiet"
|
||||
className="portal-notif__action"
|
||||
onClick={onMarkAllRead}
|
||||
disabled={!hasUnread}
|
||||
>
|
||||
{t("portal.notifications.markAllRead")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="quiet"
|
||||
className="portal-notif__action"
|
||||
>
|
||||
{t("portal.notifications.viewAll")}
|
||||
</Button>
|
||||
</div>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown.Root>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
<RoutedContent />
|
||||
</AppShell>
|
||||
</ToolRegistryProvider>
|
||||
<AssistantMount />
|
||||
<SearchModal />
|
||||
<PortalSettingsHost />
|
||||
</>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<FleetStats>(() => 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 (
|
||||
<div className="portal-sidebar__usage">
|
||||
<div className="portal-sidebar__usage-line">
|
||||
<span className="portal-sidebar__plan">
|
||||
<span className="portal-sidebar__plan-dot" aria-hidden />
|
||||
{planLabel}
|
||||
</span>
|
||||
<span className="portal-sidebar__usage-value">
|
||||
{docs != null ? t("portal.shell.sidebar.docsCount", { docs }) : "—"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const { activeView, setActiveView } = useView();
|
||||
const { theme } = useTheme();
|
||||
@@ -135,7 +102,6 @@ export function Sidebar() {
|
||||
icon={<SettingsIcon />}
|
||||
onClick={() => openSettings()}
|
||||
/>
|
||||
<UsageFooter />
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user