Build compliance portal home page sections

Add the frameworks (Compliance), security commitments, trusted-by, and
recent updates sections to the home page, plus a site-wide "Powered by
Probo" footer. Frameworks, references, and updates are relay-driven; the
security commitments section uses placeholder POJO data until a backend
exists.

Keep generic primitives in the v2 kit (ProboLogo, a faint Text tone, and
a soft Card variant with padding="none") and place the product-specific
cards under the app's components folder, composing the base Card. Resolve
each framework's themed logo in the graph via Framework.themedLogoUrl,
mirroring the trust center logo resolver.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-29 09:15:50 +02:00
parent 6bcb7461be
commit 930c6bab45
40 changed files with 1490 additions and 24 deletions

View File

@@ -13,7 +13,16 @@
"signOut": "Sign out"
},
"home": {
"heroTitle": "Trust at {{name}}."
"heroTitle": "Trust at {{name}}.",
"heroDescription": "Welcome to our Compliance Portal. Find our security documentation and certifications here.",
"sections": {
"compliance": "Compliance",
"trustedBy": "Trusted by",
"recentUpdates": "Recent updates"
},
"recentUpdates": {
"viewAll": "View all"
}
},
"documents": {
"title": "Documents"
@@ -33,5 +42,8 @@
"title": "Page not found",
"description": "The page you are looking for does not exist or has moved.",
"backHome": "Back to home"
},
"footer": {
"poweredBy": "Powered by"
}
}

View File

@@ -13,7 +13,16 @@
"signOut": "Se déconnecter"
},
"home": {
"heroTitle": "La confiance chez {{name}}."
"heroTitle": "La confiance chez {{name}}.",
"heroDescription": "Bienvenue dans notre Compliance Portal. Retrouvez ici notre documentation et nos certifications de sécurité.",
"sections": {
"compliance": "Conformité",
"trustedBy": "Ils nous font confiance",
"recentUpdates": "Mises à jour récentes"
},
"recentUpdates": {
"viewAll": "Voir tout"
}
},
"documents": {
"title": "Documents"
@@ -33,5 +42,8 @@
"title": "Page introuvable",
"description": "La page que vous recherchez n'existe pas ou a été déplacée.",
"backHome": "Retour à l'accueil"
},
"footer": {
"poweredBy": "Propulsé par"
}
}

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 { Card } from "@probo/ui/src/v2/Card/Card";
import type { ReactNode } from "react";
import { dotPatternStyle } from "#/components/MediaTile/variants";
import { commitmentCard } from "./variants";
export interface CommitmentCardProps {
// Leading icon shown at 32px over the dotted backdrop.
icon: ReactNode;
// Small accent eyebrow above the title.
eyebrow: ReactNode;
// Card heading.
title: ReactNode;
// Supporting body copy.
description: ReactNode;
}
// Commitment card, composing the soft Card surface. Region props are placed into
// the layout slots; the consumer supplies typography (Text). Purely
// presentational, so it doubles as the layout for its skeleton.
export function CommitmentCard({ icon, eyebrow, title, description }: CommitmentCardProps) {
const slots = commitmentCard();
return (
<Card variant="soft" size={3} padding="none">
<div className={slots.icon()}>
<div className={slots.backdrop()} style={dotPatternStyle} />
<div className={slots.backdropFade()} />
<div className={slots.iconContent()}>{icon}</div>
</div>
<div className={slots.body()}>
{eyebrow}
{title}
{description}
</div>
</Card>
);
}

View File

@@ -0,0 +1,30 @@
// 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 { TextSkeleton } from "@probo/ui/src/v2/typography/TextSkeleton";
import { CommitmentCard } from "./CommitmentCard";
// Loading placeholder paired with CommitmentCard: renders the same layout with a
// pulse icon and skeleton text lines.
export function CommitmentCardSkeleton() {
return (
<CommitmentCard
icon={<div className="size-full animate-pulse rounded-2 bg-sand-3" />}
eyebrow={<TextSkeleton size={1} className="w-20" />}
title={<TextSkeleton size={4} className="w-48" />}
description={<TextSkeleton size={2} className="w-full" />}
/>
);
}

View File

@@ -0,0 +1,28 @@
// 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";
// Commitment card (Figma "Commitment Card"): an icon container with the dotted
// + gradient backdrop above an eyebrow / title / description body, rendered
// inside a soft Card. Slots are shared by the shell and its skeleton.
export const commitmentCard = tv({
slots: {
icon: "relative flex w-full items-center overflow-hidden p-8",
backdrop: "pointer-events-none absolute inset-0",
backdropFade: "pointer-events-none absolute inset-0 bg-linear-to-b from-sand-1/0 to-sand-1",
iconContent: "relative z-10 flex size-8 items-center justify-center text-gold-9",
body: "flex flex-col gap-2 px-8 pb-8",
},
});

View File

@@ -0,0 +1,56 @@
// 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 { Text } from "@probo/ui/src/v2/typography/Text";
import type { ReactNode } from "react";
import { complianceArticleItem } from "./variants";
export interface ComplianceArticleItemProps {
// Leading category icon (pass a phosphor icon).
icon: ReactNode;
// Primary line.
title: ReactNode;
// Optional accent sub-label below the title.
eyebrow?: ReactNode;
// Right-aligned metadata (e.g. relative time).
meta?: ReactNode;
}
// A single row of a compliance article list. Encodes the row typography so
// callers pass plain content; wrap rows in a bordered list container.
export function ComplianceArticleItem({ icon, title, eyebrow, meta }: ComplianceArticleItemProps) {
const slots = complianceArticleItem();
return (
<div className={slots.root()}>
<span className={slots.icon()}>{icon}</span>
<div className={slots.content()}>
<Text size={2} weight="medium" color="neutral" highContrast>
{title}
</Text>
{eyebrow != null && (
<Text size={1} color="gold">
{eyebrow}
</Text>
)}
</div>
{meta != null && (
<Text size={1} color="faint" className={slots.meta()}>
{meta}
</Text>
)}
</div>
);
}

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 { TextSkeleton } from "@probo/ui/src/v2/typography/TextSkeleton";
import { complianceArticleItem } from "./variants";
// Loading placeholder paired with ComplianceArticleItem: same row layout with
// a pulse icon and skeleton text.
export function ComplianceArticleItemSkeleton() {
const slots = complianceArticleItem();
return (
<div className={slots.root()} aria-hidden>
<div className={slots.iconPlaceholder()} />
<div className={slots.content()}>
<TextSkeleton size={2} className="w-48" />
<TextSkeleton size={1} className="w-16" />
</div>
<TextSkeleton size={1} className="w-20 shrink-0" />
</div>
);
}

View File

@@ -0,0 +1,29 @@
// 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";
// Compliance article list row (Figma "Compliance Article Item"): a leading
// icon, a title (+ optional eyebrow), and right-aligned meta. Designed to sit
// inside a bordered list container; the last row drops its divider. Slots are
// shared by the row and its skeleton.
export const complianceArticleItem = tv({
slots: {
root: "flex w-full items-center gap-4 border-b border-sand-a2 px-8 py-4 last:border-b-0",
icon: "flex size-6 shrink-0 items-center justify-center text-gold-9 [&_svg]:size-full",
content: "flex min-w-0 flex-1 flex-col gap-1",
meta: "shrink-0",
iconPlaceholder: "size-6 shrink-0 animate-pulse rounded-2 bg-sand-3",
},
});

View File

@@ -0,0 +1,55 @@
// 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 { CertificateIcon } from "@phosphor-icons/react";
import { Text } from "@probo/ui/src/v2/typography/Text";
import { graphql, useFragment } from "react-relay";
import { MediaTile } from "#/components/MediaTile/MediaTile";
import type { ComplianceFrameworkListItem_complianceFramework$key } from "./__generated__/ComplianceFrameworkListItem_complianceFramework.graphql";
const complianceFrameworkListItemFragment = graphql`
fragment ComplianceFrameworkListItem_complianceFramework on ComplianceFramework {
framework {
name
themedLogoUrl
}
}
`;
interface ComplianceFrameworkListItemProps {
complianceFrameworkKey: ComplianceFrameworkListItem_complianceFramework$key;
}
// A single framework tile in the Compliance grid.
export function ComplianceFrameworkListItem({ complianceFrameworkKey }: ComplianceFrameworkListItemProps) {
const { framework } = useFragment(complianceFrameworkListItemFragment, complianceFrameworkKey);
const logoUrl = framework.themedLogoUrl;
return (
<MediaTile
media={
logoUrl != null && logoUrl !== ""
? <img src={logoUrl} alt={framework.name} />
: <CertificateIcon size={48} weight="duotone" className="text-gold-9" />
}
label={(
<Text size={2} weight="medium" color="neutral" highContrast align="center">
{framework.name}
</Text>
)}
/>
);
}

View File

@@ -0,0 +1,60 @@
// 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 { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay";
import { HomeSection } from "#/components/HomeSection/HomeSection";
import type { ComplianceFrameworksSection_trustCenter$key } from "./__generated__/ComplianceFrameworksSection_trustCenter.graphql";
import { ComplianceFrameworkListItem } from "./ComplianceFrameworkListItem";
const complianceFrameworksSectionFragment = graphql`
fragment ComplianceFrameworksSection_trustCenter on TrustCenter {
complianceFrameworks(first: 8) {
edges {
node {
id
...ComplianceFrameworkListItem_complianceFramework
}
}
}
}
`;
interface ComplianceFrameworksSectionProps {
trustCenterKey: ComplianceFrameworksSection_trustCenter$key;
}
// "Compliance" section: the grid of certification frameworks the trust center
// covers.
export function ComplianceFrameworksSection({ trustCenterKey }: ComplianceFrameworksSectionProps) {
const { t } = useTranslation();
const data = useFragment(complianceFrameworksSectionFragment, trustCenterKey);
const frameworks = data.complianceFrameworks.edges.map(edge => edge.node);
if (frameworks.length === 0) {
return null;
}
return (
<HomeSection title={t("home.sections.compliance")}>
<div className="grid grid-cols-6 gap-4">
{frameworks.map(framework => (
<ComplianceFrameworkListItem key={framework.id} complianceFrameworkKey={framework} />
))}
</div>
</HomeSection>
);
}

View File

@@ -0,0 +1,35 @@
// 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 { TextSkeleton } from "@probo/ui/src/v2/typography/TextSkeleton";
import { homeSection } from "#/components/HomeSection/variants";
import { MediaTileSkeleton } from "#/components/MediaTile/MediaTileSkeleton";
export function ComplianceFrameworksSectionSkeleton() {
const slots = homeSection();
return (
<section className={slots.root()} aria-hidden>
<div className={slots.header()}>
<TextSkeleton size={2} className="w-24" />
</div>
<div className="grid grid-cols-6 gap-4">
{Array.from({ length: 4 }, (_, index) => (
<MediaTileSkeleton key={index} />
))}
</div>
</section>
);
}

View File

@@ -19,7 +19,7 @@ import { tv } from "tailwind-variants/lite";
// 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",
band: "flex w-full flex-col items-center border-b border-sand-a3 bg-sand-1 px-8 py-8",
inner: "w-full max-w-5xl",
},
});

View File

@@ -30,7 +30,7 @@ export const hero = tv({
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",
root: "flex w-full items-center gap-6 border-t border-sand-a2 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,43 @@
// 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 { Text } from "@probo/ui/src/v2/typography/Text";
import type { ReactNode } from "react";
import { homeSection } from "./variants";
interface HomeSectionProps {
title: ReactNode;
// Optional trailing element in the header row (e.g. a "View all" link).
action?: ReactNode;
children: ReactNode;
}
// Shared frame for the home page body sections: a labelled header above the
// section content.
export function HomeSection({ title, action, children }: HomeSectionProps) {
const slots = homeSection();
return (
<section className={slots.root()}>
<div className={slots.header()}>
<Text size={2} weight="medium" color="neutral" highContrast>
{title}
</Text>
{action}
</div>
{children}
</section>
);
}

View File

@@ -0,0 +1,24 @@
// 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";
// Home page section frame: a labelled header row (with an optional trailing
// action) above the section body. Shared by the live sections and skeletons.
export const homeSection = tv({
slots: {
root: "flex w-full flex-col gap-4 py-8",
header: "flex w-full items-center justify-between",
},
});

View File

@@ -0,0 +1,50 @@
// 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 { Card } from "@probo/ui/src/v2/Card/Card";
import type { ReactNode } from "react";
import type { VariantProps } from "tailwind-variants/lite";
import { dotPatternStyle, mediaTile } from "./variants";
export type MediaTileProps = VariantProps<typeof mediaTile> & {
// Centered media (logo/icon image) shown over the backdrop.
media: ReactNode;
// Caption below the media (pass a Text primitive).
label: ReactNode;
// For the "logo" variant: the image URL rendered as a blurred, magnified
// backdrop behind the media. When omitted, the dotted texture is shown.
backdropSrc?: string;
};
// Captioned media tile, composing the soft Card surface. The "icon" variant
// shows a dotted backdrop; the "logo" variant shows a blurred, magnified copy of
// the logo (via backdropSrc). Purely presentational, so it doubles as the layout
// for its skeleton.
export function MediaTile({ media, label, variant = "icon", backdropSrc }: MediaTileProps) {
const slots = mediaTile({ variant });
return (
<Card variant="soft" size={3} padding="none">
<div className={slots.media()}>
{backdropSrc != null
? <img src={backdropSrc} alt="" aria-hidden className={slots.blurBackdrop()} />
: <div className={slots.backdrop()} style={dotPatternStyle} />}
<div className={slots.backdropFade()} />
<div className={slots.mediaContent()}>{media}</div>
</div>
<div className={slots.caption()}>{label}</div>
</Card>
);
}

View File

@@ -0,0 +1,33 @@
// 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 { TextSkeleton } from "@probo/ui/src/v2/typography/TextSkeleton";
import type { VariantProps } from "tailwind-variants/lite";
import { MediaTile } from "./MediaTile";
import type { mediaTile } from "./variants";
export type MediaTileSkeletonProps = VariantProps<typeof mediaTile>;
// Loading placeholder paired with MediaTile: renders the same layout with a
// pulse block for the media and a TextSkeleton for the caption.
export function MediaTileSkeleton({ variant = "icon" }: MediaTileSkeletonProps) {
return (
<MediaTile
variant={variant}
media={<div className="size-full animate-pulse rounded-3 bg-sand-3" />}
label={<TextSkeleton size={2} className="w-20" />}
/>
);
}

View File

@@ -0,0 +1,50 @@
// 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 type { CSSProperties } from "react";
import { tv } from "tailwind-variants/lite";
// Captioned media tile (Figma "Framework Card" / "Logo Card"): a media container
// with a decorative backdrop above a centered caption, rendered inside a soft
// Card. The "icon" variant (default) uses a padded square with a dotted texture;
// the "logo" variant fills a square container and uses a blurred, magnified copy
// of the logo as the backdrop. Slots are shared by the shell and its skeleton.
export const mediaTile = tv({
slots: {
media: "relative flex w-full items-center justify-center overflow-hidden border-b border-sand-a2",
backdrop: "pointer-events-none absolute inset-0",
blurBackdrop: "pointer-events-none absolute inset-0 size-full scale-150 object-cover opacity-10 blur-lg",
backdropFade: "pointer-events-none absolute inset-0 bg-linear-to-b from-sand-1/0 to-sand-1",
mediaContent: "relative z-10 flex size-16 items-center justify-center [&_img]:size-full [&_img]:object-contain",
caption: "flex w-full items-center justify-center px-4 py-3",
},
variants: {
variant: {
icon: { media: "p-4" },
logo: { media: "aspect-square" },
},
},
defaultVariants: {
variant: "icon",
},
});
// Repeating dot texture as a radial-gradient using a Radix sand alpha token
// (theme-aware, no binary asset), faded toward the card surface by the
// backdropFade gradient slot. Maps Figma's neutral-alpha dots to sand-a5. Shared
// by the media/commitment cards, the updates list, and the footer.
export const dotPatternStyle: CSSProperties = {
backgroundImage: "radial-gradient(var(--sand-a5) 0.75px, transparent 1.25px)",
backgroundSize: "9px 9px",
};

View File

@@ -0,0 +1,50 @@
// 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 { ProboLogo } from "@probo/ui/src/v2/ProboLogo/ProboLogo";
import { Text } from "@probo/ui/src/v2/typography/Text";
import type { ReactNode } from "react";
import { dotPatternStyle } from "#/components/MediaTile/variants";
import { poweredBy } from "./variants";
export interface PoweredByProps {
// Localized "Powered by" label. Defaults to the English string.
label?: ReactNode;
// Destination for the Probo logo link.
href?: string;
}
// "Powered by Probo" footer: a borderless attribution on the body surface, with
// a dotted texture fading in from the bottom and the full Probo logo (picto +
// wordmark).
export function PoweredBy({ label = "Powered by", href = "https://www.probo.com/" }: PoweredByProps) {
const slots = poweredBy();
return (
<footer className={slots.root()}>
<div className={slots.backdrop()} style={dotPatternStyle} />
<div className={slots.backdropFade()} />
<div className={slots.content()}>
<Text size={1} color="neutral">
{label}
</Text>
<a href={href} target="_blank" rel="noopener noreferrer" aria-label="Probo">
<ProboLogo className={slots.logo()} aria-hidden />
</a>
</div>
</footer>
);
}

View File

@@ -0,0 +1,27 @@
// 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";
// "Powered by Probo" footer (Figma "Powered by"): a borderless, full-width row
// on the body surface, with a dotted texture that fades in from the bottom.
export const poweredBy = tv({
slots: {
root: "relative flex w-full items-center justify-center overflow-hidden bg-sand-2 py-6 text-sand-11",
backdrop: "pointer-events-none absolute inset-0",
backdropFade: "pointer-events-none absolute inset-0 bg-linear-to-b from-sand-2 to-sand-2/0",
content: "relative z-10 flex items-center justify-center gap-2",
logo: "h-6 w-auto text-sand-9",
},
});

View File

@@ -0,0 +1,48 @@
// 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 { NewspaperIcon } from "@phosphor-icons/react";
import { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay";
import { ComplianceArticleItem } from "#/components/ComplianceArticleItem/ComplianceArticleItem";
import { formatRelativeTime } from "#/lib/datetime/relativeTime";
import type { MailingListUpdateListItem_update$key } from "./__generated__/MailingListUpdateListItem_update.graphql";
const mailingListUpdateListItemFragment = graphql`
fragment MailingListUpdateListItem_update on MailingListUpdate {
title
updatedAt
}
`;
interface MailingListUpdateListItemProps {
updateKey: MailingListUpdateListItem_update$key;
}
// A single "Recent updates" row. The schema has no category, so we show the
// title and relative date with a single generic icon.
export function MailingListUpdateListItem({ updateKey }: MailingListUpdateListItemProps) {
const { i18n } = useTranslation();
const update = useFragment(mailingListUpdateListItemFragment, updateKey);
return (
<ComplianceArticleItem
icon={<NewspaperIcon weight="light" />}
title={update.title}
meta={formatRelativeTime(update.updatedAt, i18n.language)}
/>
);
}

View File

@@ -0,0 +1,73 @@
// 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 { Link } from "@probo/ui/src/v2/Button/Link";
import { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay";
import { HomeSection } from "#/components/HomeSection/HomeSection";
import { dotPatternStyle } from "#/components/MediaTile/variants";
import type { RecentUpdatesSection_trustCenter$key } from "./__generated__/RecentUpdatesSection_trustCenter.graphql";
import { MailingListUpdateListItem } from "./MailingListUpdateListItem";
const recentUpdatesSectionFragment = graphql`
fragment RecentUpdatesSection_trustCenter on TrustCenter {
updates(first: 5) {
edges {
node {
id
...MailingListUpdateListItem_update
}
}
}
}
`;
interface RecentUpdatesSectionProps {
trustCenterKey: RecentUpdatesSection_trustCenter$key;
}
// "Recent updates" section: the latest mailing-list updates as a list, with a
// link to the full updates page.
export function RecentUpdatesSection({ trustCenterKey }: RecentUpdatesSectionProps) {
const { t } = useTranslation();
const data = useFragment(recentUpdatesSectionFragment, trustCenterKey);
const updates = data.updates.edges.map(edge => edge.node);
if (updates.length === 0) {
return null;
}
return (
<HomeSection
title={t("home.sections.recentUpdates")}
action={(
<Link to="/updates" variant="ghost" color="neutral" size={2}>
{t("home.recentUpdates.viewAll")}
</Link>
)}
>
<div className="relative overflow-hidden rounded-5 border border-sand-3 bg-sand-1">
<div aria-hidden className="pointer-events-none absolute inset-0" style={dotPatternStyle} />
<div aria-hidden className="pointer-events-none absolute inset-0 bg-linear-to-r from-sand-1/0 to-sand-1 to-[96px]" />
<div className="relative">
{updates.map(update => (
<MailingListUpdateListItem key={update.id} updateKey={update} />
))}
</div>
</div>
</HomeSection>
);
}

View File

@@ -0,0 +1,41 @@
// 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 { TextSkeleton } from "@probo/ui/src/v2/typography/TextSkeleton";
import { ComplianceArticleItemSkeleton } from "#/components/ComplianceArticleItem/ComplianceArticleItemSkeleton";
import { homeSection } from "#/components/HomeSection/variants";
import { dotPatternStyle } from "#/components/MediaTile/variants";
export function RecentUpdatesSectionSkeleton() {
const slots = homeSection();
return (
<section className={slots.root()} aria-hidden>
<div className={slots.header()}>
<TextSkeleton size={2} className="w-28" />
<TextSkeleton size={2} className="w-12" />
</div>
<div className="relative overflow-hidden rounded-5 border border-sand-3 bg-sand-1">
<div aria-hidden className="pointer-events-none absolute inset-0" style={dotPatternStyle} />
<div aria-hidden className="pointer-events-none absolute inset-0 bg-linear-to-r from-sand-1/0 to-sand-1 to-[96px]" />
<div className="relative">
{Array.from({ length: 5 }, (_, index) => (
<ComplianceArticleItemSkeleton key={index} />
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,66 @@
// 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 { Text } from "@probo/ui/src/v2/typography/Text";
import { CommitmentCard } from "#/components/CommitmentCard/CommitmentCard";
import { SECURITY_COMMITMENT_GROUPS } from "./securityCommitments";
import { securityCommitments } from "./variants";
// "Security Commitments" section.
//
// TODO: This section renders placeholder data from a local POJO
// (./securityCommitments.ts) because there is no backend / DB structure for it
// yet. Replace it with a relay-driven fragment (and i18n copy) once available.
export function SecurityCommitmentsSection() {
const slots = securityCommitments();
return (
<section className={slots.root()}>
{SECURITY_COMMITMENT_GROUPS.map(group => (
<div key={group.title} className={slots.group()}>
<div className={slots.groupHeader()}>
{group.eyebrow != null && (
<Text size={1} color="gold">
{group.eyebrow}
</Text>
)}
<Text size={2} weight="medium" color="neutral" highContrast>
{group.title}
</Text>
<Text size={2} color="neutral">
{group.description}
</Text>
</div>
<div className={slots.grid()}>
{group.items.map(item => (
<CommitmentCard
key={item.title}
icon={<item.Icon size={32} weight="light" />}
eyebrow={<Text size={1} color="gold">{item.eyebrow}</Text>}
title={(
<Text size={4} weight="medium" color="neutral" highContrast>
{item.title}
</Text>
)}
description={<Text size={2} color="neutral">{item.description}</Text>}
/>
))}
</div>
</div>
))}
</section>
);
}

View File

@@ -0,0 +1,40 @@
// 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 { TextSkeleton } from "@probo/ui/src/v2/typography/TextSkeleton";
import { CommitmentCardSkeleton } from "#/components/CommitmentCard/CommitmentCardSkeleton";
import { securityCommitments } from "./variants";
export function SecurityCommitmentsSectionSkeleton() {
const slots = securityCommitments();
return (
<section className={slots.root()} aria-hidden>
<div className={slots.group()}>
<div className={slots.groupHeader()}>
<TextSkeleton size={1} className="w-32" />
<TextSkeleton size={2} className="w-28" />
<TextSkeleton size={2} className="w-full max-w-2xl" />
</div>
<div className={slots.grid()}>
{Array.from({ length: 3 }, (_, index) => (
<CommitmentCardSkeleton key={index} />
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,87 @@
// 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 type { Icon } from "@phosphor-icons/react";
import { EyeSlashIcon, FingerprintIcon, LockKeyIcon, ShieldWarningIcon, SirenIcon } from "@phosphor-icons/react";
export interface SecurityCommitmentItem {
eyebrow: string;
title: string;
description: string;
Icon: Icon;
}
export interface SecurityCommitmentGroup {
// Only the first group carries the section eyebrow in the design.
eyebrow?: string;
title: string;
description: string;
items: SecurityCommitmentItem[];
}
// TODO: This content is placeholder data. There is no backend / DB structure for
// security commitments yet — replace this POJO with relay-sourced data (and move
// the copy into i18n) once the schema exists.
export const SECURITY_COMMITMENT_GROUPS: SecurityCommitmentGroup[] = [
{
eyebrow: "Security Commitments",
title: "Data Protection",
description:
"Your data is protected with industry-leading encryption and strict privacy controls, from storage to transit to processing.",
items: [
{
eyebrow: "Data Protection",
title: "Encrypted at rest. Encrypted in transit. Always.",
description:
"Customer data is protected with AES-256 at rest and TLS 1.3 in transit. Keys rotated on a fixed schedule.",
Icon: LockKeyIcon,
},
{
eyebrow: "Privacy",
title: "Customer data stays customer data.",
description:
"PII is masked in non-production environments. Access requires SSO with hardware-backed keys.",
Icon: EyeSlashIcon,
},
{
eyebrow: "Identity",
title: "Phishing-resistant authentication, by default.",
description:
"Every employee authenticates via WebAuthn. Production access is just-in-time and recorded.",
Icon: FingerprintIcon,
},
],
},
{
title: "Operational Security",
description:
"We maintain continuous uptime and rapid incident response so you can rely on our platform around the clock.",
items: [
{
eyebrow: "Threat Detection",
title: "Anomalies do not wait. Neither do we.",
description:
"24/7 SIEM monitoring with automated triage. Critical incidents page on-call within 90 seconds.",
Icon: ShieldWarningIcon,
},
{
eyebrow: "Incident Response",
title: "Issues are caught fast. And handled.",
description:
"Security incidents are investigated within hours. Customers notified within 24 hours of a confirmed breach.",
Icon: SirenIcon,
},
],
},
];

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";
// Security Commitments section: stacked groups, each a header above a grid of
// commitment cards. Slots shared by the section and its skeleton.
export const securityCommitments = tv({
slots: {
root: "flex w-full flex-col gap-8 py-8",
group: "flex w-full flex-col gap-4",
groupHeader: "flex w-full flex-col gap-2",
grid: "grid grid-cols-3 gap-4",
},
});

View File

@@ -0,0 +1,60 @@
// 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 { Text } from "@probo/ui/src/v2/typography/Text";
import { graphql, useFragment } from "react-relay";
import { MediaTile } from "#/components/MediaTile/MediaTile";
import { externalHref } from "#/lib/url/hostname";
import type { TrustCenterReferenceListItem_reference$key } from "./__generated__/TrustCenterReferenceListItem_reference.graphql";
const trustCenterReferenceListItemFragment = graphql`
fragment TrustCenterReferenceListItem_reference on TrustCenterReference {
name
websiteUrl
logo {
downloadUrl
}
}
`;
interface TrustCenterReferenceListItemProps {
referenceKey: TrustCenterReferenceListItem_reference$key;
}
// A single "Trusted by" logo tile, linking to the reference's website.
export function TrustCenterReferenceListItem({ referenceKey }: TrustCenterReferenceListItemProps) {
const reference = useFragment(trustCenterReferenceListItemFragment, referenceKey);
return (
<a
className="block"
href={externalHref(reference.websiteUrl)}
target="_blank"
rel="noopener noreferrer"
>
<MediaTile
variant="logo"
backdropSrc={reference.logo.downloadUrl}
media={<img src={reference.logo.downloadUrl} alt={reference.name} />}
label={(
<Text size={2} weight="medium" color="neutral" highContrast align="center">
{reference.name}
</Text>
)}
/>
</a>
);
}

View File

@@ -0,0 +1,59 @@
// 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 { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay";
import { HomeSection } from "#/components/HomeSection/HomeSection";
import type { TrustedBySection_trustCenter$key } from "./__generated__/TrustedBySection_trustCenter.graphql";
import { TrustCenterReferenceListItem } from "./TrustCenterReferenceListItem";
const trustedBySectionFragment = graphql`
fragment TrustedBySection_trustCenter on TrustCenter {
references(first: 12) {
edges {
node {
id
...TrustCenterReferenceListItem_reference
}
}
}
}
`;
interface TrustedBySectionProps {
trustCenterKey: TrustedBySection_trustCenter$key;
}
// "Trusted by" section: a grid of customer / reference logos.
export function TrustedBySection({ trustCenterKey }: TrustedBySectionProps) {
const { t } = useTranslation();
const data = useFragment(trustedBySectionFragment, trustCenterKey);
const references = data.references.edges.map(edge => edge.node);
if (references.length === 0) {
return null;
}
return (
<HomeSection title={t("home.sections.trustedBy")}>
<div className="grid grid-cols-6 gap-4">
{references.map(reference => (
<TrustCenterReferenceListItem key={reference.id} referenceKey={reference} />
))}
</div>
</HomeSection>
);
}

View File

@@ -0,0 +1,35 @@
// 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 { TextSkeleton } from "@probo/ui/src/v2/typography/TextSkeleton";
import { homeSection } from "#/components/HomeSection/variants";
import { MediaTileSkeleton } from "#/components/MediaTile/MediaTileSkeleton";
export function TrustedBySectionSkeleton() {
const slots = homeSection();
return (
<section className={slots.root()} aria-hidden>
<div className={slots.header()}>
<TextSkeleton size={2} className="w-20" />
</div>
<div className="grid grid-cols-6 gap-4">
{Array.from({ length: 6 }, (_, index) => (
<MediaTileSkeleton key={index} variant="logo" />
))}
</div>
</section>
);
}

View File

@@ -0,0 +1,41 @@
// 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.
const DIVISIONS: { amount: number; unit: Intl.RelativeTimeFormatUnit }[] = [
{ amount: 60, unit: "second" },
{ amount: 60, unit: "minute" },
{ amount: 24, unit: "hour" },
{ amount: 7, unit: "day" },
{ amount: 4.34524, unit: "week" },
{ amount: 12, unit: "month" },
{ amount: Number.POSITIVE_INFINITY, unit: "year" },
];
// Locale-aware "x days ago" style formatting via Intl.RelativeTimeFormat. The
// locale must be the active i18next language so the output follows the UI.
export function formatRelativeTime(date: Date | string | number, locale: string): string {
const target = date instanceof Date ? date : new Date(date);
const formatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
let duration = (target.getTime() - Date.now()) / 1000;
for (const division of DIVISIONS) {
if (Math.abs(duration) < division.amount) {
return formatter.format(Math.round(duration), division.unit);
}
duration /= division.amount;
}
return formatter.format(Math.round(duration), "year");
}

View File

@@ -0,0 +1,63 @@
// 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 { graphql } from "react-relay";
import { type LiveState, readFragment } from "relay-runtime";
import type { FrameworkLogoResolverFragment$key } from "./__generated__/FrameworkLogoResolverFragment.graphql";
function prefersDark(): boolean {
return typeof window !== "undefined"
&& !!window.matchMedia
&& window.matchMedia("(prefers-color-scheme: dark)").matches;
}
/**
* @relayField Framework.themedLogoUrl: String
* @rootFragment FrameworkLogoResolverFragment
* @live
*
* Resolves the framework logo download URL for the current system color scheme:
* the dark logo (falling back to the light one) when the OS prefers dark,
* otherwise the light logo. Lives in the graph so consumers select a single
* field instead of threading `useSystemTheme` through URL selection.
*/
export function themedLogoUrl(
key: FrameworkLogoResolverFragment$key,
): LiveState<string | null> {
const data = readFragment(
graphql`
fragment FrameworkLogoResolverFragment on Framework {
lightLogo {
downloadUrl
}
darkLogo {
downloadUrl
}
}
`,
key,
);
const lightUrl = data.lightLogo?.downloadUrl ?? null;
const darkUrl = data.darkLogo?.downloadUrl ?? lightUrl;
return {
read: () => (prefersDark() ? darkUrl : lightUrl),
subscribe: (callback) => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener("change", callback);
return () => mediaQuery.removeEventListener("change", callback);
},
};
}

View File

@@ -16,8 +16,12 @@ import { useTranslation } from "react-i18next";
import type { PreloadedQuery } from "react-relay";
import { graphql, usePreloadedQuery } from "react-relay";
import { ComplianceFrameworksSection } from "#/components/ComplianceFrameworks/ComplianceFrameworksSection";
import { Hero } from "#/components/Hero/Hero";
import { OrganizationContactInfo } from "#/components/Hero/OrganizationContactInfo";
import { RecentUpdatesSection } from "#/components/RecentUpdates/RecentUpdatesSection";
import { SecurityCommitmentsSection } from "#/components/SecurityCommitments/SecurityCommitmentsSection";
import { TrustedBySection } from "#/components/TrustedBy/TrustedBySection";
import type { HomePageQuery } from "./__generated__/HomePageQuery.graphql";
@@ -26,9 +30,11 @@ export const homePageQuery = graphql`
currentTrustCenter @required(action: THROW) {
organization {
name
description
...OrganizationContactInfo_organization
}
...ComplianceFrameworksSection_trustCenter
...TrustedBySection_trustCenter
...RecentUpdatesSection_trustCenter
}
}
`;
@@ -40,14 +46,25 @@ interface HomePageProps {
export function HomePage({ queryRef }: HomePageProps) {
const { t } = useTranslation();
const data = usePreloadedQuery<HomePageQuery>(homePageQuery, queryRef);
const { organization } = data.currentTrustCenter;
const { currentTrustCenter } = data;
const { organization } = currentTrustCenter;
return (
<Hero
title={t("home.heroTitle", { name: organization.name })}
description={organization.description}
>
<OrganizationContactInfo organizationKey={organization} />
</Hero>
<>
<Hero
title={t("home.heroTitle", { name: organization.name })}
description={t("home.heroDescription")}
>
<OrganizationContactInfo organizationKey={organization} />
</Hero>
<div className="flex w-full flex-col items-center px-8">
<div className="flex w-full max-w-5xl flex-col">
<ComplianceFrameworksSection trustCenterKey={currentTrustCenter} />
<SecurityCommitmentsSection />
<TrustedBySection trustCenterKey={currentTrustCenter} />
<RecentUpdatesSection trustCenterKey={currentTrustCenter} />
</div>
</div>
</>
);
}

View File

@@ -12,8 +12,24 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { ComplianceFrameworksSectionSkeleton } from "#/components/ComplianceFrameworks/ComplianceFrameworksSectionSkeleton";
import { HeroSkeleton } from "#/components/Hero/HeroSkeleton";
import { RecentUpdatesSectionSkeleton } from "#/components/RecentUpdates/RecentUpdatesSectionSkeleton";
import { SecurityCommitmentsSectionSkeleton } from "#/components/SecurityCommitments/SecurityCommitmentsSectionSkeleton";
import { TrustedBySectionSkeleton } from "#/components/TrustedBy/TrustedBySectionSkeleton";
export function HomePageSkeleton() {
return <HeroSkeleton />;
return (
<>
<HeroSkeleton />
<div className="flex w-full flex-col items-center px-8">
<div className="flex w-full max-w-5xl flex-col">
<ComplianceFrameworksSectionSkeleton />
<SecurityCommitmentsSectionSkeleton />
<TrustedBySectionSkeleton />
<RecentUpdatesSectionSkeleton />
</div>
</div>
</>
);
}

View File

@@ -12,10 +12,12 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { useTranslation } from "react-i18next";
import type { PreloadedQuery } from "react-relay";
import { graphql, usePreloadedQuery } from "react-relay";
import { Outlet } from "react-router";
import { PoweredBy } from "#/components/PoweredBy/PoweredBy";
import { TopBar } from "#/components/TopBar/TopBar";
import type { MainLayoutQuery } from "./__generated__/MainLayoutQuery.graphql";
@@ -31,12 +33,16 @@ interface MainLayoutProps {
}
export function MainLayout({ queryRef }: MainLayoutProps) {
const { t } = useTranslation();
const data = usePreloadedQuery<MainLayoutQuery>(mainLayoutQuery, queryRef);
return (
<div className="min-h-screen bg-sand-2">
<div className="flex min-h-screen flex-col bg-sand-2">
<TopBar queryKey={data} />
<Outlet />
<div className="flex-1">
<Outlet />
</div>
<PoweredBy label={t("footer.poweredBy")} />
</div>
);
}