@@ -86,6 +91,6 @@ export function CompliancePortalContactInfo({ compliancePortalKey }: ComplianceP
)}
-
+ >
);
}
diff --git a/apps/compliance-portal/src/components/Hero/CompliancePortalCustomLinks.tsx b/apps/compliance-portal/src/components/Hero/CompliancePortalCustomLinks.tsx
new file mode 100644
index 000000000..d9865d240
--- /dev/null
+++ b/apps/compliance-portal/src/components/Hero/CompliancePortalCustomLinks.tsx
@@ -0,0 +1,108 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import {
+ FacebookLogoIcon,
+ GlobeSimpleIcon,
+ type Icon,
+ LinkedinLogoIcon,
+ XLogoIcon,
+} from "@phosphor-icons/react";
+import { detectSocialName } from "@probo/helpers";
+import { Anchor } from "@probo/ui/src/v2/Link/Anchor";
+import { graphql, useFragment } from "react-relay";
+
+import { externalHref } from "#/lib/url/hostname";
+
+import type { CompliancePortalCustomLinks_compliancePortal$key } from "./__generated__/CompliancePortalCustomLinks_compliancePortal.graphql";
+import { organizationContactInfo } from "./variants";
+
+const compliancePortalCustomLinksFragment = graphql`
+ fragment CompliancePortalCustomLinks_compliancePortal on CompliancePortal {
+ customLinks(first: 20) {
+ edges {
+ node {
+ id
+ name
+ url
+ }
+ }
+ }
+ }
+`;
+
+interface CompliancePortalCustomLinksProps {
+ compliancePortalKey: CompliancePortalCustomLinks_compliancePortal$key;
+}
+
+function iconForUrl(url: string): Icon {
+ switch (detectSocialName(url)) {
+ case "LinkedIn":
+ return LinkedinLogoIcon;
+ case "X":
+ return XLogoIcon;
+ case "Facebook":
+ return FacebookLogoIcon;
+ default:
+ return GlobeSimpleIcon;
+ }
+}
+
+// Organization custom links (social / external URLs) as icon + label items,
+// appended after contact details in the shared hero meta row.
+export function CompliancePortalCustomLinks({
+ compliancePortalKey,
+}: CompliancePortalCustomLinksProps) {
+ const compliancePortal = useFragment(
+ compliancePortalCustomLinksFragment,
+ compliancePortalKey,
+ );
+ const links = compliancePortal.customLinks.edges.map(edge => edge.node);
+
+ if (links.length === 0) {
+ return null;
+ }
+
+ const { link } = organizationContactInfo();
+
+ return (
+ <>
+ {links.map((customLink) => {
+ const Icon = iconForUrl(customLink.url);
+
+ return (
+ }
+ >
+ {customLink.name}
+
+ );
+ })}
+ >
+ );
+}
diff --git a/apps/compliance-portal/src/components/Hero/CompliancePortalHeroMeta.tsx b/apps/compliance-portal/src/components/Hero/CompliancePortalHeroMeta.tsx
new file mode 100644
index 000000000..fc36ca744
--- /dev/null
+++ b/apps/compliance-portal/src/components/Hero/CompliancePortalHeroMeta.tsx
@@ -0,0 +1,76 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { graphql, useFragment } from "react-relay";
+
+import type { CompliancePortalHeroMeta_compliancePortal$key } from "./__generated__/CompliancePortalHeroMeta_compliancePortal.graphql";
+import { CompliancePortalContactInfo } from "./CompliancePortalContactInfo";
+import { CompliancePortalCustomLinks } from "./CompliancePortalCustomLinks";
+import { organizationContactInfo } from "./variants";
+
+const compliancePortalHeroMetaFragment = graphql`
+ fragment CompliancePortalHeroMeta_compliancePortal on CompliancePortal {
+ websiteUrl
+ email
+ headquarterAddress
+ customLinks(first: 20) {
+ edges {
+ __typename
+ }
+ }
+ ...CompliancePortalContactInfo_compliancePortal
+ ...CompliancePortalCustomLinks_compliancePortal
+ }
+`;
+
+interface CompliancePortalHeroMetaProps {
+ compliancePortalKey: CompliancePortalHeroMeta_compliancePortal$key;
+}
+
+// Shared hero bottom band: contact details and custom links in one row with a
+// single top divider. Hidden entirely when both are empty.
+export function CompliancePortalHeroMeta({
+ compliancePortalKey,
+}: CompliancePortalHeroMetaProps) {
+ const compliancePortal = useFragment(
+ compliancePortalHeroMetaFragment,
+ compliancePortalKey,
+ );
+
+ const hasWebsite = compliancePortal.websiteUrl != null && compliancePortal.websiteUrl !== "";
+ const hasEmail = compliancePortal.email != null && compliancePortal.email !== "";
+ const hasAddress
+ = compliancePortal.headquarterAddress != null && compliancePortal.headquarterAddress !== "";
+ const hasContact = hasWebsite || hasEmail || hasAddress;
+ const hasCustomLinks = compliancePortal.customLinks.edges.length > 0;
+
+ if (!hasContact && !hasCustomLinks) {
+ return null;
+ }
+
+ const { root } = organizationContactInfo();
+
+ return (
+
+
+
+
+ );
+}
diff --git a/apps/compliance-portal/src/components/Hero/Hero.tsx b/apps/compliance-portal/src/components/Hero/Hero.tsx
index 6e120e3d8..4cf9f12e9 100644
--- a/apps/compliance-portal/src/components/Hero/Hero.tsx
+++ b/apps/compliance-portal/src/components/Hero/Hero.tsx
@@ -34,8 +34,8 @@ export interface HeroProps {
// Landing hero (home): a size-8 title (+ optional description) in the shared
// white band, plus an optional bottom slot for page-specific content (the org
-// contact row). The slot content owns its own divider/spacing so it disappears
-// cleanly when empty.
+// meta row: contact + custom links). The slot content owns its own
+// divider/spacing so it disappears cleanly when empty.
export function Hero({ title, description, children }: HeroProps) {
const { content, section } = hero();
diff --git a/apps/compliance-portal/src/components/Hero/HeroSkeleton.tsx b/apps/compliance-portal/src/components/Hero/HeroSkeleton.tsx
index 9ce6d0a7c..610c72de1 100644
--- a/apps/compliance-portal/src/components/Hero/HeroSkeleton.tsx
+++ b/apps/compliance-portal/src/components/Hero/HeroSkeleton.tsx
@@ -25,12 +25,14 @@ import { HeaderBand } from "#/components/HeaderBand/HeaderBand";
import { hero, organizationContactInfo } from "./variants";
-// Width per contact item, roughly sized to its typical content (hostname /
-// email / address). See .cursor/rules/skeleton-width-sync.mdc.
-const CONTACT_ITEMS = [
+// Width per meta item, roughly sized to its typical content (hostname /
+// email / address / custom link). See .cursor/rules/skeleton-width-sync.mdc.
+const META_ITEMS = [
{ key: "website", width: "w-28" },
{ key: "email", width: "w-40" },
{ key: "location", width: "w-36" },
+ { key: "customLink1", width: "w-24" },
+ { key: "customLink2", width: "w-16" },
] as const;
// Loading placeholder paired with Hero: reuses the same layout slots with
@@ -47,9 +49,9 @@ export function HeroSkeleton() {
- {CONTACT_ITEMS.map(contact => (
-
-
+ {META_ITEMS.map(meta => (
+
+
))}
diff --git a/apps/compliance-portal/src/components/Hero/variants.ts b/apps/compliance-portal/src/components/Hero/variants.ts
index ecbd033ea..e9c4614ff 100644
--- a/apps/compliance-portal/src/components/Hero/variants.ts
+++ b/apps/compliance-portal/src/components/Hero/variants.ts
@@ -21,8 +21,8 @@
import { tv } from "tailwind-variants/lite";
// Landing hero content rendered inside the shared HeaderBand: a centered
-// title/description section above an optional bottom slot (the contact row).
-// Slots are shared by the live Hero and its skeleton.
+// title/description section above an optional bottom slot (contact + custom
+// links). Slots are shared by the live Hero and its skeleton.
export const hero = tv({
slots: {
content: "flex w-full flex-col gap-10",
@@ -30,14 +30,15 @@ export const hero = tv({
},
});
-// Organization contact block: a horizontal row of icon + label items, with a
-// top divider so it reads as the hero's bottom section. Self-contained so the
-// divider only appears when there is contact info to show.
+// Hero meta band: contact details and custom links as one icon + label row,
+// with a single top divider. Link chrome comes from v2 Anchor; these slots
+// only own the band layout and non-link items (HQ address).
export const organizationContactInfo = tv({
slots: {
// Only a top divider gap; the band's py-8 provides the bottom spacing.
root: "flex w-full flex-wrap items-center gap-x-6 gap-y-2 border-t border-sand-a2 pt-4 max-md:flex-col max-md:items-start",
- item: "flex min-w-0 items-center gap-2 text-sand-11 [&_svg]:size-5 [&_svg]:shrink-0 [&>*]:min-w-0 [&>*:not(svg)]:break-words",
- link: "flex min-w-0 items-center gap-2 text-sand-11 hover:underline [&_svg]:size-5 [&_svg]:shrink-0 [&>*]:min-w-0 [&>*:not(svg)]:break-all",
+ item: "flex min-w-0 items-center gap-2 text-sand-11 [&_svg]:size-4 [&_svg]:shrink-0 *:min-w-0 [&>*:not(svg)]:wrap-break-word",
+ // Layout extras on top of Anchor (underline/color/size live on Anchor).
+ link: "min-w-0 max-w-full [&>:not(svg)]:break-all",
},
});
diff --git a/apps/compliance-portal/src/components/RecentUpdates/RecentUpdatesSection.tsx b/apps/compliance-portal/src/components/RecentUpdates/RecentUpdatesSection.tsx
index ae6c359e3..5e8767912 100644
--- a/apps/compliance-portal/src/components/RecentUpdates/RecentUpdatesSection.tsx
+++ b/apps/compliance-portal/src/components/RecentUpdates/RecentUpdatesSection.tsx
@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
-import { Link } from "@probo/ui/src/v2/Button/Link";
+import { ButtonLink } from "@probo/ui/src/v2/Button/ButtonLink";
import { ErrorBoundary } from "@probo/ui/src/v2/ErrorBoundary/ErrorBoundary";
import { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay";
@@ -84,9 +84,9 @@ function RecentUpdatesSectionContent({ compliancePortalKey }: RecentUpdatesSecti
+
{t("home.recentUpdates.viewAll")}
-
+
)}
>
diff --git a/apps/compliance-portal/src/components/TopBar/TopBar.tsx b/apps/compliance-portal/src/components/TopBar/TopBar.tsx
index af6fa0938..584e8a609 100644
--- a/apps/compliance-portal/src/components/TopBar/TopBar.tsx
+++ b/apps/compliance-portal/src/components/TopBar/TopBar.tsx
@@ -21,7 +21,7 @@
import { LockSimpleIcon } from "@phosphor-icons/react";
import { Avatar } from "@probo/ui/src/v2/Avatar/Avatar";
import { Button } from "@probo/ui/src/v2/Button/Button";
-import { Link } from "@probo/ui/src/v2/Button/Link";
+import { ButtonLink } from "@probo/ui/src/v2/Button/ButtonLink";
import { Text } from "@probo/ui/src/v2/typography/Text";
import { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay";
@@ -103,7 +103,7 @@ export function TopBar({ queryKey }: TopBarProps) {
{TOP_BAR_NAV_ITEMS.map((item) => {
const to = localizedPath(item.to);
return (
-
{t(item.labelKey)}
-
+
);
})}
{data.viewer == null
diff --git a/apps/compliance-portal/src/components/TopBar/TopBarMobileNav.tsx b/apps/compliance-portal/src/components/TopBar/TopBarMobileNav.tsx
index 44ea3f653..c6d0f9196 100644
--- a/apps/compliance-portal/src/components/TopBar/TopBarMobileNav.tsx
+++ b/apps/compliance-portal/src/components/TopBar/TopBarMobileNav.tsx
@@ -29,7 +29,7 @@ import {
XIcon,
} from "@phosphor-icons/react";
import { Button } from "@probo/ui/src/v2/Button/Button";
-import { Link } from "@probo/ui/src/v2/Button/Link";
+import { ButtonLink } from "@probo/ui/src/v2/Button/ButtonLink";
import { Drawer } from "@probo/ui/src/v2/Drawer/Drawer";
import { DrawerBody } from "@probo/ui/src/v2/Drawer/DrawerBody";
import { DrawerClose } from "@probo/ui/src/v2/Drawer/DrawerClose";
@@ -133,7 +133,7 @@ export function TopBarMobileNav({ identityKey }: TopBarMobileNavProps) {
{TOP_BAR_NAV_ITEMS.map((item) => {
const to = localizedPath(item.to);
return (
-
{t(item.labelKey)}
-
+
);
})}
diff --git a/apps/compliance-portal/src/components/UnsignedNDABanner/UnsignedNDABanner.tsx b/apps/compliance-portal/src/components/UnsignedNDABanner/UnsignedNDABanner.tsx
index 4d846cc9a..e9f57a491 100644
--- a/apps/compliance-portal/src/components/UnsignedNDABanner/UnsignedNDABanner.tsx
+++ b/apps/compliance-portal/src/components/UnsignedNDABanner/UnsignedNDABanner.tsx
@@ -19,7 +19,7 @@
// SOFTWARE.
import { FileTextIcon } from "@phosphor-icons/react";
-import { Link } from "@probo/ui/src/v2/Button/Link";
+import { ButtonLink } from "@probo/ui/src/v2/Button/ButtonLink";
import { Text } from "@probo/ui/src/v2/typography/Text";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -72,14 +72,14 @@ export function UnsignedNDABanner({ compliancePortalKey }: UnsignedNDABannerProp
)}
actions={(
-
{t("nda.unsignedBanner.sign")}
-
+
)}
dismissLabel={t("nda.unsignedBanner.dismiss")}
onDismiss={() => setDismissed(true)}
diff --git a/apps/compliance-portal/src/components/errors/GlobalError.tsx b/apps/compliance-portal/src/components/errors/GlobalError.tsx
index c67916e2a..430cd4572 100644
--- a/apps/compliance-portal/src/components/errors/GlobalError.tsx
+++ b/apps/compliance-portal/src/components/errors/GlobalError.tsx
@@ -14,7 +14,7 @@
import { ForbiddenError, InternalServerError, UnAuthenticatedError } from "@probo/relay";
import { Button } from "@probo/ui/src/v2/Button/Button";
-import { Link } from "@probo/ui/src/v2/Button/Link";
+import { ButtonLink } from "@probo/ui/src/v2/Button/ButtonLink";
import { ErrorState } from "@probo/ui/src/v2/ErrorState/ErrorState";
import { useTranslation } from "react-i18next";
@@ -76,9 +76,9 @@ export function GlobalError({ error, onRetry, fullPage = false }: GlobalErrorPro
description={t(descriptionKey)}
actions={(
<>
-
+
{t("errors.actions.backToCompliancePortal")}
-
+
{onRetry && (