diff --git a/apps/compliance-portal/src/components/BackdropCard/BackdropCard.tsx b/apps/compliance-portal/src/components/BackdropCard/BackdropCard.tsx new file mode 100644 index 000000000..01006d2ae --- /dev/null +++ b/apps/compliance-portal/src/components/BackdropCard/BackdropCard.tsx @@ -0,0 +1,51 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 { backdropCard } from "./variants"; + +interface BackdropCardProps { + // Centered content shown above the backdrop (an icon or a logo box). The node + // owns its own sizing/color; position it above the backdrop with `relative + // z-10`. + media: ReactNode; + // When set, a blurred, magnified copy of this image becomes the backdrop; + // otherwise a dotted texture is shown. + backdropSrc?: string; + // Left-aligned body below the header. + children: ReactNode; +} + +// Soft Card frame with a decorative-backdrop header above a body. Purely +// presentational, so it doubles as the layout for its consumers' skeletons. +export function BackdropCard({ media, backdropSrc, children }: BackdropCardProps) { + const slots = backdropCard(); + + return ( + +
+ {backdropSrc != null + ? + :
} +
+ {media} +
+
{children}
+ + ); +} diff --git a/apps/compliance-portal/src/components/BackdropCard/variants.ts b/apps/compliance-portal/src/components/BackdropCard/variants.ts new file mode 100644 index 000000000..8b1d2a715 --- /dev/null +++ b/apps/compliance-portal/src/components/BackdropCard/variants.ts @@ -0,0 +1,28 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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"; + +// Soft Card frame shared by the commitment / subprocessor cards: a header with a +// decorative backdrop (dotted texture or a blurred, magnified logo) faded toward +// the card surface, with centered media on top, above a left-aligned body. +export const backdropCard = tv({ + slots: { + header: "relative flex w-full items-center overflow-hidden p-8", + blurBackdrop: "pointer-events-none absolute inset-0 size-full scale-150 object-cover opacity-10 blur-lg", + 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", + body: "flex flex-col gap-2 px-8 pb-8", + }, +}); diff --git a/apps/compliance-portal/src/components/CommitmentCard/CommitmentCard.tsx b/apps/compliance-portal/src/components/CommitmentCard/CommitmentCard.tsx index 3db3bff6b..649b807c4 100644 --- a/apps/compliance-portal/src/components/CommitmentCard/CommitmentCard.tsx +++ b/apps/compliance-portal/src/components/CommitmentCard/CommitmentCard.tsx @@ -12,10 +12,9 @@ // 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 { BackdropCard } from "#/components/BackdropCard/BackdropCard"; import { commitmentCard } from "./variants"; @@ -30,24 +29,18 @@ export interface CommitmentCardProps { 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. +// Commitment card, composing the shared BackdropCard frame over its dotted +// backdrop. Region props are placed into the body; 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(); + const { icon: iconSlot } = commitmentCard(); return ( - -
-
-
-
{icon}
-
-
- {eyebrow} - {title} - {description} -
- + {icon}
}> + {eyebrow} + {title} + {description} + ); } diff --git a/apps/compliance-portal/src/components/CommitmentCard/variants.ts b/apps/compliance-portal/src/components/CommitmentCard/variants.ts index 42b46f724..375e93d9e 100644 --- a/apps/compliance-portal/src/components/CommitmentCard/variants.ts +++ b/apps/compliance-portal/src/components/CommitmentCard/variants.ts @@ -14,15 +14,11 @@ 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. +// Commitment card (Figma "Commitment Card"): the leading icon box placed over +// the BackdropCard header. The card frame and dotted backdrop live in +// BackdropCard; this slot only styles the icon container. 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", + icon: "relative z-10 flex size-8 items-center justify-center text-gold-9", }, }); diff --git a/apps/compliance-portal/src/components/EmptyState/EmptyState.tsx b/apps/compliance-portal/src/components/EmptyState/EmptyState.tsx new file mode 100644 index 000000000..f3b18311a --- /dev/null +++ b/apps/compliance-portal/src/components/EmptyState/EmptyState.tsx @@ -0,0 +1,51 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 { emptyState } from "./variants"; + +interface EmptyStateProps { + // Leading phosphor icon, toned and sized by the icon slot. + icon: ReactNode; + title: ReactNode; + description?: ReactNode; + // Optional call to action (e.g. a Button) below the copy. + action?: ReactNode; +} + +// Generic empty-state placeholder shared across list/collection regions: a +// faint icon, a title, optional guidance, and an optional call to action. +// Purely presentational; consumers supply their own copy and action. +export function EmptyState({ icon, title, description, action }: EmptyStateProps) { + const slots = emptyState(); + + return ( +
+ {icon} +
+ + {title} + + {description != null && ( + + {description} + + )} +
+ {action} +
+ ); +} diff --git a/apps/compliance-portal/src/components/EmptyState/variants.ts b/apps/compliance-portal/src/components/EmptyState/variants.ts new file mode 100644 index 000000000..6253c25e5 --- /dev/null +++ b/apps/compliance-portal/src/components/EmptyState/variants.ts @@ -0,0 +1,26 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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"; + +// Empty-state placeholder: a faint icon above a centered title / description +// and an optional call to action. The icon slot tones and sizes any phosphor +// icon passed in, so consumers hand in a bare icon element. +export const emptyState = tv({ + slots: { + root: "flex flex-col items-center gap-5 py-8 text-center", + icon: "text-sand-a8 [&_svg]:size-6", + body: "flex max-w-xs flex-col items-center gap-2", + }, +}); diff --git a/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorListItem.tsx b/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorListItem.tsx index f313edc8c..96d6420ab 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorListItem.tsx +++ b/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorListItem.tsx @@ -14,13 +14,15 @@ import { BuildingsIcon, MapPinSimpleIcon } from "@phosphor-icons/react"; import { faviconUrl } from "@probo/helpers"; -import { Card } from "@probo/ui/src/v2/Card/Card"; import { Text } from "@probo/ui/src/v2/typography/Text"; import { graphql, useFragment } from "react-relay"; +import { BackdropCard } from "#/components/BackdropCard/BackdropCard"; + import { useCountryLabel } from "../_lib/useCountryLabel"; import type { SubprocessorListItem_subprocessor$key } from "./__generated__/SubprocessorListItem_subprocessor.graphql"; +import { subprocessorListItem } from "./variants"; const subprocessorListItemFragment = graphql` fragment SubprocessorListItem_subprocessor on Subprocessor { @@ -42,43 +44,35 @@ export function SubprocessorListItem({ subprocessorKey }: SubprocessorListItemPr const countryLabel = useCountryLabel(); const logoUrl = faviconUrl(subprocessor.websiteUrl); const countries = subprocessor.countries.map(countryLabel).join(", "); + const slots = subprocessorListItem(); return ( - -
- {logoUrl != null && ( - - )} -
-
+ {logoUrl != null - ? - : } + ? + : }
-
-
- - {subprocessor.name} + )} + > + + {subprocessor.name} + + {subprocessor.description != null && subprocessor.description !== "" && ( + + {subprocessor.description} - {subprocessor.description != null && subprocessor.description !== "" && ( - - {subprocessor.description} + )} + {countries !== "" && ( +
+ + + {countries} - )} - {countries !== "" && ( -
- - - {countries} - -
- )} -
- +
+ )} + ); } diff --git a/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorsEmpty.tsx b/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorsEmpty.tsx index 555bc4dfb..fe18fa3a9 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorsEmpty.tsx +++ b/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorsEmpty.tsx @@ -14,9 +14,10 @@ import { ArrowCounterClockwiseIcon, MagnifyingGlassIcon } from "@phosphor-icons/react"; import { Button } from "@probo/ui/src/v2/Button/Button"; -import { Text } from "@probo/ui/src/v2/typography/Text"; import { useTranslation } from "react-i18next"; +import { EmptyState } from "#/components/EmptyState/EmptyState"; + import { useSubprocessorFilters } from "../_lib/useSubprocessorFilters"; // Empty state for the subprocessor list. When filters are active it offers to @@ -26,17 +27,11 @@ export function SubprocessorsEmpty() { const { hasActiveFilters, clear } = useSubprocessorFilters(); return ( -
- -
- - {hasActiveFilters ? t("empty.filteredTitle") : t("empty.title")} - - - {hasActiveFilters ? t("empty.filteredDescription") : t("empty.description")} - -
- {hasActiveFilters && ( + } + title={hasActiveFilters ? t("empty.filteredTitle") : t("empty.title")} + description={hasActiveFilters ? t("empty.filteredDescription") : t("empty.description")} + action={hasActiveFilters && (
+ /> ); } diff --git a/apps/compliance-portal/src/pages/subprocessors/_components/variants.ts b/apps/compliance-portal/src/pages/subprocessors/_components/variants.ts new file mode 100644 index 000000000..64bbb019b --- /dev/null +++ b/apps/compliance-portal/src/pages/subprocessors/_components/variants.ts @@ -0,0 +1,28 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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"; + +// Subprocessor card: the logo box placed over the BackdropCard header (backed by +// a blurred, magnified copy of the logo) and the hosting-regions row shown in +// the body. The card frame and backdrop live in BackdropCard. +export const subprocessorListItem = tv({ + slots: { + logo: "relative z-10 flex size-10 items-center justify-center overflow-hidden rounded-2 bg-sand-1", + logoImage: "size-full object-cover", + logoFallbackIcon: "text-sand-9", + region: "flex items-start gap-1", + regionIcon: "mt-0.5 size-4 shrink-0 text-gold-11", + }, +});