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>
);
}

View File

@@ -24,10 +24,10 @@ export type CardProps = ComponentProps<"div"> & VariantProps<typeof card>;
// clickable card set `interactive` and wrap the content in an anchor/link. Use
// CardInset for content that should bleed to the card's edges.
export function Card(props: CardProps) {
const { size = 1, variant, interactive, className, children, ...rest } = props;
const { size = 1, padding = size, variant, interactive, className, children, ...rest } = props;
return (
<div className={card({ size, variant, interactive, className })} {...rest}>
<div className={card({ size, padding, variant, interactive, className })} {...rest}>
<CardProvider value={size}>{children}</CardProvider>
</div>
);

View File

@@ -19,17 +19,29 @@ import { tv } from "tailwind-variants/lite";
export const card = tv({
base: "block overflow-hidden",
variants: {
// Padding scales with size; radius scales with it too (per the Figma).
// Radius scales with size (per the Figma).
size: {
1: "rounded-4 p-3",
2: "rounded-4 p-4",
3: "rounded-5 p-5",
4: "rounded-5 p-6",
5: "rounded-6 p-8",
1: "rounded-4",
2: "rounded-4",
3: "rounded-5",
4: "rounded-5",
5: "rounded-6",
},
// Padding defaults to match `size` (set in Card.tsx); pass `padding="none"`
// for cards whose regions own their own padding (e.g. full-bleed media).
padding: {
1: "p-3",
2: "p-4",
3: "p-5",
4: "p-6",
5: "p-8",
none: "p-0",
},
variant: {
surface: "border border-sand-6 bg-sand-2",
classic: "border border-sand-6 bg-sand-2 shadow-2",
// Soft surface: translucent neutral border on the app background.
soft: "border border-sand-a3 bg-sand-1",
ghost: "",
},
// Hover/active affordance for clickable cards. This tunes look only; wrap
@@ -40,7 +52,7 @@ export const card = tv({
},
},
compoundVariants: [
{ variant: ["surface", "classic"], interactive: true, class: "hover:bg-sand-3 active:bg-sand-4" },
{ variant: ["surface", "classic", "soft"], interactive: true, class: "hover:bg-sand-3 active:bg-sand-4" },
{ variant: "ghost", interactive: true, class: "hover:bg-sand-3 active:bg-sand-4" },
],
defaultVariants: {

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import type { Meta, StoryObj } from "@storybook/react";
import { ProboLogo } from "./ProboLogo";
export default {
title: "v2/ProboLogo",
component: ProboLogo,
render: () => <ProboLogo className="h-6 w-auto text-sand-12" />,
} satisfies Meta<typeof ProboLogo>;
type Story = StoryObj<typeof ProboLogo>;
export const Playground: Story = {};
export const Sizes: Story = {
render: () => (
<div className="flex items-center gap-6 text-sand-11">
<ProboLogo className="h-4 w-auto" />
<ProboLogo className="h-6 w-auto" />
<ProboLogo className="h-10 w-auto" />
</div>
),
};

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 type { ComponentProps } from "react";
import { useId } from "react";
export type ProboLogoProps = ComponentProps<"svg">;
// The Probo brand logo (picto + wordmark). Rendered in `currentColor`; the
// picto's mark is a transparent cutout (via mask) so the underlying surface
// shows through on any background. Size it with a height class (e.g. `h-6`).
export function ProboLogo(props: ProboLogoProps) {
const maskId = useId();
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 90 24"
role="img"
{...props}
>
<mask id={maskId} maskUnits="userSpaceOnUse">
<rect width="24" height="24" x=".85" fill="#fff" rx="5.14" />
<path
stroke="#000"
strokeWidth="3.53"
d="M20.57 6.17c2.96 4.47-6.43 13.97-20.32 5.66"
/>
<path
fill="#000"
d="M19.07 7.1a1.77 1.77 0 0 0 3-1.86l-1.5.93-1.5.94ZM8.82 25.18l1.71-.41c-2.3-9.67.01-14.66 2.54-16.83A6.25 6.25 0 0 1 17 6.33c1.22-.01 1.87.44 2.08.78l1.5-.94 1.5-.93c-1.1-1.74-3.16-2.46-5.13-2.44-2.03.03-4.26.81-6.17 2.45-3.9 3.35-6.16 9.92-3.67 20.33l1.72-.41Z"
/>
</mask>
<rect width="24" height="24" x=".85" fill="currentColor" rx="5.14" mask={`url(#${maskId})`} />
<path
fill="currentColor"
d="M66.87 0v7.32a4.84 4.84 0 0 1 1.48-.74 5.63 5.63 0 0 1 1.76-.28c1.63 0 3 .55 4.1 1.68a5.5 5.5 0 0 1 1.67 4.06c0 1.6-.56 2.93-1.69 4.06a5.49 5.49 0 0 1-4.08 1.69 5.8 5.8 0 0 1-4.06-1.56 5.48 5.48 0 0 1-1.71-4.27V0h2.53Zm3.24 8.72a3.2 3.2 0 0 0-3.24 3.32 3.18 3.18 0 0 0 3.24 3.32 3.2 3.2 0 0 0 3.24-3.32c0-1.9-1.43-3.32-3.24-3.32ZM52.75 7.98a5.53 5.53 0 0 1 4.08-1.68c1.61 0 2.93.55 4.06 1.68a5.51 5.51 0 0 1 1.69 4.06c0 1.58-.55 2.95-1.69 4.09a5.59 5.59 0 0 1-4.06 1.66 5.64 5.64 0 0 1-4.08-1.66 5.53 5.53 0 0 1-1.69-4.09c0-1.6.56-2.92 1.7-4.06Zm.84 4.06c0 .98.32 1.77.93 2.4.63.6 1.4.92 2.31.92.92 0 1.7-.31 2.3-.92.63-.64.94-1.42.94-2.4 0-.97-.31-1.76-.94-2.37a3.03 3.03 0 0 0-2.3-.95 3.22 3.22 0 0 0-3.24 3.32ZM79.32 7.98A5.53 5.53 0 0 1 83.4 6.3c1.6 0 2.93.55 4.06 1.68a5.51 5.51 0 0 1 1.69 4.06c0 1.58-.56 2.95-1.7 4.09a5.59 5.59 0 0 1-4.05 1.66 5.64 5.64 0 0 1-4.08-1.66 5.53 5.53 0 0 1-1.69-4.09c0-1.6.55-2.92 1.69-4.06Zm.84 4.06c0 .98.32 1.77.92 2.4.64.6 1.4.92 2.32.92.92 0 1.69-.31 2.3-.92.63-.64.94-1.42.94-2.4 0-.97-.31-1.76-.95-2.37a3.03 3.03 0 0 0-2.29-.95 3.22 3.22 0 0 0-3.24 3.32ZM34.24 16.76V24h-2.53V12.12c0-1.82.58-3.24 1.71-4.27a5.87 5.87 0 0 1 4.06-1.55c1.56 0 2.98.55 4.08 1.68a5.51 5.51 0 0 1 1.7 4.06 5.66 5.66 0 0 1-5.77 5.75 5.43 5.43 0 0 1-2.88-.77l-.36-.26Zm3.24-8.04a3.2 3.2 0 0 0-3.24 3.32c0 1.9 1.42 3.32 3.24 3.32a3.2 3.2 0 0 0 3.24-3.32 3.2 3.2 0 0 0-3.24-3.32ZM47.4 17.65h-2.5v-6.7c0-3.23 1.82-4.52 4.27-4.52h.6v2.42h-.47c-1.26 0-1.9.71-1.9 2.1v6.7Z"
/>
</svg>
);
}

View File

@@ -46,8 +46,10 @@ const typographyVariants = {
},
// Hue only; the resolved text step comes from the color × highContrast
// compound variants below (step 11 low-contrast, step 12 high-contrast).
// `faint` is a de-emphasized neutral (sand alpha 8) for fine-print metadata.
color: {
neutral: "",
faint: "",
gold: "",
red: "",
green: "",
@@ -63,6 +65,9 @@ const typographyVariants = {
const colorCompoundVariants = [
{ color: "neutral", highContrast: false, class: "text-sand-11" },
{ color: "neutral", highContrast: true, class: "text-sand-12" },
// Faint metadata; a single de-emphasized step regardless of highContrast.
{ color: "faint", highContrast: false, class: "text-sand-a8" },
{ color: "faint", highContrast: true, class: "text-sand-a8" },
{ color: "gold", highContrast: false, class: "text-gold-11" },
{ color: "gold", highContrast: true, class: "text-gold-12" },
{ color: "red", highContrast: false, class: "text-red-11" },