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",
+ ],
+});