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:
@@ -12,19 +12,20 @@
|
|||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import { Heading } from "@probo/ui/src/v2/typography/Heading";
|
import type { PropsWithChildren } from "react";
|
||||||
import { useLocation } from "react-router";
|
|
||||||
|
|
||||||
// Temporary stub for the Trust Center sections so the top-bar nav links resolve
|
import { headerBand } from "./variants";
|
||||||
// and the active state renders. Real pages replace these per section.
|
|
||||||
export default function PlaceholderPage() {
|
export type HeaderBandProps = PropsWithChildren;
|
||||||
const { pathname } = useLocation();
|
|
||||||
const segment = pathname.replace(/^\//, "").split("/")[0] ?? "";
|
// Pure layout shell: the white band + centered content column. Consumers (Hero,
|
||||||
const title = segment.charAt(0).toUpperCase() + segment.slice(1);
|
// PageHeader) supply a single content column and own its internal spacing.
|
||||||
|
export function HeaderBand({ children }: HeaderBandProps) {
|
||||||
|
const { band, inner } = headerBand();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto w-full max-w-[1024px] px-8 py-10">
|
<header className={band()}>
|
||||||
<Heading>{title}</Heading>
|
<div className={inner()}>{children}</div>
|
||||||
</main>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
25
apps/compliance-portal/src/components/HeaderBand/variants.ts
Normal file
25
apps/compliance-portal/src/components/HeaderBand/variants.ts
Normal 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",
|
||||||
|
},
|
||||||
|
});
|
||||||
53
apps/compliance-portal/src/components/Hero/Hero.tsx
Normal file
53
apps/compliance-portal/src/components/Hero/Hero.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
apps/compliance-portal/src/components/Hero/HeroSkeleton.tsx
Normal file
47
apps/compliance-portal/src/components/Hero/HeroSkeleton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
apps/compliance-portal/src/components/Hero/variants.ts
Normal file
37
apps/compliance-portal/src/components/Hero/variants.ts
Normal 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",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
apps/compliance-portal/src/components/PageHeader/variants.ts
Normal file
26
apps/compliance-portal/src/components/PageHeader/variants.ts
Normal 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",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -19,7 +19,7 @@ import { tv } from "tailwind-variants/lite";
|
|||||||
export const topBar = tv({
|
export const topBar = tv({
|
||||||
slots: {
|
slots: {
|
||||||
bar: "flex h-14 items-center bg-sand-1 px-8",
|
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",
|
brand: "flex items-center gap-2",
|
||||||
logo: "shrink-0",
|
logo: "shrink-0",
|
||||||
spacer: "h-px flex-1",
|
spacer: "h-px flex-1",
|
||||||
|
|||||||
23
apps/compliance-portal/src/lib/url/hostname.ts
Normal file
23
apps/compliance-portal/src/lib/url/hostname.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
apps/compliance-portal/src/pages/DocumentsPage.tsx
Normal file
21
apps/compliance-portal/src/pages/DocumentsPage.tsx
Normal 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" />;
|
||||||
|
}
|
||||||
@@ -12,10 +12,37 @@
|
|||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// 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 (
|
return (
|
||||||
<main className="flex min-h-screen items-center justify-center">
|
<Hero title={`Trust at ${organization.name}.`} description={organization.description}>
|
||||||
<h1 className="text-6 font-bold text-sand-12">Compliance Portal</h1>
|
<OrganizationContactInfo organizationKey={organization} />
|
||||||
</main>
|
</Hero>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
34
apps/compliance-portal/src/pages/HomePageLoader.tsx
Normal file
34
apps/compliance-portal/src/pages/HomePageLoader.tsx
Normal 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} />;
|
||||||
|
}
|
||||||
19
apps/compliance-portal/src/pages/HomePageSkeleton.tsx
Normal file
19
apps/compliance-portal/src/pages/HomePageSkeleton.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ export function MainLayout({ queryRef }: MainLayoutProps) {
|
|||||||
const data = usePreloadedQuery<MainLayoutQuery>(mainLayoutQuery, queryRef);
|
const data = usePreloadedQuery<MainLayoutQuery>(mainLayoutQuery, queryRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-sand-1">
|
<div className="min-h-screen bg-sand-2">
|
||||||
<TopBar queryKey={data} />
|
<TopBar queryKey={data} />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { TopBarSkeleton } from "#/components/TopBar/TopBarSkeleton";
|
|||||||
|
|
||||||
export function MainLayoutSkeleton() {
|
export function MainLayoutSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-sand-1">
|
<div className="min-h-screen bg-sand-2">
|
||||||
<TopBarSkeleton />
|
<TopBarSkeleton />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
31
apps/compliance-portal/src/pages/RequestsPage.tsx
Normal file
31
apps/compliance-portal/src/pages/RequestsPage.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
apps/compliance-portal/src/pages/SubprocessorsPage.tsx
Normal file
21
apps/compliance-portal/src/pages/SubprocessorsPage.tsx
Normal 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" />;
|
||||||
|
}
|
||||||
31
apps/compliance-portal/src/pages/UpdatesPage.tsx
Normal file
31
apps/compliance-portal/src/pages/UpdatesPage.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import { createBrowserRouter } from "react-router";
|
|||||||
|
|
||||||
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
|
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
|
||||||
import { getPathPrefix } from "#/lib/http/pathPrefix";
|
import { getPathPrefix } from "#/lib/http/pathPrefix";
|
||||||
|
import { HomePageSkeleton } from "#/pages/HomePageSkeleton";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -27,23 +28,24 @@ const routes = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
Component: lazy(() => import("#/pages/HomePage")),
|
Fallback: HomePageSkeleton,
|
||||||
|
Component: lazy(() => import("#/pages/HomePageLoader")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "documents",
|
path: "documents",
|
||||||
Component: lazy(() => import("#/pages/PlaceholderPage")),
|
Component: lazy(() => import("#/pages/DocumentsPage")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "subprocessors",
|
path: "subprocessors",
|
||||||
Component: lazy(() => import("#/pages/PlaceholderPage")),
|
Component: lazy(() => import("#/pages/SubprocessorsPage")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "updates",
|
path: "updates",
|
||||||
Component: lazy(() => import("#/pages/PlaceholderPage")),
|
Component: lazy(() => import("#/pages/UpdatesPage")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "requests",
|
path: "requests",
|
||||||
Component: lazy(() => import("#/pages/PlaceholderPage")),
|
Component: lazy(() => import("#/pages/RequestsPage")),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user