Portal home: trim to onboarding + processor flow, secondary Set up buttons (#7111)

Simplifies the processor home page:

- Removes everything below the processor flow (processing status strip,
recent activity, quick actions, policy summary), leaving just the
onboarding hero and processor flow.
- Changes the policy "Set up" buttons (Security/Classification) from
primary to secondary variant.

<img width="2056" height="1047" alt="Screenshot 2026-07-20 at 7 27
56 PM"
src="https://github.com/user-attachments/assets/8625b274-b52a-47be-9052-80ac3d32dd93"
/>
This commit is contained in:
EthanHealy01
2026-07-22 20:08:45 +00:00
committed by GitHub
parent 3bf0019d7c
commit 718277a934
12 changed files with 3 additions and 928 deletions
@@ -6744,22 +6744,6 @@ title = "Invite teammates"
blurb = "{{active}} active · {{recommended}} recommended"
title = "Confirm your policies"
[portal.home.quickActions]
subtitle = "Top tasks for today"
title = "Quick actions"
[portal.home.quickActions.buildPipeline]
blurb = "3-step composer over the typed op library"
title = "Build a pipeline"
[portal.home.quickActions.connectSource]
blurb = "S3, agents, webhooks, watched folders"
title = "Connect a source"
[portal.home.quickActions.issueApiKey]
blurb = "Scoped key with rate limits and IP allowlist"
title = "Issue an API key"
[portal.infrastructure]
manageEditorDeployment = "Manage Editor deployment"
sectionsAriaLabel = "Infrastructure sections"
@@ -7531,35 +7515,9 @@ setUp = "Set up {{category}} policy"
[portal.policies.wizard.workflow]
description = "Choose what this policy does to every document it processes."
[portal.policySummary]
activeSummary = "{{active}} / {{total}} active"
noRule = "No rule enforced yet"
subtitle = "Standing automations every document passes through, regardless of which pipeline handles it."
title = "What runs on your PDFs"
[portal.policySummary.action]
comingSoon = "Upgrade to Enterprise"
configure = "Configure"
setUp = "Set up"
[portal.policySummary.column]
activeRule = "Active rule"
policy = "Policy"
status = "Status"
[portal.policySummary.empty]
description = "Once policies are configured, the categories appear here."
title = "No policies yet"
[portal.policySummary.state]
active = "Active"
off = "Off"
soon = "Soon"
[portal.processingStatus]
managePlan = "Manage plan"
volumeSuffix = "PDFs processed · last 30 days"
[portal.processorFlow]
footnote = "Counts are over the last 24 hours. Flow speed is illustrative."
liveBadge = "Live"
@@ -7878,14 +7836,6 @@ homeBody = "Committed volume pricing, org-wide SSO + SCIM + RBAC, 90-day immutab
homeCta = "Start Trial →"
homeHeadline = "Process millions of PDFs."
[portal.recentActivity]
title = "Recent activity"
viewAll = "View all"
[portal.recentActivity.empty]
description = "Pipeline runs, deploys and agent events will appear here."
title = "Nothing here yet"
[portal.search]
ariaLabel = "Search"
placeholder = "Search Stirling — endpoints, pipelines, docs…"
@@ -1,76 +0,0 @@
.portal-policysum__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
padding: 1rem 1.125rem 0.875rem;
border-bottom: 1px solid var(--color-border-light);
}
.portal-policysum__title {
margin: 0;
font-size: 0.9375rem;
font-weight: 600;
color: var(--color-text-1);
}
.portal-policysum__sub {
margin: 0.125rem 0 0;
font-size: 0.75rem;
color: var(--color-text-4);
max-width: 34rem;
}
.portal-policysum__cat {
display: flex;
align-items: center;
gap: 0.625rem;
}
.portal-policysum__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
flex: none;
border-radius: var(--radius-md);
background: var(--color-bg-subtle);
border: 1px solid var(--color-border-light);
font-size: 0.9375rem;
}
.portal-policysum__cat-text {
display: flex;
flex-direction: column;
min-width: 0;
}
.portal-policysum__cat-text strong {
font-size: 0.8125rem;
font-weight: 600;
color: var(--color-text-1);
}
.portal-policysum__cat-text span {
font-size: 0.6875rem;
color: var(--color-text-4);
}
.portal-policysum__rule {
font-size: 0.75rem;
color: var(--color-text-3);
}
.portal-policysum__loading {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem 1.125rem;
}
.portal-policysum__loading-row {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
@@ -1,48 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { http, HttpResponse, delay } from "msw";
import { PolicySummary } from "@portal/components/PolicySummary";
const meta: Meta<typeof PolicySummary> = {
title: "Portal/Home/PolicySummary",
component: PolicySummary,
parameters: { layout: "padded" },
decorators: [
(S) => (
<div style={{ maxWidth: "60rem" }}>
<S />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof PolicySummary>;
export const Default: Story = {};
export const Loading: Story = {
parameters: {
msw: {
handlers: [
http.get("/api/v1/policies", async () => {
await delay("infinite");
return HttpResponse.json({});
}),
],
},
},
};
export const Empty: Story = {
parameters: {
msw: {
handlers: [
http.get("/api/v1/policies", () =>
HttpResponse.json({
summary: { active: 0, paused: 0, categories: 0, docsEnforced: 0 },
catalogue: [],
}),
),
],
},
},
};
@@ -1,183 +0,0 @@
import { useTranslation } from "react-i18next";
import {
Button,
Card,
EmptyState,
Skeleton,
StatusBadge,
Table,
type TableColumn,
} from "@app/ui";
import { useView } from "@portal/contexts/ViewContext";
import { useAsync, useSectionFlags } from "@portal/hooks/useAsync";
import {
fetchPolicies,
type CatalogueEntry,
type PoliciesResponse,
} from "@portal/api/policies";
import { PolicyCategoryBadge } from "@portal/components/policies/PolicyCategoryIcon";
import "@portal/components/PolicySummary.css";
/**
* Each row's display state collapses a category's facts into one of three
* mutually-exclusive shapes:
*
* - `locked` — a coming-soon category; show a "Soon" affordance
* - `active` — a configured policy that's enabled; offer "Configure"
* - `off` — available but not set up (or paused); offer "Set up"
*/
type RowState = "locked" | "active" | "off";
interface PolicyRow {
entry: CatalogueEntry;
state: RowState;
}
const STATE_BADGE: Record<
RowState,
{ tone: "success" | "neutral" | "info"; labelKey: string }
> = {
active: { tone: "success", labelKey: "portal.policySummary.state.active" },
off: { tone: "neutral", labelKey: "portal.policySummary.state.off" },
locked: { tone: "info", labelKey: "portal.policySummary.state.soon" },
};
function toRow(entry: CatalogueEntry): PolicyRow {
if (entry.category.comingSoon) return { entry, state: "locked" };
const active = entry.policy?.state.status === "active";
return { entry, state: active ? "active" : "off" };
}
export function PolicySummary() {
const { t } = useTranslation();
const { setActiveView } = useView();
const state = useAsync<PoliciesResponse>(() => fetchPolicies(), []);
const { data } = state;
const { isLoading, isEmpty } = useSectionFlags(state);
const goToPolicies = () => setActiveView("policies");
const columns: TableColumn<PolicyRow>[] = [
{
key: "category",
header: t("portal.policySummary.column.policy"),
render: ({ entry }) => (
<div className="portal-policysum__cat">
<PolicyCategoryBadge category={entry.category} />
<div className="portal-policysum__cat-text">
<strong>{t(entry.category.label)}</strong>
<span>{t(entry.category.desc)}</span>
</div>
</div>
),
},
{
key: "status",
header: t("portal.policySummary.column.status"),
width: "7rem",
render: ({ state }) => {
const badge = STATE_BADGE[state];
return (
<StatusBadge tone={badge.tone} size="sm">
{t(badge.labelKey)}
</StatusBadge>
);
},
},
{
key: "rule",
header: t("portal.policySummary.column.activeRule"),
render: ({ entry, state }) => (
<span className="portal-policysum__rule">
{state === "active"
? t(entry.config.summary)
: t("portal.policySummary.noRule")}
</span>
),
},
{
key: "action",
header: "",
align: "right",
width: "9rem",
render: ({ state }) => {
if (state === "locked") {
return (
<Button size="sm" variant="tertiary" onClick={goToPolicies}>
{t("portal.policySummary.action.comingSoon")}
</Button>
);
}
return (
<Button
size="sm"
variant={state === "active" ? "tertiary" : "secondary"}
onClick={goToPolicies}
>
{state === "active"
? t("portal.policySummary.action.configure")
: t("portal.policySummary.action.setUp")}
</Button>
);
},
},
];
const rows: PolicyRow[] = data?.catalogue?.map(toRow) ?? [];
return (
<section
className="portal-policysum"
aria-label={t("portal.policySummary.title")}
>
<Card padding="none">
<header className="portal-policysum__head">
<div>
<h2 className="portal-policysum__title">
{t("portal.policySummary.title")}
</h2>
<p className="portal-policysum__sub">
{t("portal.policySummary.subtitle")}
</p>
</div>
{data?.summary && (
<StatusBadge tone="info" size="sm">
{t("portal.policySummary.activeSummary", {
active: data.summary.active,
total: data.summary.categories,
})}
</StatusBadge>
)}
</header>
{isLoading && (
<div className="portal-policysum__loading" aria-hidden>
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="portal-policysum__loading-row">
<Skeleton width="9rem" />
<Skeleton width="60%" height="0.625rem" />
</div>
))}
</div>
)}
{isEmpty && (
<EmptyState
size="compact"
title={t("portal.policySummary.empty.title")}
description={t("portal.policySummary.empty.description")}
/>
)}
{data && rows.length > 0 && (
<Table
columns={columns}
rows={rows}
rowKey={(r) => r.entry.category.id}
onRowClick={goToPolicies}
/>
)}
</Card>
</section>
);
}
@@ -1,43 +0,0 @@
/* Single inline row: plan + real processed-PDF volume + manage-plan shortcut. */
.portal-statusstrip--paid {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.625rem 0.875rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
}
.portal-statusstrip__plan {
display: inline-flex;
align-items: center;
gap: 0.4375rem;
font-size: 0.8125rem;
font-weight: 600;
color: var(--color-text-1);
}
.portal-statusstrip__dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
}
.portal-statusstrip__sep {
color: var(--color-text-5);
}
.portal-statusstrip__volume {
font-size: 0.8125rem;
color: var(--color-text-3);
}
.portal-statusstrip__volume strong {
color: var(--color-text-1);
font-weight: 600;
}
.portal-statusstrip__manage {
margin-left: auto;
}
@@ -1,36 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { http, HttpResponse, delay } from "msw";
import { ProcessingStatusStrip } from "@portal/components/ProcessingStatusStrip";
const meta: Meta<typeof ProcessingStatusStrip> = {
title: "Portal/Home/ProcessingStatusStrip",
component: ProcessingStatusStrip,
parameters: { layout: "padded" },
decorators: [
(S) => (
<div style={{ maxWidth: "60rem" }}>
<S />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof ProcessingStatusStrip>;
/** Switch the Tier toolbar to compare the free meter vs the paid plan row. */
export const Default: Story = {};
/** Free tier pushed near its cap so the meter and upgrade nudge turn amber. */
export const FreeNearCap: Story = {
globals: { tier: "free" },
parameters: {
msw: {
handlers: [
http.get("/v1/home/kpis", async () => {
await delay(80);
return HttpResponse.json([{ value: "472 / 500" }]);
}),
],
},
},
};
@@ -1,52 +0,0 @@
import { useTranslation } from "react-i18next";
import { Button, Skeleton } from "@app/ui";
import { TIER_INFO, useTier } from "@portal/contexts/TierContext";
import { useView } from "@portal/contexts/ViewContext";
import { useAsync } from "@portal/hooks/useAsync";
import { fetchFleetStats, type FleetStats } from "@portal/api/fleetStats";
import "@portal/components/ProcessingStatusStrip.css";
/**
* A thin one-line header above the home content: current plan + the real 30-day
* processed-PDF volume (from the fleet-usage endpoint), with a shortcut to the
* Usage page. Renders "—" while loading or when the backend can't compute the
* figure (e.g. EE auditing disabled) — never a fabricated number.
*/
export function ProcessingStatusStrip() {
const { t } = useTranslation();
const { tier } = useTier();
const { setActiveView } = useView();
const { data, loading } = useAsync<FleetStats>(() => fetchFleetStats(), []);
return (
<div className="portal-statusstrip portal-statusstrip--paid">
<span className="portal-statusstrip__plan">
<span
className="portal-statusstrip__dot"
style={{ background: TIER_INFO[tier].dotColor }}
aria-hidden
/>
{t(TIER_INFO[tier].labelKey)}
</span>
<span className="portal-statusstrip__sep" aria-hidden>
·
</span>
<span className="portal-statusstrip__volume">
{loading ? (
<Skeleton width="3rem" height="0.875rem" />
) : (
<strong>{data?.pdfsProcessed?.toLocaleString() ?? "—"}</strong>
)}{" "}
{t("portal.processingStatus.volumeSuffix")}
</span>
<Button
size="sm"
variant="tertiary"
className="portal-statusstrip__manage"
onClick={() => setActiveView("usage")}
>
{t("portal.processingStatus.managePlan")}
</Button>
</div>
);
}
@@ -1,138 +0,0 @@
.portal-activity {
display: flex;
flex-direction: column;
}
.portal-activity__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.875rem 1rem;
border-bottom: 1px solid var(--color-border-light);
}
.portal-activity__title {
margin: 0;
font-size: 0.9375rem;
font-weight: 600;
color: var(--color-text-1);
}
.portal-activity__more {
font-size: 0.75rem;
font-weight: 500;
color: var(--color-blue);
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
transition: background var(--motion-fast);
}
.portal-activity__more:hover {
background: var(--color-bg-hover);
}
.portal-activity__list {
list-style: none;
margin: 0;
padding: 0;
}
.portal-activity__item {
display: flex;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--color-border-light);
transition: background var(--motion-fast);
}
.portal-activity__item:last-child {
border-bottom: none;
}
.portal-activity__item:hover {
background: var(--color-bg-hover);
}
.portal-activity__rail {
width: 0.1875rem;
border-radius: var(--radius-pill);
flex-shrink: 0;
}
.portal-activity__body {
flex: 1 1 auto;
min-width: 0;
}
.portal-activity__row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.5rem;
}
.portal-activity__action {
font-size: 0.8125rem;
font-weight: 600;
color: var(--color-text-1);
}
.portal-activity__time {
font-size: 0.6875rem;
color: var(--color-text-5);
}
.portal-activity__subject {
font-size: 0.75rem;
color: var(--color-text-3);
margin-top: 0.0625rem;
}
.portal-activity__detail-row {
margin-top: 0.375rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.portal-activity__detail {
font-size: 0.75rem;
color: var(--color-text-4);
font-family: var(--font-mono);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.portal-activity__item--skeleton {
cursor: default;
}
.portal-activity__item--skeleton .portal-activity__rail {
background: var(--color-border-light);
}
.portal-activity__skel {
height: 0.75rem;
border-radius: var(--radius-pill);
background: linear-gradient(
90deg,
var(--color-bg-muted) 0%,
var(--color-bg-hover) 50%,
var(--color-bg-muted) 100%
);
background-size: 200% 100%;
animation: shimmer 1.4s linear infinite;
margin-bottom: 0.375rem;
}
.portal-activity__skel--md {
width: 70%;
}
.portal-activity__skel--sm {
width: 50%;
height: 0.625rem;
}
@@ -1,41 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { http, HttpResponse, delay } from "msw";
import { RecentActivity } from "@portal/components/RecentActivity";
const meta: Meta<typeof RecentActivity> = {
title: "Portal/Home/RecentActivity",
component: RecentActivity,
parameters: { layout: "padded" },
decorators: [
(S) => (
<div style={{ maxWidth: "40rem" }}>
<S />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof RecentActivity>;
export const Default: Story = {};
export const Loading: Story = {
parameters: {
msw: {
handlers: [
http.get("/v1/activity", async () => {
await delay("infinite");
return HttpResponse.json([]);
}),
],
},
},
};
export const Empty: Story = {
parameters: {
msw: {
handlers: [http.get("/v1/activity", () => HttpResponse.json([]))],
},
},
};
@@ -1,127 +0,0 @@
import { useTranslation } from "react-i18next";
import { Button, Card, EmptyState, Skeleton, StatusBadge } from "@app/ui";
import { useTier } from "@portal/contexts/TierContext";
import { useView } from "@portal/contexts/ViewContext";
import { useAsync } from "@portal/hooks/useAsync";
import {
fetchAuditLog,
type AuditLogResponse,
type AuditStatus,
} from "@portal/api/infrastructure";
import {
AUDIT_CAT_LABEL,
AUDIT_STATUS_LABEL,
AUDIT_TONE,
} from "@portal/components/infrastructure/infraFormat";
import "@portal/components/RecentActivity.css";
/** Rail colour by outcome — mirrors the audit status tones. */
const STATUS_COLOUR: Record<AuditStatus, string> = {
success: "var(--color-green)",
warning: "var(--color-amber)",
danger: "var(--color-red)",
info: "var(--color-blue)",
};
/** How many of the most recent audit events the home card shows. */
const MAX_EVENTS = 6;
/**
* The home "Recent activity" card, backed by the real audit log (the same
* endpoint the Infrastructure → Audit tab reads). Shows the latest few events;
* "View all" jumps to the full audit view. Failures and an empty log both fall
* through to the empty state — never an error banner on the dashboard.
*/
export function RecentActivity() {
const { t } = useTranslation();
const { tier } = useTier();
const { setActiveView } = useView();
const { data, loading } = useAsync<AuditLogResponse>(
() => fetchAuditLog(tier),
[tier],
);
const events = data?.events.slice(0, MAX_EVENTS) ?? [];
const isLoading = loading && data === null;
const isEmpty = !isLoading && events.length === 0;
return (
<Card
padding="none"
className="portal-activity"
aria-label={t("portal.recentActivity.title")}
>
<header className="portal-activity__head">
<h2 className="portal-activity__title">
{t("portal.recentActivity.title")}
</h2>
<Button
variant="quiet"
type="button"
className="portal-activity__more"
onClick={() => setActiveView("infrastructure")}
>
{t("portal.recentActivity.viewAll")}
</Button>
</header>
{isLoading && (
<ul className="portal-activity__list" aria-hidden>
{Array.from({ length: 5 }).map((_, i) => (
<li
key={i}
className="portal-activity__item portal-activity__item--skeleton"
>
<span className="portal-activity__rail" />
<div className="portal-activity__body">
<Skeleton width="70%" />
<Skeleton width="50%" height="0.625rem" />
</div>
</li>
))}
</ul>
)}
{isEmpty && (
<EmptyState
size="compact"
title={t("portal.recentActivity.empty.title")}
description={t("portal.recentActivity.empty.description")}
/>
)}
{events.length > 0 && (
<ol className="portal-activity__list">
{events.map((event) => (
<li key={event.id} className="portal-activity__item">
<span
className="portal-activity__rail"
style={{ background: STATUS_COLOUR[event.status] }}
aria-hidden
/>
<div className="portal-activity__body">
<div className="portal-activity__row">
<span className="portal-activity__action">
{event.action}
</span>
<span className="portal-activity__time">
{event.timestamp}
</span>
</div>
<div className="portal-activity__subject">{event.target}</div>
<div className="portal-activity__detail-row">
<span className="portal-activity__detail">
{t(AUDIT_CAT_LABEL[event.category])} · {event.actor}
</span>
<StatusBadge tone={AUDIT_TONE[event.status]} size="sm">
{t(AUDIT_STATUS_LABEL[event.status])}
</StatusBadge>
</div>
</div>
</li>
))}
</ol>
)}
</Card>
);
}
@@ -59,8 +59,10 @@ export function FlowPolicies({
) : policy.state === "off" ? (
<Button
size="sm"
px="sm"
py="xs"
variant="primary"
fontSize="xs"
variant="secondary"
className="portal-pf__setup"
onClick={() => onSetup(policy.key)}
>
-133
View File
@@ -1,133 +1,9 @@
import { useTranslation } from "react-i18next";
import { Button, Card } from "@app/ui";
import { useTier } from "@portal/contexts/TierContext";
import { useView, type ViewId } from "@portal/contexts/ViewContext";
import { HomeHero } from "@portal/components/HomeHero";
import { HomeGreeting } from "@portal/components/HomeGreeting";
import { ProcessorFlow } from "@portal/components/ProcessorFlow";
import { RecentActivity } from "@portal/components/RecentActivity";
import { ProcessingStatusStrip } from "@portal/components/ProcessingStatusStrip";
import { PolicySummary } from "@portal/components/PolicySummary";
import "@portal/views/Home.css";
/* ──────────────────────────────────────────────────────────────────────── */
/* Quick actions card */
/* ──────────────────────────────────────────────────────────────────────── */
/** A stroke icon used inside the quick-action badge (replaces the old text
glyphs ⌃ ⇢ ⚙, which rendered off-style vs the portal's icon set). */
function QuickIcon({ d }: { d: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d={d} />
</svg>
);
}
/** Rows for the Quick Actions list. Each `view` navigates the portal. */
const QUICK_ACTIONS: Array<{
key: string;
iconD: string;
bg: string;
fg: string;
view: ViewId;
}> = [
{
key: "buildPipeline",
iconD:
"M6 4a2 2 0 100 4 2 2 0 000-4zM18 16a2 2 0 100 4 2 2 0 000-4zM6 8v6a4 4 0 004 4h4",
bg: "var(--color-purple-light)",
fg: "var(--color-purple)",
view: "pipelines",
},
{
key: "connectSource",
iconD:
"M10 13a5 5 0 007 0l3-3a5 5 0 00-7-7l-1 1M14 11a5 5 0 00-7 0l-3 3a5 5 0 007 7l1-1",
bg: "var(--color-green-light)",
fg: "var(--color-green-dark)",
view: "sources",
},
{
key: "issueApiKey",
iconD:
"M2.6 17.4A2 2 0 002 18.8V21a1 1 0 001 1h3a1 1 0 001-1v-1a1 1 0 011-1h1a1 1 0 001-1v-1a1 1 0 011-1h.2a2 2 0 001.4-.6l.8-.8a6.5 6.5 0 10-4-4z M16.5 7.5 h.01",
bg: "var(--color-amber-light)",
fg: "var(--color-amber-dark)",
view: "infrastructure",
},
];
function QuickActions() {
const { t } = useTranslation();
const { setActiveView } = useView();
return (
<Card padding="loose" className="portal-home__quick">
<div className="portal-home__quick-head">
<h2 className="portal-home__quick-title">
{t("portal.home.quickActions.title")}
</h2>
<span className="portal-home__quick-sub">
{t("portal.home.quickActions.subtitle")}
</span>
</div>
<div className="portal-home__quick-list">
{QUICK_ACTIONS.map((action) => (
<Button
key={action.key}
variant="quiet"
justify="start"
fullWidth
px="sm"
py="sm"
type="button"
className="portal-home__quick-row"
onClick={() => setActiveView(action.view)}
leftSection={
<span
className="portal-home__quick-icon"
style={{ background: action.bg, color: action.fg }}
aria-hidden
>
<QuickIcon d={action.iconD} />
</span>
}
rightSection={
<span className="portal-home__quick-arrow" aria-hidden>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M9 6l6 6-6 6" />
</svg>
</span>
}
>
<span className="portal-home__quick-text">
<strong>
{t(`portal.home.quickActions.${action.key}.title`)}
</strong>
<span>{t(`portal.home.quickActions.${action.key}.blurb`)}</span>
</span>
</Button>
))}
</div>
</Card>
);
}
/* ──────────────────────────────────────────────────────────────────────── */
/* Home view */
/* ──────────────────────────────────────────────────────────────────────── */
@@ -144,15 +20,6 @@ export function Home() {
deal is underway (a bolt-on to any tier), otherwise the setup checklist. */}
<HomeHero tier={tier} />
<ProcessorFlow />
{/* One unified layout across tiers: real processed-PDF volume, real audit
activity, quick actions, and the standing-policy summary. */}
<ProcessingStatusStrip />
<div className="portal-home__row">
<RecentActivity />
<QuickActions />
</div>
<PolicySummary />
</div>
);
}