From 5469a960e2ede3a490f76fbe7b8531a9b4ede0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Wed, 29 Jul 2026 17:03:57 +0200 Subject: [PATCH] Truncate long country lists with a popover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AWS-scale region lists blew out subprocessor cards. Cap the visible labels and open the rest from a +N trigger using a new Base UI Popover in the v2 kit. Signed-off-by: Émile Ré --- .../_components/SubprocessorListItem.tsx | 37 +++++++++++-- .../subprocessors/_components/variants.ts | 7 +++ .../pages/subprocessors/_locales/de-DE.json | 3 +- .../pages/subprocessors/_locales/en-US.json | 3 +- .../pages/subprocessors/_locales/es-ES.json | 3 +- .../pages/subprocessors/_locales/fr-FR.json | 3 +- .../pages/subprocessors/_locales/id-ID.json | 3 +- .../pages/subprocessors/_locales/it-IT.json | 3 +- .../pages/subprocessors/_locales/ja-JP.json | 3 +- .../pages/subprocessors/_locales/ko-KR.json | 3 +- .../pages/subprocessors/_locales/pl-PL.json | 3 +- .../pages/subprocessors/_locales/pt-PT.json | 3 +- .../pages/subprocessors/_locales/tr-TR.json | 3 +- .../pages/subprocessors/_locales/uk-UA.json | 3 +- .../pages/subprocessors/_locales/zh-CN.json | 3 +- .../ui/src/v2/Popover/Popover.stories.tsx | 44 +++++++++++++++ packages/ui/src/v2/Popover/Popover.tsx | 30 +++++++++++ packages/ui/src/v2/Popover/PopoverPopup.tsx | 54 +++++++++++++++++++ packages/ui/src/v2/Popover/PopoverTrigger.tsx | 30 +++++++++++ packages/ui/src/v2/Popover/variants.ts | 32 +++++++++++ 20 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 packages/ui/src/v2/Popover/Popover.stories.tsx create mode 100644 packages/ui/src/v2/Popover/Popover.tsx create mode 100644 packages/ui/src/v2/Popover/PopoverPopup.tsx create mode 100644 packages/ui/src/v2/Popover/PopoverTrigger.tsx create mode 100644 packages/ui/src/v2/Popover/variants.ts diff --git a/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorListItem.tsx b/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorListItem.tsx index ddc533b88..3287af160 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorListItem.tsx +++ b/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorListItem.tsx @@ -20,7 +20,11 @@ import { BuildingsIcon, MapPinSimpleIcon } from "@phosphor-icons/react"; import { faviconUrl } from "@probo/helpers"; +import { Popover } from "@probo/ui/src/v2/Popover/Popover"; +import { PopoverPopup } from "@probo/ui/src/v2/Popover/PopoverPopup"; +import { PopoverTrigger } from "@probo/ui/src/v2/Popover/PopoverTrigger"; import { Text } from "@probo/ui/src/v2/typography/Text"; +import { useTranslation } from "react-i18next"; import { graphql, useFragment } from "react-relay"; import { BackdropCard } from "#/components/BackdropCard/BackdropCard"; @@ -30,6 +34,8 @@ import { useCountryLabel } from "../_lib/useCountryLabel"; import type { SubprocessorListItem_subprocessor$key } from "./__generated__/SubprocessorListItem_subprocessor.graphql"; import { subprocessorListItem } from "./variants"; +const VISIBLE_COUNTRY_COUNT = 5; + const subprocessorListItemFragment = graphql` fragment SubprocessorListItem_subprocessor on Subprocessor @throwOnFieldError { name @@ -47,9 +53,11 @@ interface SubprocessorListItemProps { // description, and the hosting regions. export function SubprocessorListItem({ subprocessorKey }: SubprocessorListItemProps) { const subprocessor = useFragment(subprocessorListItemFragment, subprocessorKey); + const { t } = useTranslation("subprocessors"); const countryLabel = useCountryLabel(); const logoUrl = faviconUrl(subprocessor.websiteUrl); - const countries = subprocessor.countries.map(countryLabel).join(", "); + const visibleCodes = subprocessor.countries.slice(0, VISIBLE_COUNTRY_COUNT); + const hiddenCodes = subprocessor.countries.slice(VISIBLE_COUNTRY_COUNT); const slots = subprocessorListItem(); return ( @@ -71,11 +79,34 @@ export function SubprocessorListItem({ subprocessorKey }: SubprocessorListItemPr {subprocessor.description} )} - {countries !== "" && ( + {subprocessor.countries.length > 0 && (
- {countries} + {visibleCodes.map(countryLabel).join(", ")} + {hiddenCodes.length > 0 && ( + <> + {" "} + + + + + {hiddenCodes.length} + + +
+ {hiddenCodes.map(code => ( + + {countryLabel(code)} + + ))} +
+
+
+ + )}
)} diff --git a/apps/compliance-portal/src/pages/subprocessors/_components/variants.ts b/apps/compliance-portal/src/pages/subprocessors/_components/variants.ts index 7db599bb2..343d222fa 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_components/variants.ts +++ b/apps/compliance-portal/src/pages/subprocessors/_components/variants.ts @@ -30,5 +30,12 @@ export const subprocessorListItem = tv({ logoFallbackIcon: "text-sand-9", region: "flex items-start gap-1", regionIcon: "size-4 shrink-0 text-gold-11", + // Button reset so the +N control keeps the Text line box (and pin alignment). + moreTrigger: [ + "inline border-0 bg-transparent p-0 font-medium align-baseline", + "cursor-pointer underline-offset-2 hover:underline", + "outline-none focus-visible:underline", + ], + moreList: "flex flex-wrap gap-x-2 gap-y-1", }, }); diff --git a/apps/compliance-portal/src/pages/subprocessors/_locales/de-DE.json b/apps/compliance-portal/src/pages/subprocessors/_locales/de-DE.json index a91e6c30b..2dc3be188 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_locales/de-DE.json +++ b/apps/compliance-portal/src/pages/subprocessors/_locales/de-DE.json @@ -14,7 +14,8 @@ }, "regions": { "global": "Weltweit", - "eu": "Europäische Union" + "eu": "Europäische Union", + "moreRegions": "{{count}} weitere Regionen anzeigen" }, "categories": { "ANALYTICS": { diff --git a/apps/compliance-portal/src/pages/subprocessors/_locales/en-US.json b/apps/compliance-portal/src/pages/subprocessors/_locales/en-US.json index 571de7e91..63145bebb 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_locales/en-US.json +++ b/apps/compliance-portal/src/pages/subprocessors/_locales/en-US.json @@ -14,7 +14,8 @@ }, "regions": { "global": "Global", - "eu": "European Union" + "eu": "European Union", + "moreRegions": "Show {{count}} more regions" }, "categories": { "ANALYTICS": { diff --git a/apps/compliance-portal/src/pages/subprocessors/_locales/es-ES.json b/apps/compliance-portal/src/pages/subprocessors/_locales/es-ES.json index 915a366d6..f68847d43 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_locales/es-ES.json +++ b/apps/compliance-portal/src/pages/subprocessors/_locales/es-ES.json @@ -14,7 +14,8 @@ }, "regions": { "global": "Internacional", - "eu": "Unión Europea" + "eu": "Unión Europea", + "moreRegions": "Mostrar {{count}} regiones más" }, "categories": { "ANALYTICS": { diff --git a/apps/compliance-portal/src/pages/subprocessors/_locales/fr-FR.json b/apps/compliance-portal/src/pages/subprocessors/_locales/fr-FR.json index 91198127b..fc8c979d4 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_locales/fr-FR.json +++ b/apps/compliance-portal/src/pages/subprocessors/_locales/fr-FR.json @@ -14,7 +14,8 @@ }, "regions": { "global": "International", - "eu": "Union européenne" + "eu": "Union européenne", + "moreRegions": "Afficher {{count}} régions supplémentaires" }, "categories": { "ANALYTICS": { diff --git a/apps/compliance-portal/src/pages/subprocessors/_locales/id-ID.json b/apps/compliance-portal/src/pages/subprocessors/_locales/id-ID.json index 920482ceb..15b4b0e50 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_locales/id-ID.json +++ b/apps/compliance-portal/src/pages/subprocessors/_locales/id-ID.json @@ -14,7 +14,8 @@ }, "regions": { "global": "Global", - "eu": "Uni Eropa" + "eu": "Uni Eropa", + "moreRegions": "Tampilkan {{count}} wilayah lainnya" }, "categories": { "ANALYTICS": { diff --git a/apps/compliance-portal/src/pages/subprocessors/_locales/it-IT.json b/apps/compliance-portal/src/pages/subprocessors/_locales/it-IT.json index 9b4be19e7..cdc603b5e 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_locales/it-IT.json +++ b/apps/compliance-portal/src/pages/subprocessors/_locales/it-IT.json @@ -14,7 +14,8 @@ }, "regions": { "global": "Globale", - "eu": "Unione Europea" + "eu": "Unione Europea", + "moreRegions": "Mostra altre {{count}} regioni" }, "categories": { "ANALYTICS": { diff --git a/apps/compliance-portal/src/pages/subprocessors/_locales/ja-JP.json b/apps/compliance-portal/src/pages/subprocessors/_locales/ja-JP.json index 67041b5a7..1c0343da9 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_locales/ja-JP.json +++ b/apps/compliance-portal/src/pages/subprocessors/_locales/ja-JP.json @@ -14,7 +14,8 @@ }, "regions": { "global": "グローバル", - "eu": "欧州連合" + "eu": "欧州連合", + "moreRegions": "他{{count}}件の地域を表示" }, "categories": { "ANALYTICS": { diff --git a/apps/compliance-portal/src/pages/subprocessors/_locales/ko-KR.json b/apps/compliance-portal/src/pages/subprocessors/_locales/ko-KR.json index efec61126..273a39478 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_locales/ko-KR.json +++ b/apps/compliance-portal/src/pages/subprocessors/_locales/ko-KR.json @@ -14,7 +14,8 @@ }, "regions": { "global": "글로벌", - "eu": "유럽연합" + "eu": "유럽연합", + "moreRegions": "지역 {{count}}개 더 보기" }, "categories": { "ANALYTICS": { diff --git a/apps/compliance-portal/src/pages/subprocessors/_locales/pl-PL.json b/apps/compliance-portal/src/pages/subprocessors/_locales/pl-PL.json index 649dc3231..a2d40944e 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_locales/pl-PL.json +++ b/apps/compliance-portal/src/pages/subprocessors/_locales/pl-PL.json @@ -14,7 +14,8 @@ }, "regions": { "global": "Globalny", - "eu": "Unia Europejska" + "eu": "Unia Europejska", + "moreRegions": "Pokaż {{count}} więcej regionów" }, "categories": { "ANALYTICS": { diff --git a/apps/compliance-portal/src/pages/subprocessors/_locales/pt-PT.json b/apps/compliance-portal/src/pages/subprocessors/_locales/pt-PT.json index ed767b84a..38a79a8f5 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_locales/pt-PT.json +++ b/apps/compliance-portal/src/pages/subprocessors/_locales/pt-PT.json @@ -14,7 +14,8 @@ }, "regions": { "global": "Global", - "eu": "União Europeia" + "eu": "União Europeia", + "moreRegions": "Mostrar mais {{count}} regiões" }, "categories": { "ANALYTICS": { diff --git a/apps/compliance-portal/src/pages/subprocessors/_locales/tr-TR.json b/apps/compliance-portal/src/pages/subprocessors/_locales/tr-TR.json index f45959ed5..d9808aa07 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_locales/tr-TR.json +++ b/apps/compliance-portal/src/pages/subprocessors/_locales/tr-TR.json @@ -14,7 +14,8 @@ }, "regions": { "global": "Küresel", - "eu": "Avrupa Birliği" + "eu": "Avrupa Birliği", + "moreRegions": "{{count}} bölge daha göster" }, "categories": { "ANALYTICS": { diff --git a/apps/compliance-portal/src/pages/subprocessors/_locales/uk-UA.json b/apps/compliance-portal/src/pages/subprocessors/_locales/uk-UA.json index c33d0be86..fad53b8a3 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_locales/uk-UA.json +++ b/apps/compliance-portal/src/pages/subprocessors/_locales/uk-UA.json @@ -14,7 +14,8 @@ }, "regions": { "global": "Глобально", - "eu": "Європейський Союз" + "eu": "Європейський Союз", + "moreRegions": "Показати ще {{count}} регіонів" }, "categories": { "ANALYTICS": { diff --git a/apps/compliance-portal/src/pages/subprocessors/_locales/zh-CN.json b/apps/compliance-portal/src/pages/subprocessors/_locales/zh-CN.json index 09ce619de..14d232465 100644 --- a/apps/compliance-portal/src/pages/subprocessors/_locales/zh-CN.json +++ b/apps/compliance-portal/src/pages/subprocessors/_locales/zh-CN.json @@ -14,7 +14,8 @@ }, "regions": { "global": "全球", - "eu": "欧盟" + "eu": "欧盟", + "moreRegions": "显示另外 {{count}} 个地区" }, "categories": { "ANALYTICS": { diff --git a/packages/ui/src/v2/Popover/Popover.stories.tsx b/packages/ui/src/v2/Popover/Popover.stories.tsx new file mode 100644 index 000000000..063936201 --- /dev/null +++ b/packages/ui/src/v2/Popover/Popover.stories.tsx @@ -0,0 +1,44 @@ +// 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 { Button } from "../Button/Button"; +import { Text } from "../typography/Text"; + +import { Popover } from "./Popover"; +import { PopoverPopup } from "./PopoverPopup"; +import { PopoverTrigger } from "./PopoverTrigger"; + +export default { + title: "v2/Popover", + component: Popover, +}; + +export function Default() { + return ( + + Open popover} /> + + + Popover content goes here. + + + + ); +} diff --git a/packages/ui/src/v2/Popover/Popover.tsx b/packages/ui/src/v2/Popover/Popover.tsx new file mode 100644 index 000000000..a189645b7 --- /dev/null +++ b/packages/ui/src/v2/Popover/Popover.tsx @@ -0,0 +1,30 @@ +// 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 { Popover as BasePopover } from "@base-ui/react/popover"; +import type { ComponentProps } from "react"; + +// Root of the popover (Radix "Popover"). Controlled the same way as Base UI's +// Popover: `open` / `onOpenChange`, or left uncontrolled. See contrib/claude/ui.md. +export type PopoverProps = ComponentProps; + +export function Popover(props: PopoverProps) { + return ; +} diff --git a/packages/ui/src/v2/Popover/PopoverPopup.tsx b/packages/ui/src/v2/Popover/PopoverPopup.tsx new file mode 100644 index 000000000..3380f1dfb --- /dev/null +++ b/packages/ui/src/v2/Popover/PopoverPopup.tsx @@ -0,0 +1,54 @@ +// 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 { Popover as BasePopover } from "@base-ui/react/popover"; +import type { ComponentProps } from "react"; + +import { popoverPopup } from "./variants"; + +export type PopoverPopupProps + = & Omit, "className"> + & { + className?: string; + // Positioner placement passthrough. + side?: ComponentProps["side"]; + align?: ComponentProps["align"]; + sideOffset?: ComponentProps["sideOffset"]; + }; + +// Portal + positioner + styled popup. Content-agnostic — callers compose children. +export function PopoverPopup(props: PopoverPopupProps) { + const { + className, children, + side = "bottom", align = "start", sideOffset = 4, + ...popupProps + } = props; + + return ( + + {/* z-3 on the Positioner so the portaled root wins over in-page z-1. */} + + + {children} + + + + ); +} diff --git a/packages/ui/src/v2/Popover/PopoverTrigger.tsx b/packages/ui/src/v2/Popover/PopoverTrigger.tsx new file mode 100644 index 000000000..feb016110 --- /dev/null +++ b/packages/ui/src/v2/Popover/PopoverTrigger.tsx @@ -0,0 +1,30 @@ +// 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 { Popover as BasePopover } from "@base-ui/react/popover"; +import type { ComponentProps } from "react"; + +// The element that opens the popover. Compose a Button via `render` to style it, +// or pass className/children for a native button trigger. +export type PopoverTriggerProps = ComponentProps; + +export function PopoverTrigger(props: PopoverTriggerProps) { + return ; +} diff --git a/packages/ui/src/v2/Popover/variants.ts b/packages/ui/src/v2/Popover/variants.ts new file mode 100644 index 000000000..e14ee1968 --- /dev/null +++ b/packages/ui/src/v2/Popover/variants.ts @@ -0,0 +1,32 @@ +// 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"; + +// Floating popover surface (Radix "Popover" over Base UI). Same panel look as +// DropdownPopup; content-agnostic so callers compose arbitrary children. + +export const popoverPopup = tv({ + base: [ + "max-w-sm origin-(--transform-origin) rounded-3 bg-sand-1 p-3 shadow-5 outline-none", + "transition-[scale,opacity] duration-150 ease-out data-starting-style:scale-95 data-starting-style:opacity-0", + "data-ending-style:scale-95 data-ending-style:opacity-0", + ], +});