diff --git a/apps/compliance-portal/src/components/Banner/Banner.tsx b/apps/compliance-portal/src/components/Banner/Banner.tsx new file mode 100644 index 000000000..c986f401c --- /dev/null +++ b/apps/compliance-portal/src/components/Banner/Banner.tsx @@ -0,0 +1,89 @@ +// 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 { XIcon } from "@phosphor-icons/react"; +import { IconButton } from "@probo/ui/src/v2/IconButton/IconButton"; +import type { ReactNode } from "react"; +import type { VariantProps } from "tailwind-variants/lite"; + +import { banner } from "./variants"; + +export type BannerProps = { + color: NonNullable["color"]>; + icon: ReactNode; + message: ReactNode; + actions: ReactNode; + dismissLabel: string; + onDismiss: () => void; + dismissDisabled?: boolean; +}; + +// Presentational full-bleed portal banner: tinted band, TopBar-aligned column, +// leading icon + message, actions, and dual dismiss (mobile in the message row, +// desktop beside actions). +export function Banner({ + color, + icon, + message, + actions, + dismissLabel, + onDismiss, + dismissDisabled = false, +}: BannerProps) { + const slots = banner({ color }); + + return ( + + ); +} diff --git a/apps/compliance-portal/src/components/LocaleMismatchCallout/variants.ts b/apps/compliance-portal/src/components/Banner/variants.ts similarity index 65% rename from apps/compliance-portal/src/components/LocaleMismatchCallout/variants.ts rename to apps/compliance-portal/src/components/Banner/variants.ts index 4c2c20a54..7a6f33876 100644 --- a/apps/compliance-portal/src/components/LocaleMismatchCallout/variants.ts +++ b/apps/compliance-portal/src/components/Banner/variants.ts @@ -20,21 +20,33 @@ import { tv } from "tailwind-variants/lite"; -// Full-bleed locale-mismatch banner under the TopBar: message row + actions, -// stacking on small screens. Sky (info) so it reads apart from the amber -// unsigned-NDA callout when both are visible. Content column matches TopBar / -// page layout (px-8 + centered max-w-5xl). -export const localeMismatchCallout = tv({ +// Full-bleed portal banner under the TopBar: message row + actions, stacking on +// small screens. Content column matches TopBar / page layout (px-8 + centered +// max-w-5xl). Color tints the band and leading icon (sky = info, amber = warning). +export const banner = tv({ slots: { - root: "w-full bg-sky-3 px-8 py-2.5 max-md:px-4 max-md:py-3", + root: "w-full px-8 py-2.5 max-md:px-4 max-md:py-3", inner: "mx-auto flex w-full max-w-5xl items-center gap-3 max-md:flex-col max-md:items-stretch max-md:gap-3", content: "flex min-w-0 flex-1 items-start gap-2", - icon: "mt-0.5 size-4 shrink-0 text-sky-11", + icon: "mt-0.5 shrink-0 [&_svg]:size-4", message: "min-w-0 flex-1", dismissMobile: "md:hidden", - actions: "flex shrink-0 items-center gap-2 max-md:flex-col max-md:items-stretch", - action: "max-md:w-full", + // Full-width CTAs on small screens; leave the trailing dismiss control alone. + actions: + "flex shrink-0 items-center gap-2 max-md:flex-col max-md:items-stretch [&>*:not(:last-child)]:max-md:w-full", dismissDesktop: "max-md:hidden", }, + variants: { + color: { + sky: { + root: "bg-sky-3", + icon: "text-sky-11", + }, + amber: { + root: "bg-amber-3", + icon: "text-amber-11", + }, + }, + }, }); diff --git a/apps/compliance-portal/src/components/LocaleMismatchCallout/LocaleMismatchCallout.tsx b/apps/compliance-portal/src/components/LocaleMismatchBanner/LocaleMismatchBanner.tsx similarity index 70% rename from apps/compliance-portal/src/components/LocaleMismatchCallout/LocaleMismatchCallout.tsx rename to apps/compliance-portal/src/components/LocaleMismatchBanner/LocaleMismatchBanner.tsx index 4b6b29861..5e0c1d6b9 100644 --- a/apps/compliance-portal/src/components/LocaleMismatchCallout/LocaleMismatchCallout.tsx +++ b/apps/compliance-portal/src/components/LocaleMismatchBanner/LocaleMismatchBanner.tsx @@ -18,14 +18,14 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { GlobeIcon, XIcon } from "@phosphor-icons/react"; +import { GlobeIcon } from "@phosphor-icons/react"; import { Button } from "@probo/ui/src/v2/Button/Button"; -import { IconButton } from "@probo/ui/src/v2/IconButton/IconButton"; import { Text } from "@probo/ui/src/v2/typography/Text"; import { useDeferredValue, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { graphql, useFragment } from "react-relay"; +import { Banner } from "#/components/Banner/Banner"; import { DEFAULT_NAMESPACE } from "#/lib/i18n/backend"; import { isUrlLocale, @@ -36,24 +36,23 @@ import { useChangeLocale } from "#/lib/i18n/useChangeLocale"; import { useLocale } from "#/lib/i18n/useLocale"; import { useUpdateLocale } from "#/lib/i18n/useUpdateLocale"; -import type { LocaleMismatchCallout_identity$key } from "./__generated__/LocaleMismatchCallout_identity.graphql"; -import { localeMismatchCallout } from "./variants"; +import type { LocaleMismatchBanner_identity$key } from "./__generated__/LocaleMismatchBanner_identity.graphql"; -const localeMismatchCalloutFragment = graphql` - fragment LocaleMismatchCallout_identity on Identity { +const localeMismatchBannerFragment = graphql` + fragment LocaleMismatchBanner_identity on Identity { locale } `; -interface LocaleMismatchCalloutProps { - identityKey: LocaleMismatchCallout_identity$key; +interface LocaleMismatchBannerProps { + identityKey: LocaleMismatchBanner_identity$key; } // Full-bleed notice when the URL locale differs from the signed-in identity // preference. Dismissed state is React-only (no localStorage/cookies). -export function LocaleMismatchCallout({ identityKey }: LocaleMismatchCalloutProps) { +export function LocaleMismatchBanner({ identityKey }: LocaleMismatchBannerProps) { const { t, i18n } = useTranslation(); - const identity = useFragment(localeMismatchCalloutFragment, identityKey); + const identity = useFragment(localeMismatchBannerFragment, identityKey); const urlLocale = useLocale(); const [changeLocale, isChanging] = useChangeLocale(); const [updateLocale, isUpdating] = useUpdateLocale(); @@ -95,7 +94,6 @@ export function LocaleMismatchCallout({ identityKey }: LocaleMismatchCalloutProp const urlLabel = URL_LOCALE_LABELS[urlLocale]; const savedLabel = URL_LOCALE_LABELS[savedLocale]; const busy = isChanging || isUpdating; - const slots = localeMismatchCallout(); const switchToSaved = () => { void changeLocale(savedLocale, { persist: false }); @@ -108,32 +106,21 @@ export function LocaleMismatchCallout({ identityKey }: LocaleMismatchCalloutProp }; return ( - + + )} + dismissLabel={t("locale.mismatch.dismiss")} + onDismiss={() => setDismissed(true)} + dismissDisabled={busy} + /> ); } diff --git a/apps/compliance-portal/src/components/UnsignedNDACallout/UnsignedNDACallout.tsx b/apps/compliance-portal/src/components/UnsignedNDABanner/UnsignedNDABanner.tsx similarity index 51% rename from apps/compliance-portal/src/components/UnsignedNDACallout/UnsignedNDACallout.tsx rename to apps/compliance-portal/src/components/UnsignedNDABanner/UnsignedNDABanner.tsx index 6990a2f5c..59972aaf2 100644 --- a/apps/compliance-portal/src/components/UnsignedNDACallout/UnsignedNDACallout.tsx +++ b/apps/compliance-portal/src/components/UnsignedNDABanner/UnsignedNDABanner.tsx @@ -18,21 +18,20 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { FileTextIcon, XIcon } from "@phosphor-icons/react"; +import { FileTextIcon } from "@phosphor-icons/react"; import { Link } from "@probo/ui/src/v2/Button/Link"; -import { IconButton } from "@probo/ui/src/v2/IconButton/IconButton"; import { Text } from "@probo/ui/src/v2/typography/Text"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { graphql, useFragment } from "react-relay"; +import { Banner } from "#/components/Banner/Banner"; import { useLocalizedPath } from "#/lib/i18n/useLocale"; -import type { UnsignedNDACallout_compliancePortal$key } from "./__generated__/UnsignedNDACallout_compliancePortal.graphql"; -import { unsignedNDACallout } from "./variants"; +import type { UnsignedNDABanner_compliancePortal$key } from "./__generated__/UnsignedNDABanner_compliancePortal.graphql"; -const unsignedNDACalloutFragment = graphql` - fragment UnsignedNDACallout_compliancePortal on CompliancePortal { +const unsignedNDABannerFragment = graphql` + fragment UnsignedNDABanner_compliancePortal on CompliancePortal { nonDisclosureAgreement { viewerSignature { status @@ -41,16 +40,16 @@ const unsignedNDACalloutFragment = graphql` } `; -interface UnsignedNDACalloutProps { - compliancePortalKey: UnsignedNDACallout_compliancePortal$key; +interface UnsignedNDABannerProps { + compliancePortalKey: UnsignedNDABanner_compliancePortal$key; } // Full-bleed notice when a signed-in user still needs to complete the portal // NDA. Dismissed state is React-only (no localStorage/cookies). -export function UnsignedNDACallout({ compliancePortalKey }: UnsignedNDACalloutProps) { +export function UnsignedNDABanner({ compliancePortalKey }: UnsignedNDABannerProps) { const { t } = useTranslation(); const localizedPath = useLocalizedPath(); - const portal = useFragment(unsignedNDACalloutFragment, compliancePortalKey); + const portal = useFragment(unsignedNDABannerFragment, compliancePortalKey); const [dismissed, setDismissed] = useState(false); const signature = portal.nonDisclosureAgreement?.viewerSignature; @@ -62,49 +61,28 @@ export function UnsignedNDACallout({ compliancePortalKey }: UnsignedNDACalloutPr } const ndaHref = `${localizedPath("/nda")}?continue=${encodeURIComponent(window.location.href)}`; - const slots = unsignedNDACallout(); return ( - + } + message={( + + {t("nda.unsignedBanner.message")} + + )} + actions={( + + {t("nda.unsignedBanner.sign")} + + )} + dismissLabel={t("nda.unsignedBanner.dismiss")} + onDismiss={() => setDismissed(true)} + /> ); } diff --git a/apps/compliance-portal/src/components/UnsignedNDACallout/variants.ts b/apps/compliance-portal/src/components/UnsignedNDACallout/variants.ts deleted file mode 100644 index 28058f986..000000000 --- a/apps/compliance-portal/src/components/UnsignedNDACallout/variants.ts +++ /dev/null @@ -1,40 +0,0 @@ -// 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 { tv } from "tailwind-variants/lite"; - -// Full-bleed unsigned-NDA banner under the TopBar: message row + CTA/dismiss, -// stacking on small screens. Amber (warning) so it reads apart from the sky -// locale-mismatch callout when both are visible. Content column matches TopBar / -// page layout (px-8 + centered max-w-5xl). -export const unsignedNDACallout = tv({ - slots: { - root: "w-full bg-amber-3 px-8 py-2.5 max-md:px-4 max-md:py-3", - inner: - "mx-auto flex w-full max-w-5xl items-center gap-3 max-md:flex-col max-md:items-stretch max-md:gap-3", - content: "flex min-w-0 flex-1 items-start gap-2", - icon: "mt-0.5 size-4 shrink-0 text-amber-11", - message: "min-w-0 flex-1", - dismissMobile: "md:hidden", - actions: "flex shrink-0 items-center gap-2 max-md:flex-col max-md:items-stretch", - cta: "max-md:w-full", - dismissDesktop: "max-md:hidden", - }, -}); diff --git a/apps/compliance-portal/src/pages/MainLayout.tsx b/apps/compliance-portal/src/pages/MainLayout.tsx index 1708cd686..82a69a6a7 100644 --- a/apps/compliance-portal/src/pages/MainLayout.tsx +++ b/apps/compliance-portal/src/pages/MainLayout.tsx @@ -23,10 +23,10 @@ import type { PreloadedQuery } from "react-relay"; import { graphql, usePreloadedQuery } from "react-relay"; import { Outlet, useMatch } from "react-router"; -import { LocaleMismatchCallout } from "#/components/LocaleMismatchCallout/LocaleMismatchCallout"; +import { LocaleMismatchBanner } from "#/components/LocaleMismatchBanner/LocaleMismatchBanner"; import { PoweredBy } from "#/components/PoweredBy/PoweredBy"; import { TopBar } from "#/components/TopBar/TopBar"; -import { UnsignedNDACallout } from "#/components/UnsignedNDACallout/UnsignedNDACallout"; +import { UnsignedNDABanner } from "#/components/UnsignedNDABanner/UnsignedNDABanner"; import { useResumeAccessRequest } from "#/lib/auth/useResumeAccessRequest"; import { useEnsureViewerLocale } from "#/lib/i18n/useEnsureViewerLocale"; import { SubscribeDialogProvider } from "#/lib/mailingList/SubscribeDialogProvider"; @@ -37,11 +37,11 @@ export const mainLayoutQuery = graphql` query MainLayoutQuery { viewer { __typename - ...LocaleMismatchCallout_identity + ...LocaleMismatchBanner_identity ...useEnsureViewerLocale_identity } currentCompliancePortal { - ...UnsignedNDACallout_compliancePortal + ...UnsignedNDABanner_compliancePortal } ...TopBar_query ...SubscribeDialogProvider_query @@ -76,10 +76,10 @@ export function MainLayout({ queryRef }: MainLayoutProps) { > {data.viewer != null - ? + ? : null} {data.viewer != null && data.currentCompliancePortal != null - ? + ? : null}