Add Trust Center hero and page headers

Build the Trust Center landing hero and the nav page headers on a shared
HeaderBand shell. The home page loads its own query and feeds the Hero
(org headline, description, and contact info) via a colocated fragment;
the Documents, Subprocessors, Updates, and Data Requests pages use a
PageHeader (size-7 title, optional count, inline actions). Switch the
layout body to the grey sand-2 canvas so the white header bands read as
surfaces on top.

Split the header UI into focused components rather than one
over-configurable Hero: HeaderBand owns the band, Hero the landing
content, PageHeader the nav-page content. Add a hostname helper under
lib/url and standardize the 1024px container on the max-w-5xl token.

The Documents/Subprocessors filter/search/tabs toolbars and item counts
are deferred until the matching v2 components and queries exist.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-26 22:33:03 +02:00
parent aa20bc4484
commit fe8ac00c70
20 changed files with 557 additions and 23 deletions

View File

@@ -12,19 +12,20 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { Heading } from "@probo/ui/src/v2/typography/Heading";
import { useLocation } from "react-router";
import type { PropsWithChildren } from "react";
// Temporary stub for the Trust Center sections so the top-bar nav links resolve
// and the active state renders. Real pages replace these per section.
export default function PlaceholderPage() {
const { pathname } = useLocation();
const segment = pathname.replace(/^\//, "").split("/")[0] ?? "";
const title = segment.charAt(0).toUpperCase() + segment.slice(1);
import { headerBand } from "./variants";
export type HeaderBandProps = PropsWithChildren;
// Pure layout shell: the white band + centered content column. Consumers (Hero,
// PageHeader) supply a single content column and own its internal spacing.
export function HeaderBand({ children }: HeaderBandProps) {
const { band, inner } = headerBand();
return (
<main className="mx-auto w-full max-w-[1024px] px-8 py-10">
<Heading>{title}</Heading>
</main>
<header className={band()}>
<div className={inner()}>{children}</div>
</header>
);
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { tv } from "tailwind-variants/lite";
// The white header band that sits over the grey body: full-width surface with a
// bottom border and a centered max-w-5xl content column. Shared by the landing
// Hero and the page headers so the band is defined once.
export const headerBand = tv({
slots: {
band: "flex w-full flex-col items-center border-b border-sand-6 bg-sand-1 px-8 py-8",
inner: "w-full max-w-5xl",
},
});

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { Heading } from "@probo/ui/src/v2/typography/Heading";
import { Text } from "@probo/ui/src/v2/typography/Text";
import type { ReactNode } from "react";
import { HeaderBand } from "#/components/HeaderBand/HeaderBand";
import { hero } from "./variants";
export interface HeroProps {
title: ReactNode;
description?: ReactNode;
children?: ReactNode;
}
// Landing hero (home): a size-8 title (+ optional description) in the shared
// white band, plus an optional bottom slot for page-specific content (the org
// contact row). The slot content owns its own divider/spacing so it disappears
// cleanly when empty.
export function Hero({ title, description, children }: HeroProps) {
const { content, section } = hero();
return (
<HeaderBand>
<div className={content()}>
<div className={section()}>
<Heading level={1} size={8} weight="medium" highContrast>
{title}
</Heading>
{description != null && description !== "" && (
<Text size={2} color="neutral">
{description}
</Text>
)}
</div>
{children}
</div>
</HeaderBand>
);
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { HeadingSkeleton } from "@probo/ui/src/v2/typography/HeadingSkeleton";
import { TextSkeleton } from "@probo/ui/src/v2/typography/TextSkeleton";
import { HeaderBand } from "#/components/HeaderBand/HeaderBand";
import { hero, organizationContactInfo } from "./variants";
const CONTACT_ITEM_KEYS = ["website", "email", "location"] as const;
// Loading placeholder paired with Hero: reuses the same layout slots with
// skeleton primitives. Imports no Relay, so it renders instantly.
export function HeroSkeleton() {
const { content, section } = hero();
const { root, item } = organizationContactInfo();
return (
<HeaderBand>
<div className={content()}>
<div className={section()}>
<HeadingSkeleton size={8} className="w-96" />
<TextSkeleton size={2} className="w-full max-w-2xl" />
</div>
<div className={root()}>
{CONTACT_ITEM_KEYS.map(key => (
<div key={key} className={item()}>
<TextSkeleton size={2} className="w-28" />
</div>
))}
</div>
</div>
</HeaderBand>
);
}

View File

@@ -0,0 +1,85 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { EnvelopeIcon, GlobeSimpleIcon, MapPinSimpleIcon } from "@phosphor-icons/react";
import { Text } from "@probo/ui/src/v2/typography/Text";
import { graphql, useFragment } from "react-relay";
import { hostnameOf } from "#/lib/url/hostname";
import type { OrganizationContactInfo_organization$key } from "./__generated__/OrganizationContactInfo_organization.graphql";
import { organizationContactInfo } from "./variants";
const organizationContactInfoFragment = graphql`
fragment OrganizationContactInfo_organization on Organization {
websiteUrl
email
headquarterAddress
}
`;
interface OrganizationContactInfoProps {
organizationKey: OrganizationContactInfo_organization$key;
}
// Organization contact details (website, email, HQ) rendered as an icon + label
// row. Owns its fragment so it can be reused wherever the org is in scope.
export function OrganizationContactInfo({ organizationKey }: OrganizationContactInfoProps) {
const organization = useFragment(organizationContactInfoFragment, organizationKey);
const hasWebsite = organization.websiteUrl != null && organization.websiteUrl !== "";
const hasEmail = organization.email != null && organization.email !== "";
const hasAddress = organization.headquarterAddress != null && organization.headquarterAddress !== "";
// Nothing to show — render no row (and therefore no divider) at all.
if (!hasWebsite && !hasEmail && !hasAddress) {
return null;
}
const { root, item, link } = organizationContactInfo();
return (
<div className={root()}>
{hasWebsite && (
<a
className={link()}
href={organization.websiteUrl}
target="_blank"
rel="noopener noreferrer"
>
<GlobeSimpleIcon />
<Text size={2} color="neutral">
{hostnameOf(organization.websiteUrl)}
</Text>
</a>
)}
{hasEmail && (
<a className={link()} href={`mailto:${organization.email}`}>
<EnvelopeIcon />
<Text size={2} color="neutral">
{organization.email}
</Text>
</a>
)}
{hasAddress && (
<div className={item()}>
<MapPinSimpleIcon />
<Text size={2} color="neutral">
{organization.headquarterAddress}
</Text>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { tv } from "tailwind-variants/lite";
// Landing hero content rendered inside the shared HeaderBand: a centered
// title/description section above an optional bottom slot (the contact row).
// Slots are shared by the live Hero and its skeleton.
export const hero = tv({
slots: {
content: "flex w-full flex-col gap-10",
section: "flex w-full flex-col gap-4",
},
});
// Organization contact block: a horizontal row of icon + label items, with a
// top divider so it reads as the hero's bottom section. Self-contained so the
// divider only appears when there is contact info to show.
export const organizationContactInfo = tv({
slots: {
// Only a top divider gap; the band's py-8 provides the bottom spacing.
root: "flex w-full items-center gap-6 border-t border-sand-6 pt-4",
item: "flex items-center gap-2 text-sand-11 [&_svg]:size-5",
link: "flex items-center gap-2 text-sand-11 hover:underline [&_svg]:size-5",
},
});

View File

@@ -0,0 +1,51 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { Heading } from "@probo/ui/src/v2/typography/Heading";
import type { ReactNode } from "react";
import { HeaderBand } from "#/components/HeaderBand/HeaderBand";
import { pageHeader } from "./variants";
export interface PageHeaderProps {
title: ReactNode;
// Optional muted item count appended to the title (e.g. "Documents (3)").
count?: number;
// Right-aligned controls on the title line (e.g. a "New Request" button).
actions?: ReactNode;
// Optional toolbar (tabs / filters / search) rendered below the title.
children?: ReactNode;
}
// Page header for the Trust Center nav pages: a size-7 title in the shared white
// band, with an optional count, inline actions, and a toolbar slot below.
export function PageHeader({ title, count, actions, children }: PageHeaderProps) {
const { content, titleRow, count: countSlot } = pageHeader();
return (
<HeaderBand>
<div className={content()}>
<div className={titleRow()}>
<Heading level={1} size={7} weight="medium" highContrast>
{title}
{count != null && <span className={countSlot()}>{` (${count})`}</span>}
</Heading>
{actions}
</div>
{children}
</div>
</HeaderBand>
);
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { tv } from "tailwind-variants/lite";
// Page header content rendered inside the shared HeaderBand: a size-7 title row
// (with optional count and right-aligned actions) above an optional toolbar.
export const pageHeader = tv({
slots: {
content: "flex w-full flex-col gap-2",
titleRow: "flex w-full items-center justify-between gap-4",
// Muted item count appended to the title (e.g. "Documents (3)").
count: "font-light text-sand-8",
},
});

View File

@@ -19,7 +19,7 @@ import { tv } from "tailwind-variants/lite";
export const topBar = tv({
slots: {
bar: "flex h-14 items-center bg-sand-1 px-8",
inner: "mx-auto flex w-full max-w-[1024px] items-center gap-10",
inner: "mx-auto flex w-full max-w-5xl items-center gap-10",
brand: "flex items-center gap-2",
logo: "shrink-0",
spacer: "h-px flex-1",

View File

@@ -0,0 +1,23 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
// Show only the hostname for a URL (e.g. "https://blaxel.ai/x" -> "blaxel.ai"),
// falling back to the raw value when it cannot be parsed.
export function hostnameOf(url: string): string {
try {
return new URL(url).host;
} catch {
return url;
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { PageHeader } from "#/components/PageHeader/PageHeader";
// Toolbar (All/Public/Private tabs, framework filter, search) and the document
// count are deferred until the v2 Tabs/Select/TextField components exist.
export default function DocumentsPage() {
return <PageHeader title="Documents" />;
}

View File

@@ -12,10 +12,37 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
export default function HomePage() {
import type { PreloadedQuery } from "react-relay";
import { graphql, usePreloadedQuery } from "react-relay";
import { Hero } from "#/components/Hero/Hero";
import { OrganizationContactInfo } from "#/components/Hero/OrganizationContactInfo";
import type { HomePageQuery } from "./__generated__/HomePageQuery.graphql";
export const homePageQuery = graphql`
query HomePageQuery {
currentTrustCenter @required(action: THROW) {
organization {
name
description
...OrganizationContactInfo_organization
}
}
}
`;
interface HomePageProps {
queryRef: PreloadedQuery<HomePageQuery>;
}
export function HomePage({ queryRef }: HomePageProps) {
const data = usePreloadedQuery<HomePageQuery>(homePageQuery, queryRef);
const { organization } = data.currentTrustCenter;
return (
<main className="flex min-h-screen items-center justify-center">
<h1 className="text-6 font-bold text-sand-12">Compliance Portal</h1>
</main>
<Hero title={`Trust at ${organization.name}.`} description={organization.description}>
<OrganizationContactInfo organizationKey={organization} />
</Hero>
);
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { useEffect } from "react";
import { useQueryLoader } from "react-relay";
import type { HomePageQuery } from "./__generated__/HomePageQuery.graphql";
import { HomePage, homePageQuery } from "./HomePage";
import { HomePageSkeleton } from "./HomePageSkeleton";
export default function HomePageLoader() {
const [queryRef, loadQuery] = useQueryLoader<HomePageQuery>(homePageQuery);
useEffect(() => {
loadQuery({});
}, [loadQuery]);
if (!queryRef) {
return <HomePageSkeleton />;
}
return <HomePage queryRef={queryRef} />;
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { HeroSkeleton } from "#/components/Hero/HeroSkeleton";
export function HomePageSkeleton() {
return <HeroSkeleton />;
}

View File

@@ -34,7 +34,7 @@ export function MainLayout({ queryRef }: MainLayoutProps) {
const data = usePreloadedQuery<MainLayoutQuery>(mainLayoutQuery, queryRef);
return (
<div className="min-h-screen bg-sand-1">
<div className="min-h-screen bg-sand-2">
<TopBar queryKey={data} />
<Outlet />
</div>

View File

@@ -16,7 +16,7 @@ import { TopBarSkeleton } from "#/components/TopBar/TopBarSkeleton";
export function MainLayoutSkeleton() {
return (
<div className="min-h-screen bg-sand-1">
<div className="min-h-screen bg-sand-2">
<TopBarSkeleton />
</div>
);

View File

@@ -0,0 +1,31 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { PlusIcon } from "@phosphor-icons/react";
import { Button } from "@probo/ui/src/v2/Button/Button";
import { PageHeader } from "#/components/PageHeader/PageHeader";
export default function RequestsPage() {
return (
<PageHeader
title="Data Requests"
actions={(
<Button variant="soft" color="neutral" highContrast iconStart={<PlusIcon />}>
New Request
</Button>
)}
/>
);
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { PageHeader } from "#/components/PageHeader/PageHeader";
// Toolbar (category/region filters, search, Download CSV) and the subprocessor
// count are deferred until the v2 Select/TextField components exist.
export default function SubprocessorsPage() {
return <PageHeader title="Subprocessors" />;
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { BellIcon } from "@phosphor-icons/react";
import { Button } from "@probo/ui/src/v2/Button/Button";
import { PageHeader } from "#/components/PageHeader/PageHeader";
export default function UpdatesPage() {
return (
<PageHeader
title="Updates"
actions={(
<Button variant="soft" color="neutral" highContrast iconStart={<BellIcon />}>
Subscribe to updates
</Button>
)}
/>
);
}

View File

@@ -18,6 +18,7 @@ import { createBrowserRouter } from "react-router";
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
import { getPathPrefix } from "#/lib/http/pathPrefix";
import { HomePageSkeleton } from "#/pages/HomePageSkeleton";
const routes = [
{
@@ -27,23 +28,24 @@ const routes = [
children: [
{
index: true,
Component: lazy(() => import("#/pages/HomePage")),
Fallback: HomePageSkeleton,
Component: lazy(() => import("#/pages/HomePageLoader")),
},
{
path: "documents",
Component: lazy(() => import("#/pages/PlaceholderPage")),
Component: lazy(() => import("#/pages/DocumentsPage")),
},
{
path: "subprocessors",
Component: lazy(() => import("#/pages/PlaceholderPage")),
Component: lazy(() => import("#/pages/SubprocessorsPage")),
},
{
path: "updates",
Component: lazy(() => import("#/pages/PlaceholderPage")),
Component: lazy(() => import("#/pages/UpdatesPage")),
},
{
path: "requests",
Component: lazy(() => import("#/pages/PlaceholderPage")),
Component: lazy(() => import("#/pages/RequestsPage")),
},
],
},