Extract shared Banner for portal notices
Locale and NDA notices shared layout chrome but were not kit Callouts. Pull the full-bleed band into Banner and rename the feature wrappers so the names match the role. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
89
apps/compliance-portal/src/components/Banner/Banner.tsx
Normal file
89
apps/compliance-portal/src/components/Banner/Banner.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// 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<VariantProps<typeof banner>["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 (
|
||||||
|
<aside className={slots.root()} role="status">
|
||||||
|
<div className={slots.inner()}>
|
||||||
|
<div className={slots.content()}>
|
||||||
|
<span aria-hidden className={slots.icon()}>
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
<div className={slots.message()}>{message}</div>
|
||||||
|
<IconButton
|
||||||
|
size={1}
|
||||||
|
variant="ghost"
|
||||||
|
color="neutral"
|
||||||
|
aria-label={dismissLabel}
|
||||||
|
disabled={dismissDisabled}
|
||||||
|
className={slots.dismissMobile()}
|
||||||
|
onClick={onDismiss}
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<div className={slots.actions()}>
|
||||||
|
{actions}
|
||||||
|
<IconButton
|
||||||
|
size={1}
|
||||||
|
variant="ghost"
|
||||||
|
color="neutral"
|
||||||
|
aria-label={dismissLabel}
|
||||||
|
disabled={dismissDisabled}
|
||||||
|
className={slots.dismissDesktop()}
|
||||||
|
onClick={onDismiss}
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,21 +20,33 @@
|
|||||||
|
|
||||||
import { tv } from "tailwind-variants/lite";
|
import { tv } from "tailwind-variants/lite";
|
||||||
|
|
||||||
// Full-bleed locale-mismatch banner under the TopBar: message row + actions,
|
// Full-bleed portal banner under the TopBar: message row + actions, stacking on
|
||||||
// stacking on small screens. Sky (info) so it reads apart from the amber
|
// small screens. Content column matches TopBar / page layout (px-8 + centered
|
||||||
// unsigned-NDA callout when both are visible. Content column matches TopBar /
|
// max-w-5xl). Color tints the band and leading icon (sky = info, amber = warning).
|
||||||
// page layout (px-8 + centered max-w-5xl).
|
export const banner = tv({
|
||||||
export const localeMismatchCallout = tv({
|
|
||||||
slots: {
|
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:
|
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",
|
"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",
|
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",
|
message: "min-w-0 flex-1",
|
||||||
dismissMobile: "md:hidden",
|
dismissMobile: "md:hidden",
|
||||||
actions: "flex shrink-0 items-center gap-2 max-md:flex-col max-md:items-stretch",
|
// Full-width CTAs on small screens; leave the trailing dismiss control alone.
|
||||||
action: "max-md:w-full",
|
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",
|
dismissDesktop: "max-md:hidden",
|
||||||
},
|
},
|
||||||
|
variants: {
|
||||||
|
color: {
|
||||||
|
sky: {
|
||||||
|
root: "bg-sky-3",
|
||||||
|
icon: "text-sky-11",
|
||||||
|
},
|
||||||
|
amber: {
|
||||||
|
root: "bg-amber-3",
|
||||||
|
icon: "text-amber-11",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
@@ -18,14 +18,14 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { GlobeIcon, XIcon } from "@phosphor-icons/react";
|
import { GlobeIcon } from "@phosphor-icons/react";
|
||||||
import { Button } from "@probo/ui/src/v2/Button/Button";
|
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 { Text } from "@probo/ui/src/v2/typography/Text";
|
||||||
import { useDeferredValue, useEffect, useState } from "react";
|
import { useDeferredValue, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { graphql, useFragment } from "react-relay";
|
import { graphql, useFragment } from "react-relay";
|
||||||
|
|
||||||
|
import { Banner } from "#/components/Banner/Banner";
|
||||||
import { DEFAULT_NAMESPACE } from "#/lib/i18n/backend";
|
import { DEFAULT_NAMESPACE } from "#/lib/i18n/backend";
|
||||||
import {
|
import {
|
||||||
isUrlLocale,
|
isUrlLocale,
|
||||||
@@ -36,24 +36,23 @@ import { useChangeLocale } from "#/lib/i18n/useChangeLocale";
|
|||||||
import { useLocale } from "#/lib/i18n/useLocale";
|
import { useLocale } from "#/lib/i18n/useLocale";
|
||||||
import { useUpdateLocale } from "#/lib/i18n/useUpdateLocale";
|
import { useUpdateLocale } from "#/lib/i18n/useUpdateLocale";
|
||||||
|
|
||||||
import type { LocaleMismatchCallout_identity$key } from "./__generated__/LocaleMismatchCallout_identity.graphql";
|
import type { LocaleMismatchBanner_identity$key } from "./__generated__/LocaleMismatchBanner_identity.graphql";
|
||||||
import { localeMismatchCallout } from "./variants";
|
|
||||||
|
|
||||||
const localeMismatchCalloutFragment = graphql`
|
const localeMismatchBannerFragment = graphql`
|
||||||
fragment LocaleMismatchCallout_identity on Identity {
|
fragment LocaleMismatchBanner_identity on Identity {
|
||||||
locale
|
locale
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface LocaleMismatchCalloutProps {
|
interface LocaleMismatchBannerProps {
|
||||||
identityKey: LocaleMismatchCallout_identity$key;
|
identityKey: LocaleMismatchBanner_identity$key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full-bleed notice when the URL locale differs from the signed-in identity
|
// Full-bleed notice when the URL locale differs from the signed-in identity
|
||||||
// preference. Dismissed state is React-only (no localStorage/cookies).
|
// preference. Dismissed state is React-only (no localStorage/cookies).
|
||||||
export function LocaleMismatchCallout({ identityKey }: LocaleMismatchCalloutProps) {
|
export function LocaleMismatchBanner({ identityKey }: LocaleMismatchBannerProps) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const identity = useFragment(localeMismatchCalloutFragment, identityKey);
|
const identity = useFragment(localeMismatchBannerFragment, identityKey);
|
||||||
const urlLocale = useLocale();
|
const urlLocale = useLocale();
|
||||||
const [changeLocale, isChanging] = useChangeLocale();
|
const [changeLocale, isChanging] = useChangeLocale();
|
||||||
const [updateLocale, isUpdating] = useUpdateLocale();
|
const [updateLocale, isUpdating] = useUpdateLocale();
|
||||||
@@ -95,7 +94,6 @@ export function LocaleMismatchCallout({ identityKey }: LocaleMismatchCalloutProp
|
|||||||
const urlLabel = URL_LOCALE_LABELS[urlLocale];
|
const urlLabel = URL_LOCALE_LABELS[urlLocale];
|
||||||
const savedLabel = URL_LOCALE_LABELS[savedLocale];
|
const savedLabel = URL_LOCALE_LABELS[savedLocale];
|
||||||
const busy = isChanging || isUpdating;
|
const busy = isChanging || isUpdating;
|
||||||
const slots = localeMismatchCallout();
|
|
||||||
|
|
||||||
const switchToSaved = () => {
|
const switchToSaved = () => {
|
||||||
void changeLocale(savedLocale, { persist: false });
|
void changeLocale(savedLocale, { persist: false });
|
||||||
@@ -108,32 +106,21 @@ export function LocaleMismatchCallout({ identityKey }: LocaleMismatchCalloutProp
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={slots.root()} role="status">
|
<Banner
|
||||||
<div className={slots.inner()}>
|
color="sky"
|
||||||
<div className={slots.content()}>
|
icon={<GlobeIcon weight="fill" />}
|
||||||
<GlobeIcon weight="fill" className={slots.icon()} aria-hidden />
|
message={(
|
||||||
<Text size={2} color="neutral" highContrast className={slots.message()}>
|
<Text size={2} color="neutral" highContrast>
|
||||||
{t("locale.mismatch.message", { language: urlLabel })}
|
{t("locale.mismatch.message", { language: urlLabel })}
|
||||||
</Text>
|
</Text>
|
||||||
<IconButton
|
)}
|
||||||
size={1}
|
actions={(
|
||||||
variant="ghost"
|
<>
|
||||||
color="neutral"
|
|
||||||
aria-label={t("locale.mismatch.dismiss")}
|
|
||||||
disabled={busy}
|
|
||||||
className={slots.dismissMobile()}
|
|
||||||
onClick={() => setDismissed(true)}
|
|
||||||
>
|
|
||||||
<XIcon />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
<div className={slots.actions()}>
|
|
||||||
<Button
|
<Button
|
||||||
size={1}
|
size={1}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
color="sky"
|
color="sky"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
className={slots.action()}
|
|
||||||
onClick={switchToSaved}
|
onClick={switchToSaved}
|
||||||
>
|
>
|
||||||
{t("locale.mismatch.switchToMine", {
|
{t("locale.mismatch.switchToMine", {
|
||||||
@@ -149,24 +136,15 @@ export function LocaleMismatchCallout({ identityKey }: LocaleMismatchCalloutProp
|
|||||||
color="neutral"
|
color="neutral"
|
||||||
highContrast
|
highContrast
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
className={slots.action()}
|
|
||||||
onClick={adoptUrlLocale}
|
onClick={adoptUrlLocale}
|
||||||
>
|
>
|
||||||
{t("locale.mismatch.useThis", { language: urlLabel })}
|
{t("locale.mismatch.useThis", { language: urlLabel })}
|
||||||
</Button>
|
</Button>
|
||||||
<IconButton
|
</>
|
||||||
size={1}
|
)}
|
||||||
variant="ghost"
|
dismissLabel={t("locale.mismatch.dismiss")}
|
||||||
color="neutral"
|
onDismiss={() => setDismissed(true)}
|
||||||
aria-label={t("locale.mismatch.dismiss")}
|
dismissDisabled={busy}
|
||||||
disabled={busy}
|
/>
|
||||||
className={slots.dismissDesktop()}
|
|
||||||
onClick={() => setDismissed(true)}
|
|
||||||
>
|
|
||||||
<XIcon />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -18,21 +18,20 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { FileTextIcon, XIcon } from "@phosphor-icons/react";
|
import { FileTextIcon } from "@phosphor-icons/react";
|
||||||
import { Link } from "@probo/ui/src/v2/Button/Link";
|
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 { Text } from "@probo/ui/src/v2/typography/Text";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { graphql, useFragment } from "react-relay";
|
import { graphql, useFragment } from "react-relay";
|
||||||
|
|
||||||
|
import { Banner } from "#/components/Banner/Banner";
|
||||||
import { useLocalizedPath } from "#/lib/i18n/useLocale";
|
import { useLocalizedPath } from "#/lib/i18n/useLocale";
|
||||||
|
|
||||||
import type { UnsignedNDACallout_compliancePortal$key } from "./__generated__/UnsignedNDACallout_compliancePortal.graphql";
|
import type { UnsignedNDABanner_compliancePortal$key } from "./__generated__/UnsignedNDABanner_compliancePortal.graphql";
|
||||||
import { unsignedNDACallout } from "./variants";
|
|
||||||
|
|
||||||
const unsignedNDACalloutFragment = graphql`
|
const unsignedNDABannerFragment = graphql`
|
||||||
fragment UnsignedNDACallout_compliancePortal on CompliancePortal {
|
fragment UnsignedNDABanner_compliancePortal on CompliancePortal {
|
||||||
nonDisclosureAgreement {
|
nonDisclosureAgreement {
|
||||||
viewerSignature {
|
viewerSignature {
|
||||||
status
|
status
|
||||||
@@ -41,16 +40,16 @@ const unsignedNDACalloutFragment = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface UnsignedNDACalloutProps {
|
interface UnsignedNDABannerProps {
|
||||||
compliancePortalKey: UnsignedNDACallout_compliancePortal$key;
|
compliancePortalKey: UnsignedNDABanner_compliancePortal$key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full-bleed notice when a signed-in user still needs to complete the portal
|
// Full-bleed notice when a signed-in user still needs to complete the portal
|
||||||
// NDA. Dismissed state is React-only (no localStorage/cookies).
|
// NDA. Dismissed state is React-only (no localStorage/cookies).
|
||||||
export function UnsignedNDACallout({ compliancePortalKey }: UnsignedNDACalloutProps) {
|
export function UnsignedNDABanner({ compliancePortalKey }: UnsignedNDABannerProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const localizedPath = useLocalizedPath();
|
const localizedPath = useLocalizedPath();
|
||||||
const portal = useFragment(unsignedNDACalloutFragment, compliancePortalKey);
|
const portal = useFragment(unsignedNDABannerFragment, compliancePortalKey);
|
||||||
const [dismissed, setDismissed] = useState(false);
|
const [dismissed, setDismissed] = useState(false);
|
||||||
|
|
||||||
const signature = portal.nonDisclosureAgreement?.viewerSignature;
|
const signature = portal.nonDisclosureAgreement?.viewerSignature;
|
||||||
@@ -62,49 +61,28 @@ export function UnsignedNDACallout({ compliancePortalKey }: UnsignedNDACalloutPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ndaHref = `${localizedPath("/nda")}?continue=${encodeURIComponent(window.location.href)}`;
|
const ndaHref = `${localizedPath("/nda")}?continue=${encodeURIComponent(window.location.href)}`;
|
||||||
const slots = unsignedNDACallout();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={slots.root()} role="status">
|
<Banner
|
||||||
<div className={slots.inner()}>
|
color="amber"
|
||||||
<div className={slots.content()}>
|
icon={<FileTextIcon weight="fill" />}
|
||||||
<FileTextIcon weight="fill" className={slots.icon()} aria-hidden />
|
message={(
|
||||||
<Text size={2} color="neutral" highContrast className={slots.message()}>
|
<Text size={2} color="neutral" highContrast>
|
||||||
{t("nda.unsignedBanner.message")}
|
{t("nda.unsignedBanner.message")}
|
||||||
</Text>
|
</Text>
|
||||||
<IconButton
|
)}
|
||||||
size={1}
|
actions={(
|
||||||
variant="ghost"
|
<Link
|
||||||
color="neutral"
|
to={ndaHref}
|
||||||
aria-label={t("nda.unsignedBanner.dismiss")}
|
size={1}
|
||||||
className={slots.dismissMobile()}
|
variant="solid"
|
||||||
onClick={() => setDismissed(true)}
|
color="amber"
|
||||||
>
|
>
|
||||||
<XIcon />
|
{t("nda.unsignedBanner.sign")}
|
||||||
</IconButton>
|
</Link>
|
||||||
</div>
|
)}
|
||||||
<div className={slots.actions()}>
|
dismissLabel={t("nda.unsignedBanner.dismiss")}
|
||||||
<Link
|
onDismiss={() => setDismissed(true)}
|
||||||
to={ndaHref}
|
/>
|
||||||
size={1}
|
|
||||||
variant="solid"
|
|
||||||
color="amber"
|
|
||||||
className={slots.cta()}
|
|
||||||
>
|
|
||||||
{t("nda.unsignedBanner.sign")}
|
|
||||||
</Link>
|
|
||||||
<IconButton
|
|
||||||
size={1}
|
|
||||||
variant="ghost"
|
|
||||||
color="neutral"
|
|
||||||
aria-label={t("nda.unsignedBanner.dismiss")}
|
|
||||||
className={slots.dismissDesktop()}
|
|
||||||
onClick={() => setDismissed(true)}
|
|
||||||
>
|
|
||||||
<XIcon />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
|
||||||
//
|
|
||||||
// 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",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -23,10 +23,10 @@ import type { PreloadedQuery } from "react-relay";
|
|||||||
import { graphql, usePreloadedQuery } from "react-relay";
|
import { graphql, usePreloadedQuery } from "react-relay";
|
||||||
import { Outlet, useMatch } from "react-router";
|
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 { PoweredBy } from "#/components/PoweredBy/PoweredBy";
|
||||||
import { TopBar } from "#/components/TopBar/TopBar";
|
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 { useResumeAccessRequest } from "#/lib/auth/useResumeAccessRequest";
|
||||||
import { useEnsureViewerLocale } from "#/lib/i18n/useEnsureViewerLocale";
|
import { useEnsureViewerLocale } from "#/lib/i18n/useEnsureViewerLocale";
|
||||||
import { SubscribeDialogProvider } from "#/lib/mailingList/SubscribeDialogProvider";
|
import { SubscribeDialogProvider } from "#/lib/mailingList/SubscribeDialogProvider";
|
||||||
@@ -37,11 +37,11 @@ export const mainLayoutQuery = graphql`
|
|||||||
query MainLayoutQuery {
|
query MainLayoutQuery {
|
||||||
viewer {
|
viewer {
|
||||||
__typename
|
__typename
|
||||||
...LocaleMismatchCallout_identity
|
...LocaleMismatchBanner_identity
|
||||||
...useEnsureViewerLocale_identity
|
...useEnsureViewerLocale_identity
|
||||||
}
|
}
|
||||||
currentCompliancePortal {
|
currentCompliancePortal {
|
||||||
...UnsignedNDACallout_compliancePortal
|
...UnsignedNDABanner_compliancePortal
|
||||||
}
|
}
|
||||||
...TopBar_query
|
...TopBar_query
|
||||||
...SubscribeDialogProvider_query
|
...SubscribeDialogProvider_query
|
||||||
@@ -76,10 +76,10 @@ export function MainLayout({ queryRef }: MainLayoutProps) {
|
|||||||
>
|
>
|
||||||
<TopBar queryKey={data} />
|
<TopBar queryKey={data} />
|
||||||
{data.viewer != null
|
{data.viewer != null
|
||||||
? <LocaleMismatchCallout identityKey={data.viewer} />
|
? <LocaleMismatchBanner identityKey={data.viewer} />
|
||||||
: null}
|
: null}
|
||||||
{data.viewer != null && data.currentCompliancePortal != null
|
{data.viewer != null && data.currentCompliancePortal != null
|
||||||
? <UnsignedNDACallout compliancePortalKey={data.currentCompliancePortal} />
|
? <UnsignedNDABanner compliancePortalKey={data.currentCompliancePortal} />
|
||||||
: null}
|
: null}
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
|
|||||||
Reference in New Issue
Block a user