Add documents page to the compliance portal

Build the Trust Center documents page: a unified list of published
documents, uploaded files, and audit reports, grouped into category
sections. An All/Public/Private tab bar filters the list by trust
center visibility.

Expose that filter over the trust v1 API by adding a
TrustCenterVisibility enum and a shared TrustCenterVisibilityFilter
input, wiring it through the documents, audits, and trustCenterFiles
connections down to the existing coredata SQL filters. "All" keeps the
default public+private slice; the other tabs pin a single visibility.

Access controls are display-only for now (auth is handled separately):
authorized or public entries open their exported PDF via the export
mutations, requested entries show a pending state, and everything else
shows an inert Get Access affordance.

Add the v2 Tabs and Toaster kit components (Base UI headless) needed by
the page and mount a toast provider at the app root for mutation
feedback.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-15 18:33:46 +02:00
parent 3c3276a90b
commit 158861ccdd
41 changed files with 1589 additions and 43 deletions

View File

@@ -0,0 +1,70 @@
// 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 { useState } from "react";
import { Tabs } from "./Tabs";
import { TabsIndicator } from "./TabsIndicator";
import { TabsList } from "./TabsList";
import { TabsSkeleton } from "./TabsSkeleton";
import { TabsTab } from "./TabsTab";
export default {
title: "v2/Tabs",
component: Tabs,
};
export function Default() {
return (
<Tabs defaultValue="all">
<TabsList>
<TabsTab value="all">All</TabsTab>
<TabsTab value="public">Public</TabsTab>
<TabsTab value="private">Private</TabsTab>
<TabsIndicator />
</TabsList>
</Tabs>
);
}
export function Controlled() {
const [value, setValue] = useState<string>("all");
return (
<div className="flex flex-col gap-3">
<Tabs value={value} onValueChange={next => setValue(next as string)}>
<TabsList>
<TabsTab value="all">All</TabsTab>
<TabsTab value="public">Public</TabsTab>
<TabsTab value="private">Private</TabsTab>
<TabsIndicator />
</TabsList>
</Tabs>
<span className="text-2 text-sand-11">
Selected:
{" "}
{value}
</span>
</div>
);
}
export function Skeleton() {
return <TabsSkeleton />;
}

View File

@@ -0,0 +1,29 @@
// 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 { Tabs as BaseTabs } from "@base-ui/react/tabs";
// Root of the tabs (Radix "Tabs"). Controlled the same way as Base UI's Tabs:
// `value` / `onValueChange`, or left uncontrolled. See contrib/claude/ui.md.
export type TabsProps = BaseTabs.Root.Props;
export function Tabs(props: TabsProps) {
return <BaseTabs.Root {...props} />;
}

View File

@@ -0,0 +1,36 @@
// 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 { Tabs as BaseTabs } from "@base-ui/react/tabs";
import type { ComponentProps } from "react";
import { tabsIndicator } from "./variants";
export type TabsIndicatorProps = Omit<ComponentProps<typeof BaseTabs.Indicator>, "className"> & {
className?: string;
};
// The sliding underline tracking the active tab, positioned from Base UI's
// `--active-tab-left` / `--active-tab-width` CSS variables.
export function TabsIndicator(props: TabsIndicatorProps) {
const { className, ...rest } = props;
return <BaseTabs.Indicator className={tabsIndicator({ className })} {...rest} />;
}

View File

@@ -0,0 +1,35 @@
// 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 { Tabs as BaseTabs } from "@base-ui/react/tabs";
import type { ComponentProps } from "react";
import { tabsList } from "./variants";
export type TabsListProps = Omit<ComponentProps<typeof BaseTabs.List>, "className"> & {
className?: string;
};
// The tab bar: holds the tab triggers and (optionally) the sliding indicator.
export function TabsList(props: TabsListProps) {
const { className, ...rest } = props;
return <BaseTabs.List className={tabsList({ className })} {...rest} />;
}

View File

@@ -0,0 +1,43 @@
// 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 type { ComponentProps } from "react";
import { tabsSkeleton } from "./variants";
export type TabsSkeletonProps = Omit<ComponentProps<"div">, "children"> & {
// Number of placeholder tabs to render (defaults to 3).
count?: number;
};
// Loading placeholder paired with the tab bar: a row of pulse blocks over the
// shared bottom border.
export function TabsSkeleton(props: TabsSkeletonProps) {
const { count = 3, className, ...rest } = props;
const { root, item } = tabsSkeleton();
return (
<div className={root({ className })} aria-hidden {...rest}>
{Array.from({ length: count }, (_, index) => (
<span key={index} className={item()} style={{ width: `${64 + (index % 3) * 16}px` }} />
))}
</div>
);
}

View File

@@ -0,0 +1,35 @@
// 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 { Tabs as BaseTabs } from "@base-ui/react/tabs";
import type { ComponentProps } from "react";
import { tabsTab } from "./variants";
export type TabsTabProps = Omit<ComponentProps<typeof BaseTabs.Tab>, "className"> & {
className?: string;
};
// A single tab trigger. Selected state is driven by Base UI (`data-active`).
export function TabsTab(props: TabsTabProps) {
const { className, ...rest } = props;
return <BaseTabs.Tab className={tabsTab({ className })} {...rest} />;
}

View File

@@ -0,0 +1,54 @@
// 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";
// Tabs (Radix "Tabs" over Base UI's Tabs). An underlined tab bar whose active
// item is tracked by a sliding indicator positioned from Base UI's
// `--active-tab-*` CSS variables.
export const tabsList = tv({
base: "relative flex items-center gap-1 border-b border-sand-a3",
});
export const tabsTab = tv({
base: [
"relative flex h-full cursor-pointer items-center justify-center gap-2 px-2 py-4 text-2 text-sand-a11",
"select-none outline-none transition-colors",
"hover:text-sand-12",
"focus-visible:ring-2 focus-visible:ring-sand-8 focus-visible:ring-offset-1 focus-visible:ring-offset-sand-1",
"data-active:font-medium data-active:text-sand-12",
"data-disabled:pointer-events-none data-disabled:opacity-50",
],
});
export const tabsIndicator = tv({
base: [
"absolute bottom-0 left-0 h-[2px] w-(--active-tab-width) rounded-1 bg-sand-12",
"translate-x-(--active-tab-left) transition-all duration-200 ease-out",
],
});
export const tabsSkeleton = tv({
slots: {
root: "flex items-center gap-4 border-b border-sand-a3",
item: "my-4 h-5 animate-pulse rounded-1 bg-sand-3",
},
});

View File

@@ -0,0 +1,59 @@
// 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 { Toast } from "@base-ui/react/toast";
import { XIcon } from "@phosphor-icons/react";
import { toaster } from "./variants";
type ToastType = "success" | "error" | "neutral";
function resolveType(type: string | undefined): ToastType {
return type === "success" || type === "error" ? type : "neutral";
}
// Renders the active toasts from the Base UI toast manager. Mount once at the
// app root, inside a `<Toast.Provider>`.
export function Toaster() {
const { toasts } = Toast.useToastManager();
const slots = toaster();
return (
<Toast.Portal>
<Toast.Viewport className={slots.viewport()}>
{toasts.map((toast) => {
const variant = { type: resolveType(toast.type) };
return (
<Toast.Root key={toast.id} toast={toast} className={slots.toast(variant)}>
<div className={slots.content()}>
<Toast.Title className={slots.title(variant)} />
<Toast.Description className={slots.description()} />
</div>
<Toast.Close className={slots.close()} aria-label="Close">
<XIcon />
</Toast.Close>
</Toast.Root>
);
})}
</Toast.Viewport>
</Toast.Portal>
);
}

View File

@@ -0,0 +1,50 @@
// 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";
// Toaster (Radix "Toast" over Base UI's Toast). A bottom-right stack of
// dismissible notifications, colored by type.
export const toaster = tv({
slots: {
viewport: "fixed right-4 bottom-4 z-50 flex w-full max-w-sm flex-col gap-2 outline-none",
toast: [
"pointer-events-auto flex items-start gap-3 rounded-3 border border-sand-a6 bg-sand-1 p-3 shadow-3",
"transition-all duration-200 ease-out",
"data-starting-style:translate-x-4 data-starting-style:opacity-0",
"data-ending-style:translate-x-4 data-ending-style:opacity-0",
],
content: "flex min-w-0 flex-1 flex-col gap-1",
title: "text-2 font-medium text-sand-12",
description: "text-2 break-words text-sand-11",
close: "flex size-5 shrink-0 items-center justify-center rounded-1 text-sand-a10 outline-none transition-colors hover:bg-sand-3 hover:text-sand-12 [&_svg]:size-4",
},
variants: {
type: {
success: { toast: "border-green-6 bg-green-2", title: "text-green-12" },
error: { toast: "border-red-6 bg-red-2", title: "text-red-12" },
neutral: {},
},
},
defaultVariants: {
type: "neutral",
},
});