diff --git a/apps/compliance-portal/package.json b/apps/compliance-portal/package.json index 66552bd58..77f07ef94 100644 --- a/apps/compliance-portal/package.json +++ b/apps/compliance-portal/package.json @@ -25,6 +25,8 @@ "relay-runtime": "^21.0.1" }, "devDependencies": { + "@babel/core": "^7.29.0", + "@rolldown/plugin-babel": "^0.2.3", "@tailwindcss/vite": "^4.3.1", "@types/node": "^25.9.3", "@types/react": "^19.2.17", diff --git a/apps/compliance-portal/src/App.tsx b/apps/compliance-portal/src/App.tsx index c63f49ac8..85ffa6398 100644 --- a/apps/compliance-portal/src/App.tsx +++ b/apps/compliance-portal/src/App.tsx @@ -14,8 +14,13 @@ import { RouterProvider } from "react-router"; +import { RelayProvider } from "#/lib/relay/RelayProvider"; import { router } from "#/routes"; export function App() { - return ; + return ( + + + + ); } diff --git a/apps/compliance-portal/src/components/TopBar/TopBar.tsx b/apps/compliance-portal/src/components/TopBar/TopBar.tsx new file mode 100644 index 000000000..35d7edd33 --- /dev/null +++ b/apps/compliance-portal/src/components/TopBar/TopBar.tsx @@ -0,0 +1,116 @@ +// 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 { LockSimpleIcon } from "@phosphor-icons/react"; +import { Avatar } from "@probo/ui/src/v2/Avatar/Avatar"; +import { Button } from "@probo/ui/src/v2/Button/Button"; +import { Link } from "@probo/ui/src/v2/Button/Link"; +import { Text } from "@probo/ui/src/v2/typography/Text"; +import { graphql, useFragment } from "react-relay"; +import { Link as RouterLink, useLocation } from "react-router"; + +import type { TopBar_query$key } from "./__generated__/TopBar_query.graphql"; +import { TopBarUserMenu } from "./TopBarUserMenu"; +import { topBar } from "./variants"; + +const NAV_ITEMS = [ + { to: "/documents", label: "Documents" }, + { to: "/subprocessors", label: "Subprocessors" }, + { to: "/updates", label: "Updates" }, + { to: "/requests", label: "Requests" }, +] as const; + +const topBarFragment = graphql` + fragment TopBar_query on Query { + viewer { + id + ...TopBarUserMenu_identity + } + currentTrustCenter @required(action: THROW) { + organization { + name + logo { + downloadUrl + } + } + } + } +`; + +interface TopBarProps { + queryKey: TopBar_query$key; +} + +function isActive(pathname: string, to: string): boolean { + return pathname === to || pathname.startsWith(`${to}/`); +} + +export function TopBar({ queryKey }: TopBarProps) { + const data = useFragment(topBarFragment, queryKey); + const { pathname } = useLocation(); + + const { organization } = data.currentTrustCenter; + const organizationName = organization.name; + const logoUrl = organization.logo?.downloadUrl ?? undefined; + + const slots = topBar(); + + return ( +
+
+ + + + {organizationName} + + + Compliance Portal + + + +
+ + +
+
+ ); +} diff --git a/apps/compliance-portal/src/components/TopBar/TopBarSkeleton.tsx b/apps/compliance-portal/src/components/TopBar/TopBarSkeleton.tsx new file mode 100644 index 000000000..88470af70 --- /dev/null +++ b/apps/compliance-portal/src/components/TopBar/TopBarSkeleton.tsx @@ -0,0 +1,47 @@ +// 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 { AvatarSkeleton } from "@probo/ui/src/v2/Avatar/AvatarSkeleton"; +import { ButtonSkeleton } from "@probo/ui/src/v2/Button/ButtonSkeleton"; +import { TextSkeleton } from "@probo/ui/src/v2/typography/TextSkeleton"; + +import { topBar } from "./variants"; + +const NAV_ITEM_KEYS = ["documents", "subprocessors", "updates", "requests"] as const; + +// Loading placeholder paired with TopBar: reuses the same layout slots with +// skeleton primitives. Imports no Relay / Base UI, so it renders instantly. +export function TopBarSkeleton() { + const slots = topBar(); + + return ( +
+
+
+ + +
+ +
+ + +
+
+ ); +} diff --git a/apps/compliance-portal/src/components/TopBar/TopBarUserMenu.tsx b/apps/compliance-portal/src/components/TopBar/TopBarUserMenu.tsx new file mode 100644 index 000000000..50e27e5ad --- /dev/null +++ b/apps/compliance-portal/src/components/TopBar/TopBarUserMenu.tsx @@ -0,0 +1,71 @@ +// 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 { CaretDownIcon, SignOutIcon, UserIcon } from "@phosphor-icons/react"; +import { Avatar } from "@probo/ui/src/v2/Avatar/Avatar"; +import { Dropdown } from "@probo/ui/src/v2/Dropdown/Dropdown"; +import { DropdownGroupLabel } from "@probo/ui/src/v2/Dropdown/DropdownGroupLabel"; +import { DropdownItem } from "@probo/ui/src/v2/Dropdown/DropdownItem"; +import { DropdownPopup } from "@probo/ui/src/v2/Dropdown/DropdownPopup"; +import { DropdownSeparator } from "@probo/ui/src/v2/Dropdown/DropdownSeparator"; +import { DropdownTrigger } from "@probo/ui/src/v2/Dropdown/DropdownTrigger"; +import { Text } from "@probo/ui/src/v2/typography/Text"; +import { graphql, useFragment } from "react-relay"; + +import type { TopBarUserMenu_identity$key } from "./__generated__/TopBarUserMenu_identity.graphql"; +import { topBarUserMenuTrigger } from "./variants"; + +const topBarUserMenuFragment = graphql` + fragment TopBarUserMenu_identity on Identity { + fullName + email + } +`; + +interface TopBarUserMenuProps { + identityKey: TopBarUserMenu_identity$key; +} + +export function TopBarUserMenu({ identityKey }: TopBarUserMenuProps) { + const identity = useFragment(topBarUserMenuFragment, identityKey); + + return ( + + + } + /> + + {identity.fullName} + + + + )} + /> + + {identity.email} + + }> + Sign out + + + + ); +} diff --git a/apps/compliance-portal/src/components/TopBar/variants.ts b/apps/compliance-portal/src/components/TopBar/variants.ts new file mode 100644 index 000000000..5a1dcfef4 --- /dev/null +++ b/apps/compliance-portal/src/components/TopBar/variants.ts @@ -0,0 +1,38 @@ +// 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"; + +// Trust Center top navigation bar. Slots are shared by the live TopBar and its +// skeleton so the loading placeholder is structurally identical. +export const topBar = tv({ + slots: { + bar: "flex h-14 items-center bg-sand-1 px-8", + inner: "mx-auto flex w-full max-w-[1024px] items-center gap-10", + brand: "flex items-center gap-2", + logo: "shrink-0", + spacer: "h-px flex-1", + nav: "flex items-center gap-1", + }, +}); + +// Rounded pill that opens the authenticated user menu. +export const topBarUserMenuTrigger = tv({ + base: [ + "flex h-8 items-center gap-2 rounded-full py-1 pr-2.5 pl-1", + "cursor-pointer outline-none transition-colors select-none", + "hover:bg-sand-3 data-[popup-open]:bg-sand-3", + "focus-visible:ring-2 focus-visible:ring-sand-8 focus-visible:ring-offset-1 focus-visible:ring-offset-sand-1", + ], +}); diff --git a/apps/compliance-portal/src/lib/relay/RelayProvider.tsx b/apps/compliance-portal/src/lib/relay/RelayProvider.tsx new file mode 100644 index 000000000..a20559b5f --- /dev/null +++ b/apps/compliance-portal/src/lib/relay/RelayProvider.tsx @@ -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 type { PropsWithChildren } from "react"; +import { RelayEnvironmentProvider } from "react-relay"; + +import { environment } from "#/lib/relay/environment"; + +export function RelayProvider({ children }: PropsWithChildren) { + return ( + + {children} + + ); +} diff --git a/apps/compliance-portal/src/lib/relay/environment.ts b/apps/compliance-portal/src/lib/relay/environment.ts new file mode 100644 index 000000000..553cdd893 --- /dev/null +++ b/apps/compliance-portal/src/lib/relay/environment.ts @@ -0,0 +1,29 @@ +// 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 { makeFetchQuery } from "@probo/relay"; +import { Environment, Network, RecordSource, Store } from "relay-runtime"; + +import { buildEndpoint } from "#/lib/http/endpoint"; + +const store = new Store(new RecordSource(), { + queryCacheExpirationTime: 1 * 60 * 1000, + gcReleaseBufferSize: 20, +}); + +export const environment = new Environment({ + configName: "complianceportal", + network: Network.create(makeFetchQuery(buildEndpoint())), + store, +}); diff --git a/apps/compliance-portal/src/pages/MainLayout.tsx b/apps/compliance-portal/src/pages/MainLayout.tsx index 2111cf97c..a810670ff 100644 --- a/apps/compliance-portal/src/pages/MainLayout.tsx +++ b/apps/compliance-portal/src/pages/MainLayout.tsx @@ -12,11 +12,30 @@ // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. +import type { PreloadedQuery } from "react-relay"; +import { graphql, usePreloadedQuery } from "react-relay"; import { Outlet } from "react-router"; -export default function MainLayout() { +import { TopBar } from "#/components/TopBar/TopBar"; + +import type { MainLayoutQuery } from "./__generated__/MainLayoutQuery.graphql"; + +export const mainLayoutQuery = graphql` + query MainLayoutQuery { + ...TopBar_query + } +`; + +interface MainLayoutProps { + queryRef: PreloadedQuery; +} + +export function MainLayout({ queryRef }: MainLayoutProps) { + const data = usePreloadedQuery(mainLayoutQuery, queryRef); + return (
+
); diff --git a/apps/compliance-portal/src/pages/MainLayoutLoader.tsx b/apps/compliance-portal/src/pages/MainLayoutLoader.tsx new file mode 100644 index 000000000..bdb111449 --- /dev/null +++ b/apps/compliance-portal/src/pages/MainLayoutLoader.tsx @@ -0,0 +1,34 @@ +// 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 { useEffect } from "react"; +import { useQueryLoader } from "react-relay"; + +import type { MainLayoutQuery } from "./__generated__/MainLayoutQuery.graphql"; +import { MainLayout, mainLayoutQuery } from "./MainLayout"; +import { MainLayoutSkeleton } from "./MainLayoutSkeleton"; + +export default function MainLayoutLoader() { + const [queryRef, loadQuery] = useQueryLoader(mainLayoutQuery); + + useEffect(() => { + loadQuery({}); + }, [loadQuery]); + + if (!queryRef) { + return ; + } + + return ; +} diff --git a/apps/compliance-portal/src/pages/MainLayoutSkeleton.tsx b/apps/compliance-portal/src/pages/MainLayoutSkeleton.tsx new file mode 100644 index 000000000..74d354850 --- /dev/null +++ b/apps/compliance-portal/src/pages/MainLayoutSkeleton.tsx @@ -0,0 +1,23 @@ +// 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 { TopBarSkeleton } from "#/components/TopBar/TopBarSkeleton"; + +export function MainLayoutSkeleton() { + return ( +
+ +
+ ); +} diff --git a/apps/compliance-portal/src/pages/PlaceholderPage.tsx b/apps/compliance-portal/src/pages/PlaceholderPage.tsx new file mode 100644 index 000000000..a9b3c0e29 --- /dev/null +++ b/apps/compliance-portal/src/pages/PlaceholderPage.tsx @@ -0,0 +1,30 @@ +// 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 { Heading } from "@probo/ui/src/v2/typography/Heading"; +import { useLocation } from "react-router"; + +// Temporary stub for the Trust Center sections so the top-bar nav links resolve +// and the active state renders. Real pages replace these per section. +export default function PlaceholderPage() { + const { pathname } = useLocation(); + const segment = pathname.replace(/^\//, "").split("/")[0] ?? ""; + const title = segment.charAt(0).toUpperCase() + segment.slice(1); + + return ( +
+ {title} +
+ ); +} diff --git a/apps/compliance-portal/src/routes.tsx b/apps/compliance-portal/src/routes.tsx index 216df1a77..d37a610ee 100644 --- a/apps/compliance-portal/src/routes.tsx +++ b/apps/compliance-portal/src/routes.tsx @@ -17,19 +17,40 @@ import { type AppRoute, routeFromAppRoute } from "@probo/routes"; import { createBrowserRouter } from "react-router"; import { PageSkeleton } from "#/components/skeletons/PageSkeleton"; +import { getPathPrefix } from "#/lib/http/pathPrefix"; const routes = [ { path: "/", Fallback: PageSkeleton, - Component: lazy(() => import("#/pages/MainLayout")), + Component: lazy(() => import("#/pages/MainLayoutLoader")), children: [ { index: true, Component: lazy(() => import("#/pages/HomePage")), }, + { + path: "documents", + Component: lazy(() => import("#/pages/PlaceholderPage")), + }, + { + path: "subprocessors", + Component: lazy(() => import("#/pages/PlaceholderPage")), + }, + { + path: "updates", + Component: lazy(() => import("#/pages/PlaceholderPage")), + }, + { + path: "requests", + Component: lazy(() => import("#/pages/PlaceholderPage")), + }, ], }, ] satisfies AppRoute[]; -export const router = createBrowserRouter(routes.map(routeFromAppRoute)); +// The portal is served under a /trust/{slug} path prefix (or a bare custom +// domain). Match the router basename to that prefix so the routes resolve. +export const router = createBrowserRouter(routes.map(routeFromAppRoute), { + basename: getPathPrefix() || "/", +}); diff --git a/apps/compliance-portal/vite.config.ts b/apps/compliance-portal/vite.config.ts index b19801ff5..dc8c25959 100644 --- a/apps/compliance-portal/vite.config.ts +++ b/apps/compliance-portal/vite.config.ts @@ -14,13 +14,16 @@ import { fileURLToPath, URL } from "node:url"; +import babel from "@rolldown/plugin-babel"; import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; // https://vite.dev/config/ +// @vitejs/plugin-react@6 (Vite 8) no longer runs Babel, so the Relay tagged +// template transform is applied via @rolldown/plugin-babel instead. export default defineConfig({ - plugins: [react(), tailwindcss()], + plugins: [react(), babel({ plugins: ["relay"] }), tailwindcss()], build: { assetsDir: "assets", }, diff --git a/package-lock.json b/package-lock.json index c063dc29d..809727c1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,8 @@ "relay-runtime": "^21.0.1" }, "devDependencies": { + "@babel/core": "^7.29.0", + "@rolldown/plugin-babel": "^0.2.3", "@tailwindcss/vite": "^4.3.1", "@types/node": "^25.9.3", "@types/react": "^19.2.17",