feat(health-check): add TLS verification option to health check configuration
Build and Push CFDM Docker Image / build-and-push (push) Successful in 1m59s
Build and Push CFDM Docker Image / create-release (push) Skipped
Build and Push CFDM Docker Image / update-wiki (push) Successful in 7s

Introduced a new `verify_tls` boolean option in health check configurations across various services, allowing users to specify whether to validate TLS certificates during health checks. Updated related components and services to accommodate this new option, ensuring proper handling in both the backend and frontend. Enhanced tests to validate the new functionality and ensure correct behavior with different configurations.
This commit is contained in:
Denozordec
2026-07-20 17:28:02 +07:00
parent 9783974949
commit 1d497d01c2
123 changed files with 351 additions and 15912 deletions
+5 -4
View File
@@ -20,13 +20,14 @@ Fastify backend: `apps/api/`. Контракты — `@cfdm/shared` (Zod). Front
- Дублирование Zod schemas в `apps/web` — только re-export из `@cfdm/shared`
- Самописные формы без `Field` + RHF + Zod
## shadcn-паттерны
## UI-паттерны (ReUI Frame)
| API-данные | UI |
|------------|-----|
| Список | `Table` / `DataTableCard` |
| Создание | `Card` + `FieldGroup` + RHF |
| Статус | `Badge` variants |
| Список | `ResourcePage` (Frame + DataGrid + Filters) |
| Создание / редактирование | `FormSheet` + `FieldGroup` + RHF |
| Settings | `SettingsShell` + Frame + `SettingRow` |
| Статус | ReUI `Badge` / `StatusBadge` |
| Ошибка | `sonner` `toast.error` |
## Согласованность
+1 -1
View File
@@ -36,7 +36,7 @@ alwaysApply: false
| Forms | [form-7](https://reui.io/preview/base/form-7) |
| Shell | [app-shell-12](https://reui.io/preview/base/app-shell-12) |
**SettingRow:** `FieldSeparator` только между соседними rows; не между toggle-row и nested fields (Health-check).
**SettingRow:** `FieldSeparator` opt-in (`separated`); не между toggle-row и nested fields (Health-check). Settings-секции — отдельные Frame + `gap`, без hairline под PageHeader.
## Иерархия компонентов
-5
View File
@@ -1,7 +1,6 @@
import type { FastifyInstance } from "fastify";
import { z } from "zod";
import { healthCheck } from "../plugins/db.js";
import { getAppSwitcher } from "@cfdm/db";
import * as authService from "../services/auth.js";
export async function healthRoutes(app: FastifyInstance) {
@@ -40,10 +39,6 @@ export async function authRoutes(app: FastifyInstance) {
};
});
app.get("/settings/app-switcher", async (request) => {
return getAppSwitcher(request.server.db);
});
const loginSchema = z.object({
username: z.string(),
password: z.string(),
@@ -85,9 +85,10 @@ function createIpPinnedAgent(
sniHost: string,
useTls: boolean,
timeoutMs: number,
verifyTls: boolean,
): Agent {
const connector = buildConnector({
rejectUnauthorized: false,
rejectUnauthorized: verifyTls,
timeout: timeoutMs,
});
return new Agent({
@@ -126,14 +127,15 @@ async function httpProbe(
const connectAddr = String(ip || "").trim();
const headerHost = (target.hostname || "").trim() || connectAddr;
const url = buildHttpProbeUrl(headerHost, port, pathWithSlash, useTls);
const verifyTls = target.verify_tls === true;
const family = isIP(connectAddr);
const pinToIp = family === 4 || family === 6;
const dispatcher = pinToIp
? createIpPinnedAgent(connectAddr, headerHost, useTls, timeoutMs)
? createIpPinnedAgent(connectAddr, headerHost, useTls, timeoutMs, verifyTls)
: useTls
? createIpPinnedAgent(connectAddr, headerHost, useTls, timeoutMs)
? createIpPinnedAgent(connectAddr, headerHost, useTls, timeoutMs, verifyTls)
: undefined;
try {
@@ -373,6 +375,7 @@ export async function runDomainMonitors(
path: monitor.path,
expected_status: monitor.expected_status,
timeout_ms: monitor.timeout_ms,
verify_tls: false,
};
let result: ProbeResult;
if (monitor.type === "http") {
@@ -40,6 +40,7 @@ export interface ServiceDomainInput {
health_check_expected_status?: number | null;
health_check_interval_sec?: number;
health_check_timeout_ms?: number;
health_check_verify_tls?: boolean;
}
export interface ToggleRequest {
@@ -59,6 +60,7 @@ export interface ServiceGroupBody {
health_check_expected_status?: number | null;
health_check_interval_sec?: number;
health_check_timeout_ms?: number;
health_check_verify_tls?: boolean;
}
export interface UpdateServiceGroupBody {
@@ -74,6 +76,7 @@ export interface UpdateServiceGroupBody {
health_check_expected_status?: number | null;
health_check_interval_sec?: number;
health_check_timeout_ms?: number;
health_check_verify_tls?: boolean;
}
export interface UpdateServiceConfigRequest {
@@ -322,6 +325,7 @@ async function buildView(db: Db, serviceId: number): Promise<ServiceView> {
health_check_expected_status: binding.health_check_expected_status,
health_check_interval_sec: binding.health_check_interval_sec,
health_check_timeout_ms: binding.health_check_timeout_ms,
health_check_verify_tls: binding.health_check_verify_tls,
sync_status: aggregateSyncStatus(statuses),
};
});
@@ -1209,7 +1213,8 @@ export async function updateConfig(
input.health_check_path !== undefined ||
input.health_check_expected_status !== undefined ||
input.health_check_interval_sec !== undefined ||
input.health_check_timeout_ms !== undefined
input.health_check_timeout_ms !== undefined ||
input.health_check_verify_tls !== undefined
) {
repos.updateBindingLbConfig(db, binding.id, {
lb_mode: input.lb_mode,
@@ -1220,6 +1225,7 @@ export async function updateConfig(
health_check_expected_status: input.health_check_expected_status,
health_check_interval_sec: input.health_check_interval_sec,
health_check_timeout_ms: input.health_check_timeout_ms,
health_check_verify_tls: input.health_check_verify_tls,
});
}
@@ -1314,6 +1320,7 @@ export async function createGroup(
health_check_expected_status: body.health_check_expected_status,
health_check_interval_sec: body.health_check_interval_sec,
health_check_timeout_ms: body.health_check_timeout_ms,
health_check_verify_tls: body.health_check_verify_tls,
},
);
}
@@ -1349,6 +1356,7 @@ export async function updateGroup(
health_check_expected_status: body.health_check_expected_status,
health_check_interval_sec: body.health_check_interval_sec,
health_check_timeout_ms: body.health_check_timeout_ms,
health_check_verify_tls: body.health_check_verify_tls,
},
);
if (!domain && group.enabled) {
+27
View File
@@ -55,6 +55,7 @@ describe("health-check probeTarget", () => {
path: null,
expected_status: null,
timeout_ms: 1000,
verify_tls: false,
};
const result = await healthCheckService.probeTarget(target);
expect(result.ok).toBe(true);
@@ -73,6 +74,7 @@ describe("health-check probeTarget", () => {
path: null,
expected_status: null,
timeout_ms: 500,
verify_tls: false,
};
const result = await healthCheckService.probeTarget(target);
expect(result.ok).toBe(false);
@@ -104,6 +106,7 @@ describe("health-check probeTarget", () => {
path: "/",
expected_status: 200,
timeout_ms: 1000,
verify_tls: false,
};
const bindingTarget: HealthCheckTarget = {
...groupTarget,
@@ -138,6 +141,7 @@ describe("health-check probeTarget", () => {
path: null,
expected_status: null,
timeout_ms: 3000,
verify_tls: false,
};
const binding: HealthCheckTarget = {
...group,
@@ -204,4 +208,27 @@ describe("health-check state derivation via runAllChecks", () => {
);
expect(status?.status).toBe("down");
});
it("listHealthCheckTargets includes verify_tls from binding config", async () => {
const { createMemoryDb, repos, runMigrations } = await import("@cfdm/db");
const { db, sqlite } = createMemoryDb();
runMigrations(sqlite);
const domain = repos.createDomain(db, null, "example.com", "zone-id");
const service = repos.createService(db, "Svc", "svc");
const binding = repos.insertBinding(db, domain.id, service.id, "@", null);
repos.updateBindingLbConfig(db, binding.id, {
health_check_enabled: true,
health_check_type: "http",
health_check_port: 443,
health_check_verify_tls: true,
});
repos.replaceBindingIpsWithMeta(db, binding.id, [
{ ip: "10.0.0.1", weight: 1, priority: 1 },
]);
const targets = repos.listHealthCheckTargets(db);
expect(targets).toHaveLength(1);
expect(targets[0]?.verify_tls).toBe(true);
});
});
@@ -1,46 +0,0 @@
import { type CSSProperties } from "react"
import { cn } from "@cfdm/ui/lib/utils"
import { SidebarInset, SidebarProvider } from "@cfdm/ui/components/sidebar"
import { AppSidebar } from "./app-sidebar"
import { PageToolbar } from "./page-toolbar"
import { SiteFooter } from "./site-footer"
import { SiteHeader } from "./site-header"
export function AppShell() {
return (
<SidebarProvider
className={cn(
"[--sidebar:color-mix(in_oklab,var(--color-sidebar)_60%,transparent)]",
"[--sidebar-border:transparent]",
"[--sidebar-accent:color-mix(in_oklab,var(--color-primary)_5%,transparent)]",
"[--sidebar-accent-foreground:var(--color-primary)]"
)}
style={
{
"--sidebar-width": "240px",
} as CSSProperties
}
>
{/* Sidebar */}
<AppSidebar />
<SidebarInset>
{/* Header */}
<SiteHeader />
{/* Content */}
<main className="flex flex-1 flex-col gap-5 px-4 py-4 md:px-6 md:py-5">
<PageToolbar />
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="border-border/70 bg-muted/20 aspect-video rounded-lg border border-dashed" />
<div className="border-border/70 bg-muted/20 aspect-video rounded-lg border border-dashed" />
<div className="border-border/70 bg-muted/20 aspect-video rounded-lg border border-dashed" />
</div>
<div className="border-border/70 bg-muted/20 min-h-80 flex-1 rounded-lg border border-dashed" />
</main>
{/* Footer */}
<SiteFooter />
</SidebarInset>
</SidebarProvider>
)
}
@@ -1,92 +0,0 @@
import { cn } from "@cfdm/ui/lib/utils"
import { Separator } from "@cfdm/ui/components/separator"
import {
SidebarContent,
SidebarFooter,
SidebarHeader,
Sidebar as SidebarRoot,
useSidebar,
} from "@cfdm/ui/components/sidebar"
import { Brand } from "./brand"
import { NavMain } from "./nav-main"
import { NavProjects } from "./nav-projects"
import { NavSecondary } from "./nav-secondary"
function SidebarRailToggle() {
const { state, toggleSidebar } = useSidebar()
const isExpanded = state === "expanded"
return (
<button
type="button"
aria-label={isExpanded ? "Collapse sidebar" : "Expand sidebar"}
onClick={toggleSidebar}
style={{
left: isExpanded ? "var(--sidebar-width)" : "var(--sidebar-width-icon)",
}}
className={cn(
"group/rail fixed top-1/2 z-30 flex h-12 w-7 -translate-y-1/2 cursor-pointer items-center pl-2 outline-none",
"transition-[left] duration-200 ease-linear"
)}
>
<span className="flex flex-col items-center">
<span
aria-hidden="true"
className={cn(
"bg-foreground/40 block h-2 w-0.5 rounded-t-full",
"origin-bottom transition-all duration-100 ease-linear",
isExpanded
? "group-hover/rail:bg-foreground/60 group-hover/rail:rotate-40"
: "group-hover/rail:bg-foreground/60 group-hover/rail:-rotate-40"
)}
/>
<span
aria-hidden="true"
className={cn(
"bg-foreground/40 block h-2 w-0.5 rounded-b-full",
"origin-top transition-all duration-100 ease-linear",
isExpanded
? "group-hover/rail:bg-foreground/60 group-hover/rail:-rotate-40"
: "group-hover/rail:bg-foreground/60 group-hover/rail:rotate-40"
)}
/>
</span>
<span
className={cn(
"border-border bg-foreground text-background absolute left-full -ml-2 rounded-md border px-2 py-0.5 text-[11px] font-medium whitespace-nowrap shadow-xs shadow-black/5",
"pointer-events-none transition-all duration-200 ease-out",
"-translate-x-0.5 opacity-0",
"group-hover/rail:translate-x-0 group-hover/rail:opacity-100"
)}
>
{isExpanded ? "Collapse" : "Expand"}
</span>
</button>
)
}
export function AppSidebar() {
return (
<SidebarRoot collapsible="icon">
<SidebarHeader className="pb-0">
<Brand />
</SidebarHeader>
<SidebarContent>
<NavMain />
<NavProjects />
</SidebarContent>
<SidebarFooter className="px-1! in-data-[state=collapsed]:px-1!">
<div className="px-2">
<Separator />
</div>
<NavSecondary />
</SidebarFooter>
<SidebarRailToggle />
</SidebarRoot>
)
}
@@ -1,54 +0,0 @@
import { Button } from "@cfdm/ui/components/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@cfdm/ui/components/dropdown-menu"
import { APPS } from "./data"
import { LayoutGridIcon } from "lucide-react"
export function AppsMenu() {
return (
<DropdownMenu>
<DropdownMenuTrigger
render={<Button variant="ghost" size="icon" aria-label="Apps" />}
>
<LayoutGridIcon className="size-4.5 transition-colors" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent
side="bottom"
align="end"
sideOffset={8}
className="w-72"
>
<DropdownMenuGroup>
<DropdownMenuLabel>Apps</DropdownMenuLabel>
<div className="grid grid-cols-3 gap-1 p-1">
{APPS.map((app) => (
<DropdownMenuItem
key={app.id}
render={<a href="#" />}
className="h-auto flex-col gap-1.5 py-3 text-center [&_svg]:size-5"
>
<span className="text-muted-foreground">{app.icon}</span>
<span className="text-xs font-medium">{app.label}</span>
</DropdownMenuItem>
))}
</div>
<DropdownMenuSeparator />
<DropdownMenuItem
render={<a href="#" />}
className="justify-center text-sm font-medium"
>
Browse All Apps
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -1,72 +0,0 @@
import { cn } from "@cfdm/ui/lib/utils"
import { Item, ItemMedia } from "@cfdm/ui/components/item"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@cfdm/ui/components/sidebar"
import { BRAND } from "./data"
function AuthLogo({ className }: { className?: string }) {
return (
<Item
className={cn(
"p-0",
"bg-primary text-primary-foreground flex size-8 shrink-0 items-center justify-center",
className
)}
aria-hidden="true"
>
<ItemMedia variant="icon" className="size-auto">
<svg
width="50"
height="50"
viewBox="25.668 25.1352 49.6644 50"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="size-4"
>
<circle cx="70.634" cy="29.8334" r="4.69799" fill="currentColor" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M25.668 57.0144V29.8332C25.668 27.2386 27.7713 25.1352 30.366 25.1352C32.9606 25.1352 35.0639 27.2386 35.0639 29.8332V57.0144C35.0639 61.833 38.9702 65.7392 43.7888 65.7392H57.2116C62.0302 65.7392 65.9364 61.833 65.9364 57.0144V43.7258C65.9364 41.1312 68.0398 39.0278 70.6344 39.0278C73.229 39.0278 75.3324 41.1312 75.3324 43.7258V57.0144C75.3324 67.0222 67.2194 75.1352 57.2116 75.1352H43.7888C33.7809 75.1352 25.668 67.0222 25.668 57.0144Z"
fill="currentColor"
/>
</svg>
</ItemMedia>
</Item>
)
}
function BrandLogo() {
return (
<span className="relative flex size-7 shrink-0 items-center justify-center">
<AuthLogo className="absolute top-1/2 left-1/2 origin-center -translate-x-1/2 -translate-y-1/2 scale-[0.875]" />
</span>
)
}
export function Brand() {
return (
<SidebarMenu>
{/* Sidebar */}
<SidebarMenuItem>
<SidebarMenuButton
size="lg"
render={<a href="#" aria-label={`${BRAND.name} home`} />}
className="gap-2.5"
>
<BrandLogo />
<div className="grid min-w-0 flex-1 text-left leading-tight">
<span className="truncate text-sm font-semibold">{BRAND.name}</span>
<span className="text-muted-foreground truncate text-xs">
{BRAND.tier} plan
</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
)
}
@@ -1,604 +0,0 @@
import { type ReactNode } from "react"
import { SettingsIcon, UsersIcon, BookOpenIcon, HouseIcon, BarChart3Icon, GlobeIcon, ExternalLinkIcon, UserPlusIcon, FlagIcon, CopyIcon, ArchiveIcon, InboxIcon, CalendarIcon, FileTextIcon, ListChecksIcon } from "lucide-react"
// ── Types ──
export type NavChild = {
id: string
label: string
isActive?: boolean
disabled?: boolean
}
export type NavItem = {
id: string
label: string
icon: ReactNode
badge?: string | number
isActive?: boolean
disabled?: boolean
children?: NavChild[]
}
export type NavGroup = {
id: string
label?: string
items: NavItem[]
}
export type SecondaryItem = {
id: string
label: string
icon: ReactNode
}
export type Workspace = {
id: string
name: string
tier: string
description: string
}
export type Project = {
id: string
name: string
progress: number
color: string
}
export type ItemAction = {
id: string
label: string
icon: ReactNode
destructive: boolean
}
export type AppShortcut = {
id: string
label: string
icon: ReactNode
}
export type NotificationType =
| "mention"
| "comment"
| "share"
| "invite"
| "billing"
| "security"
| "feature"
| "deployment"
| "usage"
| "system"
| "task"
| "approval"
| "integration"
| "achievement"
| "feedback"
| "team_join"
| "reaction"
| "review"
| "event"
export type NotificationVariant = "info" | "success" | "warning" | "destructive"
export type NotificationAction = {
label: string
variant?: "default" | "outline" | "destructive"
}
export type NotificationAttachment = {
name: string
size: string
}
export type NotificationAvatar = {
src: string
fallback: string
}
export type NotificationGroupMember = {
src: string
fallback: string
online?: boolean
}
export type NotificationMeta = {
label: string
value: string
color?: string
}
export type Notification = {
id: string
type: NotificationType
variant?: NotificationVariant
title: string
body?: string
time: string
unread?: boolean
avatar?: NotificationAvatar
username?: string
link?: string
badge?: string
actions?: NotificationAction[]
attachment?: NotificationAttachment
meta?: NotificationMeta
progress?: number
progressVariant?: "default" | "success"
avatarGroup?: NotificationGroupMember[]
avatarGroupCount?: number
rating?: number
eventDate?: string
eventTime?: string
}
export type FooterLink = { id: string; label: string }
// ── Brand + account ──
export const BRAND = {
name: "ReUI",
tier: "Pro",
} as const
export const USER = {
name: "Theo Park",
email: "[email protected]",
image:
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=96&h=96&dpr=2&q=80",
initials: "TP",
} as const
export const WORKSPACES: Workspace[] = [
{
id: "reui",
name: "ReUI Labs",
tier: "Enterprise",
description: "Design systems and block releases",
},
{
id: "studio",
name: "ReUI Studio",
tier: "Pro",
description: "Customer workspace builds",
},
{
id: "ops",
name: "ReUI Ops",
tier: "Team",
description: "Billing, support, and reliability",
},
]
// ── Nav secondary ──
export const NAV_SECONDARY: SecondaryItem[] = [
{
id: "settings",
label: "Settings",
icon: (
<SettingsIcon aria-hidden="true" />
),
},
{
id: "invite",
label: "Invite Team",
icon: (
<UsersIcon aria-hidden="true" />
),
},
{
id: "docs",
label: "Documentation",
icon: (
<BookOpenIcon aria-hidden="true" />
),
},
]
// ── Sidebar navigation ──
export const NAV_GROUPS: NavGroup[] = [
{
id: "dashboards",
label: "Dashboards",
items: [
{
id: "overview",
label: "Overview",
icon: (
<HouseIcon aria-hidden="true" />
),
isActive: true,
},
{
id: "analytics",
label: "Analytics",
icon: (
<BarChart3Icon aria-hidden="true" />
),
badge: "Soon",
},
],
},
{
id: "workspace",
label: "Workspace",
items: [
{
id: "audience",
label: "Audience",
icon: (
<UsersIcon aria-hidden="true" />
),
children: [
{ id: "people", label: "People" },
{ id: "segments", label: "Segments" },
{ id: "companies", label: "Companies" },
],
},
{
id: "account",
label: "Account",
icon: (
<SettingsIcon aria-hidden="true" />
),
children: [
{ id: "profile", label: "Profile" },
{ id: "billing", label: "Billing", isActive: true },
{ id: "security", label: "Security" },
{ id: "members", label: "Members" },
],
},
{
id: "network",
label: "Network",
icon: (
<GlobeIcon aria-hidden="true" />
),
children: [
{ id: "directory", label: "Directory" },
{ id: "activity", label: "Activity" },
{ id: "leads", label: "Leads", disabled: true },
],
},
],
},
]
// ── Active projects ──
export const ACTIVE_PROJECTS: Project[] = [
{
id: "billing-portal",
name: "Billing Portal",
progress: 78,
color: "stroke-blue-500",
},
{
id: "audience-sync",
name: "Audience Sync",
progress: 64,
color: "stroke-emerald-500",
},
{
id: "report-builder",
name: "Report Builder",
progress: 42,
color: "stroke-violet-500",
},
{
id: "network-map",
name: "Network Map",
progress: 56,
color: "stroke-orange-500",
},
{
id: "access-review",
name: "Access Review",
progress: 88,
color: "stroke-rose-500",
},
]
export const ITEM_ACTIONS: ItemAction[] = [
{
id: "open",
label: "Open Project",
icon: (
<ExternalLinkIcon aria-hidden="true" />
),
destructive: false,
},
{
id: "owner",
label: "Assign Owner",
icon: (
<UserPlusIcon aria-hidden="true" />
),
destructive: false,
},
{
id: "milestone",
label: "Set Milestone",
icon: (
<FlagIcon aria-hidden="true" />
),
destructive: false,
},
{
id: "duplicate",
label: "Duplicate",
icon: (
<CopyIcon aria-hidden="true" />
),
destructive: false,
},
{
id: "archive",
label: "Archive",
icon: (
<ArchiveIcon aria-hidden="true" />
),
destructive: true,
},
]
// ── Header apps ──
export const APPS: AppShortcut[] = [
{
id: "inbox",
label: "Inbox",
icon: (
<InboxIcon aria-hidden="true" />
),
},
{
id: "calendar",
label: "Calendar",
icon: (
<CalendarIcon aria-hidden="true" />
),
},
{
id: "docs",
label: "Docs",
icon: (
<FileTextIcon aria-hidden="true" />
),
},
{
id: "tasks",
label: "Tasks",
icon: (
<ListChecksIcon aria-hidden="true" />
),
},
{
id: "reports",
label: "Reports",
icon: (
<BarChart3Icon aria-hidden="true" />
),
},
{
id: "settings",
label: "Settings",
icon: (
<SettingsIcon aria-hidden="true" />
),
},
]
// ── Header notifications ──
export const NOTIFICATIONS: Notification[] = [
{
id: "mention",
type: "mention",
title: "mentioned you in",
body: '"Can you review the token cleanup?"',
time: "2m ago",
unread: true,
avatar: {
src: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=96&h=96&dpr=2&q=80",
fallback: "MS",
},
username: "@mira",
link: "Block QA",
},
{
id: "approval",
type: "approval",
variant: "warning",
title: "Release Approval",
body: "App shell updates need signoff before registry publish.",
time: "8m ago",
unread: true,
actions: [
{ label: "Approve", variant: "default" },
{ label: "Review", variant: "outline" },
],
meta: { label: "Priority", value: "High", color: "text-warning" },
},
{
id: "share",
type: "share",
title: "shared",
body: "Dashboard polish brief",
time: "16m ago",
unread: true,
avatar: {
src: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=96&h=96&dpr=2&q=80",
fallback: "LG",
},
username: "@leo",
attachment: {
name: "release-notes.pdf",
size: "2 MB",
},
},
{
id: "task",
type: "task",
variant: "info",
title: "Task Assigned",
body: "Audit dark sidebar contrast in app-shell-18.",
time: "28m ago",
unread: true,
meta: { label: "Due", value: "Today", color: "text-destructive" },
},
{
id: "team",
type: "team_join",
variant: "success",
title: "4 people joined ReUI Labs",
body: "Mira, Leo, Anika and James joined the Pro workspace.",
time: "42m ago",
unread: true,
avatarGroup: [
{
src: "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
fallback: "MR",
online: true,
},
{
src: "https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
fallback: "AR",
},
{
src: "https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
fallback: "JW",
},
],
avatarGroupCount: 1,
},
{
id: "reaction",
type: "reaction",
variant: "info",
title: "Reactions On Your Comment",
body: "Mira and 2 others reacted in #blocks-review.",
time: "1h ago",
avatarGroup: [
{
src: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=96&h=96&dpr=2&q=80",
fallback: "MS",
online: true,
},
{
src: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=96&h=96&dpr=2&q=80",
fallback: "LG",
},
],
avatarGroupCount: 1,
badge: "5 reacts",
},
{
id: "review-score",
type: "review",
variant: "success",
title: "New Review Received",
body: "Anika rated the app shell updates.",
time: "2h ago",
avatar: {
src: "https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
fallback: "AR",
},
username: "@anika",
rating: 4,
},
{
id: "event",
type: "event",
variant: "info",
title: "Release Review",
body: "Production readiness with the design systems team.",
time: "3h ago",
unread: true,
eventDate: "Mar 3, 2026",
eventTime: "10:00 to 11:00 AM",
meta: { label: "Where", value: "Meet", color: "text-info" },
actions: [
{ label: "Join", variant: "default" },
{ label: "Decline", variant: "outline" },
],
},
{
id: "invite",
type: "invite",
variant: "info",
title: "Workspace Invitation",
body: "Priya invited you to ReUI Studio.",
time: "1h ago",
actions: [
{ label: "Accept", variant: "default" },
{ label: "Decline", variant: "outline" },
],
},
{
id: "integration",
type: "integration",
variant: "success",
title: "Linear Connected",
body: "Issue activity now syncs with ReUI notifications.",
time: "2h ago",
},
{
id: "billing",
type: "billing",
variant: "info",
title: "Payment Processed",
body: "Your ReUI Pro subscription was renewed.",
time: "3h ago",
badge: "$49.00",
},
{
id: "achievement",
type: "achievement",
variant: "success",
title: "Monthly Goal Reached",
body: "100 of 100 review tasks completed this month.",
time: "4h ago",
progress: 100,
progressVariant: "success",
meta: { label: "Goal", value: "100 tasks", color: "text-success" },
},
{
id: "security",
type: "security",
variant: "destructive",
title: "New Sign-In Detected",
body: "New login from Mac OS, Chrome.",
time: "Yesterday",
},
{
id: "usage",
type: "usage",
variant: "warning",
title: "API Usage At 80%",
body: "Your workspace used 80% of the monthly API quota.",
time: "5 days ago",
progress: 80,
},
{
id: "system",
type: "system",
variant: "info",
title: "Scheduled Maintenance",
body: "Registry maintenance is planned for Feb 28, 2026 at 2:00 AM UTC.",
time: "1 week ago",
},
]
// ── Footer ──
export const FOOTER_LINKS: FooterLink[] = [
{ id: "docs", label: "Docs" },
{ id: "changelog", label: "Changelog" },
{ id: "support", label: "Support" },
{ id: "privacy", label: "Privacy" },
{ id: "status", label: "Status" },
]
@@ -1,51 +0,0 @@
import { Fragment } from "react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@cfdm/ui/components/dropdown-menu"
import { SidebarMenuAction } from "@cfdm/ui/components/sidebar"
import { ITEM_ACTIONS } from "./data"
import { MoreHorizontalIcon } from "lucide-react"
export function ItemActionMenu({ label }: { label: string }) {
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuAction showOnHover aria-label={`Actions for ${label}`} />
}
>
<MoreHorizontalIcon aria-hidden="true" />
</DropdownMenuTrigger>
{/* Content */}
<DropdownMenuContent
side="right"
align="start"
sideOffset={4}
className="w-44"
>
<DropdownMenuGroup>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
{ITEM_ACTIONS.map((action) => (
<Fragment key={action.id}>
{action.destructive && <DropdownMenuSeparator />}
<DropdownMenuItem
variant={action.destructive ? "destructive" : "default"}
className="[&_svg]:size-3.5 [&_svg]:opacity-60"
>
{action.icon}
{action.label}
</DropdownMenuItem>
</Fragment>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -1,140 +0,0 @@
"use client"
import { useState } from "react"
import { Badge } from "@/components/reui/badge"
import { cn } from "@cfdm/ui/lib/utils"
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@cfdm/ui/components/sidebar"
import { NAV_GROUPS, type NavChild, type NavItem } from "./data"
import { ChevronRightIcon } from "lucide-react"
function NavSubItem({ child }: { child: NavChild }) {
if (child.disabled) {
return (
<SidebarMenuSubItem>
{/* Row */}
<SidebarMenuSubButton
aria-disabled="true"
className="pointer-events-none opacity-60"
>
<span>{child.label}</span>
<Badge className="ml-auto h-5 rounded-full px-2 text-[10px] font-medium">
Soon
</Badge>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
)
}
return (
<SidebarMenuSubItem>
{/* Row */}
<SidebarMenuSubButton render={<a href="#" />} isActive={child.isActive}>
{child.label}
</SidebarMenuSubButton>
</SidebarMenuSubItem>
)
}
// CollapsibleNavItem: isolated so only this item re-renders on open/close.
function CollapsibleNavItem({
item,
}: {
item: NavItem & { children: NavChild[] }
}) {
const [open, setOpen] = useState(() => item.children.some((c) => c.isActive))
return (
<SidebarMenuItem>
{/* Sidebar */}
<SidebarMenuButton
tooltip={item.label}
isActive={item.isActive}
onClick={() => setOpen((prev) => !prev)}
aria-expanded={open}
aria-controls={`subnav-${item.id}`}
>
{item.icon}
<span>{item.label}</span>
<ChevronRightIcon className={cn(
"ml-auto size-4 shrink-0 opacity-60 transition-transform duration-200 group-data-[collapsible=icon]:hidden",
open && "rotate-90"
)} aria-hidden="true" />
</SidebarMenuButton>
{open && (
<SidebarMenuSub id={`subnav-${item.id}`}>
{item.children.map((child) => (
<NavSubItem key={child.id} child={child} />
))}
</SidebarMenuSub>
)}
</SidebarMenuItem>
)
}
function LeafNavItem({ item }: { item: NavItem }) {
return (
<SidebarMenuItem>
{/* Sidebar */}
<SidebarMenuButton
tooltip={item.label}
isActive={item.isActive}
render={<a href="#" />}
>
{item.icon}
<span>{item.label}</span>
{item.badge !== undefined && (
<SidebarMenuBadge className="group-data-[collapsible=icon]:hidden">
<Badge
className={cn(
"h-5 min-w-5 rounded-full px-2 text-[10px] font-medium",
typeof item.badge === "number" && "tabular-nums"
)}
>
{item.badge}
</Badge>
</SidebarMenuBadge>
)}
</SidebarMenuButton>
</SidebarMenuItem>
)
}
export function NavMain() {
return (
<>
{NAV_GROUPS.map((group) => (
<SidebarGroup key={group.id}>
{/* Sidebar */}
{group.label && <SidebarGroupLabel>{group.label}</SidebarGroupLabel>}
<SidebarGroupContent>
<SidebarMenu className="gap-0.25">
{group.items.map((item) =>
item.children ? (
<CollapsibleNavItem
key={item.id}
item={item as NavItem & { children: NavChild[] }}
/>
) : (
<LeafNavItem key={item.id} item={item} />
)
)}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
))}
</>
)
}
@@ -1,113 +0,0 @@
import { useState } from "react"
import { cn } from "@cfdm/ui/lib/utils"
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@cfdm/ui/components/sidebar"
import { ACTIVE_PROJECTS, type Project } from "./data"
import { ItemActionMenu } from "./item-action-menu"
import { ChevronDownIcon } from "lucide-react"
const RADIUS = 6
const CX = 8
const CY = 8
const CIRCUMFERENCE = 2 * Math.PI * RADIUS
function PieProgress({ progress, color }: { progress: number; color: string }) {
const offset =
CIRCUMFERENCE * (1 - Math.min(100, Math.max(0, progress)) / 100)
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
className="shrink-0 -rotate-90 opacity-100!"
aria-hidden="true"
>
<circle
cx={CX}
cy={CY}
r={RADIUS}
fill="none"
className="stroke-muted-foreground/20"
strokeWidth="2.5"
/>
<circle
cx={CX}
cy={CY}
r={RADIUS}
fill="none"
className={color}
strokeWidth="2.5"
strokeDasharray={CIRCUMFERENCE}
strokeDashoffset={offset}
strokeLinecap="round"
/>
</svg>
)
}
function ProjectItem({ project }: { project: Project }) {
return (
<SidebarMenuItem>
{/* Sidebar */}
<SidebarMenuButton
tooltip={`${project.name} · ${project.progress}% complete`}
render={<a href="#" />}
>
<PieProgress progress={project.progress} color={project.color} />
<span className="min-w-0 truncate">{project.name}</span>
</SidebarMenuButton>
{/* Row */}
<ItemActionMenu label={project.name} />
</SidebarMenuItem>
)
}
function ProjectList() {
return (
<SidebarGroupContent id="active-projects-list">
{/* Sidebar */}
<SidebarMenu className="gap-0.25">
{ACTIVE_PROJECTS.map((project) => (
<ProjectItem key={project.id} project={project} />
))}
</SidebarMenu>
</SidebarGroupContent>
)
}
export function NavProjects() {
const [open, setOpen] = useState(true)
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
{/* Sidebar */}
<SidebarGroupLabel
render={
<button
type="button"
onClick={() => setOpen((prev) => !prev)}
aria-expanded={open}
aria-controls="active-projects-list"
/>
}
className="focus-visible:ring-sidebar-ring w-full cursor-pointer focus-visible:ring-2 focus-visible:outline-none"
>
Active Projects
<ChevronDownIcon className={cn(
"ml-auto size-4 shrink-0 opacity-60 transition-transform duration-200",
!open && "-rotate-90"
)} aria-hidden="true" />
</SidebarGroupLabel>
{open && <ProjectList />}
</SidebarGroup>
)
}
@@ -1,36 +0,0 @@
import {
SidebarGroup,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@cfdm/ui/components/sidebar"
import { NAV_SECONDARY, type SecondaryItem } from "./data"
function SecondaryNavItem({ item }: { item: SecondaryItem }) {
return (
<SidebarMenuItem>
<SidebarMenuButton
size="sm"
tooltip={item.label}
render={<a href="#" />}
className="h-8! in-data-[state=collapsed]:h-8! [&_svg]:size-3.5"
>
{item.icon}
<span>{item.label}</span>
</SidebarMenuButton>
</SidebarMenuItem>
)
}
export function NavSecondary() {
return (
<SidebarGroup className="py-1">
<SidebarMenu>
{NAV_SECONDARY.map((item) => (
<SecondaryNavItem key={item.id} item={item} />
))}
</SidebarMenu>
</SidebarGroup>
)
}
@@ -1,460 +0,0 @@
"use client"
import type { ReactNode } from "react"
import { Badge } from "@/components/reui/badge"
import { Rating } from "@/components/reui/rating"
import { cn } from "@cfdm/ui/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage,
} from "@cfdm/ui/components/avatar"
import { Button } from "@cfdm/ui/components/button"
import { ButtonGroup } from "@cfdm/ui/components/button-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@cfdm/ui/components/popover"
import { Progress } from "@cfdm/ui/components/progress"
import { ScrollArea } from "@cfdm/ui/components/scroll-area"
import { Separator } from "@cfdm/ui/components/separator"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@cfdm/ui/components/tooltip"
import {
NOTIFICATIONS,
type Notification,
type NotificationGroupMember,
type NotificationType,
type NotificationVariant,
} from "./data"
import { MessageSquareIcon, PaperclipIcon, UserPlusIcon, CreditCardIcon, ShieldAlertIcon, SparklesIcon, RocketIcon, ActivityIcon, AlertCircleIcon, CircleCheckIcon, LinkIcon, StarIcon, ThumbsUpIcon, UsersIcon, SmileIcon, CalendarIcon, DownloadIcon, CheckCheckIcon, BellIcon } from "lucide-react"
const NOTIFICATION_ICONS: Record<NotificationType, ReactNode> = {
mention: (
<MessageSquareIcon aria-hidden="true" />
),
comment: (
<MessageSquareIcon aria-hidden="true" />
),
share: (
<PaperclipIcon aria-hidden="true" />
),
invite: (
<UserPlusIcon aria-hidden="true" />
),
billing: (
<CreditCardIcon aria-hidden="true" />
),
security: (
<ShieldAlertIcon aria-hidden="true" />
),
feature: (
<SparklesIcon aria-hidden="true" />
),
deployment: (
<RocketIcon aria-hidden="true" />
),
usage: (
<ActivityIcon aria-hidden="true" />
),
system: (
<AlertCircleIcon aria-hidden="true" />
),
task: (
<CircleCheckIcon aria-hidden="true" />
),
approval: (
<CircleCheckIcon aria-hidden="true" />
),
integration: (
<LinkIcon aria-hidden="true" />
),
achievement: (
<StarIcon aria-hidden="true" />
),
feedback: (
<ThumbsUpIcon aria-hidden="true" />
),
team_join: (
<UsersIcon aria-hidden="true" />
),
reaction: (
<SmileIcon aria-hidden="true" />
),
review: (
<StarIcon aria-hidden="true" />
),
event: (
<CalendarIcon aria-hidden="true" />
),
}
const VARIANT_COLORS: Record<NotificationVariant, string> = {
info: "text-info",
success: "text-success",
warning: "text-warning",
destructive: "text-destructive",
}
function NotifAvatarGroup({
members,
count,
}: {
members: NotificationGroupMember[]
count?: number
}) {
return (
<AvatarGroup className="mt-1.5 -space-x-1">
{members.map((member) => (
<Avatar key={member.src} className="size-5">
<AvatarImage src={member.src} alt={member.fallback} />
<AvatarFallback className="text-[9px]">
{member.fallback}
</AvatarFallback>
</Avatar>
))}
{count && count > 0 ? (
<AvatarGroupCount className="size-5 text-[9px] leading-none">
+{count}
</AvatarGroupCount>
) : null}
</AvatarGroup>
)
}
function MetaBadge({
label,
value,
color,
}: {
label: string
value: string
color?: string
}) {
return (
<span className="border-border/60 inline-flex h-[18px] shrink-0 items-center overflow-hidden rounded-full border text-[10px] font-medium">
<span className="bg-muted/60 text-muted-foreground border-border/50 flex h-full items-center border-r px-1.5 leading-none">
{label}
</span>
<span
className={cn(
"flex h-full items-center px-1.5 leading-none",
color || "text-foreground"
)}
>
{value}
</span>
</span>
)
}
function MiniProgress({
value,
variant = "default",
}: {
value: number
variant?: "default" | "success"
}) {
const indicatorColor =
variant === "success"
? "**:data-[slot=progress-indicator]:bg-success"
: value >= 80
? "**:data-[slot=progress-indicator]:bg-warning"
: "**:data-[slot=progress-indicator]:bg-primary"
return (
<div className="bg-muted/55 relative mt-1.5 h-1 overflow-hidden rounded-full">
<div
className="text-muted-foreground pointer-events-none absolute inset-0 opacity-20"
aria-hidden="true"
style={{
backgroundImage:
"repeating-linear-gradient(-45deg, currentColor 0, currentColor 1px, transparent 0, transparent 4px)",
}}
/>
<Progress
value={value}
className={cn(
"absolute inset-0 gap-0",
"**:data-[slot=progress-track]:h-full **:data-[slot=progress-track]:bg-transparent",
"**:data-[slot=progress-indicator]:h-full",
indicatorColor
)}
/>
</div>
)
}
function NotificationItem({ notification }: { notification: Notification }) {
const {
type,
variant = "info",
title,
body,
time,
unread,
avatar,
username,
link,
badge,
actions,
attachment,
meta,
progress,
avatarGroup,
avatarGroupCount,
rating,
eventDate,
eventTime,
progressVariant,
} = notification
const iconColor = VARIANT_COLORS[variant]
const hasAvatar = Boolean(avatar)
const hasActions = Boolean(actions?.length)
const hasInteractiveChildren = hasActions || Boolean(attachment)
const content = (
<>
<div className="shrink-0">
{hasAvatar ? (
<Avatar size="sm">
<AvatarImage src={avatar?.src} alt={avatar?.fallback} />
<AvatarFallback>{avatar?.fallback}</AvatarFallback>
</Avatar>
) : (
<div
className={cn(
"flex size-6 items-center justify-center [&_svg]:size-4",
iconColor
)}
>
{NOTIFICATION_ICONS[type]}
</div>
)}
</div>
<div className="min-w-0 flex-1 space-y-1">
<div className="flex items-start justify-between gap-2">
<p className="text-foreground text-xs leading-snug">
{username && (
<span className="text-primary font-medium">{username}</span>
)}{" "}
{hasAvatar ? (
<>
{title}{" "}
{link && (
<span className="text-primary font-medium">{link}</span>
)}
</>
) : (
<span className="font-medium">{title}</span>
)}
</p>
{badge && (
<Badge
variant="outline"
className="h-5 shrink-0 rounded-full px-1.5 text-[10px]"
>
{badge}
</Badge>
)}
</div>
{body && (
<p className="text-muted-foreground line-clamp-2 text-xs">{body}</p>
)}
{avatarGroup && avatarGroup.length > 0 && (
<NotifAvatarGroup members={avatarGroup} count={avatarGroupCount} />
)}
{rating !== undefined && (
<Rating rating={rating} size="sm" className="mt-0.5" />
)}
{(eventDate || eventTime) && (
<div className="bg-muted/50 border-border/50 text-muted-foreground mt-1 inline-flex items-center gap-1.5 rounded-full border px-2 py-1 text-[11px]">
<CalendarIcon aria-hidden="true" className="size-3 shrink-0 opacity-60" />
{eventDate && (
<span className="text-foreground font-medium">{eventDate}</span>
)}
{eventTime && <span className="opacity-70">{eventTime}</span>}
</div>
)}
{progress !== undefined && (
<MiniProgress value={progress} variant={progressVariant} />
)}
{attachment && (
<div className="flex items-center gap-1 py-1">
<ButtonGroup>
<Button variant="outline" size="xs">
<PaperclipIcon aria-hidden="true" />
{attachment.name}
<span className="opacity-60">({attachment.size})</span>
</Button>
<Button variant="outline" size="icon-xs" aria-label="Download">
<DownloadIcon aria-hidden="true" />
</Button>
</ButtonGroup>
</div>
)}
{hasActions && (
<div className="flex items-center gap-1 py-1">
{actions?.map((action) => (
<Button
key={action.label}
size="xs"
variant={action.variant === "outline" ? "outline" : "default"}
>
{action.label}
</Button>
))}
</div>
)}
<div className="flex items-center gap-2 pt-0.5">
<p className="text-muted-foreground text-[11px]">{time}</p>
{meta && (
<MetaBadge
label={meta.label}
value={meta.value}
color={meta.color}
/>
)}
</div>
</div>
</>
)
return (
<div className="relative">
{unread && (
<span
className="bg-primary ring-background pointer-events-none absolute top-3 right-3 z-10 size-1.5 rounded-full ring-1"
aria-hidden="true"
/>
)}
{hasInteractiveChildren ? (
<div className="flex w-full items-start gap-2 p-2 text-left">
{content}
</div>
) : (
<Button
variant="ghost"
className="h-auto w-full items-start justify-start p-2 text-left whitespace-normal"
>
{content}
</Button>
)}
</div>
)
}
function NotificationsPanel() {
const unreadCount = NOTIFICATIONS.filter(
(notification) => notification.unread
).length
return (
<>
<div className="border-border/40 flex items-center justify-between border-b px-4 py-3">
<div className="flex min-w-0 items-center gap-2">
<span className="truncate text-sm font-semibold">Notifications</span>
{unreadCount > 0 && (
<Badge className="h-5 min-w-5 rounded-full px-1.5 text-[11px]">
{unreadCount}
</Badge>
)}
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="icon-xs"
className="opacity-60 hover:opacity-100"
aria-label="Mark all as read"
/>
}
>
<CheckCheckIcon className="size-3.5" aria-hidden="true" />
</TooltipTrigger>
<TooltipContent>Mark all as read</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="relative flex max-h-full">
<ScrollArea className="max-h-[320px] grow">
{NOTIFICATIONS.map((notification, index) => (
<div key={notification.id}>
<NotificationItem notification={notification} />
{index < NOTIFICATIONS.length - 1 && (
<Separator className="opacity-60" />
)}
</div>
))}
</ScrollArea>
</div>
<div className="border-border/60 border-t px-2 py-1">
<Button variant="ghost" size="sm" className="w-full text-xs">
View All Notifications
</Button>
</div>
</>
)
}
export function NotificationsMenu() {
const unreadCount = NOTIFICATIONS.filter(
(notification) => notification.unread
).length
return (
<Popover>
<PopoverTrigger
aria-label="Open notifications"
render={
<Button
variant="ghost"
size="icon"
aria-label={`Notifications, ${unreadCount} unread`}
/>
}
>
<span className="relative inline-flex">
<BellIcon className="size-4.5 transition-colors" aria-hidden="true" />
{unreadCount > 0 && (
<span
className="bg-primary ring-background absolute -top-1 -right-1 size-1.5 rounded-full ring-2"
aria-hidden="true"
/>
)}
</span>
</PopoverTrigger>
<PopoverContent
side="bottom"
align="end"
sideOffset={8}
className="!bg-background w-80 gap-0 p-0"
>
<NotificationsPanel />
</PopoverContent>
</Popover>
)
}
@@ -1,27 +0,0 @@
import { Button } from "@cfdm/ui/components/button"
import { DownloadIcon, PlusIcon } from "lucide-react"
export function PageToolbar() {
return (
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-col gap-0.5">
<h1 className="text-foreground text-xl leading-6 font-semibold tracking-tight">
Overview
</h1>
<p className="text-muted-foreground text-sm">
Your workspace at a glance.
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<DownloadIcon aria-hidden="true" />
Export
</Button>
<Button size="sm">
<PlusIcon aria-hidden="true" />
New Report
</Button>
</div>
</div>
)
}
@@ -1,64 +0,0 @@
import { useEffect, useId, useState } from "react"
import { Button } from "@cfdm/ui/components/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@cfdm/ui/components/dialog"
import { Input } from "@cfdm/ui/components/input"
import { SearchIcon } from "lucide-react"
export function SearchMenu() {
const [open, setOpen] = useState(false)
const searchInputId = useId()
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (event.key.toLowerCase() === "k" && (event.metaKey || event.ctrlKey)) {
event.preventDefault()
setOpen(true)
}
}
window.addEventListener("keydown", onKeyDown)
return () => window.removeEventListener("keydown", onKeyDown)
}, [])
return (
<>
<Button
type="button"
variant="ghost"
size="icon"
aria-label="Search"
aria-haspopup="dialog"
aria-expanded={open}
onClick={() => setOpen(true)}
>
<SearchIcon className="size-4.5 transition-colors" aria-hidden="true" />
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogHeader className="sr-only">
<DialogTitle>Search</DialogTitle>
<DialogDescription>Search your workspace content.</DialogDescription>
</DialogHeader>
<DialogContent className="max-w-md px-4 py-2 **:data-[slot=dialog-close]:top-3 **:data-[slot=dialog-close]:right-3 **:data-[slot=dialog-close]:opacity-60">
<div className="relative flex items-center gap-3">
<SearchIcon aria-hidden="true" className="pointer-events-none size-4 opacity-60 select-none" />
<Input
id={searchInputId}
className="h-10 border-none p-0 shadow-none outline-none focus-visible:ring-0"
autoFocus
placeholder="Type to search..."
aria-label="Search"
/>
</div>
</DialogContent>
</Dialog>
</>
)
}
@@ -1,26 +0,0 @@
import { FOOTER_LINKS } from "./data"
export function SiteFooter() {
return (
<footer className="border-t px-4 py-2.5 md:px-6">
<div className="flex flex-col items-center justify-between gap-2 text-xs md:flex-row">
<div className="text-muted-foreground flex items-center gap-1.5">
<span>2026 ©</span>
<a
href="#"
className="text-secondary-foreground hover:text-primary font-medium"
>
ReUI
</a>
</div>
<nav className="text-muted-foreground flex flex-wrap items-center justify-center gap-x-3 gap-y-1">
{FOOTER_LINKS.map((link) => (
<a key={link.id} href="#" className="hover:text-primary">
{link.label}
</a>
))}
</nav>
</div>
</footer>
)
}
@@ -1,44 +0,0 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@cfdm/ui/components/breadcrumb"
import { SidebarTrigger } from "@cfdm/ui/components/sidebar"
import { AppsMenu } from "./apps-menu"
import { NotificationsMenu } from "./notifications-menu"
import { SearchMenu } from "./search-menu"
import { UserMenu } from "./user-menu"
export function SiteHeader() {
return (
<header className="bg-background sticky top-0 z-10 flex h-12 shrink-0 items-center gap-2 border-b px-4 md:px-6">
<div className="flex min-w-0 items-center gap-2">
<SidebarTrigger className="-ml-1 md:hidden" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink render={<a href="#" />}>
Dashboards
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Overview</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="text-muted-foreground [&_button_svg]:text-muted-foreground [&_button:active_svg]:text-foreground! [&_button:hover>span>svg]:text-foreground! [&_button:hover>svg]:text-foreground! [&_button[aria-expanded=true]_svg]:text-foreground! [&_button[data-popup-open]_svg]:text-foreground! ml-auto flex items-center gap-2">
<SearchMenu />
<NotificationsMenu />
<AppsMenu />
<UserMenu />
</div>
</header>
)
}
@@ -1,340 +0,0 @@
"use client"
import { useEffect, useState, type ComponentType } from "react"
import { useTheme } from "next-themes"
import { cn } from "@cfdm/ui/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@cfdm/ui/components/avatar"
import { Button } from "@cfdm/ui/components/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@cfdm/ui/components/dropdown-menu"
import { USER, WORKSPACES, type Workspace } from "./data"
import { SunIcon, MoonIcon, MonitorIcon, CheckIcon, PlusIcon, UserIcon, CreditCardIcon, SettingsIcon, PaletteIcon, LogOutIcon } from "lucide-react"
function ReuiLogo({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 28 28"
fill="none"
width="100%"
height="100%"
className={className}
aria-hidden="true"
>
<defs>
<linearGradient
id="sb12-reui"
x1="0"
y1="0"
x2="28"
y2="28"
gradientUnits="userSpaceOnUse"
>
<stop offset="0%" stopColor="#6366f1" />
<stop offset="52%" stopColor="#8b5cf6" />
<stop offset="100%" stopColor="#ec4899" />
</linearGradient>
</defs>
<circle cx="14" cy="14" r="14" fill="url(#sb12-reui)" />
</svg>
)
}
function StudioLogo({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 28 28"
fill="none"
width="100%"
height="100%"
className={className}
aria-hidden="true"
>
<defs>
<linearGradient
id="sb12-studio"
x1="0"
y1="0"
x2="28"
y2="28"
gradientUnits="userSpaceOnUse"
>
<stop offset="0%" stopColor="#0ea5e9" />
<stop offset="50%" stopColor="#06b6d4" />
<stop offset="100%" stopColor="#10b981" />
</linearGradient>
</defs>
<circle cx="14" cy="14" r="14" fill="url(#sb12-studio)" />
</svg>
)
}
function OpsLogo({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 28 28"
fill="none"
width="100%"
height="100%"
className={className}
aria-hidden="true"
>
<defs>
<linearGradient
id="sb12-ops"
x1="0"
y1="0"
x2="28"
y2="28"
gradientUnits="userSpaceOnUse"
>
<stop offset="0%" stopColor="#f97316" />
<stop offset="50%" stopColor="#f59e0b" />
<stop offset="100%" stopColor="#84cc16" />
</linearGradient>
</defs>
<circle cx="14" cy="14" r="14" fill="url(#sb12-ops)" />
</svg>
)
}
const WORKSPACE_LOGOS: Record<string, ComponentType<{ className?: string }>> = {
reui: ReuiLogo,
studio: StudioLogo,
ops: OpsLogo,
}
const THEMES = [
{
value: "light",
label: "Light",
icon: (
<SunIcon className="size-3.5" aria-hidden="true" />
),
},
{
value: "dark",
label: "Dark",
icon: (
<MoonIcon className="size-3.5" aria-hidden="true" />
),
},
{
value: "system",
label: "System",
icon: (
<MonitorIcon className="size-3.5" aria-hidden="true" />
),
},
]
function ThemeSegmentedToggle() {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
const currentTheme = mounted ? (theme ?? "system") : "system"
return (
<div
role="radiogroup"
aria-label="Theme"
className="bg-muted/60 inline-flex items-center gap-0.5 rounded-full p-0.5"
>
{THEMES.map(({ value, label, icon }) => {
const isActive = currentTheme === value
return (
<Button
key={value}
type="button"
role="radio"
aria-checked={isActive}
aria-label={label}
variant="ghost"
size="icon-xs"
onClick={() => setTheme(value)}
className={cn(
"rounded-full",
isActive
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
>
{icon}
</Button>
)
})}
</div>
)
}
function WorkspaceAvatar({
workspace,
className,
}: {
workspace: Workspace
className?: string
}) {
const LogoComponent = WORKSPACE_LOGOS[workspace.id]
if (LogoComponent) {
return <LogoComponent className={cn("shrink-0", className)} />
}
return (
<Avatar className={cn("shrink-0", className)}>
<AvatarFallback className="bg-background border-border text-foreground border text-sm font-medium">
{workspace.name.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
)
}
function WorkspaceItem({
workspace,
isActive,
onSelect,
}: {
workspace: Workspace
isActive: boolean
onSelect: (id: string) => void
}) {
return (
<DropdownMenuItem onClick={() => onSelect(workspace.id)}>
<WorkspaceAvatar workspace={workspace} className="size-5" />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm font-medium">{workspace.name}</span>
<span className="text-muted-foreground truncate text-xs">
{workspace.tier}
</span>
</div>
{isActive && (
<CheckIcon className="ml-auto size-3.5 shrink-0 opacity-60" aria-hidden="true" />
)}
</DropdownMenuItem>
)
}
export function UserMenu() {
const [activeWorkspaceId, setActiveWorkspaceId] = useState(WORKSPACES[0].id)
const activeWorkspace =
WORKSPACES.find((workspace) => workspace.id === activeWorkspaceId) ??
WORKSPACES[0]
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="ghost" size="icon" aria-label="Open user menu" />
}
>
<Avatar className="size-6">
<AvatarImage src={USER.image} alt={USER.name} />
<AvatarFallback className="text-[9px]">
{USER.initials}
</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent
side="bottom"
align="end"
sideOffset={8}
className="!bg-background w-64"
>
<DropdownMenuGroup>
<DropdownMenuLabel className="flex items-center gap-2 py-2">
<Avatar className="size-6">
<AvatarImage src={USER.image} alt={USER.name} />
<AvatarFallback className="text-[9px]">
{USER.initials}
</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-col">
<span className="text-foreground truncate text-sm font-semibold">
{USER.name}
</span>
<span className="text-muted-foreground truncate text-xs">
{USER.email}
</span>
</div>
</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="text-muted-foreground text-xs font-normal">
Organizations
</DropdownMenuLabel>
{WORKSPACES.map((workspace) => (
<WorkspaceItem
key={workspace.id}
workspace={workspace}
isActive={activeWorkspaceId === workspace.id}
onSelect={setActiveWorkspaceId}
/>
))}
<DropdownMenuItem>
<PlusIcon aria-hidden="true" className="mx-0.5" />
New Organization
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="text-muted-foreground text-xs font-normal">
Account
</DropdownMenuLabel>
<DropdownMenuItem>
<UserIcon aria-hidden="true" />
Profile
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCardIcon aria-hidden="true" />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<SettingsIcon aria-hidden="true" />
Preferences
</DropdownMenuItem>
<DropdownMenuItem className="cursor-default focus:bg-transparent!">
<PaletteIcon aria-hidden="true" />
Theme
<div className="ml-auto">
<ThemeSegmentedToggle />
</div>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<LogOutIcon aria-hidden="true" />
Sign Out
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -1,5 +0,0 @@
import { AppShell } from "./components/app-shell"
export function Page() {
return <AppShell />
}
@@ -1,34 +0,0 @@
import { cn } from "@cfdm/ui/lib/utils"
import { Item, ItemMedia } from "@cfdm/ui/components/item"
export function AuthLogo({ className }: { className?: string }) {
return (
<Item
className={cn(
"p-0",
"bg-primary text-primary-foreground flex size-8 shrink-0 items-center justify-center",
className
)}
aria-hidden="true"
>
<ItemMedia variant="icon" className="size-auto">
<svg
width="50"
height="50"
viewBox="25.668 25.1352 49.6644 50"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="size-4"
>
<circle cx="70.634" cy="29.8334" r="4.69799" fill="currentColor" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M25.668 57.0144V29.8332C25.668 27.2386 27.7713 25.1352 30.366 25.1352C32.9606 25.1352 35.0639 27.2386 35.0639 29.8332V57.0144C35.0639 61.833 38.9702 65.7392 43.7888 65.7392H57.2116C62.0302 65.7392 65.9364 61.833 65.9364 57.0144V43.7258C65.9364 41.1312 68.0398 39.0278 70.6344 39.0278C73.229 39.0278 75.3324 41.1312 75.3324 43.7258V57.0144C75.3324 67.0222 67.2194 75.1352 57.2116 75.1352H43.7888C33.7809 75.1352 25.668 67.0222 25.668 57.0144Z"
fill="currentColor"
/>
</svg>
</ItemMedia>
</Item>
)
}
@@ -1,58 +0,0 @@
import { type ComponentProps } from "react"
import { Frame, FramePanel } from "@/components/reui/frame"
import { AUTH13_SIDEBAR_IMAGE_DARK, AUTH13_SIDEBAR_IMAGE_LIGHT } from "./data"
import { LoginForm } from "./login-form"
type FormSubmitHandler = NonNullable<ComponentProps<"form">["onSubmit"]>
type FormSubmitEvent = Parameters<FormSubmitHandler>[0]
function SidebarFrame() {
return (
<Frame
spacing="lg"
className="border-border/70 h-full w-full bg-transparent"
>
{/* Content */}
<FramePanel className="border-border/70 h-full min-h-[32rem] overflow-hidden p-0 shadow-none before:hidden">
{/* Light */}
<img
src={AUTH13_SIDEBAR_IMAGE_LIGHT}
alt=""
aria-hidden="true"
className="h-full w-full object-cover dark:hidden"
/>
{/* Dark */}
<img
src={AUTH13_SIDEBAR_IMAGE_DARK}
alt=""
aria-hidden="true"
className="hidden h-full w-full object-cover dark:block"
/>
</FramePanel>
</Frame>
)
}
export function Auth() {
function handleSubmit(event: FormSubmitEvent) {
event.preventDefault()
}
return (
<div className="min-h-svh w-full lg:h-svh lg:overflow-hidden">
{/* Grid */}
<div className="grid min-h-svh w-full gap-8 px-4 py-4 sm:px-6 sm:py-6 lg:h-svh lg:min-h-0 lg:grid-cols-[32rem_minmax(0,1fr)] lg:gap-8 lg:px-0 lg:py-0">
<section className="order-2 flex min-h-[calc(100svh-2rem)] items-center justify-center px-4 py-8 sm:px-8 lg:order-2 lg:h-full lg:min-h-0 lg:px-12 lg:py-0 xl:px-16 2xl:px-20">
<LoginForm onSubmit={handleSubmit} />
</section>
<aside className="order-1 flex min-h-full items-stretch justify-start lg:order-1 lg:h-full lg:min-h-0 lg:w-full lg:self-stretch lg:justify-self-start lg:py-7 lg:pl-7">
<div className="w-full lg:sticky lg:top-7 lg:h-[calc(100svh-3.5rem)]">
<SidebarFrame />
</div>
</aside>
</div>
</div>
)
}
@@ -1,60 +0,0 @@
import type { ReactNode } from "react"
import { Apple } from "@cfdm/ui/components/svgs/apple"
import { AppleDark } from "@cfdm/ui/components/svgs/appleDark"
import { GithubDark } from "@cfdm/ui/components/svgs/githubDark"
import { GithubLight } from "@cfdm/ui/components/svgs/githubLight"
import { Google } from "@cfdm/ui/components/svgs/google"
export type AuthProvider = {
id: string
label: string
logo: ReactNode
}
function ThemeLogo({ light, dark }: { light: ReactNode; dark: ReactNode }) {
return (
<>
<span aria-hidden="true" className="dark:hidden">
{light}
</span>
<span aria-hidden="true" className="hidden dark:block">
{dark}
</span>
</>
)
}
export const AUTH13_SOCIAL_PROVIDERS: AuthProvider[] = [
{
id: "google",
label: "Continue with Google",
logo: <Google aria-hidden="true" data-icon="inline-start" />,
},
{
id: "apple",
label: "Continue with Apple",
logo: (
<ThemeLogo
light={<Apple aria-hidden="true" data-icon="inline-start" />}
dark={<AppleDark aria-hidden="true" data-icon="inline-start" />}
/>
),
},
{
id: "github",
label: "Continue with GitHub",
logo: (
<ThemeLogo
light={<GithubLight aria-hidden="true" data-icon="inline-start" />}
dark={<GithubDark aria-hidden="true" data-icon="inline-start" />}
/>
),
},
]
export const AUTH13_SIDEBAR_IMAGE_LIGHT =
"https://images.unsplash.com/photo-1556139943-4bdca53adf1e?auto=format&fit=crop&w=1200&h=1800&q=80"
export const AUTH13_SIDEBAR_IMAGE_DARK =
"https://images.unsplash.com/photo-1709990740078-05aa8ee5b9b7?auto=format&fit=crop&w=1200&h=1800&q=80"
@@ -1,86 +0,0 @@
import { type ComponentProps } from "react"
import { Button } from "@cfdm/ui/components/button"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@cfdm/ui/components/field"
import { Input } from "@cfdm/ui/components/input"
import { Separator } from "@cfdm/ui/components/separator"
import { AuthLogo } from "./auth-logo"
import { AUTH13_SOCIAL_PROVIDERS } from "./data"
import { ArrowRightIcon } from "lucide-react"
type FormSubmitHandler = NonNullable<ComponentProps<"form">["onSubmit"]>
type FormSubmitEvent = Parameters<FormSubmitHandler>[0]
export function LoginForm({
onSubmit,
}: {
onSubmit: (event: FormSubmitEvent) => void
}) {
return (
<div className="mx-auto flex w-full max-w-[22rem] flex-col gap-8">
{/* Heading */}
<div className="flex flex-col gap-6">
<AuthLogo />
<div className="flex flex-col gap-2">
<h1 className="text-3xl leading-tight font-semibold text-balance">
Sign in
</h1>
<p className="text-muted-foreground text-base text-pretty">
Enter your work email to get a secure magic link.
</p>
</div>
</div>
{/* Form */}
<form className="flex flex-col gap-5" onSubmit={onSubmit}>
<FieldGroup className="gap-4">
<Field className="gap-2">
<FieldLabel htmlFor="auth-13-email">Work email</FieldLabel>
<Input
id="auth-13-email"
type="email"
autoComplete="email"
placeholder="[email protected]"
className="bg-background"
/>
<FieldDescription>
Your sign-in link stays active for 15 minutes.
</FieldDescription>
</Field>
</FieldGroup>
<Button type="submit" className="w-full">
Send magic link
<ArrowRightIcon aria-hidden="true" data-icon="inline-end" />
</Button>
</form>
<div className="flex items-center gap-3">
<Separator className="flex-1" />
<span className="text-muted-foreground text-xs">Or continue with</span>
<Separator className="flex-1" />
</div>
{/* Grid */}
<div className="grid gap-3">
{AUTH13_SOCIAL_PROVIDERS.map((provider) => (
<Button
key={provider.id}
type="button"
variant="outline"
className="w-full justify-center px-4 [&_svg:not([class*='size-'])]:size-4"
>
{provider.logo}
{provider.label}
</Button>
))}
</div>
</div>
)
}
@@ -1,57 +0,0 @@
"use client"
import { useId, type ComponentProps } from "react"
import { cn } from "@cfdm/ui/lib/utils"
export interface NoiseTextureProps extends ComponentProps<"svg"> {
className?: string
frequency?: number
octaves?: number
slope?: number
noiseOpacity?: number
}
export const NoiseTexture = ({
className,
frequency = 0.4,
octaves = 6,
slope = 0.15,
noiseOpacity = 0.6,
...props
}: NoiseTextureProps) => {
const filterId = useId()
return (
<svg
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 z-0 h-full w-full opacity-50 select-none dark:opacity-[0.75]",
className
)}
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<filter id={filterId}>
<feTurbulence
type="fractalNoise"
baseFrequency={frequency}
numOctaves={octaves}
stitchTiles="stitch"
/>
<feColorMatrix type="saturate" values="0" />
<feComponentTransfer>
<feFuncR type="linear" slope={slope} />
<feFuncG type="linear" slope={slope} />
<feFuncB type="linear" slope={slope} />
</feComponentTransfer>
</filter>
<rect
width="100%"
height="100%"
filter={`url(#${filterId})`}
opacity={noiseOpacity}
/>
</svg>
)
}
@@ -1,25 +0,0 @@
import { Auth } from "./components/auth"
import { NoiseTexture } from "./components/noise-texture"
export function Page() {
return (
<div className="bg-background relative min-h-svh w-full overflow-hidden">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 overflow-hidden"
>
<NoiseTexture
className="text-foreground/[0.015] dark:text-foreground/[0.03]"
frequency={0.5}
octaves={5}
slope={0.08}
noiseOpacity={0.28}
/>
</div>
<div className="relative z-10 min-h-svh w-full">
<Auth />
</div>
</div>
)
}
@@ -1,15 +0,0 @@
import { CardItem } from "./card-item"
import { CARDS } from "./data"
export function CardGrid() {
return (
<div className="@container w-full">
{/* Grid */}
<div className="grid gap-5 @2xl:grid-cols-3">
{CARDS.map((card) => (
<CardItem key={card.label} card={card} />
))}
</div>
</div>
)
}
@@ -1,34 +0,0 @@
import {
Frame,
FrameHeader,
FramePanel,
} from "@/components/reui/frame"
import { ICard } from "./data"
import { LinkIcon } from "lucide-react"
export function CardItem({ card }: { card: ICard }) {
return (
<Frame spacing="sm">
{/* Header */}
<FrameHeader className="px-1! py-1!">
<div className="[&_svg]:text-muted-foreground flex items-center gap-2 [&_svg]:size-4">
{card.icon}
<span className="text-foreground text-sm font-medium">
{card.label}
</span>
</div>
</FrameHeader>
{/* Content */}
<FramePanel className="space-y-3.5">
<p className="text-xs leading-relaxed">{card.description}</p>
<a
href="#"
className="text-primary inline-flex items-center gap-1 text-xs font-medium underline-offset-2 hover:underline"
>
<LinkIcon aria-hidden="true" className="size-2.5 shrink-0" />
{card.link}
</a>
</FramePanel>
</Frame>
)
}
@@ -1,39 +0,0 @@
import { type ReactNode } from "react"
import { PackageIcon, TrendingUp, MapPinIcon } from "lucide-react"
export interface ICard {
label: string
icon: ReactNode
description: string
link: string
}
export const CARDS: ICard[] = [
{
label: "Binance",
icon: (
<PackageIcon aria-hidden="true" />
),
description:
"Track trading volumes, liquidity shifts, and price movements for informed decisions",
link: "https://www.binance.com/en/markets/over..",
},
{
label: "Revenue",
icon: (
<TrendingUp aria-hidden="true" />
),
description:
"Get instant insights into earnings and cash flow performance.",
link: "https://nexo.io/earn/crypto-detailed-portfol..",
},
{
label: "Shipments",
icon: (
<MapPinIcon aria-hidden="true" />
),
description:
"Stay on top of deliveries and track shipment statuses efficiently.",
link: "https://www.educare.io/platform/analytics/e..",
},
]
@@ -1,9 +0,0 @@
import { CardGrid } from "./components/card-grid"
export function Page() {
return (
<div className="flex min-h-svh w-full max-w-5xl items-center justify-center p-6">
<CardGrid />
</div>
)
}
@@ -1,17 +0,0 @@
import { Frame } from "@/components/reui/frame"
import { CardItem } from "./card-item"
import { CARDS } from "./data"
export function CardGrid() {
return (
<Frame className="@container w-full">
{/* Grid */}
<div className="grid gap-1 @2xl:grid-cols-2 @4xl:grid-cols-4">
{CARDS.map((card) => (
<CardItem key={card.title} card={card} />
))}
</div>
</Frame>
)
}
@@ -1,233 +0,0 @@
import { useId } from "react"
import { FramePanel } from "@/components/reui/frame"
import { ICard } from "./data"
function CardDotPatternRtl() {
const id = useId().replace(/:/g, "")
const width = 240
const height = 136
const patternId = `${id}-pattern`
const maskId = `${id}-mask`
const gradientId = `${id}-gradient`
return (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 z-0 [--card-dot-pattern-fill:var(--color-foreground)] [--card-dot-pattern-opacity:0.12] dark:[--card-dot-pattern-opacity:0.2]"
>
<svg
viewBox={`0 0 ${width} ${height}`}
className="size-full"
fill="none"
preserveAspectRatio="none"
shapeRendering="crispEdges"
xmlns="http://www.w3.org/2000/svg"
style={{ opacity: "var(--card-dot-pattern-opacity)" }}
>
<defs>
<mask
id={maskId}
style={{ maskType: "alpha" }}
maskUnits="userSpaceOnUse"
x="0"
y="0"
width={width}
height={height}
>
<rect width={width} height={height} fill={`url(#${gradientId})`} />
</mask>
<linearGradient
id={gradientId}
x1={width}
y1={0}
x2={width - 83.8613}
y2={95.7483}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="white" />
<stop offset="0.538873" stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0" />
</linearGradient>
<pattern
id={patternId}
patternUnits="userSpaceOnUse"
patternTransform="matrix(48 0 0 48 0 0)"
preserveAspectRatio="none"
viewBox="0 0 48 48"
width="1"
height="1"
>
<g fill="var(--card-dot-pattern-fill)">
<rect x="0" y="0" width="2" height="2" fillOpacity="0.216" />
<rect x="4" y="0" width="2" height="2" fillOpacity="0.489" />
<rect x="8" y="0" width="2" height="2" fillOpacity="0.366" />
<rect x="12" y="0" width="2" height="2" fillOpacity="0.305" />
<rect x="16" y="0" width="2" height="2" fillOpacity="0.514" />
<rect x="20" y="0" width="2" height="2" fillOpacity="0.129" />
<rect x="24" y="0" width="2" height="2" fillOpacity="0.431" />
<rect x="28" y="0" width="2" height="2" fillOpacity="0.142" />
<rect x="32" y="0" width="2" height="2" fillOpacity="0.288" />
<rect x="36" y="0" width="2" height="2" fillOpacity="0.144" />
<rect x="40" y="0" width="2" height="2" fillOpacity="0.313" />
<rect x="44" y="0" width="2" height="2" fillOpacity="0.214" />
<rect x="0" y="4" width="2" height="2" fillOpacity="0.4" />
<rect x="4" y="4" width="2" height="2" fillOpacity="0.13" />
<rect x="8" y="4" width="2" height="2" fillOpacity="0.498" />
<rect x="12" y="4" width="2" height="2" fillOpacity="0.473" />
<rect x="16" y="4" width="2" height="2" fillOpacity="0.386" />
<rect x="20" y="4" width="2" height="2" fillOpacity="0.248" />
<rect x="24" y="4" width="2" height="2" fillOpacity="0.168" />
<rect x="28" y="4" width="2" height="2" fillOpacity="0.193" />
<rect x="32" y="4" width="2" height="2" fillOpacity="0.528" />
<rect x="36" y="4" width="2" height="2" fillOpacity="0.307" />
<rect x="40" y="4" width="2" height="2" fillOpacity="0.314" />
<rect x="44" y="4" width="2" height="2" fillOpacity="0.444" />
<rect x="0" y="8" width="2" height="2" fillOpacity="0.33" />
<rect x="4" y="8" width="2" height="2" fillOpacity="0.438" />
<rect x="8" y="8" width="2" height="2" fillOpacity="0.477" />
<rect x="12" y="8" width="2" height="2" fillOpacity="0.434" />
<rect x="16" y="8" width="2" height="2" fillOpacity="0.47" />
<rect x="20" y="8" width="2" height="2" fillOpacity="0.383" />
<rect x="24" y="8" width="2" height="2" fillOpacity="0.514" />
<rect x="28" y="8" width="2" height="2" fillOpacity="0.287" />
<rect x="32" y="8" width="2" height="2" fillOpacity="0.346" />
<rect x="36" y="8" width="2" height="2" fillOpacity="0.52" />
<rect x="40" y="8" width="2" height="2" fillOpacity="0.524" />
<rect x="44" y="8" width="2" height="2" fillOpacity="0.126" />
<rect x="0" y="12" width="2" height="2" fillOpacity="0.259" />
<rect x="4" y="12" width="2" height="2" fillOpacity="0.281" />
<rect x="8" y="12" width="2" height="2" fillOpacity="0.311" />
<rect x="12" y="12" width="2" height="2" fillOpacity="0.212" />
<rect x="16" y="12" width="2" height="2" fillOpacity="0.524" />
<rect x="20" y="12" width="2" height="2" fillOpacity="0.353" />
<rect x="24" y="12" width="2" height="2" fillOpacity="0.34" />
<rect x="28" y="12" width="2" height="2" fillOpacity="0.417" />
<rect x="32" y="12" width="2" height="2" fillOpacity="0.258" />
<rect x="36" y="12" width="2" height="2" fillOpacity="0.158" />
<rect x="40" y="12" width="2" height="2" fillOpacity="0.388" />
<rect x="44" y="12" width="2" height="2" fillOpacity="0.184" />
<rect x="0" y="16" width="2" height="2" fillOpacity="0.491" />
<rect x="4" y="16" width="2" height="2" fillOpacity="0.417" />
<rect x="8" y="16" width="2" height="2" fillOpacity="0.231" />
<rect x="12" y="16" width="2" height="2" fillOpacity="0.209" />
<rect x="16" y="16" width="2" height="2" fillOpacity="0.305" />
<rect x="20" y="16" width="2" height="2" fillOpacity="0.287" />
<rect x="24" y="16" width="2" height="2" fillOpacity="0.353" />
<rect x="28" y="16" width="2" height="2" fillOpacity="0.513" />
<rect x="32" y="16" width="2" height="2" fillOpacity="0.483" />
<rect x="36" y="16" width="2" height="2" fillOpacity="0.455" />
<rect x="40" y="16" width="2" height="2" fillOpacity="0.483" />
<rect x="44" y="16" width="2" height="2" fillOpacity="0.189" />
<rect x="0" y="20" width="2" height="2" fillOpacity="0.264" />
<rect x="4" y="20" width="2" height="2" fillOpacity="0.295" />
<rect x="8" y="20" width="2" height="2" fillOpacity="0.51" />
<rect x="12" y="20" width="2" height="2" fillOpacity="0.305" />
<rect x="16" y="20" width="2" height="2" fillOpacity="0.438" />
<rect x="20" y="20" width="2" height="2" fillOpacity="0.289" />
<rect x="24" y="20" width="2" height="2" fillOpacity="0.322" />
<rect x="28" y="20" width="2" height="2" fillOpacity="0.196" />
<rect x="32" y="20" width="2" height="2" fillOpacity="0.486" />
<rect x="36" y="20" width="2" height="2" fillOpacity="0.388" />
<rect x="40" y="20" width="2" height="2" fillOpacity="0.489" />
<rect x="44" y="20" width="2" height="2" fillOpacity="0.208" />
<rect x="0" y="24" width="2" height="2" fillOpacity="0.239" />
<rect x="4" y="24" width="2" height="2" fillOpacity="0.463" />
<rect x="8" y="24" width="2" height="2" fillOpacity="0.539" />
<rect x="12" y="24" width="2" height="2" fillOpacity="0.408" />
<rect x="16" y="24" width="2" height="2" fillOpacity="0.527" />
<rect x="20" y="24" width="2" height="2" fillOpacity="0.357" />
<rect x="24" y="24" width="2" height="2" fillOpacity="0.362" />
<rect x="28" y="24" width="2" height="2" fillOpacity="0.151" />
<rect x="32" y="24" width="2" height="2" fillOpacity="0.324" />
<rect x="36" y="24" width="2" height="2" fillOpacity="0.347" />
<rect x="40" y="24" width="2" height="2" fillOpacity="0.299" />
<rect x="44" y="24" width="2" height="2" fillOpacity="0.127" />
<rect x="0" y="28" width="2" height="2" fillOpacity="0.476" />
<rect x="4" y="28" width="2" height="2" fillOpacity="0.461" />
<rect x="8" y="28" width="2" height="2" fillOpacity="0.377" />
<rect x="12" y="28" width="2" height="2" fillOpacity="0.415" />
<rect x="16" y="28" width="2" height="2" fillOpacity="0.217" />
<rect x="20" y="28" width="2" height="2" fillOpacity="0.297" />
<rect x="24" y="28" width="2" height="2" fillOpacity="0.258" />
<rect x="28" y="28" width="2" height="2" fillOpacity="0.296" />
<rect x="32" y="28" width="2" height="2" fillOpacity="0.276" />
<rect x="36" y="28" width="2" height="2" fillOpacity="0.182" />
<rect x="40" y="28" width="2" height="2" fillOpacity="0.422" />
<rect x="44" y="28" width="2" height="2" fillOpacity="0.392" />
<rect x="0" y="32" width="2" height="2" fillOpacity="0.273" />
<rect x="4" y="32" width="2" height="2" fillOpacity="0.143" />
<rect x="8" y="32" width="2" height="2" fillOpacity="0.338" />
<rect x="12" y="32" width="2" height="2" fillOpacity="0.264" />
<rect x="16" y="32" width="2" height="2" fillOpacity="0.218" />
<rect x="20" y="32" width="2" height="2" fillOpacity="0.29" />
<rect x="24" y="32" width="2" height="2" fillOpacity="0.336" />
<rect x="28" y="32" width="2" height="2" fillOpacity="0.313" />
<rect x="32" y="32" width="2" height="2" fillOpacity="0.514" />
<rect x="36" y="32" width="2" height="2" fillOpacity="0.289" />
<rect x="40" y="32" width="2" height="2" fillOpacity="0.25" />
<rect x="44" y="32" width="2" height="2" fillOpacity="0.507" />
<rect x="0" y="36" width="2" height="2" fillOpacity="0.346" />
<rect x="4" y="36" width="2" height="2" fillOpacity="0.208" />
<rect x="8" y="36" width="2" height="2" fillOpacity="0.313" />
<rect x="12" y="36" width="2" height="2" fillOpacity="0.365" />
<rect x="16" y="36" width="2" height="2" fillOpacity="0.491" />
<rect x="20" y="36" width="2" height="2" fillOpacity="0.121" />
<rect x="24" y="36" width="2" height="2" fillOpacity="0.472" />
<rect x="28" y="36" width="2" height="2" fillOpacity="0.281" />
<rect x="32" y="36" width="2" height="2" fillOpacity="0.297" />
<rect x="36" y="36" width="2" height="2" fillOpacity="0.33" />
<rect x="40" y="36" width="2" height="2" fillOpacity="0.463" />
<rect x="44" y="36" width="2" height="2" fillOpacity="0.447" />
<rect x="0" y="40" width="2" height="2" fillOpacity="0.217" />
<rect x="4" y="40" width="2" height="2" fillOpacity="0.467" />
<rect x="8" y="40" width="2" height="2" fillOpacity="0.297" />
<rect x="12" y="40" width="2" height="2" fillOpacity="0.303" />
<rect x="16" y="40" width="2" height="2" fillOpacity="0.497" />
<rect x="20" y="40" width="2" height="2" fillOpacity="0.201" />
<rect x="24" y="40" width="2" height="2" fillOpacity="0.5" />
<rect x="28" y="40" width="2" height="2" fillOpacity="0.458" />
<rect x="32" y="40" width="2" height="2" fillOpacity="0.165" />
<rect x="36" y="40" width="2" height="2" fillOpacity="0.36" />
<rect x="40" y="40" width="2" height="2" fillOpacity="0.329" />
<rect x="44" y="40" width="2" height="2" fillOpacity="0.419" />
<rect x="0" y="44" width="2" height="2" fillOpacity="0.47" />
<rect x="4" y="44" width="2" height="2" fillOpacity="0.161" />
<rect x="8" y="44" width="2" height="2" fillOpacity="0.191" />
<rect x="12" y="44" width="2" height="2" fillOpacity="0.341" />
<rect x="16" y="44" width="2" height="2" fillOpacity="0.279" />
<rect x="20" y="44" width="2" height="2" fillOpacity="0.387" />
<rect x="24" y="44" width="2" height="2" fillOpacity="0.173" />
<rect x="28" y="44" width="2" height="2" fillOpacity="0.537" />
<rect x="32" y="44" width="2" height="2" fillOpacity="0.218" />
<rect x="36" y="44" width="2" height="2" fillOpacity="0.368" />
<rect x="40" y="44" width="2" height="2" fillOpacity="0.5" />
<rect x="44" y="44" width="2" height="2" fillOpacity="0.414" />
</g>
</pattern>
</defs>
<g mask={`url(#${maskId})`}>
<rect width={width} height={height} fill={`url(#${patternId})`} />
</g>
</svg>
</div>
)
}
export function CardItem({ card }: { card: ICard }) {
return (
<FramePanel className="relative isolate flex flex-col gap-3">
{/* Card */}
<CardDotPatternRtl />
<div className="relative z-10 flex flex-col gap-3">
<span className="text-muted-foreground text-relaxed text-sm">
{card.title}
</span>
<span className="text-foreground text-xl leading-tight font-semibold">
{card.total}
</span>
</div>
</FramePanel>
)
}
@@ -1,23 +0,0 @@
export interface ICard {
title: string
total: string
}
export const CARDS: ICard[] = [
{
title: "Avg. Deal Size",
total: "$14,9M",
},
{
title: "Due Deals",
total: "16",
},
{
title: "Pending Invoices",
total: "$2.7M",
},
{
title: "Active Clients",
total: "36",
},
]
@@ -1,9 +0,0 @@
import { CardGrid } from "./components/card-grid"
export function Page() {
return (
<div className="flex min-h-svh w-full max-w-5xl items-center justify-center p-10">
<CardGrid />
</div>
)
}
@@ -1,416 +0,0 @@
import { Frame, FramePanel } from "@/components/reui/frame"
import { Cell, Pie, PieChart } from "recharts"
import { cn } from "@cfdm/ui/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarImage,
} from "@cfdm/ui/components/avatar"
import { Button } from "@cfdm/ui/components/button"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@cfdm/ui/components/chart"
import { Separator } from "@cfdm/ui/components/separator"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@cfdm/ui/components/tabs"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@cfdm/ui/components/tooltip"
import {
allocationMemberCount,
allocationMembers,
allocationPeriods,
inflowChartConfig,
inflowPeriods,
SEGMENT_COUNT,
type AllocationPeriod,
type InflowFund,
type InflowPeriod,
} from "./data"
import { InfoIcon } from "lucide-react"
const segments = Array.from({ length: SEGMENT_COUNT }, (_, index) => index)
const CHART_REVEAL_STYLE = `
@keyframes dashboard-1-flow-reveal-up {
from {
clip-path: inset(100% 0 0 0);
opacity: 0.75;
}
to {
clip-path: inset(0 0 0 0);
opacity: 1;
}
}
.dashboard-1-flow-reveal-up {
animation: dashboard-1-flow-reveal-up 680ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
@media (prefers-reduced-motion: reduce) {
.dashboard-1-flow-reveal-up {
animation: none;
clip-path: none;
opacity: 1;
}
}
`
function AllocationMeter({ period }: { period: AllocationPeriod }) {
return (
<div
aria-label={`${period.label} capacity allocation is ${period.allocation}`}
className="flex h-7 w-full items-stretch justify-between"
role="img"
>
{segments.map((segment) => (
<span
aria-hidden="true"
key={segment}
className={cn(
"h-full w-1 shrink-0 rounded-full",
segment < period.filledSegments ? "bg-success" : "bg-muted"
)}
/>
))}
</div>
)
}
function MemberStack() {
return (
<div className="flex items-center gap-2">
<AvatarGroup className="-space-x-2">
{allocationMembers.map((member) => (
<Avatar key={member.name} className="size-6">
{member.avatar ? (
<AvatarImage src={member.avatar} alt={member.name} />
) : null}
<AvatarFallback className="bg-background text-xs font-medium">
{member.initials}
</AvatarFallback>
</Avatar>
))}
</AvatarGroup>
<span className="text-muted-foreground text-xs whitespace-nowrap">
{allocationMemberCount} Members
</span>
</div>
)
}
function AllocationChart() {
return (
<Frame className="@container h-full w-full">
<FramePanel>
<Tabs
defaultValue={allocationPeriods[0].value}
className="h-full w-full min-w-0 gap-4"
>
{/* Header */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-1.5">
<h2 className="text-sm font-medium">Capacity Allocation</h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
className="text-muted-foreground/70 hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background inline-flex shrink-0 rounded-full p-0.5 transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
aria-label="Capacity Allocation info"
/>
}
>
<InfoIcon className="size-3.5" aria-hidden="true" />
</TooltipTrigger>
<TooltipContent
side="top"
className="max-w-56 px-2.5 py-1.5 text-xs leading-5"
>
Fulfillment capacity by selected period.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<TabsList>
{allocationPeriods.map((period) => (
<TabsTrigger key={period.value} value={period.value}>
{period.label}
</TabsTrigger>
))}
</TabsList>
</div>
{allocationPeriods.map((period) => (
<TabsContent
key={period.value}
value={period.value}
className="mt-0"
>
<div className="flex flex-col gap-4">
{/* Metric */}
<div className="flex flex-wrap items-baseline gap-x-2">
<span className="text-[26px] font-medium">
{period.allocation}
</span>
<span className="text-success text-xs font-medium">
{period.delta}
</span>
<span className="text-muted-foreground/70 text-xs">
{period.comparison}
</span>
</div>
{/* Chart */}
<AllocationMeter period={period} />
{/* Footer */}
<div className="mt-1 flex flex-wrap items-center justify-between gap-3">
<p>
<span className="text-muted-foreground/70 text-xs">
Queued Orders:
</span>{" "}
<span className="text-sm font-medium">
{period.exposure}
</span>
</p>
<MemberStack />
</div>
</div>
</TabsContent>
))}
</Tabs>
</FramePanel>
</Frame>
)
}
type DonutSlice =
| InflowFund
| {
key: "reserve"
name: string
amount: string
share: number
color: string
fill: string
}
function getDonutData(period: InflowPeriod) {
const trackedShare = period.funds.reduce(
(total, fund) => total + fund.share,
0
)
const reserveShare = Math.max(100 - trackedShare, 0)
return [
...period.funds,
{
key: "reserve",
name: "Reserve Capacity",
amount: "",
share: reserveShare,
color: "var(--muted)",
fill: "var(--color-reserve)",
},
] satisfies DonutSlice[]
}
function ChartTooltipFormatter(item: unknown) {
const fund = item as DonutSlice
const value = fund.key === "reserve" ? `${fund.share}%` : fund.amount
return (
<div className="flex min-w-40 items-center justify-between gap-6">
<div className="flex min-w-0 items-center gap-2">
<span
aria-hidden="true"
className="size-2.5 shrink-0 rounded-full"
style={{ backgroundColor: fund.color }}
/>
<span className="text-muted-foreground truncate">{fund.name}</span>
</div>
<span className="text-foreground font-medium tabular-nums">{value}</span>
</div>
)
}
function InfoTooltip() {
return (
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label="About Decision Flow"
className="text-muted-foreground/70 -my-1"
size="icon-sm"
type="button"
variant="ghost"
>
<InfoIcon aria-hidden="true" className="text-sm" data-icon="inline-start" />
</Button>
}
/>
<TooltipContent side="top" sideOffset={8}>
<p>Tracked decisions entering fulfillment lanes.</p>
</TooltipContent>
</Tooltip>
)
}
function InflowDonut({ period }: { period: InflowPeriod }) {
const chartData = getDonutData(period)
return (
<div className="dashboard-1-flow-reveal-up relative size-[8.25rem] shrink-0">
<ChartContainer
aria-label={`Decision Flow: ${period.total} total for ${period.label}`}
className="aspect-square size-[8.25rem]"
config={inflowChartConfig}
initialDimension={{ width: 132, height: 132 }}
>
<PieChart margin={{ top: 2, right: 2, bottom: 2, left: 2 }}>
<ChartTooltip
cursor={false}
wrapperStyle={{ zIndex: 30 }}
content={
<ChartTooltipContent
hideLabel
hideIndicator
formatter={(_value, _name, item) =>
ChartTooltipFormatter(item.payload)
}
/>
}
/>
<Pie
data={chartData}
dataKey="share"
endAngle={-230}
innerRadius={47}
isAnimationActive={false}
nameKey="name"
outerRadius={62}
paddingAngle={1}
cornerRadius={3}
startAngle={130}
stroke="var(--background)"
strokeWidth={2}
>
{chartData.map((item) => (
<Cell key={item.key} fill={item.fill} />
))}
</Pie>
</PieChart>
</ChartContainer>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 flex items-center justify-center"
>
<div className="bg-background/90 border-border/70 flex size-[5.25rem] flex-col items-center justify-center rounded-full border border-dashed">
<span className="text-muted-foreground/70 text-xs">Flow</span>
<span className="mt-0.5 text-sm font-semibold">{period.total}</span>
</div>
</div>
</div>
)
}
function InflowLegend({ period }: { period: InflowPeriod }) {
return (
<ul className="flex min-w-0 flex-1 flex-col">
{period.funds.map((fund, index) => (
<li key={fund.key}>
<div className="grid grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-3 py-2.5">
<div className="flex min-w-0 items-center gap-2.5">
<span
aria-hidden="true"
className="border-background size-3 shrink-0 rounded-full border-2 shadow-sm"
style={{ backgroundColor: fund.color }}
/>
<span className="text-sm font-medium">{fund.name}</span>
</div>
<span className="text-sm font-medium">{fund.amount}</span>
<span className="text-muted-foreground/70 w-8 text-right text-xs">
{fund.share}%
</span>
</div>
{index < period.funds.length - 1 ? (
<Separator className="w-auto" />
) : null}
</li>
))}
</ul>
)
}
function InflowPeriodPanel({ period }: { period: InflowPeriod }) {
return (
<div className="grid gap-6 @sm:grid-cols-[8.25rem_minmax(0,1fr)] @sm:items-center">
<InflowDonut period={period} />
<InflowLegend period={period} />
</div>
)
}
function InflowChart() {
return (
<TooltipProvider delay={150}>
<style>{CHART_REVEAL_STYLE}</style>
<Frame className="@container h-full w-full">
<FramePanel className="ps-3.5! pe-5! pt-5! pb-3.5!">
<Tabs defaultValue="week" className="gap-6">
{/* Header */}
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-0.5 ps-1.5">
<h2 className="text-sm font-medium">Decision Flow</h2>
<InfoTooltip />
</div>
<TabsList className="w-full @sm:w-auto">
{inflowPeriods.map((period) => (
<TabsTrigger key={period.value} value={period.value}>
{period.label}
</TabsTrigger>
))}
</TabsList>
</div>
{/* Content */}
{inflowPeriods.map((period) => (
<TabsContent
key={period.value}
value={period.value}
className="mt-0"
>
<InflowPeriodPanel period={period} />
</TabsContent>
))}
</Tabs>
</FramePanel>
</Frame>
</TooltipProvider>
)
}
export function Chart() {
return (
<div className="@container grid h-full w-full min-w-0 auto-rows-fr gap-3">
<AllocationChart />
<InflowChart />
</div>
)
}
@@ -1,64 +0,0 @@
import { Frame, FramePanel } from "@/components/reui/frame"
import { cn } from "@cfdm/ui/lib/utils"
import { Item, ItemMedia } from "@cfdm/ui/components/item"
import { FULFILLMENT_CARDS, type FulfillmentCard } from "./data"
function CardItem({ card }: { card: FulfillmentCard }) {
return (
<FramePanel>
{/* Heading */}
<div className="flex items-center gap-2.5">
<Item
className={cn(
"p-0",
"border-background flex size-10 items-center justify-center border-2 [background-image:radial-gradient(48.05%_48.05%_at_50%_5.95%,rgba(255,255,255,0.4)_0%,rgba(255,255,255,0)_100%)] shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-5 [&_svg]:text-white",
card.iconBg
)}
>
<ItemMedia variant="icon" className="size-auto">
{card.icon}
</ItemMedia>
</Item>
<div className="flex flex-col gap-1">
<p className="text-muted-foreground text-sm leading-tight">
{card.typeLabel}
</p>
<h3 className="text-sm leading-tight font-medium">{card.title}</h3>
</div>
</div>
<div className="mt-5 space-y-1.5">
<p className="text-muted-foreground text-sm leading-tight">
{card.metricLabel}
</p>
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-xl font-medium tracking-tight">
{card.balance}
</span>
<span
className={cn(
"text-sm font-medium",
card.change.positive ? "text-teal-600" : "text-rose-600"
)}
>
{card.change.percent} ({card.change.amount})
</span>
</div>
</div>
</FramePanel>
)
}
export function Chart() {
return (
<Frame className="@container w-full">
{/* Grid */}
<div className="grid gap-1 @2xl:grid-cols-2 @5xl:grid-cols-4">
{FULFILLMENT_CARDS.map((card) => (
<CardItem key={card.title} card={card} />
))}
</div>
</Frame>
)
}
@@ -1,155 +0,0 @@
import { Badge } from "@/components/reui/badge"
import {
Frame,
FrameFooter,
FramePanel,
} from "@/components/reui/frame"
import { cn } from "@cfdm/ui/lib/utils"
import { Button } from "@cfdm/ui/components/button"
import { Progress } from "@cfdm/ui/components/progress"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@cfdm/ui/components/select"
import { Separator } from "@cfdm/ui/components/separator"
import {
PERFORMANCE_RANGE_OPTIONS,
SHIFT_ACTIVITY,
SHIFT_PERFORMANCE,
SHIFT_PIPELINE_PROGRESS,
} from "./data"
import { TrendingUp, TrendingDown, CircleCheckIcon } from "lucide-react"
export function InvestorCard() {
return (
<Frame className="h-full w-full">
{/* Content */}
<FramePanel>
<div className="mb-6 flex items-start justify-between gap-3">
<div className="flex flex-col gap-px">
<h3 className="text-base font-semibold">Shift Performance</h3>
</div>
<div className="flex items-center gap-2">
<Select defaultValue="today" items={PERFORMANCE_RANGE_OPTIONS}>
<SelectTrigger className="h-8! w-28">
<SelectValue />
</SelectTrigger>
<SelectContent
align="start"
alignItemWithTrigger={false}
className="w-28"
>
{PERFORMANCE_RANGE_OPTIONS.map((range) => (
<SelectItem key={range.value} value={range.value}>
{range.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-5">
<div className="grid grid-cols-3 gap-2">
{SHIFT_PERFORMANCE.map((item) => (
<div
className="flex flex-col items-start justify-start"
key={item.label}
>
<div className="text-foreground text-xl font-bold">
{item.value}
</div>
<div className="text-muted-foreground mb-1 text-xs font-medium">
{item.label}
</div>
<span
className={cn(
"flex items-center gap-0.5 text-xs font-semibold [&_svg]:h-3 [&_svg]:w-3",
item.trend === "positive"
? "text-emerald-500"
: "text-destructive"
)}
>
{item.trend === "positive" ? (
<TrendingUp aria-hidden="true" />
) : (
<TrendingDown aria-hidden="true" />
)}
{item.delta}
</span>
</div>
))}
</div>
<Separator />
<div>
<div className="mb-2.5 flex items-center justify-between">
<span className="text-foreground text-sm font-medium">
Pipeline Progress
</span>
<span className="text-foreground text-xs font-semibold">
{SHIFT_PIPELINE_PROGRESS}%
</span>
</div>
<Progress
value={SHIFT_PIPELINE_PROGRESS}
className="h-1! **:data-[slot=progress-track]:h-1"
/>
</div>
<Separator />
<div>
<div className="text-foreground mb-2.5 text-sm font-medium">
Recent Activity
</div>
<ul className="space-y-2">
{SHIFT_ACTIVITY.map((activity) => (
<li
key={activity.id}
className="flex items-center justify-between gap-2.5 text-sm"
>
<span className="flex min-w-0 items-center gap-2">
<CircleCheckIcon className={cn(
"h-3.5 w-3.5 shrink-0",
activity.tone === "success" && "text-emerald-500",
activity.tone === "info" && "text-sky-500",
activity.tone === "warning" && "text-amber-500"
)} aria-hidden="true" />
<span className="text-foreground truncate text-xs">
{activity.title}
</span>
</span>
<Badge
variant={
activity.tone === "success"
? "success-light"
: activity.tone === "info"
? "info-light"
: "warning-light"
}
className="shrink-0"
>
{activity.status}
</Badge>
</li>
))}
</ul>
</div>
</div>
</FramePanel>
{/* Footer */}
<FrameFooter className="flex-row items-center gap-2.5 p-2!">
<Button variant="outline" className="flex-1">
Schedule
</Button>
<Button className="flex-1">Full Report</Button>
</FrameFooter>
</Frame>
)
}
@@ -1,35 +0,0 @@
"use client"
import { Chart as CapacityChart } from "./capacity-chart"
import { Chart as ChartCards } from "./chart-cards"
import { InvestorCard as CommanderCard } from "./commander-card"
import { ExceptionGrid } from "./exception-grid"
import { Navbar } from "./navbar"
export function Dashboard() {
return (
<div className="text-foreground @container mx-auto flex w-full max-w-7xl flex-col gap-2">
<Navbar />
<section aria-label="Fulfillment metrics">
<ChartCards />
</section>
<section
aria-label="Fulfillment operations"
className="grid min-w-0 items-stretch gap-3 @5xl:grid-cols-2"
>
<div className="flex min-w-0">
<CommanderCard />
</div>
<div className="flex min-w-0">
<CapacityChart />
</div>
</section>
<section aria-label="Fulfillment exception queue">
<ExceptionGrid />
</section>
</div>
)
}
@@ -1,676 +0,0 @@
import { type ReactNode } from "react"
import { type BadgeProps } from "@/components/reui/badge"
import { type ChartConfig } from "@cfdm/ui/components/chart"
import { PackageIcon, TruckIcon, TriangleAlertIcon, BotIcon } from "lucide-react"
export type FulfillmentStatus = "On Time" | "At Risk" | "Delayed" | "Blocked"
export type AutomationLevel = "Autopilot" | "Copilot" | "Manual"
export interface TeamMember {
name: string
initials: string
avatar: string
role: string
}
export interface FulfillmentException {
id: string
reference: string
customer: string
email: string
avatar: string
initials: string
lane: string
facility: string
stage: string
promise: string
slaMinutes: number
automation: AutomationLevel
owner: string
units: number
value: number
risk: string
status: FulfillmentStatus
}
export const STATUS_ORDER: FulfillmentStatus[] = [
"On Time",
"At Risk",
"Delayed",
"Blocked",
]
export const STATUS_BADGE_VARIANT: Record<
FulfillmentStatus,
BadgeProps["variant"]
> = {
"On Time": "success-outline",
"At Risk": "warning-outline",
Delayed: "info-outline",
Blocked: "destructive-outline",
}
export const AUTOMATION_BADGE_VARIANT: Record<
AutomationLevel,
BadgeProps["variant"]
> = {
Autopilot: "success-light",
Copilot: "info-light",
Manual: "warning-light",
}
export const NAV_MEMBERS: TeamMember[] = [
{
name: "Maya Singh",
initials: "MS",
avatar:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=96&h=96&dpr=2&q=80",
role: "Fulfillment lead",
},
{
name: "Leo Martins",
initials: "LM",
avatar:
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=96&h=96&dpr=2&q=80",
role: "Automation owner",
},
{
name: "Nora Albright",
initials: "NA",
avatar:
"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=96&h=96&dpr=2&q=80",
role: "Capacity planner",
},
]
export const TEAM_MEMBERS = NAV_MEMBERS.map((member) => ({
src: member.avatar,
initials: member.initials,
name: member.name,
}))
export const TEAM_EXTRA_COUNT = 11
export interface FulfillmentCardChange {
positive: boolean
percent: string
amount: string
}
export interface FulfillmentCard {
typeLabel: string
title: string
metricLabel: string
balance: string
change: FulfillmentCardChange
icon: ReactNode
iconBg: string
}
export const FULFILLMENT_CARDS: FulfillmentCard[] = [
{
typeLabel: "Outbound",
title: "Orders Ready",
metricLabel: "Ready Volume",
balance: "18,420",
change: {
positive: true,
percent: "+11.8%",
amount: "1,946",
},
iconBg: "bg-neutral-950",
icon: (
<PackageIcon aria-hidden="true" />
),
},
{
typeLabel: "Promise",
title: "Same-Day SLA",
metricLabel: "Service Level",
balance: "94.8%",
change: {
positive: true,
percent: "+1.2 pts",
amount: "shift",
},
iconBg: "bg-indigo-600",
icon: (
<TruckIcon aria-hidden="true" />
),
},
{
typeLabel: "Inventory",
title: "Stock Risk",
metricLabel: "Blocked SKUs",
balance: "31",
change: {
positive: true,
percent: "13 fewer",
amount: "since 06:00",
},
iconBg: "bg-amber-400",
icon: (
<TriangleAlertIcon aria-hidden="true" />
),
},
{
typeLabel: "Policy",
title: "AI Autopilot",
metricLabel: "Auto Resolved",
balance: "71.6%",
change: {
positive: true,
percent: "+8.4 pts",
amount: "policy",
},
iconBg: "bg-cyan-600",
icon: (
<BotIcon aria-hidden="true" />
),
},
]
export type AllocationPeriod = {
value: "week" | "month" | "year"
label: string
allocation: string
delta: string
comparison: string
exposure: string
filledSegments: number
}
export type AllocationMember = {
name: string
initials: string
avatar?: string
}
export const SEGMENT_COUNT = 56
export const allocationMemberCount = 6
export const allocationPeriods: AllocationPeriod[] = [
{
value: "week",
label: "Week",
allocation: "86%",
delta: "+5.8%",
comparison: "vs labor plan",
exposure: "3,840 orders",
filledSegments: 48,
},
{
value: "month",
label: "Month",
allocation: "79%",
delta: "+2.4%",
comparison: "vs prior month",
exposure: "18 priority lanes",
filledSegments: 44,
},
{
value: "year",
label: "Year",
allocation: "74%",
delta: "+9.2%",
comparison: "automation lift",
exposure: "6 facilities",
filledSegments: 41,
},
]
export const allocationMembers: AllocationMember[] = TEAM_MEMBERS.map(
(member) => ({
name: member.name,
initials: member.initials,
avatar: member.src,
})
)
export type PerformanceTrend = "positive" | "negative"
export type ActivityTone = "success" | "info" | "warning"
export interface PerformanceMetric {
label: string
value: string
trend: PerformanceTrend
delta: string
}
export interface ShiftActivity {
id: string
title: string
time: string
status: string
tone: ActivityTone
}
export const PERFORMANCE_RANGE_OPTIONS = [
{ label: "Today", value: "today" },
{ label: "Week", value: "week" },
{ label: "Month", value: "month" },
]
export const SHIFT_PERFORMANCE: PerformanceMetric[] = [
{
label: "Orders Cleared",
value: "18.4k",
trend: "positive",
delta: "+11.8%",
},
{
label: "SLA Recovery",
value: "94.8%",
trend: "positive",
delta: "+1.2 pts",
},
{
label: "Risk Exposure",
value: "$128k",
trend: "negative",
delta: "-9.4%",
},
]
export const SHIFT_PIPELINE_PROGRESS = 76
export const SHIFT_ACTIVITY: ShiftActivity[] = [
{
id: "wave-release",
title: "Released priority wave to dock B",
time: "4 min ago",
status: "Cleared",
tone: "success",
},
{
id: "carrier-reprice",
title: "Carrier mix repriced for zone 6",
time: "12 min ago",
status: "Review",
tone: "info",
},
{
id: "inventory-hold",
title: "Inventory hold isolated to 3 SKUs",
time: "23 min ago",
status: "Watch",
tone: "warning",
},
]
export type InflowFundKey = "autopilot" | "copilot" | "manual" | "reserve"
export interface InflowFund {
key: Exclude<InflowFundKey, "reserve">
name: string
amount: string
share: number
color: string
fill: string
}
export interface InflowPeriod {
value: "week" | "month" | "year"
label: string
total: string
headline: string
description: string
delta: string
funds: InflowFund[]
}
const inflowAutopilotColor = "oklch(0.62 0.19 149)"
const inflowCopilotColor = "oklch(0.58 0.18 257)"
const inflowManualColor = "oklch(0.72 0.16 78)"
export const inflowChartConfig = {
flow: {
label: "Flow",
},
autopilot: {
label: "Autopilot",
color: inflowAutopilotColor,
},
copilot: {
label: "Copilot",
color: inflowCopilotColor,
},
manual: {
label: "Manual",
color: inflowManualColor,
},
reserve: {
label: "Reserve",
color: "oklch(0.7 0.04 260)",
},
} satisfies ChartConfig
export const inflowPeriods: InflowPeriod[] = [
{
value: "week",
label: "Week",
total: "18.4k",
headline: "Exception Flow",
description: "Orders entering decision lanes",
delta: "+6.2%",
funds: [
{
key: "autopilot",
name: "Autopilot",
amount: "9.1k",
share: 49.5,
color: inflowAutopilotColor,
fill: "var(--color-autopilot)",
},
{
key: "copilot",
name: "Copilot",
amount: "5.2k",
share: 28.3,
color: inflowCopilotColor,
fill: "var(--color-copilot)",
},
{
key: "manual",
name: "Manual",
amount: "2.8k",
share: 15.2,
color: inflowManualColor,
fill: "var(--color-manual)",
},
],
},
{
value: "month",
label: "Month",
total: "76.8k",
headline: "Resolved Flow",
description: "Completed decisions this month",
delta: "+14.8%",
funds: [
{
key: "autopilot",
name: "Autopilot",
amount: "41.6k",
share: 54.2,
color: inflowAutopilotColor,
fill: "var(--color-autopilot)",
},
{
key: "copilot",
name: "Copilot",
amount: "20.3k",
share: 26.4,
color: inflowCopilotColor,
fill: "var(--color-copilot)",
},
{
key: "manual",
name: "Manual",
amount: "9.8k",
share: 12.8,
color: inflowManualColor,
fill: "var(--color-manual)",
},
],
},
{
value: "year",
label: "Year",
total: "812k",
headline: "Network Flow",
description: "Decisions across six facilities",
delta: "+21.5%",
funds: [
{
key: "autopilot",
name: "Autopilot",
amount: "428k",
share: 52.7,
color: inflowAutopilotColor,
fill: "var(--color-autopilot)",
},
{
key: "copilot",
name: "Copilot",
amount: "224k",
share: 27.6,
color: inflowCopilotColor,
fill: "var(--color-copilot)",
},
{
key: "manual",
name: "Manual",
amount: "103k",
share: 12.7,
color: inflowManualColor,
fill: "var(--color-manual)",
},
],
},
]
export const FULFILLMENT_ROWS: FulfillmentException[] = [
{
id: "row-1001",
reference: "NSC-84721",
customer: "Avery Outdoor",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=96&h=96&dpr=2&q=80",
initials: "AO",
lane: "Chicago to Austin",
facility: "ORD-2",
stage: "Carrier tender",
promise: "Today 18:00",
slaMinutes: 42,
automation: "Copilot",
owner: "Maya Singh",
units: 480,
value: 38240,
risk: "Carrier capacity is tight after midday cutoff",
status: "At Risk",
},
{
id: "row-1002",
reference: "NSC-84734",
customer: "Field & Frame",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=96&h=96&dpr=2&q=80",
initials: "FF",
lane: "Dallas to Phoenix",
facility: "DFW-1",
stage: "Pick wave",
promise: "Today 16:30",
slaMinutes: 88,
automation: "Autopilot",
owner: "Leo Martins",
units: 310,
value: 21480,
risk: "Wave optimized by carton density",
status: "On Time",
},
{
id: "row-1003",
reference: "NSC-84755",
customer: "MetroFit Labs",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1519345182560-3f2917c472ef?w=96&h=96&dpr=2&q=80",
initials: "ML",
lane: "Newark to Boston",
facility: "EWR-3",
stage: "Inventory hold",
promise: "Today 15:15",
slaMinutes: -24,
automation: "Manual",
owner: "Nora Albright",
units: 126,
value: 18760,
risk: "Lot trace requires human release",
status: "Blocked",
},
{
id: "row-1004",
reference: "NSC-84763",
customer: "Northline Studio",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1517841905240-472988babdf9?w=96&h=96&dpr=2&q=80",
initials: "NS",
lane: "Los Angeles to Seattle",
facility: "LAX-4",
stage: "Packing",
promise: "Today 19:45",
slaMinutes: 114,
automation: "Autopilot",
owner: "Leo Martins",
units: 840,
value: 52210,
risk: "Packing line is running above plan",
status: "On Time",
},
{
id: "row-1005",
reference: "NSC-84801",
customer: "Urban Pantry",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?w=96&h=96&dpr=2&q=80",
initials: "UP",
lane: "Atlanta to Miami",
facility: "ATL-2",
stage: "Cold chain",
promise: "Today 17:00",
slaMinutes: 9,
automation: "Copilot",
owner: "Maya Singh",
units: 212,
value: 30440,
risk: "Reefer handoff needs confirmation",
status: "Delayed",
},
{
id: "row-1006",
reference: "NSC-84819",
customer: "Glow Market",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1489424731084-a5d8b219a5bb?w=96&h=96&dpr=2&q=80",
initials: "GM",
lane: "Las Vegas to Denver",
facility: "LAS-1",
stage: "Labeling",
promise: "Tomorrow 09:20",
slaMinutes: 312,
automation: "Autopilot",
owner: "Nora Albright",
units: 94,
value: 10920,
risk: "No current risk",
status: "On Time",
},
{
id: "row-1007",
reference: "NSC-84827",
customer: "Ridge Supply",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=96&h=96&dpr=2&q=80",
initials: "RS",
lane: "Portland to San Jose",
facility: "PDX-1",
stage: "Split shipment",
promise: "Today 20:00",
slaMinutes: 36,
automation: "Copilot",
owner: "Maya Singh",
units: 176,
value: 14680,
risk: "Two SKUs short at primary node",
status: "At Risk",
},
{
id: "row-1008",
reference: "NSC-84842",
customer: "Casa Verde",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1544725176-7c40e5a71c5e?w=96&h=96&dpr=2&q=80",
initials: "CV",
lane: "Nashville to Charlotte",
facility: "BNA-2",
stage: "Dock queue",
promise: "Today 14:30",
slaMinutes: -51,
automation: "Manual",
owner: "Nora Albright",
units: 265,
value: 22750,
risk: "Outbound door is constrained",
status: "Delayed",
},
{
id: "row-1009",
reference: "NSC-84864",
customer: "Beacon Cycle",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1552058544-f2b08422138a?w=96&h=96&dpr=2&q=80",
initials: "BC",
lane: "Columbus to Pittsburgh",
facility: "CMH-1",
stage: "Fraud review",
promise: "Tomorrow 11:45",
slaMinutes: 510,
automation: "Manual",
owner: "Maya Singh",
units: 58,
value: 8920,
risk: "Payment review blocks release",
status: "Blocked",
},
{
id: "row-1010",
reference: "NSC-84888",
customer: "Aster Goods",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1508214751196-bcfd4ca60f91?w=96&h=96&dpr=2&q=80",
initials: "AG",
lane: "Reno to Salt Lake City",
facility: "RNO-1",
stage: "Manifest",
promise: "Today 22:15",
slaMinutes: 177,
automation: "Autopilot",
owner: "Leo Martins",
units: 390,
value: 19340,
risk: "Manifest is ready for carrier scan",
status: "On Time",
},
]
export function fulfillmentSearchBlob(row: FulfillmentException): string {
return [
row.reference,
row.customer,
row.email,
row.lane,
row.facility,
row.stage,
row.promise,
row.automation,
row.owner,
row.risk,
row.status,
String(row.units),
String(row.value),
]
.filter(Boolean)
.join(" ")
.toLowerCase()
}
@@ -1,520 +0,0 @@
import { memo } from "react"
import { Badge } from "@/components/reui/badge"
import { DataGridColumnHeader } from "@/components/reui/data-grid/data-grid-column-header"
import {
DataGridTableRowSelect,
DataGridTableRowSelectAll,
} from "@/components/reui/data-grid/data-grid-table"
import { type ColumnDef, type Row } from "@tanstack/react-table"
import { toast } from "sonner"
import { cn } from "@cfdm/ui/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@cfdm/ui/components/avatar"
import { Button } from "@cfdm/ui/components/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@cfdm/ui/components/dropdown-menu"
import { Item, ItemMedia } from "@cfdm/ui/components/item"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@cfdm/ui/components/tooltip"
import {
AUTOMATION_BADGE_VARIANT,
STATUS_BADGE_VARIANT,
type AutomationLevel,
type FulfillmentException,
type FulfillmentStatus,
} from "./data"
import { PackageIcon, InfoIcon, MoreHorizontalIcon, EyeIcon, BellIcon, CopyIcon, TriangleAlertIcon } from "lucide-react"
const currencyCompact = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
})
const numberCompact = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 0,
})
const availabilityColor: Record<FulfillmentStatus, string> = {
"On Time": "bg-success",
"At Risk": "bg-warning",
Delayed: "bg-info",
Blocked: "bg-destructive",
}
const stageProgress: Record<string, number> = {
"Carrier tender": 72,
"Pick wave": 64,
"Inventory hold": 28,
Packing: 82,
"Cold chain": 48,
Labeling: 76,
"Split shipment": 39,
"Dock queue": 31,
"Fraud review": 24,
Manifest: 90,
}
function DotSeparator() {
return (
<span
aria-hidden="true"
className="bg-muted-foreground/40 size-1 shrink-0 rounded-full"
/>
)
}
export const StatusBadge = memo(function StatusBadge({
status,
}: {
status: FulfillmentStatus
}) {
return (
<Badge variant={STATUS_BADGE_VARIANT[status]} className="gap-1.5">
<span
aria-hidden="true"
className={cn("size-1.5 rounded-full", availabilityColor[status])}
/>
{status}
</Badge>
)
})
function AutomationBadge({ level }: { level: AutomationLevel }) {
return <Badge variant={AUTOMATION_BADGE_VARIANT[level]}>{level}</Badge>
}
const ReferenceCell = memo(function ReferenceCell({
row,
}: {
row: Row<FulfillmentException>
}) {
const order = row.original
return (
<div className="flex min-w-0 flex-col gap-1">
<a
href="#"
className="text-primary truncate text-sm font-medium underline-offset-2 transition-colors hover:underline"
aria-label={`View order ${order.reference}`}
>
{order.reference}
</a>
<div className="text-muted-foreground flex min-w-0 items-center gap-1.5 text-xs">
<span className="shrink-0">{order.facility}</span>
<DotSeparator />
<span className="truncate">{order.owner}</span>
</div>
</div>
)
})
const CustomerCell = memo(function CustomerCell({
row,
}: {
row: Row<FulfillmentException>
}) {
const order = row.original
return (
<div className="flex min-w-0 items-center gap-2">
<div className="relative shrink-0">
<Avatar className="size-8">
<AvatarImage src={order.avatar} alt={order.customer} />
<AvatarFallback>{order.initials}</AvatarFallback>
</Avatar>
<span
className={cn(
"ring-background absolute right-0 bottom-0.5 size-2 rounded-full ring-2",
availabilityColor[order.status]
)}
aria-hidden="true"
/>
</div>
<div className="min-w-0">
<a
href="#"
className="text-foreground hover:text-primary line-clamp-1 font-medium underline-offset-2 transition-colors hover:underline"
aria-label={`View customer ${order.customer}`}
>
{order.customer}
</a>
<div
className="text-muted-foreground line-clamp-1 text-xs"
title={order.email}
>
{order.email}
</div>
</div>
</div>
)
})
const StageCell = memo(function StageCell({
row,
}: {
row: Row<FulfillmentException>
}) {
const order = row.original
const progress = stageProgress[order.stage] ?? 50
return (
<div className="flex min-w-0 flex-col gap-1.5">
<div className="flex min-w-0 items-center gap-1.5">
<Item render={<span />} className="w-auto shrink-0 border-0 p-0">
<ItemMedia variant="icon" className="text-muted-foreground size-auto">
<PackageIcon className="size-4" aria-hidden="true" />
</ItemMedia>
</Item>
<span className="text-foreground min-w-0 truncate font-medium">
{order.stage}
</span>
</div>
<div className="flex min-w-0 items-center gap-2">
<span className="bg-muted block h-1.5 min-w-16 flex-1 overflow-hidden rounded-full">
<span
className={cn(
"block h-full rounded-full",
availabilityColor[order.status]
)}
style={{ width: `${progress}%` }}
/>
</span>
<span className="text-muted-foreground shrink-0 text-xs tabular-nums">
{progress}%
</span>
</div>
</div>
)
})
const LaneCell = memo(function LaneCell({
row,
}: {
row: Row<FulfillmentException>
}) {
const order = row.original
return (
<div className="flex max-w-full min-w-0 flex-col gap-0.5">
<span
className="text-foreground block max-w-full min-w-0 truncate font-medium"
title={order.lane}
>
{order.lane}
</span>
<span
className="text-muted-foreground block max-w-full min-w-0 truncate text-xs"
title={order.facility}
>
{order.facility}
</span>
</div>
)
})
const ValueCell = memo(function ValueCell({
row,
}: {
row: Row<FulfillmentException>
}) {
const valueHint =
row.original.value >= 30000 ? "Priority lane" : "Standard lane"
return (
<div className="flex min-w-0 items-center gap-1.5">
<span className="text-foreground font-medium tabular-nums">
{currencyCompact.format(row.original.value)}
</span>
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background inline-flex size-5 items-center justify-center rounded-full transition-colors focus-visible:ring-2 focus-visible:ring-offset-2"
aria-label={`Value hint for ${row.original.reference}: ${valueHint}`}
/>
}
>
<InfoIcon className="size-3.5" aria-hidden="true" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-48 p-2.5 text-xs leading-5">
<div className="flex flex-col">
<span>{valueHint}</span>
<span className="text-background/80">
{numberCompact.format(row.original.units)} units
</span>
</div>
</TooltipContent>
</Tooltip>
</div>
)
})
function StateCell({ row }: { row: Row<FulfillmentException> }) {
const sla = row.original.slaMinutes
const slaHint =
sla < 0
? `${Math.abs(sla)} min overdue`
: sla <= 45
? `${sla} min buffer`
: `Due ${row.original.promise}`
return (
<div className="flex min-w-0 flex-col items-start gap-1">
<StatusBadge status={row.original.status} />
<span className="text-muted-foreground max-w-full truncate text-xs">
{slaHint}
</span>
</div>
)
}
function RiskCell({ row }: { row: Row<FulfillmentException> }) {
return (
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background inline-flex items-center gap-1.5 rounded-full focus-visible:ring-2 focus-visible:ring-offset-2"
aria-label={`Risk note for ${row.original.reference}: ${row.original.risk}`}
/>
}
>
<InfoIcon className="size-3.5" aria-hidden="true" />
<span className="max-w-32 truncate text-xs">{row.original.risk}</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs p-3 text-xs leading-5">
{row.original.risk}
</TooltipContent>
</Tooltip>
)
}
function ActionsCell({ row }: { row: Row<FulfillmentException> }) {
const copyReference = async () => {
await navigator.clipboard?.writeText(row.original.reference)
toast.success("Reference copied", {
description: row.original.reference,
})
}
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
size="icon"
variant="ghost"
className="size-7"
aria-label={`Actions for ${row.original.reference}`}
/>
}
>
<MoreHorizontalIcon aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-44">
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() =>
toast.info("Opening order", {
description: row.original.reference,
})
}
>
<EyeIcon className="size-4" aria-hidden="true" />
View order
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
toast.info("Owner notified", {
description: row.original.owner,
})
}
>
<BellIcon className="size-4" aria-hidden="true" />
Notify owner
</DropdownMenuItem>
<DropdownMenuItem onClick={copyReference}>
<CopyIcon className="size-4" aria-hidden="true" />
Copy reference
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={() =>
toast.warning("Escalation staged", {
description: "Connect this action to your incident workflow.",
})
}
>
<TriangleAlertIcon className="size-4" aria-hidden="true" />
Escalate
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
export const columns: ColumnDef<FulfillmentException>[] = [
{
accessorKey: "id",
id: "id",
header: () => <DataGridTableRowSelectAll />,
cell: ({ row }) => <DataGridTableRowSelect row={row} />,
enableSorting: false,
size: 35,
enableResizing: false,
enableHiding: false,
meta: {
headerClassName: "ps-4!",
cellClassName: "ps-4!",
},
},
{
accessorKey: "reference",
id: "reference",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <ReferenceCell row={row} />,
size: 138,
enableSorting: true,
enableHiding: false,
enableResizing: true,
meta: {
headerTitle: "Order",
},
},
{
accessorKey: "customer",
id: "customer",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <CustomerCell row={row} />,
size: 210,
enableSorting: true,
enableHiding: false,
enableResizing: true,
minSize: 190,
meta: {
headerTitle: "Customer",
autoSize: true,
},
},
{
accessorKey: "lane",
id: "lane",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <LaneCell row={row} />,
size: 165,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Lane",
},
},
{
accessorKey: "stage",
id: "stage",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <StageCell row={row} />,
size: 160,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Stage",
},
},
{
accessorKey: "automation",
id: "automation",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <AutomationBadge level={row.original.automation} />,
size: 112,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Automation",
},
},
{
accessorKey: "value",
id: "value",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <ValueCell row={row} />,
size: 120,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Value",
},
},
{
accessorKey: "risk",
id: "risk",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <RiskCell row={row} />,
size: 170,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Risk",
},
},
{
accessorKey: "status",
id: "status",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <StateCell row={row} />,
size: 142,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "State",
},
},
{
id: "actions",
header: "",
cell: ({ row }) => <ActionsCell row={row} />,
size: 46,
enableSorting: false,
enableHiding: false,
enableResizing: false,
},
]
@@ -1,369 +0,0 @@
import { useMemo, useState } from "react"
import { Badge } from "@/components/reui/badge"
import { DataGrid as ReuiDataGrid } from "@/components/reui/data-grid/data-grid"
import { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"
import {
Frame,
FrameDescription,
FrameFooter,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/reui/frame"
import {
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
type PaginationState,
type RowSelectionState,
type SortingState,
type VisibilityState,
} from "@tanstack/react-table"
import { toast } from "sonner"
import { Button } from "@cfdm/ui/components/button"
import { Checkbox } from "@cfdm/ui/components/checkbox"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@cfdm/ui/components/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@cfdm/ui/components/input-group"
import { Label } from "@cfdm/ui/components/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@cfdm/ui/components/popover"
import { Separator } from "@cfdm/ui/components/separator"
import { TooltipProvider } from "@cfdm/ui/components/tooltip"
import {
FULFILLMENT_ROWS,
fulfillmentSearchBlob,
STATUS_ORDER,
type FulfillmentStatus,
} from "./data"
import { columns, StatusBadge } from "./exception-columns"
import { SearchIcon, XIcon, FilterIcon, MoreHorizontalIcon, FileDownIcon, RefreshCwIcon, SettingsIcon, PlusIcon } from "lucide-react"
interface ToolbarProps {
searchQuery: string
onSearchChange: (value: string) => void
selectedStatuses: FulfillmentStatus[]
onStatusChange: (checked: boolean, status: FulfillmentStatus) => void
onClearFilters: () => void
hasActiveFilters: boolean
statusCounts: Record<string, number>
}
function Toolbar({
searchQuery,
onSearchChange,
selectedStatuses,
onStatusChange,
onClearFilters,
hasActiveFilters,
statusCounts,
}: ToolbarProps) {
return (
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<InputGroup className="w-full min-w-52 sm:w-60">
<InputGroupAddon align="inline-start">
<SearchIcon aria-hidden="true" />
</InputGroupAddon>
<InputGroupInput
placeholder="Search orders..."
aria-label="Search orders"
value={searchQuery}
onChange={(event) => onSearchChange(event.target.value)}
/>
{searchQuery.length > 0 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
aria-label="Clear search"
size="icon-xs"
onClick={() => onSearchChange("")}
>
<XIcon aria-hidden="true" />
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
<Popover>
<PopoverTrigger
render={
<Button variant="outline" aria-label="Filter by order status">
<FilterIcon aria-hidden="true" />
Status
{selectedStatuses.length > 0 && (
<Badge variant="info-outline">
{selectedStatuses.length}
</Badge>
)}
</Button>
}
/>
<PopoverContent
align="start"
className="flex w-48 flex-col gap-2.5 p-3"
>
<span className="text-muted-foreground text-xs font-medium">
Filter by status
</span>
{STATUS_ORDER.map((status) => (
<div key={status} className="flex items-center gap-2.5">
<Checkbox
id={`status-${status.toLowerCase().replace(/\s+/g, "-")}`}
checked={selectedStatuses.includes(status)}
onCheckedChange={(checked) =>
onStatusChange(checked === true, status)
}
/>
<Label
htmlFor={`status-${status.toLowerCase().replace(/\s+/g, "-")}`}
className="flex min-w-0 flex-1 cursor-pointer items-center justify-between gap-2 font-normal"
>
<StatusBadge status={status} />
<span className="text-muted-foreground shrink-0 text-xs tabular-nums">
{statusCounts[status] ?? 0}
</span>
</Label>
</div>
))}
</PopoverContent>
</Popover>
{hasActiveFilters && (
<Button
type="button"
variant="ghost"
size="sm"
className="text-muted-foreground"
onClick={onClearFilters}
>
Clear filters
</Button>
)}
</div>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="outline" aria-label="Exception queue actions">
<MoreHorizontalIcon aria-hidden="true" />
Actions
</Button>
}
/>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() =>
toast.success("Export ready", {
description: "Exception queue export prepared.",
})
}
>
<FileDownIcon aria-hidden="true" />
Export CSV
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
toast.message("Queue refreshed", {
description: "Live data would refresh through your API.",
})
}
>
<RefreshCwIcon aria-hidden="true" />
Refresh
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
toast.info("View settings", {
description: "Column and density controls are available.",
})
}
>
<SettingsIcon aria-hidden="true" />
View settings
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
export function ExceptionGrid() {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
})
const [sorting, setSorting] = useState<SortingState>([
{ id: "value", desc: true },
])
const [searchQuery, setSearchQuery] = useState("")
const [selectedStatuses, setSelectedStatuses] = useState<FulfillmentStatus[]>(
[]
)
const [columnOrder, setColumnOrder] = useState<string[]>(
columns.map((column) => column.id as string)
)
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
risk: false,
})
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const statusCounts = useMemo(
() =>
FULFILLMENT_ROWS.reduce(
(acc, row) => {
acc[row.status] = (acc[row.status] || 0) + 1
return acc
},
{} as Record<string, number>
),
[]
)
const filteredData = useMemo(() => {
return FULFILLMENT_ROWS.filter((row) => {
const matchesStatus =
!selectedStatuses.length || selectedStatuses.includes(row.status)
const matchesSearch =
!searchQuery ||
fulfillmentSearchBlob(row).includes(searchQuery.toLowerCase())
return matchesStatus && matchesSearch
})
}, [searchQuery, selectedStatuses])
const hasActiveFilters =
searchQuery.trim().length > 0 || selectedStatuses.length > 0
const resetToFirstPage = () => {
setPagination((current) =>
current.pageIndex === 0 ? current : { ...current, pageIndex: 0 }
)
}
const handleSearchChange = (value: string) => {
setSearchQuery(value)
resetToFirstPage()
}
const handleStatusChange = (checked: boolean, status: FulfillmentStatus) => {
setSelectedStatuses((current) =>
checked ? [...current, status] : current.filter((item) => item !== status)
)
resetToFirstPage()
}
const handleClearFilters = () => {
setSelectedStatuses([])
setSearchQuery("")
resetToFirstPage()
}
const table = useReactTable({
columns,
data: filteredData,
pageCount: Math.ceil(filteredData.length / pagination.pageSize),
getRowId: (row) => row.id,
state: { pagination, sorting, columnOrder, columnVisibility, rowSelection },
columnResizeMode: "onChange",
enableRowSelection: true,
autoResetPageIndex: false,
onColumnOrderChange: setColumnOrder,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: setPagination,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
})
return (
<TooltipProvider delay={200}>
<ReuiDataGrid
table={table}
recordCount={filteredData.length}
emptyMessage={
filteredData.length === 0
? "No fulfillment exceptions match your filters."
: undefined
}
tableLayout={{
columnsPinnable: true,
columnsResizable: true,
columnsMovable: true,
columnsVisibility: true,
headerSticky: true,
dense: true,
}}
tableClassNames={{
bodyRow: "[&>td]:h-16",
}}
>
<Frame variant="default" spacing="sm" className="w-full">
<FrameHeader className="flex-row items-center justify-between gap-3">
<div className="flex flex-col gap-0.5">
<FrameTitle className="text-balance">Exception Queue</FrameTitle>
<FrameDescription className="text-xs text-pretty">
{filteredData.length} of {FULFILLMENT_ROWS.length} fulfillment
records
</FrameDescription>
</div>
<Button
type="button"
onClick={() =>
toast.info("Create exception", {
description:
"Connect this button to your incident intake flow.",
})
}
>
<PlusIcon aria-hidden="true" />
Add exception
</Button>
</FrameHeader>
<FramePanel className="bg-card p-0! shadow-none!">
<div className="px-4 py-3">
<Toolbar
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
selectedStatuses={selectedStatuses}
onStatusChange={handleStatusChange}
onClearFilters={handleClearFilters}
hasActiveFilters={hasActiveFilters}
statusCounts={statusCounts}
/>
</div>
<Separator />
<DataGridScrollArea>
<DataGridTable />
</DataGridScrollArea>
</FramePanel>
<FrameFooter>
<DataGridPagination />
</FrameFooter>
</Frame>
</ReuiDataGrid>
</TooltipProvider>
)
}
@@ -1,246 +0,0 @@
import { useState } from "react"
import { format } from "date-fns"
import { type DateRange } from "react-day-picker"
import { toast } from "sonner"
import { Button } from "@cfdm/ui/components/button"
import { Calendar } from "@cfdm/ui/components/calendar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@cfdm/ui/components/popover"
import { CalendarIcon, DownloadIcon } from "lucide-react"
type PeriodKey = "last30" | "prev30"
type ReportDateRange = {
from: Date
to: Date
}
type DateRangePreset = {
id: string
label: string
period: PeriodKey
range: ReportDateRange
}
const reportRange = (
fromMonth: number,
fromDay: number,
toMonth: number,
toDay: number,
year = 2026
): ReportDateRange => ({
from: new Date(year, fromMonth, fromDay),
to: new Date(year, toMonth, toDay),
})
const preset = (
id: string,
label: string,
period: PeriodKey,
range: ReportDateRange
): DateRangePreset => ({ id, label, period, range })
const LAST_30_RANGE = reportRange(4, 12, 5, 10)
const PREVIOUS_30_RANGE = reportRange(3, 12, 4, 11)
const REPORT_RANGE_PRESETS: DateRangePreset[] = [
preset("today", "Today", "last30", reportRange(5, 10, 5, 10)),
preset("yesterday", "Yesterday", "last30", reportRange(5, 9, 5, 9)),
preset("last7", "Last 7 days", "last30", reportRange(5, 4, 5, 10)),
preset("last30", "Last 30 days", "last30", LAST_30_RANGE),
preset("monthToDate", "Month to date", "last30", reportRange(5, 1, 5, 10)),
preset("lastMonth", "Last month", "last30", reportRange(4, 1, 4, 31)),
preset("yearToDate", "Year to date", "last30", reportRange(0, 1, 5, 10)),
preset("lastYear", "Last year", "prev30", reportRange(0, 1, 11, 31, 2025)),
]
const MAX_REPORT_DATE = LAST_30_RANGE.to
function isSameRange(first: ReportDateRange, second: DateRange) {
const secondFrom = second.from
const secondTo = second.to ?? second.from
return (
Boolean(secondFrom && secondTo) &&
first.from.getTime() === secondFrom?.getTime() &&
first.to.getTime() === secondTo?.getTime()
)
}
function normalizeRange(
range: DateRange | undefined,
fallback: ReportDateRange
): ReportDateRange {
if (!range?.from) return fallback
const from = range.from
const to = range.to ?? range.from
return from.getTime() <= to.getTime() ? { from, to } : { from: to, to: from }
}
function formatReportRange(range: ReportDateRange) {
return `${format(range.from, "MMM d, yyyy")} - ${format(range.to, "MMM d, yyyy")}`
}
function getPeriodForRange(range: ReportDateRange) {
const matchingPreset = getMatchingPreset(range)
if (matchingPreset) return matchingPreset.period
return range.to.getTime() <= PREVIOUS_30_RANGE.to.getTime()
? "prev30"
: "last30"
}
function getMatchingPreset(range: DateRange | undefined) {
if (!range?.from || !range.to) return undefined
const normalizedRange = normalizeRange(range, LAST_30_RANGE)
return REPORT_RANGE_PRESETS.find((preset) =>
isSameRange(preset.range, normalizedRange)
)
}
function ReportDateRangePicker({
period,
onPeriodChange,
}: {
period: PeriodKey
onPeriodChange: (value: PeriodKey) => void
}) {
const initialRange = period === "prev30" ? PREVIOUS_30_RANGE : LAST_30_RANGE
const [open, setOpen] = useState(false)
const [committedRange, setCommittedRange] =
useState<ReportDateRange>(initialRange)
const [draftRange, setDraftRange] = useState<DateRange | undefined>(
initialRange
)
const selectedPresetId = getMatchingPreset(draftRange ?? committedRange)?.id
function handleOpenChange(nextOpen: boolean) {
if (nextOpen) {
setDraftRange(committedRange)
}
setOpen(nextOpen)
}
function handleApply() {
const nextRange = normalizeRange(draftRange, committedRange)
setCommittedRange(nextRange)
onPeriodChange(getPeriodForRange(nextRange))
setOpen(false)
}
return (
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger
render={
<Button
type="button"
size="sm"
variant="outline"
className="group/pick-date w-[250px] max-w-full justify-between leading-none font-normal tabular-nums"
>
<span className="truncate">
{formatReportRange(committedRange)}
</span>
<CalendarIcon className="text-muted-foreground/80 group-hover/pick-date:text-foreground shrink-0 transition-colors" aria-hidden="true" />
</Button>
}
/>
<PopoverContent align="end" className="w-auto p-0">
<div className="flex flex-col">
<div className="flex flex-col sm:grid sm:grid-cols-[10rem_1fr]">
<div className="border-border flex flex-wrap gap-1 border-b p-2 sm:flex-col sm:border-r sm:border-b-0">
{REPORT_RANGE_PRESETS.map((preset) => {
const selected = selectedPresetId === preset.id
return (
<Button
key={preset.id}
type="button"
size="sm"
variant={selected ? "secondary" : "ghost"}
className={
selected
? "justify-start"
: "text-muted-foreground justify-start"
}
onClick={() => setDraftRange(preset.range)}
>
{preset.label}
</Button>
)
})}
</div>
<Calendar
mode="range"
selected={draftRange}
onSelect={setDraftRange}
numberOfMonths={2}
defaultMonth={draftRange?.from ?? committedRange.from}
disabled={{
after: MAX_REPORT_DATE,
}}
/>
</div>
<div className="border-border flex items-center justify-between gap-2 border-t px-3 py-1.5">
<Button
type="button"
size="sm"
variant="outline"
onClick={() => setDraftRange(LAST_30_RANGE)}
>
Reset
</Button>
<div className="flex items-center gap-2">
<Button
type="button"
size="sm"
variant="ghost"
onClick={() => {
setDraftRange(committedRange)
setOpen(false)
}}
>
Cancel
</Button>
<Button type="button" size="sm" onClick={handleApply}>
Apply
</Button>
</div>
</div>
</div>
</PopoverContent>
</Popover>
)
}
// Header action controls reused from the solution-agents-8 report toolbar.
export function NavbarActions() {
const [periodKey, setPeriodKey] = useState<PeriodKey>("last30")
function handleExport() {
toast.success("Export queued", {
description: "Fulfillment command report is being prepared.",
})
}
return (
<div className="flex shrink-0 items-center gap-2">
<ReportDateRangePicker period={periodKey} onPeriodChange={setPeriodKey} />
<Button size="sm" type="button" onClick={handleExport}>
<DownloadIcon aria-hidden="true" />
<span className="hidden sm:block">Export</span>
</Button>
</div>
)
}
@@ -1,34 +0,0 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@cfdm/ui/components/breadcrumb"
// Navbar breadcrumb
export function NavbarBreadcrumb() {
return (
<Breadcrumb className="min-w-0">
<BreadcrumbList className="flex-nowrap">
<BreadcrumbItem className="hidden md:inline-flex">
<BreadcrumbLink render={<a href="#" />}>Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:flex" />
<BreadcrumbItem className="hidden md:inline-flex">
<BreadcrumbLink render={<a href="#" />}>Operations</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:flex" />
<BreadcrumbItem className="min-w-0">
<BreadcrumbPage className="truncate">Fulfillment</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}
@@ -1,80 +0,0 @@
import { useState } from "react"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage,
} from "@cfdm/ui/components/avatar"
import { Button } from "@cfdm/ui/components/button"
import { Input } from "@cfdm/ui/components/input"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@cfdm/ui/components/popover"
import { TEAM_EXTRA_COUNT, TEAM_MEMBERS } from "./data"
import { UserPlusIcon } from "lucide-react"
// Header presence controls with team avatars and invite action.
export function NavbarPresence() {
const [email, setEmail] = useState("")
const [open, setOpen] = useState(false)
const handleInvite = () => {
if (!email.trim()) return
setEmail("")
setOpen(false)
}
return (
<div className="flex shrink-0 items-center gap-1.5">
<AvatarGroup>
{TEAM_MEMBERS.map((member, index) => (
<Avatar key={index} size="sm">
<AvatarImage src={member.src} alt={member.name} />
<AvatarFallback className="text-[9px]! font-medium">
{member.initials}
</AvatarFallback>
</Avatar>
))}
<AvatarGroupCount className="text-[10px]! font-medium">
+{TEAM_EXTRA_COUNT}
</AvatarGroupCount>
</AvatarGroup>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
render={
<Button
variant="outline"
size="icon-sm"
aria-label="Invite team member"
/>
}
>
<UserPlusIcon aria-hidden="true" />
</PopoverTrigger>
<PopoverContent sideOffset={7} align="end" className="w-72">
<div className="flex flex-col gap-3">
<h4 className="text-foreground text-sm">Invite team member</h4>
<Input
type="email"
placeholder="[email protected]"
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleInvite()}
/>
<Button onClick={handleInvite} disabled={!email.trim()}>
Send invite
</Button>
</div>
</PopoverContent>
</Popover>
</div>
)
}
@@ -1,17 +0,0 @@
import { NavbarActions } from "./navbar-actions"
import { NavbarBreadcrumb } from "./navbar-breadcrumb"
// Navbar with breadcrumb and report range actions.
export function Navbar() {
return (
<header
className="flex min-h-9 w-full shrink-0 items-center justify-between gap-2 pb-1"
aria-label="Fulfillment command header"
>
<NavbarBreadcrumb />
<NavbarActions />
</header>
)
}
@@ -1,9 +0,0 @@
import { Dashboard } from "./components/dashboard"
export function Page() {
return (
<main className="bg-background min-h-svh w-full p-3 sm:p-4 lg:p-6">
<Dashboard />
</main>
)
}
@@ -1,406 +0,0 @@
import { Badge } from "@/components/reui/badge"
import { DataGridColumnHeader } from "@/components/reui/data-grid/data-grid-column-header"
import {
DataGridTableRowSelect,
DataGridTableRowSelectAll,
} from "@/components/reui/data-grid/data-grid-table"
import { Rating } from "@/components/reui/rating"
import { type ReactNode } from "react"
import { type ColumnDef } from "@tanstack/react-table"
import { format, parseISO } from "date-fns"
import { cn } from "@cfdm/ui/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@cfdm/ui/components/avatar"
import { Button } from "@cfdm/ui/components/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@cfdm/ui/components/dropdown-menu"
import { Item, ItemMedia } from "@cfdm/ui/components/item"
import { Switch } from "@cfdm/ui/components/switch"
import {
type AutomationKind,
type AutomationOwnerAvailability,
type AutomationState,
type IAutomationRecord,
} from "./data"
import { GitBranchIcon, RouteIcon, SparklesIcon, MailIcon, BellRingIcon, EllipsisVerticalIcon, PencilIcon, EyeIcon, CircleCheckIcon, ArchiveIcon } from "lucide-react"
export type AutomationAction = "edit" | "open" | "archive"
const automationKindStyles: Record<
AutomationKind,
{ chipClassName: string; icon: ReactNode }
> = {
sequence: {
chipClassName:
"bg-sky-50 text-sky-600 ring-sky-200 dark:bg-sky-500/15 dark:text-sky-300 dark:ring-sky-400/30",
icon: (
<GitBranchIcon className="size-4" aria-hidden="true" />
),
},
routing: {
chipClassName:
"bg-violet-50 text-violet-600 ring-violet-200 dark:bg-violet-500/15 dark:text-violet-300 dark:ring-violet-400/30",
icon: (
<RouteIcon className="size-4" aria-hidden="true" />
),
},
enrichment: {
chipClassName:
"bg-emerald-50 text-emerald-600 ring-emerald-200 dark:bg-emerald-500/15 dark:text-emerald-300 dark:ring-emerald-400/30",
icon: (
<SparklesIcon className="size-4" aria-hidden="true" />
),
},
digest: {
chipClassName:
"bg-amber-50 text-amber-600 ring-amber-200 dark:bg-amber-500/15 dark:text-amber-300 dark:ring-amber-400/30",
icon: (
<MailIcon className="size-4" aria-hidden="true" />
),
},
escalation: {
chipClassName:
"bg-rose-50 text-rose-600 ring-rose-200 dark:bg-rose-500/15 dark:text-rose-300 dark:ring-rose-400/30",
icon: (
<BellRingIcon className="size-4" aria-hidden="true" />
),
},
}
const stateBadgeStyles: Record<
AutomationState,
{ label: string; dotClassName?: string }
> = {
live: {
label: "Live",
dotClassName: "bg-emerald-500",
},
review: {
label: "Needs approval",
dotClassName: "bg-amber-500",
},
drafts: {
label: "Draft",
dotClassName: "bg-slate-400 dark:bg-slate-300",
},
paused: {
label: "Paused",
dotClassName: "bg-zinc-400 dark:bg-zinc-300",
},
}
const updatedBucketLabel: Record<IAutomationRecord["updatedBucket"], string> = {
today: "Today",
"this-week": "This week",
older: "Older",
}
const availabilityColor: Record<AutomationOwnerAvailability, string> = {
online: "bg-green-500",
away: "bg-yellow-400",
busy: "bg-red-500",
offline: "bg-gray-500",
}
function AutomationKindChip({ kind }: { kind: AutomationKind }) {
const style = automationKindStyles[kind]
return (
<Item
render={<span />}
className={cn(
"p-0",
"inline-flex size-10 items-center justify-center ring-1 ring-inset",
style.chipClassName
)}
aria-hidden="true"
>
<ItemMedia variant="icon" className="size-auto">
{style.icon}
</ItemMedia>
</Item>
)
}
function AutomationNameCell({ automation }: { automation: IAutomationRecord }) {
return (
<div className="flex min-w-0 items-center gap-3">
<AutomationKindChip kind={automation.kind} />
<div className="flex min-w-0 flex-col gap-px">
<span className="text-foreground truncate text-sm font-medium">
{automation.title}
</span>
<div className="text-muted-foreground flex min-w-0 items-center gap-1.5 text-xs">
<span className="truncate">{automation.runWindowLabel}</span>
<span
className="bg-input size-1 shrink-0 rounded-full"
aria-hidden="true"
/>
<span className="truncate">{automation.audienceLabel}</span>
</div>
</div>
</div>
)
}
function OwnerCell({ automation }: { automation: IAutomationRecord }) {
return (
<div className="flex items-center gap-2">
<div className="relative shrink-0">
<Avatar className="size-8">
{automation.owner.avatar ? (
<AvatarImage
src={automation.owner.avatar}
alt={automation.owner.name}
/>
) : null}
<AvatarFallback>{automation.owner.initials}</AvatarFallback>
</Avatar>
<span
className={cn(
"ring-background absolute right-0 bottom-0.5 size-2 rounded-full ring-2",
availabilityColor[automation.owner.availability]
)}
aria-hidden
/>
</div>
<div className="min-w-0">
<div className="text-foreground line-clamp-1 font-medium">
{automation.owner.name}
</div>
<div
className="text-muted-foreground line-clamp-1 text-xs"
title={automation.owner.email}
>
{automation.owner.email}
</div>
</div>
</div>
)
}
function StateCell({ automation }: { automation: IAutomationRecord }) {
const stateStyle = stateBadgeStyles[automation.state]
return (
<div className="flex min-w-0 items-center">
<Badge variant="outline" className="gap-1.5">
{stateStyle.dotClassName ? (
<span
className={cn(
"size-1.5 shrink-0 rounded-full",
stateStyle.dotClassName
)}
aria-hidden="true"
/>
) : null}
{stateStyle.label}
</Badge>
</div>
)
}
function UpdatedCell({ automation }: { automation: IAutomationRecord }) {
return (
<div className="flex min-w-0 flex-col gap-px">
<span className="text-foreground text-sm">
{format(parseISO(automation.updatedAt), "MMM d, yyyy")}
</span>
<span className="text-muted-foreground text-xs">
{updatedBucketLabel[automation.updatedBucket]}
</span>
</div>
)
}
function RatingCell({ automation }: { automation: IAutomationRecord }) {
return <Rating rating={automation.rating} size="sm" showValue={true} />
}
function AutomationActionsCell({
automation,
onAction,
onToggleEnabled,
}: {
automation: IAutomationRecord
onAction: (action: AutomationAction, automation: IAutomationRecord) => void
onToggleEnabled: (automation: IAutomationRecord, nextValue: boolean) => void
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label={`Open actions for ${automation.title}`}
>
<EllipsisVerticalIcon className="size-4" aria-hidden="true" />
</Button>
}
/>
{/* Content */}
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => onAction("edit", automation)}>
<PencilIcon className="size-4" aria-hidden="true" />
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onAction("open", automation)}>
<EyeIcon className="size-4" aria-hidden="true" />
View Details
</DropdownMenuItem>
<DropdownMenuItem
closeOnClick={false}
onClick={(event) => {
// The Switch toggles itself and its click bubbles here; skip it
// so item-level activation (row click, Enter/Space) toggles once.
if (
event.target instanceof Element &&
event.target.closest('[data-slot="switch"]')
) {
return
}
onToggleEnabled(automation, !automation.enabled)
}}
className="justify-between gap-4"
>
<span className="flex items-center gap-2">
<CircleCheckIcon className="size-4" aria-hidden="true" />
Enabled
</span>
<Switch
size="sm"
aria-label={`Toggle ${automation.title}`}
checked={automation.enabled}
onCheckedChange={(checked) =>
onToggleEnabled(automation, checked)
}
/>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={() => onAction("archive", automation)}
>
<ArchiveIcon className="size-4" aria-hidden="true" />
Archive
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
export function createAutomationColumns({
onAction,
onToggleEnabled,
}: {
onAction: (action: AutomationAction, automation: IAutomationRecord) => void
onToggleEnabled: (automation: IAutomationRecord, nextValue: boolean) => void
}): ColumnDef<IAutomationRecord>[] {
return [
{
id: "select",
header: () => <DataGridTableRowSelectAll />,
cell: ({ row }) => <DataGridTableRowSelect row={row} />,
size: 30,
enableSorting: false,
enableResizing: false,
enableHiding: false,
meta: {
headerClassName:
"[--data-grid-header-cell-ps:var(--frame-panel-header-px)]",
cellClassName: "[--data-grid-body-cell-ps:var(--frame-panel-px)]",
},
},
{
accessorFn: (row) => row.title,
id: "workflow",
header: ({ column }) => (
<DataGridColumnHeader title="Workflow" column={column} />
),
cell: ({ row }) => <AutomationNameCell automation={row.original} />,
size: 340,
minSize: 200,
enableSorting: true,
enableHiding: false,
meta: {
autoSize: true,
headerClassName: "pl-3!",
cellClassName: "pl-3!",
},
},
{
accessorFn: (row) => row.owner.name,
id: "owner",
header: ({ column }) => (
<DataGridColumnHeader title="Owner" column={column} />
),
cell: ({ row }) => <OwnerCell automation={row.original} />,
size: 175,
enableSorting: true,
},
{
accessorFn: (row) => row.rating,
id: "rating",
header: ({ column }) => (
<DataGridColumnHeader title="Score" column={column} />
),
cell: ({ row }) => <RatingCell automation={row.original} />,
size: 150,
enableSorting: true,
},
{
accessorFn: (row) => row.state,
id: "state",
header: ({ column }) => (
<DataGridColumnHeader title="State" column={column} />
),
cell: ({ row }) => <StateCell automation={row.original} />,
size: 150,
enableSorting: true,
},
{
accessorFn: (row) => parseISO(row.updatedAt).getTime(),
id: "updatedAt",
header: ({ column }) => (
<DataGridColumnHeader title="Last updated" column={column} />
),
cell: ({ row }) => <UpdatedCell automation={row.original} />,
size: 125,
enableSorting: true,
},
{
id: "actions",
header: () => null,
cell: ({ row }) => (
<div className="flex items-center justify-end">
<AutomationActionsCell
automation={row.original}
onAction={onAction}
onToggleEnabled={onToggleEnabled}
/>
</div>
),
size: 56,
enableSorting: false,
meta: {
headerClassName:
"[--data-grid-header-cell-pe:var(--frame-panel-header-px)]",
cellClassName: "[--data-grid-body-cell-pe:var(--frame-panel-px)]",
},
},
]
}
@@ -1,662 +0,0 @@
"use client"
import { useCallback, useMemo, useState } from "react"
import { Badge } from "@/components/reui/badge"
import { DataGrid } from "@/components/reui/data-grid/data-grid"
import { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"
import {
createFilter,
Filters,
type Filter,
type FilterFieldConfig,
} from "@/components/reui/filters"
import {
Frame,
FrameDescription,
FrameFooter,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/reui/frame"
import {
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
type PaginationState,
type RowSelectionState,
type SortingState,
} from "@tanstack/react-table"
import { toast } from "sonner"
import { cn } from "@cfdm/ui/lib/utils"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@cfdm/ui/components/alert-dialog"
import { Button } from "@cfdm/ui/components/button"
import { Separator } from "@cfdm/ui/components/separator"
import { Tabs, TabsList, TabsTrigger } from "@cfdm/ui/components/tabs"
import { createAutomationColumns } from "./columns"
import {
AUTOMATION_TABS,
AUTOMATIONS,
DELIVERY_FILTER_OPTIONS,
getAutomationTabFromState,
OWNER_FILTER_OPTIONS,
UPDATED_FILTER_OPTIONS,
type AutomationState,
type AutomationTab,
type IAutomationRecord,
} from "./data"
import { SearchIcon, UsersIcon, RouteIcon, ClockIcon, PlusIcon, FilterIcon, FunnelXIcon } from "lucide-react"
type ToastTone = "success" | "neutral" | "destructive"
const toneStyles: Record<ToastTone, { dot: string }> = {
success: { dot: "bg-emerald-500" },
neutral: { dot: "bg-sky-500" },
destructive: { dot: "bg-rose-500" },
}
function showAutomationToast({
tone,
title,
description,
}: {
tone: ToastTone
title: string
description: string
}) {
toast.custom((id) => (
<div className="bg-popover text-popover-foreground border-border flex w-[356px] flex-col gap-3 rounded-md border p-4 shadow-lg">
<div className="flex items-start gap-2">
<span
className={cn(
"mt-1 flex size-2 shrink-0 rounded-full",
toneStyles[tone].dot
)}
aria-hidden="true"
/>
<div className="flex flex-1 flex-col gap-1">
<p className="text-sm font-semibold">{title}</p>
<p className="text-muted-foreground text-sm leading-relaxed text-pretty">
{description}
</p>
</div>
</div>
<div className="flex gap-2">
<Button size="xs" variant="outline" onClick={() => toast.dismiss(id)}>
Done
</Button>
</div>
</div>
))
}
function getAutomationSearchBlob(automation: IAutomationRecord) {
return [
automation.title,
automation.kind,
automation.state,
automation.deliveryMode,
automation.audienceLabel,
automation.runWindowLabel,
automation.owner.name,
automation.owner.email,
automation.owner.teamLabel,
automation.approvalRequired ? "approval required" : "auto-approved",
automation.enabled ? "enabled" : "disabled",
]
.join(" ")
.toLowerCase()
}
function getActiveFilters(filters: Filter[]) {
return filters.filter((filter) => {
const { values } = filter
if (!values || values.length === 0) return false
if (
values.every((value) => typeof value === "string" && value.trim() === "")
) {
return false
}
if (values.every((value) => value === null || value === undefined)) {
return false
}
if (values.every((value) => Array.isArray(value) && value.length === 0)) {
return false
}
return true
})
}
function renderSelectedCount(values: unknown[]) {
if (values.length === 0) return "Select..."
if (values.length > 1) return `${values.length} selected`
return null
}
function renderSingleSelectedLabel(
values: unknown[],
options: { value: string; label: string }[]
) {
const state = renderSelectedCount(values)
if (state) return state
const option = options.find((item) => item.value === values[0])
return option?.label ?? String(values[0])
}
function filterFieldValue(
automation: IAutomationRecord,
field: string
): unknown {
switch (field) {
case "workflow":
return getAutomationSearchBlob(automation)
case "ownerTeam":
return automation.owner.team
case "deliveryMode":
return automation.deliveryMode
case "updatedBucket":
return automation.updatedBucket
default:
return ""
}
}
function applyFiltersToData(
data: IAutomationRecord[],
filters: Filter[]
): IAutomationRecord[] {
const active = getActiveFilters(filters)
let result = [...data]
active.forEach((filter) => {
const { field, operator, values } = filter
result = result.filter((item) => {
const raw = filterFieldValue(item, field)
const fieldValue = raw != null ? raw : ""
switch (operator) {
case "is":
return values.includes(fieldValue)
case "is_not":
return !values.includes(fieldValue)
case "is_any_of":
return values.some((value) => fieldValue === value)
case "is_not_any_of":
return !values.some((value) => fieldValue === value)
case "contains": {
const tokens = values
.map((value) => String(value).trim())
.filter(Boolean)
if (tokens.length === 0) return true
return tokens.some((token) =>
String(fieldValue).toLowerCase().includes(token.toLowerCase())
)
}
case "not_contains":
return !values.some((value) =>
String(fieldValue)
.toLowerCase()
.includes(String(value).toLowerCase())
)
case "starts_with":
return values.some((value) =>
String(fieldValue)
.toLowerCase()
.startsWith(String(value).toLowerCase())
)
case "ends_with":
return values.some((value) =>
String(fieldValue)
.toLowerCase()
.endsWith(String(value).toLowerCase())
)
case "empty":
return fieldValue === "" || fieldValue == null
case "not_empty":
return fieldValue !== "" && fieldValue != null
default:
return true
}
})
})
return result
}
const OWNER_TEAM_FILTER_OPTIONS = OWNER_FILTER_OPTIONS.filter(
(option) => option.value !== "everyone"
).map((option) => ({
value: option.value,
label: option.label,
}))
const DELIVERY_MODE_FILTER_OPTIONS = DELIVERY_FILTER_OPTIONS.filter(
(option) => option.value !== "any"
).map((option) => ({
value: option.value,
label: option.label,
}))
const UPDATED_BUCKET_FILTER_OPTIONS = UPDATED_FILTER_OPTIONS.filter(
(option) => option.value !== "any"
).map((option) => ({
value: option.value,
label: option.label,
}))
const filterFields: FilterFieldConfig[] = [
{
key: "workflow",
label: "Workflow",
icon: (
<SearchIcon className="size-3.5" aria-hidden />
),
type: "text",
className: "w-52",
placeholder: "Search...",
},
{
key: "ownerTeam",
label: "Owner team",
icon: (
<UsersIcon className="size-3.5" aria-hidden />
),
type: "select",
searchable: false,
className: "w-[168px]",
options: OWNER_TEAM_FILTER_OPTIONS,
customValueRenderer: (values) =>
renderSingleSelectedLabel(values, OWNER_TEAM_FILTER_OPTIONS),
},
{
key: "deliveryMode",
label: "Delivery mode",
icon: (
<RouteIcon className="size-3.5" aria-hidden />
),
type: "select",
searchable: false,
className: "w-[168px]",
options: DELIVERY_MODE_FILTER_OPTIONS,
customValueRenderer: (values) =>
renderSingleSelectedLabel(values, DELIVERY_MODE_FILTER_OPTIONS),
},
{
key: "updatedBucket",
label: "Last updated",
icon: (
<ClockIcon className="size-3.5" aria-hidden />
),
type: "select",
searchable: false,
className: "w-[160px]",
options: UPDATED_BUCKET_FILTER_OPTIONS,
customValueRenderer: (values) =>
renderSingleSelectedLabel(values, UPDATED_BUCKET_FILTER_OPTIONS),
},
]
function createDefaultAutomationFilters(): Filter[] {
return [createFilter("workflow", "contains", [""])]
}
function getTabCounts(records: IAutomationRecord[]) {
return {
all: records.length,
live: records.filter((record) => record.state === "live").length,
review: records.filter((record) => record.state === "review").length,
drafts: records.filter((record) => record.state === "drafts").length,
paused: records.filter((record) => record.state === "paused").length,
} satisfies Record<AutomationTab, number>
}
export function AutomationLibraryGridView() {
const [automations, setAutomations] =
useState<IAutomationRecord[]>(AUTOMATIONS)
const [activeTab, setActiveTab] = useState<AutomationTab>("all")
const [filters, setFilters] = useState<Filter[]>(
createDefaultAutomationFilters
)
const [sorting, setSorting] = useState<SortingState>([
{ id: "updatedAt", desc: true },
])
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
})
const [automationPendingArchive, setAutomationPendingArchive] =
useState<IAutomationRecord | null>(null)
const resetPagination = useCallback(() => {
setPagination((current) =>
current.pageIndex === 0 ? current : { ...current, pageIndex: 0 }
)
}, [])
const filteredBaseAutomations = useMemo(() => {
return applyFiltersToData(automations, filters)
}, [automations, filters])
const filteredAutomations = useMemo(
() =>
filteredBaseAutomations.filter((automation) =>
activeTab === "all"
? true
: getAutomationTabFromState(automation.state) === activeTab
),
[activeTab, filteredBaseAutomations]
)
const tabCounts = useMemo(
() => getTabCounts(filteredBaseAutomations),
[filteredBaseAutomations]
)
const filteredLiveCount = useMemo(
() =>
filteredAutomations.filter((automation) => automation.state === "live")
.length,
[filteredAutomations]
)
const filteredReviewCount = useMemo(
() =>
filteredAutomations.filter((automation) => automation.state === "review")
.length,
[filteredAutomations]
)
const selectedCount = useMemo(
() => Object.keys(rowSelection).length,
[rowSelection]
)
const columns = useMemo(
() =>
createAutomationColumns({
onAction: (action, automation) => {
if (action === "archive") {
setAutomationPendingArchive(automation)
return
}
if (action === "edit") {
showAutomationToast({
tone: "neutral",
title: "Workflow editor",
description: `Connect "${automation.title}" to your builder, side panel, or automation step editor.`,
})
return
}
showAutomationToast({
tone: "success",
title: "Workflow details",
description: `"${automation.title}" is ready for a detail route, run history drawer, or audit panel.`,
})
},
onToggleEnabled: (automation, nextValue) => {
const nextState: AutomationState =
nextValue && automation.state === "paused"
? "live"
: nextValue && automation.state === "drafts"
? "review"
: !nextValue && automation.state === "live"
? "paused"
: automation.state
setAutomations((current) =>
current.map((item) =>
item.id === automation.id
? {
...item,
enabled: nextValue,
state: nextState,
}
: item
)
)
showAutomationToast({
tone: nextValue ? "success" : "neutral",
title: nextValue ? "Workflow enabled" : "Workflow paused",
description: nextValue
? `"${automation.title}" is ready to run in the ${nextState === "review" ? "review" : "live"} queue.`
: `"${automation.title}" will stay available but will not continue running until resumed.`,
})
},
}),
[]
)
const table = useReactTable({
data: filteredAutomations,
columns,
getRowId: (row) => row.id,
state: {
sorting,
rowSelection,
pagination,
},
enableRowSelection: true,
onSortingChange: setSorting,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
const handleClearControls = useCallback(() => {
setFilters(createDefaultAutomationFilters())
resetPagination()
}, [resetPagination])
const handleFiltersChange = useCallback(
(nextFilters: Filter[]) => {
setFilters(nextFilters)
resetPagination()
},
[resetPagination]
)
const handleArchiveAutomation = useCallback(() => {
if (!automationPendingArchive) return
const automationToArchive = automationPendingArchive
setAutomations((current) =>
current.filter(
(automation) => automation.id !== automationPendingArchive.id
)
)
setRowSelection((current) => {
const next = { ...current }
delete next[automationPendingArchive.id]
return next
})
setAutomationPendingArchive(null)
resetPagination()
showAutomationToast({
tone: "destructive",
title: "Workflow archived",
description: `"${automationToArchive.title}" was removed from this automation library.`,
})
}, [automationPendingArchive, resetPagination])
const emptyMessage =
"No workflows match this automation slice. Switch tabs or clear the filters."
return (
<>
{/* Table */}
<DataGrid
table={table}
recordCount={filteredAutomations.length}
emptyMessage={emptyMessage}
tableLayout={{
dense: true,
}}
>
<Frame dense variant="default" spacing="sm" className="w-full">
<FrameHeader className="flex-row items-start justify-between gap-3">
<div className="flex flex-col gap-px">
<FrameTitle className="text-balance">
Automation Library
</FrameTitle>
<FrameDescription className="flex flex-wrap items-center gap-1.5 text-xs text-pretty">
<span>
{filteredAutomations.length} workflow
{filteredAutomations.length === 1 ? "" : "s"}
</span>
<span
className="bg-input size-1 shrink-0 rounded-full"
aria-hidden="true"
/>
<span>{filteredLiveCount} live</span>
<span
className="bg-input size-1 shrink-0 rounded-full"
aria-hidden="true"
/>
<span>{filteredReviewCount} review</span>
{selectedCount > 0 ? (
<>
<span
className="bg-input size-1 shrink-0 rounded-full"
aria-hidden="true"
/>
<span>{selectedCount} selected</span>
</>
) : null}
</FrameDescription>
</div>
<Button type="button" className="shrink-0">
<PlusIcon className="size-4" aria-hidden="true" />
New workflow
</Button>
</FrameHeader>
<FramePanel className="p-0 shadow-none!">
<div className="px-(--frame-panel-header-px) pt-(--frame-panel-header-py)">
<Tabs
value={activeTab}
onValueChange={(value) => {
setActiveTab(value as AutomationTab)
resetPagination()
}}
>
<TabsList variant="line" className="h-auto gap-5 bg-transparent p-0">
{AUTOMATION_TABS.map((tab) => (
<TabsTrigger
key={tab.value}
value={tab.value}
className="text-muted-foreground hover:text-foreground data-active:bg-transparent data-active:text-foreground dark:data-active:border-transparent dark:data-active:bg-transparent h-auto flex-none gap-2 rounded-none bg-transparent px-0 pb-3 text-sm shadow-none after:bottom-0 after:h-0.5 data-active:shadow-none"
>
<span>{tab.label}</span>
<span className="bg-muted text-muted-foreground inline-flex min-w-5 items-center justify-center rounded-md px-1.5 py-0.5 text-xs tabular-nums">
{tabCounts[tab.value]}
</span>
</TabsTrigger>
))}
</TabsList>
</Tabs>
</div>
<Separator />
<div className="flex flex-wrap items-center justify-between gap-3 px-(--frame-panel-header-px) py-(--frame-panel-header-py)">
<Filters
filters={filters}
fields={filterFields}
onChange={handleFiltersChange}
size="default"
trigger={
<Button variant="outline" aria-label="Filters">
<FilterIcon className="size-4" aria-hidden="true" />
Filters
</Button>
}
/>
<div className="flex flex-wrap items-center justify-end gap-2">
{selectedCount > 0 ? (
<Badge size="sm" variant="secondary">
{selectedCount} selected
</Badge>
) : null}
<Button
type="button"
variant="outline"
onClick={handleClearControls}
>
<FunnelXIcon className="size-4" aria-hidden="true" />
Clear
</Button>
</div>
</div>
<Separator />
<DataGridScrollArea>
<DataGridTable />
</DataGridScrollArea>
<Separator />
<FrameFooter>
<DataGridPagination />
</FrameFooter>
</FramePanel>
</Frame>
</DataGrid>
<AlertDialog
open={automationPendingArchive != null}
onOpenChange={(open) => {
if (!open) {
setAutomationPendingArchive(null)
}
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Archive workflow?</AlertDialogTitle>
<AlertDialogDescription>
{automationPendingArchive
? `Archive "${automationPendingArchive.title}" from this automation library. Run history and ownership context can stay available in your backend, but this row will disappear from the grid preview.`
: "Archive this workflow from the automation library."}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleArchiveAutomation}
render={
<Button type="button" variant="destructive">
Archive
</Button>
}
/>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}
@@ -1,384 +0,0 @@
export type AutomationTab = "all" | "live" | "review" | "drafts" | "paused"
export type AutomationKind =
| "sequence"
| "routing"
| "enrichment"
| "digest"
| "escalation"
export type OwnerFilter =
| "everyone"
| "product"
| "engineering"
| "operations"
| "revenue"
| "support"
export type DeliveryFilter =
| "any"
| "scheduled"
| "event-driven"
| "manual"
| "hybrid"
export type UpdatedFilter = "any" | "today" | "this-week" | "older"
export type AutomationState = Exclude<AutomationTab, "all">
export type AutomationOwnerAvailability = "online" | "away" | "busy" | "offline"
export interface IAutomationOwner {
id: string
name: string
email: string
initials: string
avatar?: string
availability: AutomationOwnerAvailability
team: Exclude<OwnerFilter, "everyone">
teamLabel: string
}
export interface IAutomationRecord {
id: string
title: string
kind: AutomationKind
state: AutomationState
rating: number
deliveryMode: Exclude<DeliveryFilter, "any">
owner: IAutomationOwner
updatedAt: string
updatedBucket: Exclude<UpdatedFilter, "any">
enabled: boolean
approvalRequired: boolean
audienceLabel: string
runWindowLabel: string
}
const OWNERS: Record<string, IAutomationOwner> = {
maya: {
id: "maya-patel",
name: "Maya Patel",
email: "[email protected]",
initials: "MP",
avatar:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=96&h=96&dpr=2&q=80",
availability: "online",
team: "product",
teamLabel: "Product",
},
jonas: {
id: "jonas-reed",
name: "Jonas Reed",
email: "[email protected]",
initials: "JR",
avatar:
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=96&h=96&dpr=2&q=80",
availability: "busy",
team: "engineering",
teamLabel: "Engineering",
},
priya: {
id: "priya-nair",
name: "Priya Nair",
email: "[email protected]",
initials: "PN",
avatar:
"https://images.unsplash.com/photo-1517841905240-472988babdf9?w=96&h=96&dpr=2&q=80",
availability: "away",
team: "operations",
teamLabel: "Operations",
},
emil: {
id: "emil-novak",
name: "Emil Novak",
email: "[email protected]",
initials: "EN",
avatar:
"https://images.unsplash.com/photo-1560250097-0b93528c311a?w=96&h=96&dpr=2&q=80",
availability: "offline",
team: "revenue",
teamLabel: "Revenue",
},
nora: {
id: "nora-ibrahim",
name: "Nora Ibrahim",
email: "[email protected]",
initials: "NI",
avatar:
"https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=96&h=96&dpr=2&q=80",
availability: "online",
team: "support",
teamLabel: "Support",
},
}
function automation(
input: Omit<IAutomationRecord, "owner"> & {
owner: keyof typeof OWNERS
}
): IAutomationRecord {
return {
...input,
owner: OWNERS[input.owner],
}
}
export const AUTOMATION_TABS: { value: AutomationTab; label: string }[] = [
{ value: "all", label: "All" },
{ value: "live", label: "Live" },
{ value: "review", label: "Needs Review" },
{ value: "drafts", label: "Drafts" },
{ value: "paused", label: "Paused" },
]
export const OWNER_FILTER_OPTIONS: {
value: OwnerFilter
label: string
}[] = [
{ value: "everyone", label: "Owner team" },
{ value: "product", label: "Product" },
{ value: "engineering", label: "Engineering" },
{ value: "operations", label: "Operations" },
{ value: "revenue", label: "Revenue" },
{ value: "support", label: "Support" },
]
export const DELIVERY_FILTER_OPTIONS: {
value: DeliveryFilter
label: string
}[] = [
{ value: "any", label: "Delivery mode" },
{ value: "scheduled", label: "Scheduled" },
{ value: "event-driven", label: "Event-driven" },
{ value: "manual", label: "Manual" },
{ value: "hybrid", label: "Hybrid" },
]
export const UPDATED_FILTER_OPTIONS: {
value: UpdatedFilter
label: string
}[] = [
{ value: "any", label: "Last updated" },
{ value: "today", label: "Today" },
{ value: "this-week", label: "This week" },
{ value: "older", label: "Older" },
]
export function getAutomationTabFromState(
state: AutomationState
): Exclude<AutomationTab, "all"> {
return state
}
export const AUTOMATIONS: IAutomationRecord[] = [
automation({
id: "renewal-touchpoint-orchestration",
title: "Renewal touchpoint orchestration",
kind: "sequence",
state: "live",
rating: 4.8,
deliveryMode: "scheduled",
owner: "emil",
updatedAt: "2026-04-11",
updatedBucket: "today",
enabled: true,
approvalRequired: false,
audienceLabel: "Renewal accounts",
runWindowLabel: "Weekdays 09:00",
}),
automation({
id: "delegated-sender-review-route",
title: "Delegated sender review route",
kind: "routing",
state: "review",
rating: 4.2,
deliveryMode: "manual",
owner: "priya",
updatedAt: "2026-04-11",
updatedBucket: "today",
enabled: false,
approvalRequired: true,
audienceLabel: "Delegated senders",
runWindowLabel: "Queue-based release",
}),
automation({
id: "launch-handoff-digest",
title: "Launch handoff digest",
kind: "digest",
state: "live",
rating: 4.7,
deliveryMode: "scheduled",
owner: "maya",
updatedAt: "2026-04-10",
updatedBucket: "this-week",
enabled: true,
approvalRequired: false,
audienceLabel: "Launch squad",
runWindowLabel: "Daily 08:30",
}),
automation({
id: "sla-escalation-watch",
title: "SLA escalation watch",
kind: "escalation",
state: "live",
rating: 4.9,
deliveryMode: "event-driven",
owner: "nora",
updatedAt: "2026-04-10",
updatedBucket: "this-week",
enabled: true,
approvalRequired: false,
audienceLabel: "Priority tickets",
runWindowLabel: "On trigger",
}),
automation({
id: "lead-enrichment-pass",
title: "Lead enrichment pass",
kind: "enrichment",
state: "paused",
rating: 3.9,
deliveryMode: "hybrid",
owner: "jonas",
updatedAt: "2026-04-09",
updatedBucket: "this-week",
enabled: false,
approvalRequired: false,
audienceLabel: "Inbound pipeline",
runWindowLabel: "Hourly batch",
}),
automation({
id: "sandbox-onboarding-sequence",
title: "Sandbox onboarding sequence",
kind: "sequence",
state: "drafts",
rating: 4.1,
deliveryMode: "scheduled",
owner: "priya",
updatedAt: "2026-04-08",
updatedBucket: "this-week",
enabled: false,
approvalRequired: false,
audienceLabel: "Trial workspaces",
runWindowLabel: "Pending QA",
}),
automation({
id: "partner-routing-fallback",
title: "Partner routing fallback",
kind: "routing",
state: "live",
rating: 4.4,
deliveryMode: "hybrid",
owner: "emil",
updatedAt: "2026-04-07",
updatedBucket: "this-week",
enabled: true,
approvalRequired: false,
audienceLabel: "Partner renewals",
runWindowLabel: "Live + nightly",
}),
automation({
id: "weekly-adoption-digest",
title: "Weekly adoption digest",
kind: "digest",
state: "paused",
rating: 3.8,
deliveryMode: "scheduled",
owner: "maya",
updatedAt: "2026-04-06",
updatedBucket: "this-week",
enabled: false,
approvalRequired: false,
audienceLabel: "Workspace champions",
runWindowLabel: "Fridays 16:00",
}),
automation({
id: "enterprise-risk-escalation",
title: "Enterprise risk escalation",
kind: "escalation",
state: "review",
rating: 4.3,
deliveryMode: "manual",
owner: "nora",
updatedAt: "2026-04-05",
updatedBucket: "older",
enabled: false,
approvalRequired: true,
audienceLabel: "Enterprise accounts",
runWindowLabel: "Manual release",
}),
automation({
id: "crm-enrichment-backfill",
title: "CRM enrichment backfill",
kind: "enrichment",
state: "live",
rating: 4.6,
deliveryMode: "scheduled",
owner: "jonas",
updatedAt: "2026-04-04",
updatedBucket: "older",
enabled: true,
approvalRequired: false,
audienceLabel: "Open opportunities",
runWindowLabel: "Nightly 01:00",
}),
automation({
id: "trial-conversion-follow-up",
title: "Trial conversion follow-up",
kind: "sequence",
state: "drafts",
rating: 4.0,
deliveryMode: "scheduled",
owner: "emil",
updatedAt: "2026-04-03",
updatedBucket: "older",
enabled: false,
approvalRequired: false,
audienceLabel: "Product-led signups",
runWindowLabel: "Awaiting copy",
}),
automation({
id: "owner-assignment-router",
title: "Owner assignment router",
kind: "routing",
state: "live",
rating: 4.5,
deliveryMode: "event-driven",
owner: "priya",
updatedAt: "2026-04-02",
updatedBucket: "older",
enabled: true,
approvalRequired: false,
audienceLabel: "Workspace requests",
runWindowLabel: "Immediate",
}),
automation({
id: "ops-exception-digest",
title: "Ops exception digest",
kind: "digest",
state: "review",
rating: 4.1,
deliveryMode: "scheduled",
owner: "priya",
updatedAt: "2026-04-11",
updatedBucket: "today",
enabled: false,
approvalRequired: true,
audienceLabel: "Ops leadership",
runWindowLabel: "Daily 18:00",
}),
automation({
id: "billing-retry-escalation",
title: "Billing retry escalation",
kind: "escalation",
state: "live",
rating: 4.7,
deliveryMode: "event-driven",
owner: "nora",
updatedAt: "2026-04-01",
updatedBucket: "older",
enabled: true,
approvalRequired: false,
audienceLabel: "Recovery queue",
runWindowLabel: "On failure",
}),
]
@@ -1,15 +0,0 @@
import { AutomationLibraryGridView } from "./components/data-grid-view"
export function Page() {
return (
<main
className="mx-auto flex min-h-svh w-full max-w-7xl items-start justify-center p-8 pt-12"
aria-labelledby="page-heading"
>
<h1 id="page-heading" className="sr-only">
Automation library data grid
</h1>
<AutomationLibraryGridView />
</main>
)
}
@@ -1,550 +0,0 @@
"use client"
import { memo, type ComponentProps } from "react"
import { Badge } from "@/components/reui/badge"
import { DataGridColumnHeader } from "@/components/reui/data-grid/data-grid-column-header"
import { type ColumnDef } from "@tanstack/react-table"
import { cn } from "@cfdm/ui/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@cfdm/ui/components/avatar"
import { Button } from "@cfdm/ui/components/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@cfdm/ui/components/dropdown-menu"
import { Item } from "@cfdm/ui/components/item"
import {
ACCOUNTS,
formatCurrency,
nextRenewal,
sumArr,
weightedNrr,
type Account,
type AccountHealth,
type AccountOwner,
type AccountRow,
type AccountTier,
type PortfolioRow,
type RegionGroupRow,
} from "./data"
import { TrendingUp, TrendingDown, ArrowRightIcon, CalendarDaysIcon, MoreHorizontalIcon, EyeIcon, CopyIcon, PencilIcon, ChevronRightIcon, GlobeIcon } from "lucide-react"
export type AccountAction = "open" | "copy" | "plan"
const tierVariant: Record<
AccountTier,
ComponentProps<typeof Badge>["variant"]
> = {
Enterprise: "outline",
Growth: "outline",
Startup: "outline",
}
const healthVariant: Record<
AccountHealth,
ComponentProps<typeof Badge>["variant"]
> = {
Healthy: "success-light",
Watch: "warning-light",
"At Risk": "destructive-light",
}
// Per-account brand mark: a colored monogram tile stands in for a real logo
// (accounts are fictional, so real brand art is off limits). Tint is stable per
// account id; the monogram is the initials of the first two words.
const BRAND_TINTS = [
"border-blue-200 bg-blue-100 text-blue-700 dark:border-blue-900 dark:bg-blue-950 dark:text-blue-300",
"border-violet-200 bg-violet-100 text-violet-700 dark:border-violet-900 dark:bg-violet-950 dark:text-violet-300",
"border-emerald-200 bg-emerald-100 text-emerald-700 dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-300",
"border-amber-200 bg-amber-100 text-amber-700 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-300",
"border-rose-200 bg-rose-100 text-rose-700 dark:border-rose-900 dark:bg-rose-950 dark:text-rose-300",
"border-cyan-200 bg-cyan-100 text-cyan-700 dark:border-cyan-900 dark:bg-cyan-950 dark:text-cyan-300",
]
// Cycle the tints by position within each region so a region's accounts get
// distinct colors (a region with more than six repeats from the first tint).
const BRAND_TINT_BY_ID = new Map<string, string>()
const regionTintCursor = new Map<Account["regionId"], number>()
for (const account of ACCOUNTS) {
const cursor = regionTintCursor.get(account.regionId) ?? 0
BRAND_TINT_BY_ID.set(account.id, BRAND_TINTS[cursor % BRAND_TINTS.length])
regionTintCursor.set(account.regionId, cursor + 1)
}
function brandTint(id: string) {
return BRAND_TINT_BY_ID.get(id) ?? BRAND_TINTS[0]
}
function accountMonogram(name: string) {
const words = name.trim().split(/\s+/)
if (words.length >= 2) {
return (words[0][0] + words[1][0]).toUpperCase()
}
return name.slice(0, 2).toUpperCase()
}
function isAccountRow(row: PortfolioRow): row is AccountRow {
return row.kind === "account"
}
function getRegionAccounts(row: RegionGroupRow): Account[] {
return row.subRows?.map((item) => item.account) ?? []
}
/** Color tone keyed on net revenue retention (100% = flat). */
function nrrTone(value: number) {
if (value >= 100) return "text-emerald-600 dark:text-emerald-500"
return "text-rose-600 dark:text-rose-500"
}
// ── Shared cells ──
const OwnerAvatar = memo(function OwnerAvatar({
owner,
className,
}: {
owner: AccountOwner
className?: string
}) {
return (
<Avatar className={cn("size-6 shrink-0", className)}>
{owner.avatarSrc ? (
<AvatarImage src={owner.avatarSrc} alt={owner.name} />
) : null}
<AvatarFallback className="text-[10px]">{owner.initials}</AvatarFallback>
</Avatar>
)
})
export function NrrValue({ value }: { value: number }) {
return (
<span
className={cn(
"inline-flex items-center gap-1 text-sm tabular-nums",
nrrTone(value)
)}
>
{value >= 100 ? (
<TrendingUp className="size-3.5 shrink-0" aria-hidden="true" />
) : (
<TrendingDown className="size-3.5 shrink-0" aria-hidden="true" />
)}
{value}%
</span>
)
}
// ── Account (leaf) cells ──
function AccountLogoTile({ account }: { account: Account }) {
return (
<Item
render={<span />}
aria-hidden="true"
className={cn(
"flex size-7 shrink-0 items-center justify-center border p-0 text-xs font-semibold tracking-tight",
brandTint(account.id)
)}
>
{accountMonogram(account.name)}
</Item>
)
}
function AccountNameAffordance({ name }: { name: string }) {
return (
<span className="group/account-name inline-flex min-w-0 items-center gap-1 truncate">
<span
data-slot="portfolio-account-name"
className="hover:text-primary text-foreground max-w-full cursor-pointer truncate py-0.25 transition-colors"
>
{name}
</span>
<ArrowRightIcon className="size-3 shrink-0 -translate-x-1 opacity-0 transition group-hover/account-name:translate-x-0 group-hover/account-name:opacity-100" aria-hidden="true" />
</span>
)
}
function AccountNameCell({ account }: { account: Account }) {
return (
<div
data-portfolio-row="account"
className="flex min-w-0 items-center gap-3 ps-8"
>
<AccountLogoTile account={account} />
<div className="min-w-0 flex-1 text-sm leading-5 font-medium">
<AccountNameAffordance name={account.name} />
</div>
</div>
)
}
function OwnerCell({ owner }: { owner: AccountOwner }) {
return (
<div className="flex min-w-0 items-center gap-2">
<OwnerAvatar owner={owner} />
<span className="text-foreground min-w-0 truncate text-sm">
{owner.name}
</span>
</div>
)
}
function RenewalCell({ account }: { account: Account }) {
return (
<Badge variant="outline" className="bg-background gap-1.5">
<CalendarDaysIcon className="text-muted-foreground size-3.5" aria-hidden="true" />
<span className="tabular-nums">{account.renewalLabel}</span>
</Badge>
)
}
function AccountActionsCell({
account,
onAction,
}: {
account: Account
onAction: (action: AccountAction, account: Account) => void
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
size="icon-sm"
variant="ghost"
aria-label={`Actions for ${account.name}`}
/>
}
>
<MoreHorizontalIcon aria-hidden="true" />
</DropdownMenuTrigger>
{/* Content */}
<DropdownMenuContent align="end" className="w-44">
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => onAction("open", account)}>
<EyeIcon aria-hidden="true" />
View account
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onAction("copy", account)}>
<CopyIcon aria-hidden="true" />
Copy name
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onAction("plan", account)}>
<PencilIcon aria-hidden="true" />
Adjust plan
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
// ── Region (group) cells ──
function RegionExpandButton({
label,
expanded,
onToggle,
}: {
label: string
expanded: boolean
onToggle: () => void
}) {
return (
<Button
type="button"
size="icon-sm"
variant="ghost"
aria-label={expanded ? `Collapse ${label}` : `Expand ${label}`}
aria-expanded={expanded}
className="text-muted-foreground hover:text-foreground size-6 shrink-0 p-0 shadow-none"
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
onToggle()
}}
>
<ChevronRightIcon className={cn(
"size-3.5 shrink-0 transition-transform duration-150",
expanded && "rotate-90"
)} aria-hidden="true" />
</Button>
)
}
function RegionGroupCell({
row,
expanded,
onToggle,
}: {
row: RegionGroupRow
expanded: boolean
onToggle: () => void
}) {
const count = row.subRows?.length ?? 0
return (
<div
data-portfolio-row="region"
className="flex min-w-0 items-center gap-2"
>
<RegionExpandButton
label={row.region.name}
expanded={expanded}
onToggle={onToggle}
/>
<GlobeIcon className="text-muted-foreground size-4 shrink-0" aria-hidden="true" />
<span className="text-foreground min-w-0 truncate text-sm font-semibold">
{row.region.name}
</span>
<Badge variant="outline" className="shrink-0">
{count}
</Badge>
</div>
)
}
/** Right-aligned numeric slot shared by account and group rows. */
function NumericSlot({
children,
className,
}: {
children: React.ReactNode
className?: string
}) {
return (
<div className={cn("flex w-full items-center justify-end", className)}>
{children}
</div>
)
}
export function createPortfolioColumns({
onAction,
}: {
onAction: (action: AccountAction, account: Account) => void
}): ColumnDef<PortfolioRow>[] {
return [
{
accessorFn: (row) =>
isAccountRow(row) ? row.account.name : row.region.name,
id: "account",
header: ({ column }) => (
<DataGridColumnHeader title="Account" column={column} />
),
cell: ({ row }) =>
isAccountRow(row.original) ? (
<AccountNameCell account={row.original.account} />
) : (
<RegionGroupCell
row={row.original}
expanded={row.getIsExpanded()}
onToggle={row.getToggleExpandedHandler()}
/>
),
enableHiding: false,
enableSorting: false,
minSize: 240,
meta: {
headerTitle: "Account",
autoSize: true,
},
},
{
accessorFn: (row) =>
isAccountRow(row) ? row.account.owner.name : row.region.summary,
id: "owner",
header: ({ column }) => (
<DataGridColumnHeader title="Owner" column={column} />
),
cell: ({ row }) =>
isAccountRow(row.original) ? (
<OwnerCell owner={row.original.account.owner} />
) : (
<span className="text-muted-foreground truncate text-sm">
{row.original.region.summary}
</span>
),
size: 190,
enableSorting: false,
meta: {
headerTitle: "Owner",
},
},
{
accessorFn: (row) => (isAccountRow(row) ? row.account.tier : ""),
id: "tier",
header: ({ column }) => (
<DataGridColumnHeader title="Tier" column={column} />
),
cell: ({ row }) =>
isAccountRow(row.original) ? (
<Badge variant={tierVariant[row.original.account.tier]}>
{row.original.account.tier}
</Badge>
) : null,
size: 120,
enableSorting: false,
meta: {
headerTitle: "Tier",
},
},
{
accessorFn: (row) => (isAccountRow(row) ? row.account.health : ""),
id: "health",
header: ({ column }) => (
<DataGridColumnHeader title="Health" column={column} />
),
cell: ({ row }) =>
isAccountRow(row.original) ? (
<Badge variant={healthVariant[row.original.account.health]}>
{row.original.account.health}
</Badge>
) : null,
size: 120,
enableSorting: false,
meta: {
headerTitle: "Health",
},
},
{
accessorFn: (row) =>
isAccountRow(row) ? row.account.arr : sumArr(getRegionAccounts(row)),
id: "arr",
header: ({ column }) => (
<DataGridColumnHeader
title="ARR"
column={column}
className="w-full justify-end text-right"
/>
),
cell: ({ row }) => {
const value = isAccountRow(row.original)
? row.original.account.arr
: sumArr(getRegionAccounts(row.original))
return (
<NumericSlot>
<span
className={cn(
"text-foreground text-sm tabular-nums",
isAccountRow(row.original) ? "font-medium" : "font-semibold"
)}
>
{formatCurrency(value)}
</span>
</NumericSlot>
)
},
size: 150,
enableSorting: false,
meta: {
headerTitle: "ARR",
headerClassName: "text-right!",
cellClassName: "text-right!",
},
},
{
accessorFn: (row) =>
isAccountRow(row)
? row.account.nrr
: weightedNrr(getRegionAccounts(row)),
id: "nrr",
header: ({ column }) => (
<DataGridColumnHeader
title="NRR"
column={column}
className="w-full justify-end text-right"
/>
),
cell: ({ row }) => {
const value = isAccountRow(row.original)
? row.original.account.nrr
: weightedNrr(getRegionAccounts(row.original))
return (
<NumericSlot>
<NrrValue value={value} />
</NumericSlot>
)
},
size: 120,
enableSorting: false,
meta: {
headerTitle: "NRR",
headerClassName: "text-right!",
cellClassName: "text-right!",
},
},
{
accessorFn: (row) =>
isAccountRow(row)
? row.account.renewalAt
: (nextRenewal(getRegionAccounts(row))?.renewalAt ?? ""),
id: "renewal",
header: ({ column }) => (
<DataGridColumnHeader
title="Renewal"
column={column}
className="w-full justify-end text-right"
/>
),
cell: ({ row }) => {
if (isAccountRow(row.original)) {
return (
<div className="flex justify-end">
<RenewalCell account={row.original.account} />
</div>
)
}
const upcoming = nextRenewal(getRegionAccounts(row.original))
return (
<div className="flex justify-end">
{upcoming ? (
<span className="inline-flex items-baseline gap-1.5 text-sm whitespace-nowrap">
<span className="text-muted-foreground">Next</span>
<span className="text-foreground tabular-nums">
{upcoming.renewalLabel}
</span>
</span>
) : (
<span className="text-muted-foreground text-sm">--</span>
)}
</div>
)
},
size: 184,
minSize: 150,
enableSorting: false,
meta: {
headerTitle: "Renewal",
headerClassName: "text-right!",
},
},
{
id: "actions",
header: "",
cell: ({ row }) =>
isAccountRow(row.original) ? (
<div className="flex justify-end">
<AccountActionsCell
account={row.original.account}
onAction={onAction}
/>
</div>
) : null,
size: 56,
enableHiding: false,
enableSorting: false,
},
]
}
@@ -1,620 +0,0 @@
import { useCallback, useMemo, useState, type ComponentProps } from "react"
import { Badge } from "@/components/reui/badge"
import {
DataGrid,
DataGridContainer,
} from "@/components/reui/data-grid/data-grid"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import {
DataGridTable,
DataGridTableFootRow,
DataGridTableFootRowCell,
} from "@/components/reui/data-grid/data-grid-table"
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/reui/frame"
import {
getCoreRowModel,
getExpandedRowModel,
useReactTable,
type ExpandedState,
type VisibilityState,
} from "@tanstack/react-table"
import { toast } from "sonner"
import { cn } from "@cfdm/ui/lib/utils"
import { Button } from "@cfdm/ui/components/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@cfdm/ui/components/dropdown-menu"
import {
Field,
FieldGroup,
FieldLabel,
FieldSeparator,
} from "@cfdm/ui/components/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@cfdm/ui/components/input-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@cfdm/ui/components/popover"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@cfdm/ui/components/select"
import { createPortfolioColumns, NrrValue, type AccountAction } from "./columns"
import {
ACCOUNT_HEALTH_OPTIONS,
ACCOUNTS,
formatCompactCurrency,
formatCurrency,
REGIONS,
sumArr,
weightedNrr,
type Account,
type AccountHealth,
type AccountRow,
type PortfolioRow,
type RegionGroupRow,
} from "./data"
import { DownloadIcon, SearchIcon, XIcon, ActivityIcon, Settings2Icon, CheckIcon } from "lucide-react"
type TableDensity = "compact" | "comfortable"
type PortfolioColumnKey = "owner" | "tier" | "health" | "nrr" | "renewal"
const TABLE_DENSITY_OPTIONS: { value: TableDensity; label: string }[] = [
{ value: "compact", label: "Compact" },
{ value: "comfortable", label: "Comfortable" },
]
const DISPLAY_COLUMN_OPTIONS: { key: PortfolioColumnKey; label: string }[] = [
{ key: "owner", label: "Owner" },
{ key: "tier", label: "Tier" },
{ key: "health", label: "Health" },
{ key: "nrr", label: "NRR" },
{ key: "renewal", label: "Renewal" },
]
function getAccountSearchBlob(account: Account) {
return [
account.name,
account.industry,
account.owner.name,
account.owner.role,
account.tier,
account.health,
]
.filter(Boolean)
.join(" ")
.toLowerCase()
}
function buildPortfolioRows(accounts: Account[]): RegionGroupRow[] {
return REGIONS.map((region) => {
const subRows: AccountRow[] = accounts
.filter((account) => account.regionId === region.id)
.map((account) => ({
kind: "account",
id: account.id,
region,
account,
}))
const row: RegionGroupRow = {
kind: "region",
id: region.id,
region,
subRows,
}
return row
}).filter((row) => (row.subRows?.length ?? 0) > 0)
}
function getExpandedRegionState(rows: RegionGroupRow[]): ExpandedState {
return rows.reduce<Record<string, boolean>>((expanded, row) => {
expanded[row.id] = true
return expanded
}, {})
}
function isExpanded(expanded: ExpandedState, rowId: string) {
if (expanded === true) return true
return expanded[rowId] === true
}
function PortfolioMetric({
label,
value,
variant = "secondary",
}: {
label: string
value: string
variant?: ComponentProps<typeof Badge>["variant"]
}) {
return (
<div className="flex min-w-0 items-center gap-2 sm:border-l sm:pl-3 sm:first:border-l-0 sm:first:pl-0">
<span className="text-muted-foreground truncate text-xs font-medium">
{label}
</span>
<Badge variant={variant} className="tabular-nums">
{value}
</Badge>
</div>
)
}
export function GroupedRevenueDataGridView() {
const [searchQuery, setSearchQuery] = useState("")
const [selectedHealth, setSelectedHealth] = useState<AccountHealth[]>([])
const [tableDensity, setTableDensity] = useState<TableDensity>("comfortable")
const [visibleColumns, setVisibleColumns] = useState<
Record<PortfolioColumnKey, boolean>
>({
owner: true,
tier: false,
health: true,
nrr: true,
renewal: true,
})
const [expandedRows, setExpandedRows] = useState<ExpandedState>(() => {
// Start collapsed; open only the first region as a preview.
const [firstRegion] = buildPortfolioRows(ACCOUNTS)
return firstRegion ? { [firstRegion.id]: true } : {}
})
const filteredAccounts = useMemo(() => {
const normalizedQuery = searchQuery.trim().toLowerCase()
return ACCOUNTS.filter((account) => {
if (
normalizedQuery.length > 0 &&
!getAccountSearchBlob(account).includes(normalizedQuery)
) {
return false
}
if (
selectedHealth.length > 0 &&
!selectedHealth.includes(account.health)
) {
return false
}
return true
})
}, [searchQuery, selectedHealth])
const groupedRows = useMemo(
() => buildPortfolioRows(filteredAccounts),
[filteredAccounts]
)
const allGroupsExpanded =
groupedRows.length > 0 &&
groupedRows.every((row) => isExpanded(expandedRows, row.id))
const activeFilterCount = selectedHealth.length
const totalArr = sumArr(filteredAccounts)
const portfolioNrr = weightedNrr(filteredAccounts)
const atRiskArr = sumArr(
filteredAccounts.filter((account) => account.health === "At Risk")
)
const columnVisibility = useMemo<VisibilityState>(
() => ({
owner: visibleColumns.owner,
tier: visibleColumns.tier,
health: visibleColumns.health,
nrr: visibleColumns.nrr,
renewal: visibleColumns.renewal,
}),
[visibleColumns]
)
const handleHealthToggle = useCallback(
(health: AccountHealth, checked: boolean) => {
setSelectedHealth((current) => {
if (checked) {
return current.includes(health) ? current : [...current, health]
}
return current.filter((item) => item !== health)
})
},
[]
)
const handleToggleGroups = useCallback(() => {
setExpandedRows(
allGroupsExpanded ? {} : getExpandedRegionState(groupedRows)
)
}, [allGroupsExpanded, groupedRows])
const handleAccountAction = useCallback(
(action: AccountAction, account: Account) => {
if (action === "open") {
toast.info("View account", {
description: `${account.name} / ${account.industry}`,
})
return
}
if (action === "copy") {
if (typeof navigator !== "undefined" && navigator.clipboard) {
void navigator.clipboard.writeText(account.name)
}
toast.success("Account name copied", {
description: account.name,
})
return
}
toast.message("Adjust plan", {
description: `Connect ${account.name} to your renewal or pricing flow.`,
})
},
[]
)
const handleExport = useCallback(() => {
toast.success("Export portfolio", {
description: "Connect this action to your CSV or warehouse export.",
})
}, [])
const columns = useMemo(
() =>
createPortfolioColumns({
onAction: handleAccountAction,
}),
[handleAccountAction]
)
const table = useReactTable({
data: groupedRows,
columns,
getRowId: (row) => row.id,
getSubRows: (row) =>
row.kind === "region"
? (row.subRows as PortfolioRow[] | undefined)
: undefined,
getRowCanExpand: (row) =>
row.original.kind === "region" && Boolean(row.original.subRows?.length),
state: {
columnVisibility,
expanded: expandedRows,
},
onExpandedChange: setExpandedRows,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
})
function toggleColumn(key: PortfolioColumnKey, checked: boolean) {
setVisibleColumns((current) => ({
...current,
[key]: checked,
}))
}
function clearFilters() {
setSearchQuery("")
setSelectedHealth([])
}
// Grand-total footer: one cell per visible column so it tracks column toggles.
const footerContent =
filteredAccounts.length > 0 ? (
<DataGridTableFootRow>
{table.getVisibleLeafColumns().map((column) => {
if (column.id === "account") {
return (
<DataGridTableFootRowCell key={column.id}>
<div className="flex min-w-0 items-center gap-2">
<span className="text-foreground text-sm font-semibold">
All Regions
</span>
<Badge variant="outline" className="tabular-nums">
{filteredAccounts.length}
</Badge>
</div>
</DataGridTableFootRowCell>
)
}
if (column.id === "arr") {
return (
<DataGridTableFootRowCell key={column.id} className="text-right!">
<span className="text-foreground text-sm font-semibold tabular-nums">
{formatCurrency(totalArr)}
</span>
</DataGridTableFootRowCell>
)
}
if (column.id === "nrr") {
return (
<DataGridTableFootRowCell key={column.id} className="text-right!">
<div className="flex w-full items-center justify-end">
<NrrValue value={portfolioNrr} />
</div>
</DataGridTableFootRowCell>
)
}
return <DataGridTableFootRowCell key={column.id} />
})}
</DataGridTableFootRow>
) : undefined
return (
<DataGrid
table={table}
recordCount={filteredAccounts.length}
emptyMessage="No accounts match this view."
tableLayout={{
dense: tableDensity === "compact",
rowBorder: true,
footerBackground: true,
columnsVisibility: false,
columnsResizable: false,
columnsMovable: false,
width: "fixed",
}}
tableClassNames={{
body: "[&>tr:has([data-portfolio-row=region])+tr:has(>td:only-child:empty)>td]:!border-b-0",
bodyRow:
"group/portfolio-row [&>td]:h-11 [&:has([data-portfolio-row=region])>td]:h-11",
edgeCell: "first:ps-3 last:pe-3 lg:first:ps-4 lg:last:pe-4",
}}
>
<section className="flex w-full max-w-7xl flex-col px-4 py-8 sm:px-6 lg:px-8">
<Frame>
{/* Header */}
<FrameHeader className="flex-col items-start gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="flex min-w-0 flex-col gap-1">
<FrameTitle>Revenue By Region</FrameTitle>
<FrameDescription>
Net retention and renewals across the book.
</FrameDescription>
</div>
<div className="flex min-w-0 flex-wrap items-center gap-3">
<PortfolioMetric
label="Total ARR"
value={formatCompactCurrency(totalArr)}
variant="outline"
/>
<PortfolioMetric
label="Portfolio NRR"
value={`${portfolioNrr}%`}
variant={
portfolioNrr >= 100 ? "success-light" : "warning-light"
}
/>
<PortfolioMetric
label="At-Risk ARR"
value={formatCompactCurrency(atRiskArr)}
variant={atRiskArr > 0 ? "destructive-light" : "secondary"}
/>
<Button type="button" variant="outline" onClick={handleExport}>
<DownloadIcon data-icon="inline-start" aria-hidden="true" />
Export
</Button>
</div>
</FrameHeader>
<FramePanel className="bg-card p-0! shadow-none!">
{/* Toolbar */}
<div className="flex flex-col gap-3 border-b px-3 py-3 lg:flex-row lg:items-center lg:justify-between lg:px-4">
<InputGroup className="w-full min-w-0 lg:max-w-xs">
<InputGroupAddon align="inline-start">
<SearchIcon className="text-muted-foreground size-4" aria-hidden="true" />
</InputGroupAddon>
<InputGroupInput
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder="Search accounts..."
aria-label="Search accounts"
/>
{searchQuery.length > 0 ? (
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
aria-label="Clear search"
onClick={() => setSearchQuery("")}
>
<XIcon className="size-4" aria-hidden="true" />
</InputGroupButton>
</InputGroupAddon>
) : null}
</InputGroup>
<div className="flex min-w-0 flex-wrap items-center gap-1.5 lg:justify-end">
<DropdownMenu modal={false}>
<DropdownMenuTrigger
render={
<Button type="button" variant="outline">
<ActivityIcon data-icon="inline-start" aria-hidden="true" />
Health
{activeFilterCount > 0 ? (
<Badge variant="secondary">{activeFilterCount}</Badge>
) : null}
</Button>
}
/>
<DropdownMenuContent align="end" className="min-w-48">
<DropdownMenuGroup>
<DropdownMenuLabel>Account health</DropdownMenuLabel>
{ACCOUNT_HEALTH_OPTIONS.map((health) => (
<DropdownMenuCheckboxItem
key={health}
checked={selectedHealth.includes(health)}
closeOnClick={false}
onCheckedChange={(checked) =>
handleHealthToggle(health, checked === true)
}
>
{health}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
{activeFilterCount > 0 ? (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
closeOnClick={false}
onClick={() => setSelectedHealth([])}
>
Reset health
</DropdownMenuItem>
</>
) : null}
</DropdownMenuContent>
</DropdownMenu>
<Popover>
<PopoverTrigger
render={
<Button type="button" variant="outline">
<Settings2Icon data-icon="inline-start" aria-hidden="true" />
Display
</Button>
}
/>
<PopoverContent align="end" className="w-[300px] p-0">
<FieldGroup className="gap-3 px-3.5 py-3">
<div className="flex flex-col gap-2">
<div className="text-muted-foreground text-xs font-medium">
Table
</div>
<Field
orientation="horizontal"
className="min-h-9 items-center justify-between gap-3"
>
<FieldLabel className="text-sm font-normal">
Density
</FieldLabel>
<Select
value={tableDensity}
onValueChange={(value) =>
setTableDensity(value as TableDensity)
}
>
<SelectTrigger
size="sm"
className="w-[132px] shrink-0"
>
<SelectValue>
{
TABLE_DENSITY_OPTIONS.find(
(option) => option.value === tableDensity
)?.label
}
</SelectValue>
</SelectTrigger>
<SelectContent align="end">
<SelectGroup>
{TABLE_DENSITY_OPTIONS.map((option) => (
<SelectItem
key={option.value}
value={option.value}
>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
</div>
<FieldSeparator className="-mx-3.5" />
<div className="flex flex-col gap-2">
<div className="text-muted-foreground text-xs font-medium">
Display properties
</div>
<div className="flex flex-wrap gap-1.5">
{DISPLAY_COLUMN_OPTIONS.map((option) => {
const active = visibleColumns[option.key]
return (
<Button
key={option.key}
type="button"
size="sm"
variant={active ? "secondary" : "outline"}
className={cn(
"rounded-full",
active && "border-foreground/10"
)}
aria-pressed={active}
onClick={() =>
toggleColumn(option.key, !active)
}
>
{active ? (
<CheckIcon className="size-4" aria-hidden="true" />
) : null}
{option.label}
</Button>
)
})}
</div>
</div>
</FieldGroup>
</PopoverContent>
</Popover>
<Button
type="button"
variant="outline"
onClick={handleToggleGroups}
>
{allGroupsExpanded ? "Collapse all" : "Expand all"}
</Button>
{searchQuery.length > 0 || selectedHealth.length > 0 ? (
<Button type="button" variant="ghost" onClick={clearFilters}>
Clear
</Button>
) : null}
</div>
</div>
{/* Grouped grid */}
<DataGridContainer>
<DataGridScrollArea>
<DataGridTable footerContent={footerContent} />
</DataGridScrollArea>
</DataGridContainer>
</FramePanel>
</Frame>
</section>
</DataGrid>
)
}
@@ -1,459 +0,0 @@
export type RegionId = "na" | "emea" | "apac" | "latam"
export type AccountTier = "Enterprise" | "Growth" | "Startup"
export type AccountHealth = "Healthy" | "Watch" | "At Risk"
export interface AccountOwner {
id: string
name: string
initials: string
avatarSrc?: string
role: string
}
export interface Region {
id: RegionId
name: string
summary: string
order: number
}
export interface Account {
id: string
name: string
industry: string
regionId: RegionId
owner: AccountOwner
tier: AccountTier
health: AccountHealth
arr: number
nrr: number
renewalAt: string
renewalLabel: string
}
// ── Row union (TanStack nested rows: region group → account leaf) ──
export interface RegionGroupRow {
kind: "region"
id: string
region: Region
subRows?: AccountRow[]
}
export interface AccountRow {
kind: "account"
id: string
region: Region
account: Account
}
export type PortfolioRow = RegionGroupRow | AccountRow
export const REGION_ORDER: RegionId[] = ["na", "emea", "apac", "latam"]
export const ACCOUNT_TIERS: AccountTier[] = ["Enterprise", "Growth", "Startup"]
export const ACCOUNT_HEALTH_OPTIONS: AccountHealth[] = [
"Healthy",
"Watch",
"At Risk",
]
export const REGIONS: Region[] = [
{
id: "na",
name: "North America",
summary: "US and Canada strategic accounts",
order: 1,
},
{
id: "emea",
name: "EMEA",
summary: "Europe, Middle East, and Africa book",
order: 2,
},
{
id: "apac",
name: "APAC",
summary: "Asia Pacific growth territory",
order: 3,
},
{
id: "latam",
name: "LATAM",
summary: "Latin America emerging accounts",
order: 4,
},
]
export const ACCOUNT_OWNERS: AccountOwner[] = [
{
id: "rina",
name: "Rina Holt",
initials: "RH",
avatarSrc:
"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=96&h=96&dpr=2&q=80",
role: "Enterprise AE",
},
{
id: "vale",
name: "Vale Aksoy",
initials: "VA",
avatarSrc:
"https://images.unsplash.com/photo-1519345182560-3f2917c472ef?w=96&h=96&dpr=2&q=80",
role: "Strategic AE",
},
{
id: "noor",
name: "Noor Albright",
initials: "NA",
avatarSrc:
"https://images.unsplash.com/photo-1531123897727-8f129e1688ce?w=96&h=96&dpr=2&q=80",
role: "Account Manager",
},
{
id: "sora",
name: "Sora Min",
initials: "SM",
avatarSrc:
"https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80",
role: "Growth AE",
},
{
id: "evren",
name: "Evren Blake",
initials: "EB",
avatarSrc:
"https://images.unsplash.com/photo-1547425260-76bcadfb4f2c?w=96&h=96&dpr=2&q=80",
role: "Enterprise AE",
},
{
id: "mina",
name: "Mina Rowe",
initials: "MR",
avatarSrc:
"https://images.unsplash.com/photo-1552058544-f2b08422138a?w=96&h=96&dpr=2&q=80",
role: "Account Manager",
},
]
function owner(id: AccountOwner["id"]) {
const match = ACCOUNT_OWNERS.find((item) => item.id === id)
if (!match) {
throw new Error(`Unknown account owner: ${id}`)
}
return match
}
export const ACCOUNTS: Account[] = [
{
id: "acc-northwind",
name: "Northwind Trading",
industry: "Logistics",
regionId: "na",
owner: owner("rina"),
tier: "Enterprise",
health: "Healthy",
arr: 920000,
nrr: 124,
renewalAt: "2026-09-14",
renewalLabel: "Sep 14, 2026",
},
{
id: "acc-cedar",
name: "Cedar Health Systems",
industry: "Healthcare",
regionId: "na",
owner: owner("evren"),
tier: "Enterprise",
health: "Watch",
arr: 760000,
nrr: 103,
renewalAt: "2026-07-02",
renewalLabel: "Jul 2, 2026",
},
{
id: "acc-vantage",
name: "Vantage Robotics",
industry: "Manufacturing",
regionId: "na",
owner: owner("rina"),
tier: "Growth",
health: "Healthy",
arr: 410000,
nrr: 118,
renewalAt: "2026-11-21",
renewalLabel: "Nov 21, 2026",
},
{
id: "acc-brightline",
name: "Brightline Media",
industry: "Media",
regionId: "na",
owner: owner("mina"),
tier: "Growth",
health: "At Risk",
arr: 285000,
nrr: 92,
renewalAt: "2026-06-30",
renewalLabel: "Jun 30, 2026",
},
{
id: "acc-summit",
name: "Summit Analytics",
industry: "Software",
regionId: "na",
owner: owner("evren"),
tier: "Startup",
health: "Healthy",
arr: 138000,
nrr: 129,
renewalAt: "2026-10-09",
renewalLabel: "Oct 9, 2026",
},
{
id: "acc-helvetia",
name: "Helvetia Pay",
industry: "Fintech",
regionId: "emea",
owner: owner("vale"),
tier: "Enterprise",
health: "Healthy",
arr: 845000,
nrr: 121,
renewalAt: "2026-08-18",
renewalLabel: "Aug 18, 2026",
},
{
id: "acc-nordwind",
name: "Nordwind Energy",
industry: "Energy",
regionId: "emea",
owner: owner("noor"),
tier: "Enterprise",
health: "Watch",
arr: 690000,
nrr: 99,
renewalAt: "2026-07-27",
renewalLabel: "Jul 27, 2026",
},
{
id: "acc-albion",
name: "Albion Retail Group",
industry: "Retail",
regionId: "emea",
owner: owner("vale"),
tier: "Growth",
health: "At Risk",
arr: 320000,
nrr: 88,
renewalAt: "2026-06-24",
renewalLabel: "Jun 24, 2026",
},
{
id: "acc-lumen",
name: "Lumen Telecom",
industry: "Telecom",
regionId: "emea",
owner: owner("noor"),
tier: "Growth",
health: "Healthy",
arr: 455000,
nrr: 115,
renewalAt: "2026-12-03",
renewalLabel: "Dec 3, 2026",
},
{
id: "acc-castellan",
name: "Castellan Bank",
industry: "Banking",
regionId: "emea",
owner: owner("vale"),
tier: "Enterprise",
health: "Healthy",
arr: 980000,
nrr: 109,
renewalAt: "2026-09-30",
renewalLabel: "Sep 30, 2026",
},
{
id: "acc-sakura",
name: "Sakura Mobility",
industry: "Mobility",
regionId: "apac",
owner: owner("sora"),
tier: "Growth",
health: "Healthy",
arr: 372000,
nrr: 126,
renewalAt: "2026-10-22",
renewalLabel: "Oct 22, 2026",
},
{
id: "acc-pacific",
name: "Pacific Cloud",
industry: "Software",
regionId: "apac",
owner: owner("mina"),
tier: "Enterprise",
health: "Watch",
arr: 615000,
nrr: 101,
renewalAt: "2026-08-05",
renewalLabel: "Aug 5, 2026",
},
{
id: "acc-banyan",
name: "Banyan AgriTech",
industry: "Agriculture",
regionId: "na",
owner: owner("sora"),
tier: "Startup",
health: "Healthy",
arr: 124000,
nrr: 132,
renewalAt: "2026-11-12",
renewalLabel: "Nov 12, 2026",
},
{
id: "acc-meridian",
name: "Meridian Shipping",
industry: "Logistics",
regionId: "apac",
owner: owner("mina"),
tier: "Growth",
health: "At Risk",
arr: 298000,
nrr: 90,
renewalAt: "2026-06-27",
renewalLabel: "Jun 27, 2026",
},
{
id: "acc-hanwoo",
name: "Hanwoo Foods",
industry: "Food and Beverage",
regionId: "apac",
owner: owner("sora"),
tier: "Growth",
health: "Healthy",
arr: 340000,
nrr: 112,
renewalAt: "2026-09-08",
renewalLabel: "Sep 8, 2026",
},
{
id: "acc-andes",
name: "Andes Fintech",
industry: "Fintech",
regionId: "na",
owner: owner("noor"),
tier: "Growth",
health: "Healthy",
arr: 268000,
nrr: 122,
renewalAt: "2026-10-30",
renewalLabel: "Oct 30, 2026",
},
{
id: "acc-costera",
name: "Costera Travel",
industry: "Travel",
regionId: "latam",
owner: owner("rina"),
tier: "Startup",
health: "Watch",
arr: 96000,
nrr: 104,
renewalAt: "2026-07-15",
renewalLabel: "Jul 15, 2026",
},
{
id: "acc-verde",
name: "Verde Logistics",
industry: "Logistics",
regionId: "latam",
owner: owner("evren"),
tier: "Growth",
health: "Healthy",
arr: 312000,
nrr: 117,
renewalAt: "2026-12-09",
renewalLabel: "Dec 9, 2026",
},
{
id: "acc-pampa",
name: "Pampa Retail",
industry: "Retail",
regionId: "latam",
owner: owner("sora"),
tier: "Startup",
health: "At Risk",
arr: 142000,
nrr: 89,
renewalAt: "2026-06-22",
renewalLabel: "Jun 22, 2026",
},
{
id: "acc-tucan",
name: "Tucan Media",
industry: "Media",
regionId: "emea",
owner: owner("noor"),
tier: "Growth",
health: "Healthy",
arr: 205000,
nrr: 113,
renewalAt: "2026-09-19",
renewalLabel: "Sep 19, 2026",
},
]
// ── Formatting + aggregate helpers (raw values stay in data, formatting here) ──
const CURRENCY_FORMATTER = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
})
export function formatCurrency(value: number) {
return CURRENCY_FORMATTER.format(value)
}
/** Compact money for dense KPI/subtotal slots: 2_513_000 -> "$2.51M". */
export function formatCompactCurrency(value: number) {
if (Math.abs(value) >= 1_000_000) {
return `$${(value / 1_000_000).toFixed(2)}M`
}
if (Math.abs(value) >= 1_000) {
return `$${Math.round(value / 1_000)}K`
}
return formatCurrency(value)
}
export function sumArr(accounts: Account[]) {
return accounts.reduce((total, account) => total + account.arr, 0)
}
/** ARR-weighted NRR so big accounts move the group average correctly. */
export function weightedNrr(accounts: Account[]) {
const totalArr = sumArr(accounts)
if (totalArr === 0) return 0
const weighted = accounts.reduce(
(total, account) => total + account.arr * account.nrr,
0
)
return Math.round(weighted / totalArr)
}
/** Earliest renewal date in the group (ISO strings sort lexicographically). */
export function nextRenewal(accounts: Account[]) {
return accounts.reduce<Account | null>((earliest, account) => {
if (!earliest || account.renewalAt < earliest.renewalAt) return account
return earliest
}, null)
}
@@ -1,15 +0,0 @@
import { GroupedRevenueDataGridView } from "./components/data-grid-view"
export function Page() {
return (
<main
className="mx-auto flex min-h-svh w-full items-start justify-center p-8 pt-12"
aria-labelledby="page-heading"
>
<h1 id="page-heading" className="sr-only">
Revenue by region grouped data grid
</h1>
<GroupedRevenueDataGridView />
</main>
)
}
@@ -1,48 +0,0 @@
export type ToolbarOption<T extends string> = {
value: T
label: string
}
export const EXPORT_AUDIENCE_OPTIONS = [
{ value: "everyone", label: "Everyone" },
{ value: "ops-leads", label: "Ops leads" },
{ value: "finance-reviewers", label: "Finance reviewers" },
{ value: "client-owners", label: "Client owners" },
] as const satisfies readonly ToolbarOption<string>[]
export const EXPORT_SCOPE_OPTIONS = [
{ value: "all-workspaces", label: "All workspaces" },
{ value: "harbor-field", label: "Harbor field" },
{ value: "market-lab", label: "Market lab" },
{ value: "support-desk", label: "Support desk" },
] as const satisfies readonly ToolbarOption<string>[]
export const EXPORT_RANGE_OPTIONS = [
{ value: "last-30-days", label: "Last 30 days" },
{ value: "this-quarter", label: "This quarter" },
{ value: "previous-cycle", label: "Previous cycle" },
{ value: "custom-window", label: "Custom window" },
] as const satisfies readonly ToolbarOption<string>[]
export const EXPORT_FORMAT_OPTIONS = [
{
value: "csv",
label: "CSV bundle",
description: "Spreadsheet-ready activity rows",
},
{
value: "pdf",
label: "PDF brief",
description: "A concise review packet for stakeholders",
},
{
value: "schedule",
label: "Schedule delivery",
description: "Send this export every Friday morning",
},
] as const
export type ExportAudience = (typeof EXPORT_AUDIENCE_OPTIONS)[number]["value"]
export type ExportScope = (typeof EXPORT_SCOPE_OPTIONS)[number]["value"]
export type ExportRange = (typeof EXPORT_RANGE_OPTIONS)[number]["value"]
export type ExportFormat = (typeof EXPORT_FORMAT_OPTIONS)[number]["value"]
@@ -1,247 +0,0 @@
import { useState } from "react"
import { IconStack } from "@/components/reui/icon-stack"
import { toast } from "sonner"
import { Button } from "@cfdm/ui/components/button"
import {
ButtonGroup,
ButtonGroupSeparator,
} from "@cfdm/ui/components/button-group"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@cfdm/ui/components/dropdown-menu"
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@cfdm/ui/components/empty"
import { Separator } from "@cfdm/ui/components/separator"
import {
EXPORT_AUDIENCE_OPTIONS,
EXPORT_FORMAT_OPTIONS,
EXPORT_RANGE_OPTIONS,
EXPORT_SCOPE_OPTIONS,
type ExportAudience,
type ExportFormat,
type ExportRange,
type ExportScope,
type ToolbarOption,
} from "./data"
import { ChevronDownIcon, PlayIcon, FileDownIcon, BookOpenIcon, CalendarClockIcon, ArchiveIcon } from "lucide-react"
type ToolbarFilterProps<T extends string> = {
label: string
value: T
options: readonly ToolbarOption<T>[]
onValueChange: (value: T) => void
}
function getOptionLabel<T extends string>(
options: readonly ToolbarOption<T>[],
value: T
) {
return options.find((option) => option.value === value)?.label ?? value
}
function ToolbarFilter<T extends string>({
label,
value,
options,
onValueChange,
}: ToolbarFilterProps<T>) {
const selectedLabel = getOptionLabel(options, value)
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
variant="outline"
size="sm"
aria-label={`${label}: ${selectedLabel}`}
>
<span className="truncate">{selectedLabel}</span>
<ChevronDownIcon data-icon="inline-end" aria-hidden="true" />
</Button>
}
/>
<DropdownMenuContent align="start" className="min-w-44">
<DropdownMenuRadioGroup
value={value}
onValueChange={(nextValue) => {
if (nextValue !== null) {
onValueChange(nextValue as T)
}
}}
>
{options.map((option) => (
<DropdownMenuRadioItem
key={option.value}
value={option.value}
closeOnClick
>
{option.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
export function EmptyState() {
const [audience, setAudience] = useState<ExportAudience>(
EXPORT_AUDIENCE_OPTIONS[0].value
)
const [scope, setScope] = useState<ExportScope>(EXPORT_SCOPE_OPTIONS[0].value)
const [range, setRange] = useState<ExportRange>(EXPORT_RANGE_OPTIONS[0].value)
const audienceLabel = getOptionLabel(EXPORT_AUDIENCE_OPTIONS, audience)
const scopeLabel = getOptionLabel(EXPORT_SCOPE_OPTIONS, scope)
const rangeLabel = getOptionLabel(EXPORT_RANGE_OPTIONS, range)
const showExportToast = (format: ExportFormat = "csv") => {
const formatOption =
EXPORT_FORMAT_OPTIONS.find((option) => option.value === format) ??
EXPORT_FORMAT_OPTIONS[0]
toast.message(`${formatOption.label} is ready to wire`, {
description: `${formatOption.description}. ${audienceLabel} · ${scopeLabel} · ${rangeLabel}. Connect this action to your export job when activity records exist.`,
})
}
return (
<section
className="flex min-h-[430px] w-full max-w-4xl flex-col"
aria-labelledby="export-ledger-heading"
>
{/* Header */}
<div className="flex flex-col gap-3 pb-4 lg:flex-row lg:items-end lg:justify-between">
{/* Heading */}
<div className="flex min-w-0 flex-col gap-5">
{/* Title and Description */}
<div className="flex flex-col gap-0.5">
{/* Title */}
<h2
id="export-ledger-heading"
className="text-2xl font-semibold tracking-tight"
>
Activity Exports
</h2>
{/* Description */}
<p className="text-muted-foreground text-sm">
Download scoped activity packets for billing review, staffing
audits, and client handoffs.
</p>
</div>
{/* Filters */}
<div className="flex flex-wrap gap-2">
<ToolbarFilter
label="Audience"
value={audience}
options={EXPORT_AUDIENCE_OPTIONS}
onValueChange={setAudience}
/>
<ToolbarFilter
label="Workspace"
value={scope}
options={EXPORT_SCOPE_OPTIONS}
onValueChange={setScope}
/>
<ToolbarFilter
label="Date range"
value={range}
options={EXPORT_RANGE_OPTIONS}
onValueChange={setRange}
/>
</div>
</div>
{/* Download Action */}
<ButtonGroup className="w-full **:data-[slot=button]:border-r-0 sm:w-fit">
<Button
type="button"
className="flex-1 sm:flex-none"
onClick={() => showExportToast()}
>
<PlayIcon className="fill-current" data-icon="inline-start" aria-hidden="true" />
<span>Execute</span>
</Button>
<ButtonGroupSeparator className="bg-primary/72" />
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
size="icon"
className="border-primary-foreground/20 rounded-l-none border-l"
aria-label="Open download options"
/>
}
>
<ChevronDownIcon aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={8} align="end" className="w-52">
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => showExportToast("csv")}>
<FileDownIcon aria-hidden="true" />
CSV bundle
</DropdownMenuItem>
<DropdownMenuItem onClick={() => showExportToast("pdf")}>
<BookOpenIcon aria-hidden="true" />
PDF brief
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => showExportToast("schedule")}>
<CalendarClockIcon aria-hidden="true" />
Schedule delivery
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</div>
<Separator />
{/* Empty State */}
<div className="flex flex-1 items-center justify-center py-14 sm:py-16">
<Empty className="max-w-md flex-none bg-transparent p-0">
<EmptyHeader className="gap-5 text-center">
<EmptyMedia className="mb-0">
<IconStack aria-hidden="true">
<ArchiveIcon strokeWidth="1.9" aria-hidden="true" />
</IconStack>
</EmptyMedia>
{/* Empty State Content */}
<div className="flex flex-col items-center gap-2">
<EmptyTitle className="text-base font-semibold tracking-tight">
No exportable activity yet
</EmptyTitle>
<EmptyDescription className="max-w-sm text-sm/relaxed">
Capture approved activity and this view will assemble your next
review packet.
</EmptyDescription>
</div>
</EmptyHeader>
</Empty>
</div>
</section>
)
}
@@ -1,15 +0,0 @@
import { EmptyState } from "./components/empty-state"
export function Page() {
return (
<main
className="flex min-h-svh w-full items-center justify-center p-4 sm:p-8 md:p-10"
aria-labelledby="page-heading"
>
<h1 id="page-heading" className="sr-only">
Activity exports empty state
</h1>
<EmptyState />
</main>
)
}
@@ -1,152 +0,0 @@
export type TicketStatus = "open" | "waiting" | "resolved"
export type TicketPriority = "low" | "medium" | "high" | "urgent"
export type TicketSource = "portal" | "inbox" | "api"
export type TicketCategory = "access" | "security" | "billing" | "workflow"
export type TicketSelectOption<TValue extends string = string> = {
value: TValue
label: string
description?: string
}
export type SupportMember = {
id: string
name: string
role: string
src: string
initials: string
}
export type TicketDetailsValue = {
dueDate: string
status: TicketStatus
slaMet: boolean
priority: TicketPriority
source: TicketSource
channel: string
requestForm: string
category: TicketCategory
notifyRequester: boolean
tags: string[]
collaboratorIds: string[]
}
export const STATUS_OPTIONS: TicketSelectOption<TicketStatus>[] = [
{
value: "open",
label: "Open",
description: "Active and ready for the next response.",
},
{
value: "waiting",
label: "Waiting",
description: "Paused until requester or vendor input arrives.",
},
{
value: "resolved",
label: "Resolved",
description: "Completed and ready for closure.",
},
]
export const PRIORITY_OPTIONS: TicketSelectOption<TicketPriority>[] = [
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
{ value: "urgent", label: "Urgent" },
]
export const SOURCE_OPTIONS: TicketSelectOption<TicketSource>[] = [
{ value: "portal", label: "Customer Portal" },
{ value: "inbox", label: "Shared Inbox" },
{ value: "api", label: "API Intake" },
]
export const REQUEST_FORM_OPTIONS: TicketSelectOption[] = [
{
value: "workspace-access",
label: "Workspace Access",
description: "Provisioning, group access, and app authorization.",
},
{
value: "vendor-review",
label: "Vendor Review",
description: "Security and procurement review for new tools.",
},
{
value: "billing-exception",
label: "Billing Exception",
description: "Invoice changes, credits, and payment routing.",
},
{
value: "automation-change",
label: "Automation Change",
description: "Workflow updates owned by operations.",
},
]
export const CATEGORY_OPTIONS: TicketSelectOption<TicketCategory>[] = [
{ value: "access", label: "Access Request" },
{ value: "security", label: "Security Review" },
{ value: "billing", label: "Billing Support" },
{ value: "workflow", label: "Workflow Change" },
]
export const TAG_OPTIONS = [
"Feature",
"VIP",
"Automation",
"Security",
"Renewal",
"Finance",
]
export const COLLABORATORS: SupportMember[] = [
{
id: "mira",
name: "Mira Stone",
role: "Identity owner",
src: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=96&h=96&dpr=2&q=80",
initials: "MS",
},
{
id: "leo",
name: "Leo Grant",
role: "Support lead",
src: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=96&h=96&dpr=2&q=80",
initials: "LG",
},
{
id: "nora",
name: "Nora Vale",
role: "Workflow admin",
src: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=96&h=96&dpr=2&q=80",
initials: "NV",
},
{
id: "theo",
name: "Theo Park",
role: "Security reviewer",
src: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=96&h=96&dpr=2&q=80",
initials: "TP",
},
]
export const DEFAULT_TICKET_DETAILS: TicketDetailsValue = {
dueDate: "2026-04-30",
status: "open",
slaMet: true,
priority: "medium",
source: "portal",
channel: "identity-access",
requestForm: "workspace-access",
category: "access",
notifyRequester: true,
tags: ["Feature", "Security"],
collaboratorIds: ["mira", "leo"],
}
export const TICKET_TIMESTAMPS = {
createdAt: "Jan 21, 2026 at 10:16 PM",
updatedAt: "Just now",
}
@@ -1,222 +0,0 @@
import { useEffect, useRef, type ReactNode } from "react"
import { cn } from "@cfdm/ui/lib/utils"
import { Button } from "@cfdm/ui/components/button"
import { Field, FieldTitle } from "@cfdm/ui/components/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
} from "@cfdm/ui/components/input-group"
import { Item, ItemMedia } from "@cfdm/ui/components/item"
import { Spinner } from "@cfdm/ui/components/spinner"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@cfdm/ui/components/tooltip"
import { InfoIcon, PencilIcon, XIcon, CheckIcon } from "lucide-react"
interface EditableDetailRowProps {
label: string
hint?: string
editing?: boolean
display: ReactNode
renderEdit?: (active: boolean) => ReactNode
align?: "center" | "start"
actionsDisabled?: boolean
saving?: boolean
onEdit?: () => void
onCancel?: () => void
onSave?: () => void
}
function RowHint({ label, children }: { label: string; children: string }) {
return (
<Tooltip>
<TooltipTrigger
render={
<Button
type="button"
variant="ghost"
size="icon-xs"
className="text-muted-foreground hover:text-foreground -my-1 shrink-0"
aria-label={label}
/>
}
>
<InfoIcon aria-hidden="true" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-64 text-xs leading-relaxed">
{children}
</TooltipContent>
</Tooltip>
)
}
export function EditableDetailRow({
label,
hint,
editing = false,
display,
renderEdit,
align = "center",
actionsDisabled = false,
saving = false,
onEdit,
onCancel,
onSave,
}: EditableDetailRowProps) {
const editable = Boolean(renderEdit && onEdit && onCancel && onSave)
const controlsDisabled = actionsDisabled || saving
const controlActive = !controlsDisabled
const editActionsDisabled = !editing || controlsDisabled
const editRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!editing) {
return
}
const frame = requestAnimationFrame(() => {
const control = editRef.current?.querySelector<HTMLElement>(
[
"[data-slot='input-group-control']:not(:disabled)",
"[data-slot='combobox-chip-input']:not(:disabled)",
"button:not(:disabled)",
"input:not(:disabled)",
].join(",")
)
control?.focus({ preventScroll: true })
})
return () => cancelAnimationFrame(frame)
}, [editing])
return (
<Field
className={cn(
"group/row grid gap-x-2 gap-y-1 px-4 py-1.5 sm:grid-cols-[minmax(7.75rem,0.5fr)_minmax(0,1.5fr)] sm:gap-4",
align === "start" ? "sm:items-start" : "sm:items-center"
)}
>
<FieldTitle
className={cn(
"text-muted-foreground flex min-w-0 items-center gap-1 text-sm font-normal",
align === "start" && "sm:min-h-8"
)}
>
<span className="min-w-0 truncate">{label}</span>
{hint ? <RowHint label={`${label} info`}>{hint}</RowHint> : null}
</FieldTitle>
{renderEdit ? (
<div
className={cn(
"relative col-start-1 row-start-2 min-h-8 min-w-0 sm:col-start-2 sm:row-start-1",
align === "start" ? "items-start" : "items-center"
)}
>
<button
type="button"
disabled={!editable || controlsDisabled || editing}
aria-label={`Edit ${label}`}
aria-hidden={editing}
className={cn(
"group/value flex w-full min-w-0 rounded-md border border-transparent px-2.5 text-left transition-[opacity,color,background-color] duration-150 outline-none",
align === "start"
? "h-auto min-h-8 items-start py-1"
: "h-8 items-center",
editable &&
"hover:bg-muted/40 active:bg-muted/40 sm:group-hover/row:bg-muted/40 focus-visible:border-transparent! focus-visible:ring-0! focus-visible:outline-none!",
controlsDisabled && "pointer-events-none",
editing
? "pointer-events-none absolute inset-x-0 top-0 opacity-0"
: "relative opacity-100"
)}
onClick={onEdit}
>
<span
className={cn(
"flex min-w-0",
align === "start" ? "items-start" : "items-center"
)}
>
{display}
</span>
{editable ? (
<Item
render={<span />}
className={cn(
"p-0",
"text-muted-foreground ml-1.5 flex size-5 shrink-0 items-center justify-center opacity-100 transition-opacity sm:opacity-0 sm:group-hover/row:opacity-100 sm:group-focus-visible/value:opacity-100",
controlsDisabled && "invisible opacity-0 sm:opacity-0"
)}
>
<ItemMedia variant="icon" className="size-auto">
<PencilIcon className="size-3.5" aria-hidden="true" />
</ItemMedia>
</Item>
) : null}
</button>
<div
ref={editRef}
aria-hidden={!editing}
inert={!editing ? true : undefined}
className={cn(
"min-w-0 transition-opacity duration-150",
editing
? "relative opacity-100"
: "pointer-events-none absolute inset-x-0 top-0 opacity-0"
)}
>
<InputGroup
className={cn(
"has-[[data-slot=input-group-control]:focus-visible]:border-input! box-border w-full has-[[data-slot=input-group-control]:focus-visible]:shadow-none! has-[[data-slot=input-group-control]:focus-visible]:ring-0!",
align === "start" ? "h-auto! min-h-8! items-start" : "h-8"
)}
>
{renderEdit(controlActive)}
{editable ? (
<InputGroupAddon
align="inline-end"
className={cn(
"gap-1 pr-2",
align === "start" && "self-start pt-1"
)}
>
<InputGroupButton
size="icon-xs"
aria-label={`Discard ${label}`}
disabled={editActionsDisabled}
onClick={onCancel}
>
<XIcon className="size-4" aria-hidden="true" />
</InputGroupButton>
<InputGroupButton
size="icon-xs"
aria-label={saving ? `Saving ${label}` : `Save ${label}`}
disabled={editActionsDisabled}
onClick={onSave}
>
{saving ? (
<Spinner className="size-3.5" />
) : (
<CheckIcon className="size-4" aria-hidden="true" />
)}
</InputGroupButton>
</InputGroupAddon>
) : null}
</InputGroup>
</div>
</div>
) : (
<div className="col-start-1 row-start-2 flex min-h-8 min-w-0 items-center px-2.5 sm:col-start-2 sm:row-start-1">
{display}
</div>
)}
</Field>
)
}
File diff suppressed because it is too large Load Diff
@@ -1,10 +0,0 @@
import { TicketDetailsForm } from "./components/ticket-details-form"
export function Page() {
return (
<main className="flex min-h-svh w-full items-center justify-center px-4 py-6 sm:px-8 sm:py-10">
<h1 className="sr-only">Inline editable ticket details form</h1>
<TicketDetailsForm />
</main>
)
}
@@ -1,392 +0,0 @@
import { type ReactNode } from "react"
import { type BadgeProps } from "@/components/reui/badge"
import { CreditCardIcon, ShieldCheckIcon, WebhookIcon, BarChart3Icon, ReceiptIcon } from "lucide-react"
export type ReleaseColumnId =
| "intake"
| "review-needed"
| "blocked"
| "scheduled"
| "ready"
| "shipped"
export type ReleaseRisk = "Critical" | "High" | "Medium" | "Low"
export type ReleaseProgressTone =
| "neutral"
| "sky"
| "violet"
| "amber"
| "rose"
| "emerald"
export type ReleaseOwner = {
name: string
initials: string
avatar: string
}
export type ReleaseService = {
label: string
icon: ReactNode
}
export type ReleaseChange = {
id: string
changeKey: string
title: string
service: ReleaseService
environment: string
owner: ReleaseOwner
risk: ReleaseRisk
launchWindow: string
checksDone: number
checksTotal: number
progressTone: ReleaseProgressTone
}
export type ReleaseColumn = {
id: ReleaseColumnId
title: string
description: string
dotClassName: string
addLabel: string
}
const OWNERS = {
maya: {
name: "Maya Patel",
initials: "MP",
avatar:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=96&h=96&dpr=2&q=80",
},
jonah: {
name: "Jonah Lee",
initials: "JL",
avatar:
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=96&h=96&dpr=2&q=80",
},
nina: {
name: "Nina Santos",
initials: "NS",
avatar:
"https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=96&h=96&dpr=2&q=80",
},
omar: {
name: "Omar Haddad",
initials: "OH",
avatar:
"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=96&h=96&dpr=2&q=80",
},
priya: {
name: "Priya Menon",
initials: "PM",
avatar:
"https://images.unsplash.com/photo-1557296387-5358ad7997bb?w=96&h=96&dpr=2&q=80",
},
theo: {
name: "Theo Vincent",
initials: "TV",
avatar:
"https://images.unsplash.com/photo-1507591064344-4c6ce005b128?w=96&h=96&dpr=2&q=80",
},
} satisfies Record<string, ReleaseOwner>
const SERVICES = {
checkout: {
label: "Checkout",
icon: (
<CreditCardIcon className="size-4" aria-hidden="true" />
),
},
identity: {
label: "Identity",
icon: (
<ShieldCheckIcon className="size-4" aria-hidden="true" />
),
},
webhooks: {
label: "Webhooks",
icon: (
<WebhookIcon className="size-4" aria-hidden="true" />
),
},
analytics: {
label: "Analytics",
icon: (
<BarChart3Icon className="size-4" aria-hidden="true" />
),
},
billing: {
label: "Billing",
icon: (
<ReceiptIcon className="size-4" aria-hidden="true" />
),
},
} satisfies Record<string, ReleaseService>
export const RISK_BADGE_VARIANT: Record<ReleaseRisk, BadgeProps["variant"]> = {
Critical: "destructive-light",
High: "warning-light",
Medium: "info-light",
Low: "secondary",
}
export const RELEASE_BOARD_TITLE = "Release Readiness Board"
export const RELEASE_BOARD_DESCRIPTION = "Readiness, risk, and launch checks."
export const RELEASE_COLUMNS: ReleaseColumn[] = [
{
id: "intake",
title: "Intake",
description: "New changes",
dotClassName: "bg-muted-foreground/45",
addLabel: "Add intake change",
},
{
id: "review-needed",
title: "Review Needed",
description: "Waiting on approvers",
dotClassName: "bg-info",
addLabel: "Add review change",
},
{
id: "blocked",
title: "Blocked",
description: "Needs escalation",
dotClassName: "bg-destructive",
addLabel: "Add blocked change",
},
{
id: "scheduled",
title: "Scheduled",
description: "Window assigned",
dotClassName: "bg-chart-2",
addLabel: "Add scheduled change",
},
{
id: "ready",
title: "Ready",
description: "Cleared to launch",
dotClassName: "bg-success",
addLabel: "Add ready change",
},
{
id: "shipped",
title: "Shipped",
description: "Post-launch watch",
dotClassName: "bg-success/70",
addLabel: "Add shipped change",
},
]
export const INITIAL_RELEASE_CHANGES: Record<ReleaseColumnId, ReleaseChange[]> =
{
intake: [
{
id: "release-8429",
changeKey: "REL-8429",
title: "Enable saved cards for guest checkout",
service: SERVICES.checkout,
environment: "Production",
owner: OWNERS.nina,
risk: "Medium",
launchWindow: "Apr 30, 20:00 UTC",
checksDone: 1,
checksTotal: 4,
progressTone: "sky",
},
{
id: "release-8427",
changeKey: "REL-8427",
title: "Add resend controls to login recovery",
service: SERVICES.identity,
environment: "Staging",
owner: OWNERS.jonah,
risk: "Low",
launchWindow: "May 1, 16:30 UTC",
checksDone: 2,
checksTotal: 4,
progressTone: "neutral",
},
{
id: "release-8425",
changeKey: "REL-8425",
title: "Log receipt resend events for admins",
service: SERVICES.billing,
environment: "Staging",
owner: OWNERS.theo,
risk: "Low",
launchWindow: "May 1, 19:00 UTC",
checksDone: 1,
checksTotal: 3,
progressTone: "neutral",
},
],
"review-needed": [
{
id: "release-8421",
changeKey: "REL-8421",
title: "Split EU checkout traffic by processor",
service: SERVICES.checkout,
environment: "Production",
owner: OWNERS.maya,
risk: "Critical",
launchWindow: "Apr 30, 22:00 UTC",
checksDone: 2,
checksTotal: 5,
progressTone: "rose",
},
{
id: "release-8418",
changeKey: "REL-8418",
title: "Raise webhook retry ceiling for partners",
service: SERVICES.webhooks,
environment: "Production",
owner: OWNERS.omar,
risk: "High",
launchWindow: "May 1, 18:30 UTC",
checksDone: 3,
checksTotal: 5,
progressTone: "amber",
},
{
id: "release-8417",
changeKey: "REL-8417",
title: "Confirm processor failover limits",
service: SERVICES.checkout,
environment: "Production",
owner: OWNERS.priya,
risk: "High",
launchWindow: "May 1, 21:00 UTC",
checksDone: 2,
checksTotal: 5,
progressTone: "amber",
},
],
blocked: [
{
id: "release-8416",
changeKey: "REL-8416",
title: "Rotate signing keys for session tokens",
service: SERVICES.identity,
environment: "Production",
owner: OWNERS.priya,
risk: "High",
launchWindow: "May 2, 19:30 UTC",
checksDone: 2,
checksTotal: 4,
progressTone: "amber",
},
{
id: "release-8413",
changeKey: "REL-8413",
title: "Rebuild analytics partitions for exports",
service: SERVICES.analytics,
environment: "Staging",
owner: OWNERS.theo,
risk: "Critical",
launchWindow: "May 2, 23:00 UTC",
checksDone: 1,
checksTotal: 5,
progressTone: "rose",
},
],
scheduled: [
{
id: "release-8408",
changeKey: "REL-8408",
title: "Move invoice workers to new queues",
service: SERVICES.billing,
environment: "Staging",
owner: OWNERS.theo,
risk: "Medium",
launchWindow: "May 3, 21:00 UTC",
checksDone: 4,
checksTotal: 5,
progressTone: "sky",
},
{
id: "release-8405",
changeKey: "REL-8405",
title: "Expand partner delivery event schema",
service: SERVICES.webhooks,
environment: "Production",
owner: OWNERS.omar,
risk: "Medium",
launchWindow: "May 4, 17:30 UTC",
checksDone: 3,
checksTotal: 4,
progressTone: "violet",
},
{
id: "release-8402",
changeKey: "REL-8402",
title: "Reshard billing export queue",
service: SERVICES.billing,
environment: "Production",
owner: OWNERS.nina,
risk: "Medium",
launchWindow: "May 4, 23:00 UTC",
checksDone: 3,
checksTotal: 5,
progressTone: "sky",
},
],
ready: [
{
id: "release-8399",
changeKey: "REL-8399",
title: "Open LATAM webhook fanout",
service: SERVICES.webhooks,
environment: "Production",
owner: OWNERS.omar,
risk: "Low",
launchWindow: "May 5, 18:00 UTC",
checksDone: 5,
checksTotal: 5,
progressTone: "emerald",
},
{
id: "release-8394",
changeKey: "REL-8394",
title: "Ship invoice PDF branding controls",
service: SERVICES.billing,
environment: "Production",
owner: OWNERS.nina,
risk: "Low",
launchWindow: "May 5, 20:30 UTC",
checksDone: 4,
checksTotal: 4,
progressTone: "emerald",
},
],
shipped: [
{
id: "release-8379",
changeKey: "REL-8379",
title: "Reduce invite session TTL",
service: SERVICES.identity,
environment: "Production",
owner: OWNERS.maya,
risk: "Low",
launchWindow: "Apr 29, 17:00 UTC",
checksDone: 3,
checksTotal: 3,
progressTone: "emerald",
},
{
id: "release-8372",
changeKey: "REL-8372",
title: "Archive legacy checkout experiment flags",
service: SERVICES.checkout,
environment: "Production",
owner: OWNERS.jonah,
risk: "Low",
launchWindow: "Apr 28, 15:00 UTC",
checksDone: 4,
checksTotal: 4,
progressTone: "emerald",
},
],
}
@@ -1,472 +0,0 @@
import { useState, type ComponentProps, type ReactNode } from "react"
import { Badge } from "@/components/reui/badge"
import {
Frame,
FrameHeader,
FrameTitle,
} from "@/components/reui/frame"
import {
Kanban,
KanbanBoard as KanbanBoardPrimitive,
KanbanColumn,
KanbanColumnContent,
KanbanColumnHandle,
KanbanItem,
KanbanItemHandle,
KanbanOverlay,
} from "@/components/reui/kanban"
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"
import { cn } from "@cfdm/ui/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@cfdm/ui/components/avatar"
import { Button } from "@cfdm/ui/components/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemFooter,
ItemHeader,
ItemMedia,
ItemTitle,
} from "@cfdm/ui/components/item"
import { Progress, ProgressLabel } from "@cfdm/ui/components/progress"
import {
INITIAL_RELEASE_CHANGES,
RELEASE_BOARD_DESCRIPTION,
RELEASE_BOARD_TITLE,
RELEASE_COLUMNS,
RISK_BADGE_VARIANT,
type ReleaseChange,
type ReleaseColumn,
type ReleaseColumnId,
type ReleaseProgressTone,
} from "./data"
import { FilterIcon, CalendarClockIcon, PlusIcon, GripVerticalIcon } from "lucide-react"
const RELEASE_COLUMN_BY_ID = new Map(
RELEASE_COLUMNS.map((column) => [column.id, column])
)
const COLUMN_HEADER_ACTION_BUTTON_CLASSNAME =
"text-muted-foreground hover:border-border! hover:bg-background! hover:text-foreground border border-transparent bg-transparent"
const progressToneClass: Record<ReleaseProgressTone, string> = {
amber: "**:data-[slot=progress-indicator]:bg-warning",
emerald: "**:data-[slot=progress-indicator]:bg-success",
neutral: "**:data-[slot=progress-indicator]:bg-muted-foreground/35",
rose: "**:data-[slot=progress-indicator]:bg-destructive",
sky: "**:data-[slot=progress-indicator]:bg-chart-2",
violet: "**:data-[slot=progress-indicator]:bg-info",
}
function BoardScrollArea({ children }: { children: ReactNode }) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className="relative w-full min-w-0 pb-3"
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 w-full rounded-lg transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
<ScrollAreaPrimitive.Content
data-slot="scroll-area-content"
className="w-max min-w-full"
>
{children}
</ScrollAreaPrimitive.Content>
</ScrollAreaPrimitive.Viewport>
<ScrollAreaPrimitive.Scrollbar
data-slot="scroll-area-scrollbar"
data-orientation="horizontal"
orientation="horizontal"
className="flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent"
>
<ScrollAreaPrimitive.Thumb
data-slot="scroll-area-thumb"
className="bg-foreground/15 relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.Scrollbar>
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function BoardToolbar() {
return (
<header className="px-1 py-1" aria-label="Release readiness toolbar">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div className="min-w-0">
<h2 className="truncate text-lg leading-7 font-semibold">
{RELEASE_BOARD_TITLE}
</h2>
<p className="text-muted-foreground mt-0.5 line-clamp-1 max-w-[44ch] text-sm leading-5">
{RELEASE_BOARD_DESCRIPTION}
</p>
</div>
<div
className="flex w-full flex-wrap items-center gap-2 lg:w-auto lg:justify-end"
role="group"
aria-label="Release board actions"
>
<Button type="button" variant="outline" size="sm">
<FilterIcon data-icon="inline-start" aria-hidden="true" />
Filters
</Button>
<Button type="button" variant="outline" size="sm">
<CalendarClockIcon data-icon="inline-start" aria-hidden="true" />
Windows
</Button>
<Button type="button" size="sm">
<PlusIcon data-icon="inline-start" aria-hidden="true" />
New change
</Button>
</div>
</div>
</header>
)
}
function ReleaseProgress({ change }: { change: ReleaseChange }) {
const percent =
change.checksTotal === 0
? 100
: Math.round((change.checksDone / change.checksTotal) * 100)
return (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2 text-sm leading-5">
<span className="text-muted-foreground">Checklist</span>
<span className="text-foreground text-xs leading-4 tabular-nums">
{change.checksDone}/{change.checksTotal} - {percent}%
</span>
</div>
<Progress
value={percent}
className={cn(
"**:data-[slot=progress-track]:bg-muted gap-0 **:data-[slot=progress-indicator]:rounded-full **:data-[slot=progress-track]:h-1.5 **:data-[slot=progress-track]:rounded-full",
progressToneClass[change.progressTone]
)}
>
<ProgressLabel className="sr-only">
{change.changeKey} checklist progress
</ProgressLabel>
</Progress>
</div>
)
}
function ReleaseMetaRow({
icon,
children,
}: {
icon: ReactNode
children: ReactNode
}) {
return (
<div className="grid min-w-0 grid-cols-[1.25rem_minmax(0,1fr)] items-center gap-2 text-sm leading-5">
<Item
render={<span />}
className="text-muted-foreground flex size-5 items-center justify-center border-0 p-0"
aria-hidden="true"
>
<ItemMedia variant="icon" className="size-auto">
{icon}
</ItemMedia>
</Item>
<div className="min-w-0">{children}</div>
</div>
)
}
interface ReleaseCardProps extends Omit<
ComponentProps<typeof KanbanItem>,
"value" | "children"
> {
change: ReleaseChange
isOverlay?: boolean
}
function ReleaseCard({ change, isOverlay, ...props }: ReleaseCardProps) {
const item = (
<Item
variant="outline"
size="sm"
className={cn(
"bg-card hover:bg-muted/20 items-stretch gap-3 transition-colors",
isOverlay && "shadow-lg"
)}
>
<ItemHeader className="min-w-0 items-start gap-2.5">
<ItemContent className="min-w-0 gap-1">
<ItemDescription className="text-muted-foreground text-xs leading-4 font-medium tracking-normal tabular-nums">
{change.changeKey}
</ItemDescription>
<ItemTitle
className="line-clamp-2 text-[0.9375rem] leading-5 font-medium"
title={change.title}
>
{change.title}
</ItemTitle>
</ItemContent>
<ItemActions className="shrink-0">
<Badge variant={RISK_BADGE_VARIANT[change.risk]}>{change.risk}</Badge>
</ItemActions>
</ItemHeader>
<ItemContent className="min-w-0 gap-2.5">
<ReleaseMetaRow icon={change.service.icon}>
<div className="flex min-w-0 items-center gap-2">
<span className="truncate font-medium">{change.service.label}</span>
<Badge variant="outline" size="sm" className="bg-background">
{change.environment}
</Badge>
</div>
</ReleaseMetaRow>
<ReleaseMetaRow
icon={
<CalendarClockIcon className="size-4" aria-hidden="true" />
}
>
<div className="flex min-w-0 items-center gap-2">
<span className="truncate tabular-nums">{change.launchWindow}</span>
</div>
</ReleaseMetaRow>
<ReleaseMetaRow
icon={
<Avatar className="size-5">
<AvatarImage src={change.owner.avatar} alt="" />
<AvatarFallback className="text-[0.5rem] font-semibold">
{change.owner.initials}
</AvatarFallback>
</Avatar>
}
>
<span className="truncate font-medium">{change.owner.name}</span>
</ReleaseMetaRow>
</ItemContent>
<ItemFooter className="min-w-0 flex-col items-stretch gap-2">
<ReleaseProgress change={change} />
</ItemFooter>
</Item>
)
return (
<KanbanItem value={change.id} {...props}>
{isOverlay ? (
item
) : (
<KanbanItemHandle className="block">{item}</KanbanItemHandle>
)}
</KanbanItem>
)
}
function EmptyColumn({ column }: { column: ReleaseColumn }) {
return (
<Button
type="button"
variant="outline"
className="text-muted-foreground hover:text-foreground bg-background/70 h-20 w-full border-dashed text-sm"
aria-label={column.addLabel}
>
<PlusIcon data-icon="inline-start" aria-hidden="true" />
{column.addLabel}
</Button>
)
}
function ColumnHeaderActions({
column,
isOverlay,
}: {
column: ReleaseColumn
isOverlay?: boolean
}) {
return (
<div
className={cn(
"ml-auto flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-focus-within/kanban-column:opacity-100 group-hover/kanban-column:opacity-100",
isOverlay && "hidden"
)}
>
<Button
type="button"
variant="ghost"
size="icon-sm"
className={COLUMN_HEADER_ACTION_BUTTON_CLASSNAME}
aria-label={column.addLabel}
title={column.addLabel}
>
<PlusIcon aria-hidden="true" />
</Button>
<KanbanColumnHandle
className="group-focus-within/kanban-column:opacity-100"
render={({ className, ...handleProps }) => (
<Button
{...handleProps}
type="button"
variant="ghost"
size="icon-sm"
aria-label={`Move ${column.title} column`}
title={`Move ${column.title} column`}
className={cn(COLUMN_HEADER_ACTION_BUTTON_CLASSNAME, className)}
>
<GripVerticalIcon aria-hidden="true" />
</Button>
)}
/>
</div>
)
}
interface ReleaseColumnProps extends Omit<
ComponentProps<typeof KanbanColumn>,
"value" | "children"
> {
column: ReleaseColumn
changes: ReleaseChange[]
isOverlay?: boolean
}
function ReleaseColumnView({
column,
changes,
isOverlay,
...props
}: ReleaseColumnProps) {
return (
<KanbanColumn
value={column.id}
className="w-[calc(100vw-3rem)] max-w-[19rem] shrink-0 sm:w-[19rem]"
{...props}
>
<Frame
spacing="sm"
className={cn("group/column", isOverlay && "shadow-lg")}
aria-label={`${column.title}: ${column.description}`}
>
<FrameHeader className="flex min-h-10 flex-row items-center gap-2 px-2 py-1.5">
<span
className={cn(
"size-2.5 shrink-0 rounded-full",
column.dotClassName
)}
aria-hidden="true"
/>
<FrameTitle
className="truncate text-sm leading-5"
title={column.title}
>
{column.title}
</FrameTitle>
<span className="text-muted-foreground shrink-0 text-sm font-medium tabular-nums">
{changes.length}
</span>
<ColumnHeaderActions column={column} isOverlay={isOverlay} />
</FrameHeader>
<KanbanColumnContent value={column.id} className="gap-2 p-0.5">
{changes.map((change) => (
<ReleaseCard key={change.id} change={change} />
))}
{changes.length === 0 ? <EmptyColumn column={column} /> : null}
</KanbanColumnContent>
</Frame>
</KanbanColumn>
)
}
function findReleaseChange(
columns: Record<string, ReleaseChange[]>,
changeId: string
) {
for (const changes of Object.values(columns)) {
const change = changes.find((item) => item.id === changeId)
if (change) {
return change
}
}
return null
}
export function KanbanBoard() {
const [changesByColumn, setChangesByColumn] = useState<
Record<string, ReleaseChange[]>
>(() => INITIAL_RELEASE_CHANGES)
return (
<section className="mx-auto flex w-full max-w-[1280px] flex-col gap-4">
<BoardToolbar />
<Kanban
value={changesByColumn}
onValueChange={setChangesByColumn}
getItemValue={(item) => item.id}
className="w-full"
>
<BoardScrollArea>
<KanbanBoardPrimitive className="grid min-w-max auto-cols-[19rem] grid-flow-col grid-cols-none items-start gap-3 p-1">
{Object.entries(changesByColumn).map(([columnId, changes]) => {
const column = RELEASE_COLUMN_BY_ID.get(
columnId as ReleaseColumnId
)
if (!column) {
return null
}
return (
<ReleaseColumnView
key={columnId}
column={column}
changes={changes}
/>
)
})}
</KanbanBoardPrimitive>
</BoardScrollArea>
<KanbanOverlay>
{({ value, variant }) => {
if (variant === "column") {
const column = RELEASE_COLUMN_BY_ID.get(
String(value) as ReleaseColumnId
)
if (!column) {
return null
}
return (
<ReleaseColumnView
column={column}
changes={changesByColumn[column.id] ?? []}
isOverlay
/>
)
}
const change = findReleaseChange(changesByColumn, String(value))
return change ? <ReleaseCard change={change} isOverlay /> : null
}}
</KanbanOverlay>
</Kanban>
</section>
)
}
@@ -1,15 +0,0 @@
import { KanbanBoard } from "./components/kanban-board"
export function Page() {
return (
<main
className="bg-background flex w-full justify-center p-4 sm:p-6"
aria-labelledby="page-heading"
>
<h1 id="page-heading" className="sr-only">
Release readiness kanban board
</h1>
<KanbanBoard />
</main>
)
}
@@ -1,49 +0,0 @@
import {
Alert,
AlertAction,
AlertDescription,
AlertTitle,
} from "@/components/reui/alert"
import { toast } from "sonner"
import { Button } from "@cfdm/ui/components/button"
import { ShieldCheckIcon } from "lucide-react"
export function AccessReviewAlert() {
return (
<Alert variant="warning">
<ShieldCheckIcon aria-hidden="true" />
<AlertTitle>Access review</AlertTitle>
{/* Description */}
<AlertDescription>
Review billing roles and approvers before renewal.
</AlertDescription>
<AlertAction>
<Button
type="button"
variant="outline"
size="xs"
onClick={() =>
toast.message("Dismissed", {
description:
"Hide the billing access reminder in your app state.",
})
}
>
Dismiss
</Button>
<Button
type="button"
size="xs"
onClick={() =>
toast.info("Review access", {
description: "Open your billing roles and approver review flow.",
})
}
>
Review
</Button>
</AlertAction>
</Alert>
)
}
@@ -1,69 +0,0 @@
"use client"
import { useState } from "react"
import { cn } from "@cfdm/ui/lib/utils"
import { Checkbox } from "@cfdm/ui/components/checkbox"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldTitle,
} from "@cfdm/ui/components/field"
import { SEAT_LIMIT_OPTIONS } from "./data"
export function BillingSeatSelectionCards() {
const [selectedSeatLimit, setSelectedSeatLimit] = useState(
SEAT_LIMIT_OPTIONS[1]?.value ?? ""
)
return (
<FieldGroup className="grid w-full gap-3 md:grid-cols-3">
{SEAT_LIMIT_OPTIONS.map((option) => {
const checked = option.value === selectedSeatLimit
const inputId = `billing-seat-${option.value}`
return (
<FieldLabel
key={option.value}
htmlFor={inputId}
className="relative flex h-full w-full min-w-0 cursor-pointer p-0"
>
<Field
orientation="horizontal"
className={cn(
"relative h-full w-full px-2 py-2 shadow-xs transition-colors",
checked
? "border-primary/40 bg-primary/5"
: "border-border/70 hover:bg-muted/40"
)}
>
<Checkbox
id={inputId}
checked={checked}
onCheckedChange={(nextChecked) => {
if (nextChecked) {
setSelectedSeatLimit(option.value)
}
}}
aria-label={option.label}
className="absolute -top-2 -right-2 size-5 rounded-full border-none shadow-none"
/>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<FieldTitle className="text-sm font-medium">
{option.label}
</FieldTitle>
<FieldDescription className="line-clamp-2 text-sm leading-5">
{option.description}
</FieldDescription>
</div>
</Field>
</FieldLabel>
)
})}
</FieldGroup>
)
}
@@ -1,166 +0,0 @@
import { Badge } from "@/components/reui/badge"
import {
Frame,
FrameDescription,
FrameFooter,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/reui/frame"
import { Button } from "@cfdm/ui/components/button"
import { FieldGroup } from "@cfdm/ui/components/field"
import { Input } from "@cfdm/ui/components/input"
import { Switch } from "@cfdm/ui/components/switch"
import { AccessReviewAlert } from "./access-review-alert"
import { BillingSeatSelectionCards } from "./billing-seat-selection-cards"
import { DIGEST_OPTIONS, WORKSPACE_IDENTITY } from "./data"
import {
BillingAmountField,
BillingApproversCombobox,
ProfileAdvancedSelectField,
} from "./profile-form-fields"
import { BillingSummaryFrame } from "./profile-summary-frames"
import { SettingRow } from "./setting-row"
export function BillingTabContent() {
return (
<div className="space-y-4">
<AccessReviewAlert />
<Frame spacing="sm">
<FrameHeader>
<FrameTitle className="capitalize">Billing setup</FrameTitle>
<FrameDescription>Plan, seats, and invoices.</FrameDescription>
</FrameHeader>
<FramePanel className="p-0!">
<FieldGroup className="gap-0">
<SettingRow
title="Current plan"
description="Includes scale, audit, and admin tools."
titleAddon={
<Badge variant="primary-light" size="sm">
Current
</Badge>
}
>
<div className="flex w-full flex-col items-end justify-end text-right">
<span className="text-sm font-medium">$149 / workspace</span>
<span className="text-muted-foreground text-sm">
Billed monthly
</span>
</div>
</SettingRow>
<SettingRow
title="Billing owner"
description="Primary approver for plan changes."
labelFor="billing-owner"
>
<Input
id="billing-owner"
defaultValue={WORKSPACE_IDENTITY.billingOwner}
/>
</SettingRow>
<SettingRow
title="Invoice approvers"
description="Users who approve plan changes."
>
<BillingApproversCombobox />
</SettingRow>
<SettingRow title="Invoice email" labelFor="billing-email">
<Input
id="billing-email"
defaultValue={WORKSPACE_IDENTITY.invoiceEmail}
/>
</SettingRow>
<SettingRow title="Cost center" labelFor="billing-cost-center">
<Input
id="billing-cost-center"
defaultValue={WORKSPACE_IDENTITY.costCenter}
/>
</SettingRow>
<SettingRow
title="Seat selection"
description="Set team capacity."
stacked
contentClassName="@md/field-group:max-w-none"
last
>
<BillingSeatSelectionCards />
</SettingRow>
</FieldGroup>
</FramePanel>
<FrameFooter className="flex-row justify-end gap-2">
<Button type="button" variant="outline">
Manage plan
</Button>
<Button type="button">Update billing</Button>
</FrameFooter>
</Frame>
<Frame spacing="sm">
<FrameHeader>
<FrameTitle className="capitalize">Billing controls</FrameTitle>
<FrameDescription>Approval and renewal rules.</FrameDescription>
</FrameHeader>
<FramePanel className="p-0!">
<FieldGroup className="gap-0">
<SettingRow
title="Auto-add seats"
description="Add seats when invites exceed your cap."
labelFor="billing-auto-add-seats"
compact
>
<Switch id="billing-auto-add-seats" />
</SettingRow>
<SettingRow
title="Purchase order required"
description="Hold invoice changes until a PO is present."
labelFor="billing-po-required"
compact
>
<Switch id="billing-po-required" defaultChecked />
</SettingRow>
<SettingRow
title="Approval threshold"
description="Invoices above this need approval."
compact
>
<BillingAmountField id="billing-approval-threshold" />
</SettingRow>
<SettingRow title="Invoice reminders" compact last>
<ProfileAdvancedSelectField
id="billing-reminders"
options={DIGEST_OPTIONS}
defaultValue={DIGEST_OPTIONS[2]}
/>
</SettingRow>
</FieldGroup>
</FramePanel>
<FrameFooter className="flex-row justify-end gap-2">
<Button type="button" variant="outline">
Review approvals
</Button>
<Button type="button" variant="outline">
Review plans
</Button>
</FrameFooter>
</Frame>
<BillingSummaryFrame />
</div>
)
}
@@ -1,32 +0,0 @@
import { cn } from "@cfdm/ui/lib/utils"
import { BRAND_COLORS } from "./data"
export function BrandAccentPicker() {
return (
<div
role="radiogroup"
aria-label="Accent color"
className="flex flex-wrap items-center gap-2"
>
{BRAND_COLORS.map((color) => (
<label key={color.id} className="relative cursor-pointer">
<input
type="radio"
name="profile-1-brand-accent"
value={color.id}
defaultChecked={color.active}
aria-label={color.label}
className="peer sr-only"
/>
<span
className={cn(
"ring-offset-background peer-checked:ring-foreground/70 peer-focus-visible:ring-foreground/70 hover:ring-foreground/20 flex size-6 items-center justify-center rounded-full border border-white/80 shadow-sm ring-offset-2 transition-[box-shadow] peer-checked:ring-2 peer-focus-visible:ring-2 hover:ring-2"
)}
style={{ backgroundColor: color.value }}
/>
</label>
))}
</div>
)
}
@@ -1,409 +0,0 @@
import { type BadgeProps } from "@/components/reui/badge"
// ── Types ──
export interface SelectOption {
value: string
label: string
description: string
}
export interface SummaryMetric {
id: string
label: string
value: string
detail?: string
badge?: {
label: string
variant: BadgeProps["variant"]
}
}
export interface BillingSpendCard {
id: "spend" | "seats" | "invoice"
title: string
subtitle: string
value: string
detail: string
badge: {
label: string
variant: BadgeProps["variant"]
}
}
export interface TeamMember {
id: string
name: string
role: string
department: string
email: string
access: TeamMemberAccess
status: TeamMemberStatus
availability: TeamMemberAvailability
location: string
timezone: string
joined: string
lastActive: string
src: string
initials: string
}
export interface ProfileIdentity {
name: string
username: string
email: string
phone: string
website: string
bio: string
}
export interface WorkspaceIdentity {
name: string
subdomain: string
domainSuffix: string
supportEmail: string
billingOwner: string
invoiceEmail: string
costCenter: string
}
export interface BrandColor {
id: string
label: string
value: string
active?: boolean
}
export interface TimezoneGroup {
value: string
items: string[]
}
export type TeamMemberStatus = "Active" | "Invited" | "Limited" | "Suspended"
export type TeamMemberAvailability = "online" | "away" | "busy" | "offline"
export type TeamMemberAccess = "Owner" | "Admin" | "Member"
// ── Data ──
export const PROFILE_IDENTITY: ProfileIdentity = {
name: "Ava Chen",
username: "avachen",
email: "[email protected]",
phone: "+17185550188",
website: "ava.atlashq.app",
bio: "Leading launch systems, product operations, and internal tooling across the Atlas workspace.",
}
export const WORKSPACE_IDENTITY: WorkspaceIdentity = {
name: "Atlas",
subdomain: "hq",
domainSuffix: ".atlashq.app",
supportEmail: "[email protected]",
billingOwner: "[email protected]",
invoiceEmail: "[email protected]",
costCenter: "RevOps / Growth",
}
export const TEAM_MEMBERS: TeamMember[] = [
{
id: "ava",
name: "Ava Chen",
role: "Owner",
department: "Product · Leadership",
email: "[email protected]",
access: "Owner",
status: "Active",
availability: "online",
location: "Brooklyn, NY",
timezone: "EST (UTC-5)",
joined: "Jan 2024",
lastActive: "Active now",
src: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=80&h=80&dpr=2&q=80",
initials: "AC",
},
{
id: "milo",
name: "Milo Harper",
role: "Growth Ops",
department: "Revenue · Growth",
email: "[email protected]",
access: "Admin",
status: "Active",
availability: "away",
location: "San Francisco, CA",
timezone: "PST (UTC-8)",
joined: "Mar 2024",
lastActive: "10 min ago",
src: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=80&h=80&dpr=2&q=80",
initials: "MH",
},
{
id: "nina",
name: "Nina Park",
role: "Design Systems",
department: "Design · Systems",
email: "[email protected]",
access: "Member",
status: "Active",
availability: "busy",
location: "Toronto, CA",
timezone: "EST (UTC-5)",
joined: "Jun 2023",
lastActive: "2 hours ago",
src: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=80&h=80&dpr=2&q=80",
initials: "NP",
},
{
id: "omar",
name: "Omar Reyes",
role: "Customer Success",
department: "Support · Success",
email: "[email protected]",
access: "Member",
status: "Limited",
availability: "offline",
location: "Austin, TX",
timezone: "CST (UTC-6)",
joined: "Sep 2024",
lastActive: "Today",
src: "https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=80&h=80&dpr=2&q=80",
initials: "OR",
},
{
id: "maya",
name: "Maya Singh",
role: "Revenue Ops",
department: "Finance · Ops",
email: "[email protected]",
access: "Admin",
status: "Invited",
availability: "away",
location: "Chicago, IL",
timezone: "CST (UTC-6)",
joined: "Invited Apr 2026",
lastActive: "Yesterday",
src: "https://images.unsplash.com/photo-1488426862026-3ee34a7d66df?w=80&h=80&dpr=2&q=80",
initials: "MS",
},
{
id: "leo",
name: "Leo Grant",
role: "Platform Engineer",
department: "Engineering · Platform",
email: "[email protected]",
access: "Member",
status: "Suspended",
availability: "offline",
location: "Seattle, WA",
timezone: "PST (UTC-8)",
joined: "Nov 2023",
lastActive: "3 days ago",
src: "https://images.unsplash.com/photo-1504593811423-6dd665756598?w=80&h=80&dpr=2&q=80",
initials: "LG",
},
]
export const TEAM_STATUS_ORDER: TeamMemberStatus[] = [
"Active",
"Invited",
"Limited",
"Suspended",
]
export const WORKSPACE_SIGNALS: SummaryMetric[] = [
{
id: "domain",
label: "Verified domain",
value: "atlashq.app",
detail: "Custom links, invites, and shared pages resolve here.",
badge: {
label: "Active",
variant: "success-light",
},
},
{
id: "region",
label: "Data region",
value: "US East",
detail: "Primary routing for customer and workspace data.",
},
{
id: "theme",
label: "Brand posture",
value: "Teal accent",
detail: "Applied to shared flows and workspace navigation.",
},
]
export const BILLING_SPEND_CARDS: BillingSpendCard[] = [
{
id: "spend",
title: "Monthly spend",
subtitle: "Business plan",
value: "$149",
detail: "Base workspace plan before seat growth or annual billing changes.",
badge: {
label: "Current",
variant: "success-light",
},
},
{
id: "seats",
title: "Seat usage",
subtitle: "12 of 24 seats",
value: "12",
detail: "Half of the available seats are assigned across the workspace.",
badge: {
label: "12 open",
variant: "info-light",
},
},
{
id: "invoice",
title: "Next invoice",
subtitle: "May 28, 2026",
value: "$149",
detail: "Approvers, reminders, and the PO rule are already attached.",
badge: {
label: "PO ready",
variant: "warning-light",
},
},
]
export const TIMEZONE_GROUPS: TimezoneGroup[] = [
{
value: "Americas",
items: [
"(GMT-5) New York",
"(GMT-8) Los Angeles",
"(GMT-6) Chicago",
"(GMT-5) Toronto",
"(GMT-8) Vancouver",
"(GMT-3) Sao Paulo",
],
},
{
value: "Europe",
items: [
"(GMT+0) London",
"(GMT+1) Paris",
"(GMT+1) Berlin",
"(GMT+1) Rome",
"(GMT+1) Madrid",
"(GMT+1) Amsterdam",
],
},
{
value: "Asia/Pacific",
items: [
"(GMT+9) Tokyo",
"(GMT+8) Shanghai",
"(GMT+8) Singapore",
"(GMT+4) Dubai",
"(GMT+11) Sydney",
"(GMT+9) Seoul",
],
},
]
export const ROLE_OPTIONS: SelectOption[] = [
{
value: "staff-product-lead",
label: "Staff Product Lead",
description: "Owns product operations",
},
{
value: "product-manager",
label: "Product Manager",
description: "Drives roadmap execution",
},
{
value: "design-lead",
label: "Design Lead",
description: "Owns system quality",
},
]
export const LANGUAGE_OPTIONS: SelectOption[] = [
{
value: "en-us",
label: "English (US)",
description: "Default product language",
},
{
value: "en-uk",
label: "English (UK)",
description: "Common team alternative",
},
{
value: "de",
label: "German",
description: "For regional ops",
},
]
export const LANDING_VIEW_OPTIONS: SelectOption[] = [
{
value: "home",
label: "Home overview",
description: "Recent activity first",
},
{
value: "pipeline",
label: "Pipeline board",
description: "Jump into execution",
},
{
value: "inbox",
label: "Priority inbox",
description: "Focus on approvals",
},
]
export const DIGEST_OPTIONS: SelectOption[] = [
{
value: "weekday-am",
label: "Weekday mornings",
description: "Sent before standup",
},
{
value: "daily-pm",
label: "Daily evenings",
description: "Wrap the workday",
},
{
value: "weekly",
label: "Weekly summary",
description: "Monday planning recap",
},
]
export const SEAT_LIMIT_OPTIONS: SelectOption[] = [
{
value: "16",
label: "16 seats",
description: "Best for your current team size and smooth growth",
},
{
value: "24",
label: "24 seats",
description: "Gives your team room for future growth",
},
{
value: "40",
label: "40 seats",
description: "Supports the next hiring cycle with 16 open seats",
},
]
export const BRAND_COLORS: BrandColor[] = [
{ id: "graphite", label: "Graphite", value: "#5B6170" },
{ id: "cobalt", label: "Cobalt", value: "#5470F7" },
{ id: "teal", label: "Teal", value: "#159E9A", active: true },
{ id: "mint", label: "Mint", value: "#44BA84" },
{ id: "orange", label: "Orange", value: "#EA7B18" },
{ id: "rose", label: "Rose", value: "#E65A8B" },
]
@@ -1,134 +0,0 @@
import { useState } from "react"
import { useFileUpload } from "@/hooks/use-file-upload"
import { cn } from "@cfdm/ui/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@cfdm/ui/components/avatar"
import { Button } from "@cfdm/ui/components/button"
import { UserCircle, ImageIcon, XIcon, UploadIcon } from "lucide-react"
interface ImageUploadFieldProps {
ariaLabel: string
inputId?: string
variant?: "avatar" | "image"
defaultImage?: string
alt?: string
}
export function ImageUploadField({
ariaLabel,
inputId,
variant = "image",
defaultImage,
alt = "Uploaded image",
}: ImageUploadFieldProps) {
const [removedCurrentImage, setRemovedCurrentImage] = useState(false)
const [{ files }, { removeFile, openFileDialog, getInputProps }] =
useFileUpload({
accept: "image/*",
})
const currentFile = files[0] ?? null
const hasSavedImage = Boolean(defaultImage) && !removedCurrentImage
const previewUrl =
currentFile?.preview ?? (hasSavedImage ? (defaultImage ?? null) : null)
const fileName = currentFile?.file.name
const hasImage = Boolean(previewUrl)
const handleCancelUpload = () => {
if (!currentFile) {
return
}
removeFile(currentFile.id)
}
const handleRemoveImage = () => {
if (currentFile) {
removeFile(currentFile.id)
}
setRemovedCurrentImage(true)
}
return (
<div className="flex grow flex-wrap items-center justify-start gap-2">
<div className="relative">
<Avatar
className={cn(
"size-10 border",
variant === "avatar"
? "rounded-full"
: "rounded-lg after:rounded-lg"
)}
>
<AvatarImage
src={previewUrl ?? undefined}
alt={fileName ?? alt}
className={cn(variant === "avatar" ? "rounded-full" : "rounded-lg")}
/>
<AvatarFallback
className={cn(
"bg-muted text-muted-foreground",
variant === "avatar" ? "rounded-full" : "rounded-lg"
)}
>
{variant === "avatar" ? (
<UserCircle aria-hidden="true" className="size-4 opacity-60" />
) : (
<ImageIcon aria-hidden="true" className="size-4 opacity-60" />
)}
</AvatarFallback>
</Avatar>
{currentFile ? (
<Button
type="button"
variant="outline"
size="icon-xs"
onClick={handleCancelUpload}
className="absolute -top-1 -right-1 size-4 rounded-full"
aria-label={`Cancel ${currentFile.file.name}`}
>
<XIcon aria-hidden="true" />
</Button>
) : null}
</div>
{/* Actions */}
<div className="relative inline-flex">
<Button
type="button"
variant="outline"
size="sm"
onClick={openFileDialog}
aria-haspopup="dialog"
>
<UploadIcon aria-hidden="true" />
{hasImage ? "Change" : "Upload"}
</Button>
<input
{...getInputProps({ id: inputId })}
className="sr-only"
aria-label={ariaLabel}
tabIndex={-1}
/>
</div>
{hasImage ? (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleRemoveImage}
>
<XIcon aria-hidden="true" />
Remove
</Button>
) : null}
</div>
)
}
@@ -1,266 +0,0 @@
"use client"
import { Fragment } from "react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@cfdm/ui/components/avatar"
import {
Combobox,
ComboboxChip,
ComboboxChips,
ComboboxChipsInput,
ComboboxCollection,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
ComboboxValue,
useComboboxAnchor,
} from "@cfdm/ui/components/combobox"
import { Field } from "@cfdm/ui/components/field"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@cfdm/ui/components/input-group"
import {
Item,
ItemContent,
ItemDescription,
ItemTitle,
} from "@cfdm/ui/components/item"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@cfdm/ui/components/select"
import { TEAM_MEMBERS, TIMEZONE_GROUPS, type SelectOption } from "./data"
const BILLING_APPROVER_OPTIONS = TEAM_MEMBERS.map((member) => ({
...member,
position: member.role,
}))
export function ProfileAdvancedSelectField({
id,
options,
defaultValue,
}: {
id: string
options: SelectOption[]
defaultValue: SelectOption
}) {
return (
<Field className="w-full">
<Select defaultValue={defaultValue} items={options}>
<SelectTrigger id={id} className="w-full [&_small]:hidden">
<SelectValue>
{(item: SelectOption | null) =>
item ? (
<span className="flex flex-col items-start gap-px">
<span className="font-medium">{item.label}</span>
<small className="text-muted-foreground text-sm">
{item.description}
</small>
</span>
) : null
}
</SelectValue>
</SelectTrigger>
<SelectContent align="end" className="w-(--anchor-width)">
<SelectGroup>
{options.map((option) => (
<SelectItem
key={option.value}
value={option}
className="[&_svg]:text-primary"
>
<span className="flex flex-col items-start gap-px">
<span className="font-medium">{option.label}</span>
<small className="text-muted-foreground text-sm">
{option.description}
</small>
</span>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
)
}
export function ProfileCompactSelectField({
id,
options,
defaultValue,
}: {
id: string
options: SelectOption[]
defaultValue: SelectOption
}) {
return (
<Select defaultValue={defaultValue} items={options}>
<SelectTrigger id={id} className="w-full">
<SelectValue>
{(item: SelectOption | null) => item?.label ?? null}
</SelectValue>
</SelectTrigger>
{/* Content */}
<SelectContent className="w-(--anchor-width)">
<SelectGroup>
{options.map((option) => (
<SelectItem key={option.value} value={option}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)
}
export function ProfileTimezoneField({
id,
placeholder = "Select a timezone",
}: {
id?: string
placeholder?: string
}) {
return (
<Field className="w-full">
<Combobox
items={TIMEZONE_GROUPS}
defaultValue={TIMEZONE_GROUPS[0]?.items[0]}
>
<ComboboxInput id={id} placeholder={placeholder} className="w-full" />
<ComboboxContent className="w-(--anchor-width) min-w-(--anchor-width)">
<ComboboxEmpty>No timezones found.</ComboboxEmpty>
<ComboboxList>
{(group) => (
<ComboboxGroup key={group.value} items={group.items}>
<ComboboxLabel>{group.value}</ComboboxLabel>
<ComboboxCollection>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxCollection>
</ComboboxGroup>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</Field>
)
}
export function BillingApproversCombobox() {
const anchor = useComboboxAnchor()
return (
<Field className="w-full">
<Combobox
multiple
items={BILLING_APPROVER_OPTIONS}
itemToStringValue={(
member: (typeof BILLING_APPROVER_OPTIONS)[number]
) => member.name}
defaultValue={[
BILLING_APPROVER_OPTIONS[0],
BILLING_APPROVER_OPTIONS[1],
BILLING_APPROVER_OPTIONS[4],
]}
>
<ComboboxChips
ref={anchor}
className="w-full has-data-[slot=combobox-chip]:pl-1"
>
<ComboboxValue>
{(selectedMembers: (typeof BILLING_APPROVER_OPTIONS)[number][]) => (
<Fragment>
{selectedMembers.map((member) => (
<ComboboxChip
key={member.id}
showRemove={true}
className="gap-1.5 rounded-full"
>
<Avatar className="size-4">
<AvatarImage src={member.src} alt={member.name} />
<AvatarFallback className="text-[8px]">
{member.initials}
</AvatarFallback>
</Avatar>
{member.name}
</ComboboxChip>
))}
<ComboboxChipsInput placeholder="Add approvers..." />
</Fragment>
)}
</ComboboxValue>
</ComboboxChips>
<ComboboxContent
anchor={anchor}
className="max-w-(--anchor-width) min-w-(--anchor-width)"
>
<ComboboxEmpty>No members found.</ComboboxEmpty>
<ComboboxList>
{(member) => (
<ComboboxItem key={member.id} value={member}>
<Item size="xs" className="p-0">
<Avatar className="size-6">
<AvatarImage src={member.src} alt={member.name} />
<AvatarFallback>{member.initials}</AvatarFallback>
</Avatar>
<ItemContent>
<ItemTitle className="whitespace-nowrap">
{member.name}
</ItemTitle>
<ItemDescription>{member.position}</ItemDescription>
</ItemContent>
</Item>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</Field>
)
}
export function BillingAmountField({
id,
placeholder = "0.00",
}: {
id: string
placeholder?: string
}) {
return (
<Field className="w-full">
{/* List */}
<InputGroup>
<InputGroupAddon>
<InputGroupText>$</InputGroupText>
</InputGroupAddon>
<InputGroupInput id={id} type="number" placeholder={placeholder} />
<InputGroupAddon align="inline-end">
<InputGroupText>USD</InputGroupText>
</InputGroupAddon>
</InputGroup>
</Field>
)
}
@@ -1,145 +0,0 @@
import { type ReactNode } from "react"
import { Badge } from "@/components/reui/badge"
import {
Frame,
FrameDescription,
FrameFooter,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/reui/frame"
import { Button } from "@cfdm/ui/components/button"
import { Separator } from "@cfdm/ui/components/separator"
import {
BILLING_SPEND_CARDS,
type BillingSpendCard,
type SummaryMetric,
} from "./data"
import { CreditCardIcon, UsersIcon, CalendarCheckIcon } from "lucide-react"
export function ProfileSummaryFrame({
title,
description,
items,
}: {
title: string
description: string
items: SummaryMetric[]
}) {
return (
<Frame spacing="sm">
{/* Header */}
<FrameHeader>
<FrameTitle className="capitalize">{title}</FrameTitle>
<FrameDescription>{description}</FrameDescription>
</FrameHeader>
{/* Content */}
<FramePanel className="space-y-3">
<ProfileSummaryList items={items} />
</FramePanel>
</Frame>
)
}
function ProfileSummaryList({ items }: { items: SummaryMetric[] }) {
return (
<div className="space-y-3">
{items.map((item, index) => (
<div key={item.id} className="space-y-3">
{index > 0 ? <Separator /> : null}
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-muted-foreground text-xs">{item.label}</p>
<p className="mt-1 text-sm font-medium">{item.value}</p>
{item.detail ? (
<p className="text-muted-foreground mt-1 text-sm leading-5">
{item.detail}
</p>
) : null}
</div>
{item.badge ? (
<Badge variant={item.badge.variant} size="sm">
{item.badge.label}
</Badge>
) : null}
</div>
</div>
))}
</div>
)
}
function getBillingSummaryIcon(id: BillingSpendCard["id"]): ReactNode {
switch (id) {
case "spend":
return (
<CreditCardIcon aria-hidden="true" className="size-3.5" />
)
case "seats":
return (
<UsersIcon aria-hidden="true" className="size-3.5" />
)
case "invoice":
return (
<CalendarCheckIcon aria-hidden="true" className="size-3.5" />
)
}
}
export function BillingSummaryFrame() {
return (
<Frame spacing="sm" className="@container">
{/* Header */}
<FrameHeader>
<FrameTitle className="capitalize">Billing summary</FrameTitle>
<FrameDescription>Plan, seats, and invoice.</FrameDescription>
</FrameHeader>
{/* Content */}
<FramePanel className="border-border grid auto-rows-fr grid-cols-1 overflow-hidden border-t p-0! md:grid-cols-3">
{BILLING_SPEND_CARDS.map((card) => (
<div
key={card.id}
className="border-border flex h-full min-w-0 flex-col gap-3 border-b p-5 last:border-b-0 md:border-b-0 md:border-l first:md:border-l-0"
>
<div className="space-y-2.5">
<div className="space-y-1">
<p className="text-sm font-medium">{card.title}</p>
<p className="text-muted-foreground text-xs">{card.subtitle}</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-xl font-semibold tracking-tight">
{card.value}
</span>
<Badge variant={card.badge.variant} size="sm">
{getBillingSummaryIcon(card.id)}
{card.badge.label}
</Badge>
</div>
</div>
<div className="min-w-0">
<p className="text-muted-foreground text-sm leading-5">
{card.detail}
</p>
</div>
</div>
))}
</FramePanel>
<FrameFooter className="flex-row justify-end gap-2">
<Button type="button" variant="outline">
Download statement
</Button>
<Button type="button" variant="outline">
Review invoices
</Button>
</FrameFooter>
</Frame>
)
}
@@ -1,244 +0,0 @@
"use client"
import { useState } from "react"
import { Badge } from "@/components/reui/badge"
import {
Frame,
FrameDescription,
FrameFooter,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/reui/frame"
import { PhoneInput } from "@/components/reui/phone-input"
import { Button } from "@cfdm/ui/components/button"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from "@cfdm/ui/components/field"
import { Input } from "@cfdm/ui/components/input"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@cfdm/ui/components/input-group"
import { Switch } from "@cfdm/ui/components/switch"
import { Textarea } from "@cfdm/ui/components/textarea"
import {
DIGEST_OPTIONS,
LANDING_VIEW_OPTIONS,
LANGUAGE_OPTIONS,
PROFILE_IDENTITY,
ROLE_OPTIONS,
} from "./data"
import { ImageUploadField } from "./image-upload-field"
import {
ProfileAdvancedSelectField,
ProfileCompactSelectField,
ProfileTimezoneField,
} from "./profile-form-fields"
import { SettingRow } from "./setting-row"
export function ProfileTabContent() {
const [phoneNumber, setPhoneNumber] = useState(PROFILE_IDENTITY.phone)
return (
<div className="space-y-4">
<Frame spacing="sm">
<FrameHeader>
<FrameTitle className="capitalize">Profile details</FrameTitle>
<FrameDescription>Personal account info.</FrameDescription>
</FrameHeader>
<FramePanel className="p-0">
<FieldGroup className="gap-0">
<SettingRow
title="Profile photo"
description="Shown in comments and mentions."
labelFor="profile-photo"
>
<ImageUploadField
ariaLabel="Upload profile photo"
inputId="profile-photo"
variant="avatar"
defaultImage="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80"
alt={PROFILE_IDENTITY.name}
/>
</SettingRow>
<SettingRow
title="Full name"
description="Used across Atlas."
labelFor="profile-name"
>
<Input id="profile-name" defaultValue={PROFILE_IDENTITY.name} />
</SettingRow>
<SettingRow
title="Email address"
description="Primary sign-in email."
labelFor="profile-email"
titleAddon={
<Badge variant="success-light" size="sm">
Verified
</Badge>
}
>
<Input id="profile-email" defaultValue={PROFILE_IDENTITY.email} />
</SettingRow>
<SettingRow
title="Phone number"
description="Recovery and urgent alerts."
>
<PhoneInput
className="w-full"
variant="sm"
placeholder="Enter phone number"
defaultCountry="US"
value={phoneNumber}
onChange={(value) => setPhoneNumber(value || "")}
/>
</SettingRow>
<SettingRow
title="Username"
description="Used in mentions and links."
labelFor="profile-username"
>
<InputGroup className="w-full">
<InputGroupAddon align="inline-start">
<InputGroupText>@</InputGroupText>
</InputGroupAddon>
<InputGroupInput
id="profile-username"
defaultValue={PROFILE_IDENTITY.username}
/>
</InputGroup>
</SettingRow>
<SettingRow
title="Public details"
description="Shown across Atlas."
contentClassName="@md/field-group:max-w-[32rem]"
>
<FieldSet className="w-full gap-3">
<FieldLegend className="sr-only">Public details</FieldLegend>
<FieldDescription className="sr-only">
Public profile and workspace defaults.
</FieldDescription>
<FieldGroup className="gap-3">
<Field>
<FieldLabel htmlFor="profile-role">Role</FieldLabel>
<ProfileCompactSelectField
id="profile-role"
options={ROLE_OPTIONS}
defaultValue={ROLE_OPTIONS[0]}
/>
</Field>
<Field>
<FieldLabel htmlFor="profile-timezone">
Time zone
</FieldLabel>
<ProfileTimezoneField
id="profile-timezone"
placeholder="Select a timezone"
/>
</Field>
<Field>
<FieldLabel htmlFor="profile-website">Website</FieldLabel>
<InputGroup className="w-full">
<InputGroupAddon align="inline-start">
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupInput
id="profile-website"
defaultValue={PROFILE_IDENTITY.website}
/>
</InputGroup>
</Field>
</FieldGroup>
</FieldSet>
</SettingRow>
<SettingRow
title="Bio"
description="Short profile summary."
labelFor="profile-bio"
contentClassName="@md/field-group:w-full"
last
>
<Textarea
id="profile-bio"
defaultValue={PROFILE_IDENTITY.bio}
className="min-h-24 w-full resize-none"
/>
</SettingRow>
</FieldGroup>
</FramePanel>
<FrameFooter className="flex-row justify-end gap-2">
<Button type="button" variant="outline">
Cancel
</Button>
<Button type="button">Save changes</Button>
</FrameFooter>
</Frame>
<Frame spacing="sm">
<FrameHeader>
<FrameTitle className="capitalize">Profile preferences</FrameTitle>
<FrameDescription>Default workspace behavior.</FrameDescription>
</FrameHeader>
<FramePanel className="p-0">
<FieldGroup className="gap-0">
<SettingRow title="Language" compact>
<ProfileAdvancedSelectField
id="profile-language"
options={LANGUAGE_OPTIONS}
defaultValue={LANGUAGE_OPTIONS[0]}
/>
</SettingRow>
<SettingRow title="Landing view" compact>
<ProfileAdvancedSelectField
id="profile-landing-view"
options={LANDING_VIEW_OPTIONS}
defaultValue={LANDING_VIEW_OPTIONS[2]}
/>
</SettingRow>
<SettingRow title="Digest cadence" compact>
<ProfileAdvancedSelectField
id="profile-digest"
options={DIGEST_OPTIONS}
defaultValue={DIGEST_OPTIONS[2]}
/>
</SettingRow>
<SettingRow
title="Daily briefing"
description="Morning recap before meetings."
labelFor="profile-daily-briefing"
compact
last
>
<Switch id="profile-daily-briefing" defaultChecked />
</SettingRow>
</FieldGroup>
</FramePanel>
</Frame>
</div>
)
}
@@ -1,148 +0,0 @@
import { useState, type ComponentType, type ReactNode } from "react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@cfdm/ui/lib/utils"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@cfdm/ui/components/tabs"
import { BillingTabContent } from "./billing-tab"
import { ProfileTabContent } from "./profile-tab"
import { TeamTabContent } from "./team-tab"
import { WorkspaceTabContent } from "./workspace-tab"
import { UserIcon, Building2Icon, UsersIcon, CreditCardIcon } from "lucide-react"
type ProfileTabValue = "profile" | "workspace" | "team" | "billing"
type ProfileTabConfig = {
value: ProfileTabValue
label: string
icon: ReactNode
component: ComponentType
}
const PROFILE_TABS: ProfileTabConfig[] = [
{
value: "profile",
label: "Profile",
icon: (
<UserIcon aria-hidden="true" />
),
component: ProfileTabContent,
},
{
value: "workspace",
label: "Workspace",
icon: (
<Building2Icon aria-hidden="true" />
),
component: WorkspaceTabContent,
},
{
value: "team",
label: "Team",
icon: (
<UsersIcon aria-hidden="true" />
),
component: TeamTabContent,
},
{
value: "billing",
label: "Billing",
icon: (
<CreditCardIcon aria-hidden="true" />
),
component: BillingTabContent,
},
]
export function Profile() {
const isMobile = useIsMobile()
const [activeTab, setActiveTab] = useState<ProfileTabValue>("profile")
return (
<div className="w-full max-w-4xl space-y-8">
{/* Header */}
<header className="px-1">
<h1 className="text-xl font-semibold tracking-tight">
Account Settings
</h1>
<p className="text-muted-foreground max-w-2xl text-sm leading-relaxed">
Update your account, workspace, team, and billing.
</p>
</header>
{/* Tabs */}
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as ProfileTabValue)}
orientation={isMobile ? "horizontal" : "vertical"}
className="w-full gap-5 lg:gap-8"
>
<SidebarRail isMobile={isMobile} activeValue={activeTab} />
<div className="min-w-0 flex-1">
{PROFILE_TABS.map((tab) => {
const TabComponent = tab.component
return (
<TabsContent key={tab.value} value={tab.value} className="mt-0">
<TabComponent />
</TabsContent>
)
})}
</div>
</Tabs>
</div>
)
}
function SidebarRail({
isMobile,
activeValue,
}: {
isMobile: boolean
activeValue: ProfileTabValue
}) {
return (
<div className={cn("min-w-0", isMobile ? "w-full" : "w-40 shrink-0")}>
{isMobile ? (
<div className="-mx-1 overflow-x-auto px-1 pb-1">
<TabsList className="h-auto w-max min-w-max justify-start gap-1 bg-transparent p-0">
{PROFILE_TABS.map((tab) => (
<TabsTrigger
key={tab.value}
value={tab.value}
className={cn(
"w-full justify-start gap-3 px-3 py-1.5 shadow-none",
activeValue === tab.value ? "bg-muted!" : "bg-transparent"
)}
>
{tab.icon}
<span className="truncate">{tab.label}</span>
</TabsTrigger>
))}
</TabsList>
</div>
) : (
<TabsList className="h-auto w-full flex-col items-stretch gap-1 bg-transparent p-0">
{PROFILE_TABS.map((tab) => (
<TabsTrigger
key={tab.value}
value={tab.value}
className={cn(
"w-full justify-start gap-3 px-3 py-1.5 shadow-none",
activeValue === tab.value ? "bg-muted!" : "bg-transparent"
)}
>
{tab.icon}
<span className="truncate">{tab.label}</span>
</TabsTrigger>
))}
</TabsList>
)}
</div>
)
}
@@ -1,3 +0,0 @@
"use client"
export { SettingRow, type SettingRowProps } from "@/components/setting-row"
@@ -1,294 +0,0 @@
import { memo, useState } from "react"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
import { Badge, type BadgeProps } from "@/components/reui/badge"
import { DataGridColumnHeader } from "@/components/reui/data-grid/data-grid-column-header"
import { type ColumnDef, type Row } from "@tanstack/react-table"
import { toast } from "sonner"
import { cn } from "@cfdm/ui/lib/utils"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@cfdm/ui/components/alert-dialog"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@cfdm/ui/components/avatar"
import { Button } from "@cfdm/ui/components/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@cfdm/ui/components/dropdown-menu"
import { type TeamMember, type TeamMemberStatus } from "./data"
import { MoreHorizontalIcon, UserIcon, ShieldCheckIcon, MailIcon, RefreshCwIcon, CopyIcon, UserRoundXIcon } from "lucide-react"
const availabilityColor: Record<TeamMember["availability"], string> = {
online: "bg-green-500",
away: "bg-yellow-400",
busy: "bg-red-500",
offline: "bg-zinc-400 dark:bg-zinc-600",
}
const accessBadgeVariant: Record<TeamMember["access"], BadgeProps["variant"]> =
{
Owner: "primary-light",
Admin: "info-light",
Member: "outline",
}
export const TeamStatusBadge = memo(function TeamStatusBadge({
status,
}: {
status: TeamMemberStatus
}) {
if (status === "Active") {
return <Badge variant="success-light">Active</Badge>
}
if (status === "Invited") {
return <Badge variant="info-light">Invited</Badge>
}
if (status === "Limited") {
return <Badge variant="warning-light">Limited</Badge>
}
return <Badge variant="destructive-light">Suspended</Badge>
})
const UserCell = memo(function UserCell({ row }: { row: Row<TeamMember> }) {
const user = row.original
return (
<div className="flex items-center gap-2">
<div className="relative shrink-0">
<Avatar className="size-8">
<AvatarImage src={user.src} alt="" />
<AvatarFallback>{user.initials}</AvatarFallback>
</Avatar>
<span
className={cn(
"ring-background absolute right-0 bottom-0.5 size-2 rounded-full ring-2",
availabilityColor[user.availability]
)}
aria-hidden
/>
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="text-foreground line-clamp-1 text-sm font-medium">
{user.name}
</span>
<Badge variant={accessBadgeVariant[user.access]} size="sm">
{user.access}
</Badge>
</div>
<div
className="text-muted-foreground line-clamp-1 text-sm"
title={user.email}
>
{user.email}
</div>
</div>
</div>
)
})
function ActionsCell({ row }: { row: Row<TeamMember> }) {
const { copyToClipboard } = useCopyToClipboard()
const [removeOpen, setRemoveOpen] = useState(false)
const handleCopyId = () => {
copyToClipboard(row.original.id)
toast.success("Member ID copied", { description: row.original.id })
}
const handleRemoveConfirm = () => {
setRemoveOpen(false)
toast.message("Removal requested", {
description: `${row.original.name}. Wire this to your user API.`,
})
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
size="icon-sm"
variant="ghost"
className="text-muted-foreground"
aria-label={`Open ${row.original.name} actions`}
/>
}
>
<MoreHorizontalIcon aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-44">
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() =>
toast.info("Open member", {
description:
"Show the profile and recent workspace activity.",
})
}
>
<UserIcon aria-hidden="true" />
View member
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
toast.info("Change role", {
description: "Open the role and seat management flow.",
})
}
>
<ShieldCheckIcon aria-hidden="true" />
Change role
</DropdownMenuItem>
{row.original.status === "Invited" ? (
<DropdownMenuItem
onClick={() =>
toast.message("Invite resent", {
description: `A fresh invite was sent to ${row.original.email}.`,
})
}
>
<MailIcon aria-hidden="true" />
Resend invite
</DropdownMenuItem>
) : (
<DropdownMenuItem
onClick={() =>
toast.message("Session reset", {
description: `Require ${row.original.name} to sign in again.`,
})
}
>
<RefreshCwIcon aria-hidden="true" />
Reset session
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={handleCopyId}>
<CopyIcon aria-hidden="true" />
Copy ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={() => setRemoveOpen(true)}
>
<UserRoundXIcon aria-hidden="true" />
Remove member
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<AlertDialog open={removeOpen} onOpenChange={setRemoveOpen}>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogTitle>Remove member?</AlertDialogTitle>
<AlertDialogDescription>
This removes{" "}
<span className="text-foreground font-medium">
{row.original.name}
</span>{" "}
from the workspace. Connect your user API to persist changes.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
onClick={handleRemoveConfirm}
>
Remove
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}
export const teamColumns: ColumnDef<TeamMember>[] = [
{
accessorKey: "name",
id: "name",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <UserCell row={row} />,
size: 300,
enableSorting: true,
enableHiding: false,
enableResizing: true,
meta: {
headerTitle: "Member",
},
},
{
accessorKey: "role",
id: "role",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => (
<div className="flex min-w-0 flex-col gap-0.5">
<span className="text-foreground line-clamp-1 text-sm font-medium">
{row.original.role}
</span>
<span className="text-muted-foreground line-clamp-1 text-sm">
{row.original.department}
</span>
</div>
),
size: 180,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Role",
},
},
{
accessorKey: "status",
id: "status",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <TeamStatusBadge status={row.original.status} />,
size: 120,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Status",
},
},
{
id: "actions",
header: "",
cell: ({ row }) => <ActionsCell row={row} />,
size: 50,
enableSorting: false,
enableHiding: false,
enableResizing: false,
},
]
@@ -1,335 +0,0 @@
"use client"
import { useMemo, useState } from "react"
import { Badge } from "@/components/reui/badge"
import { DataGrid } from "@/components/reui/data-grid/data-grid"
import { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"
import {
Frame,
FrameDescription,
FrameFooter,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/reui/frame"
import {
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
type PaginationState,
type SortingState,
} from "@tanstack/react-table"
import { toast } from "sonner"
import { Button } from "@cfdm/ui/components/button"
import { Checkbox } from "@cfdm/ui/components/checkbox"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@cfdm/ui/components/input-group"
import { Label } from "@cfdm/ui/components/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@cfdm/ui/components/popover"
import { Separator } from "@cfdm/ui/components/separator"
import {
TEAM_MEMBERS,
TEAM_STATUS_ORDER,
type TeamMember,
type TeamMemberStatus,
} from "./data"
import { teamColumns, TeamStatusBadge } from "./team-columns"
import { SearchIcon, XIcon, FilterIcon, UserPlusIcon } from "lucide-react"
const EMPTY_TEAM_STATUS_COUNTS: Record<TeamMemberStatus, number> = {
Active: 0,
Invited: 0,
Limited: 0,
Suspended: 0,
}
function teamSearchBlob(user: TeamMember): string {
const parts = [
user.id,
user.name,
user.email,
user.role,
user.department,
user.access,
user.status,
user.location,
user.timezone,
user.joined,
user.lastActive,
]
return parts.filter(Boolean).join(" ").toLowerCase()
}
function getTeamStatusCounts(
members: TeamMember[]
): Record<TeamMemberStatus, number> {
const counts = { ...EMPTY_TEAM_STATUS_COUNTS }
for (const member of members) {
counts[member.status] += 1
}
return counts
}
type ToolbarProps = {
searchQuery: string
onSearchChange: (value: string) => void
selectedStatuses: TeamMemberStatus[]
onStatusChange: (checked: boolean, status: TeamMemberStatus) => void
onClearFilters: () => void
hasActiveFilters: boolean
statusCounts: Record<TeamMemberStatus, number>
}
function Toolbar({
searchQuery,
onSearchChange,
selectedStatuses,
onStatusChange,
onClearFilters,
hasActiveFilters,
statusCounts,
}: ToolbarProps) {
return (
<div className="flex flex-wrap items-center gap-2">
{/* List */}
<InputGroup className="w-full sm:w-60">
<InputGroupAddon align="inline-start">
<SearchIcon aria-hidden="true" />
</InputGroupAddon>
<InputGroupInput
placeholder="Search team..."
aria-label="Search team"
value={searchQuery}
onChange={(event) => onSearchChange(event.target.value)}
/>
{searchQuery.length > 0 ? (
<InputGroupAddon align="inline-end">
<InputGroupButton
aria-label="Clear search"
size="icon-xs"
onClick={() => onSearchChange("")}
>
<XIcon aria-hidden="true" />
</InputGroupButton>
</InputGroupAddon>
) : null}
</InputGroup>
<Popover>
<PopoverTrigger
render={
<Button
type="button"
variant="outline"
aria-label="Filter by member status"
>
<FilterIcon aria-hidden="true" />
Status
{selectedStatuses.length > 0 ? (
<Badge size="sm" variant="info-light">
{selectedStatuses.length}
</Badge>
) : null}
</Button>
}
/>
<PopoverContent
align="start"
className="flex w-44 flex-col gap-2.5 p-3"
>
<span className="text-muted-foreground text-xs font-medium">
Filter by status
</span>
{TEAM_STATUS_ORDER.map((status) => (
<div key={status} className="flex items-center gap-2.5">
<Checkbox
id={`team-status-${status.toLowerCase()}`}
checked={selectedStatuses.includes(status)}
onCheckedChange={(checked) =>
onStatusChange(checked === true, status)
}
/>
<Label
htmlFor={`team-status-${status.toLowerCase()}`}
className="flex min-w-0 flex-1 cursor-pointer items-center justify-between gap-2 font-normal"
>
<TeamStatusBadge status={status} />
<span className="text-muted-foreground shrink-0 text-xs tabular-nums">
{statusCounts[status] ?? 0}
</span>
</Label>
</div>
))}
</PopoverContent>
</Popover>
{hasActiveFilters ? (
<Button
type="button"
variant="ghost"
size="sm"
className="text-muted-foreground"
onClick={onClearFilters}
>
Clear filters
</Button>
) : null}
</div>
)
}
export function TeamDataGridView() {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
})
const [sorting, setSorting] = useState<SortingState>([
{ id: "name", desc: false },
])
const [searchQuery, setSearchQuery] = useState("")
const [selectedStatuses, setSelectedStatuses] = useState<TeamMemberStatus[]>(
[]
)
const statusCounts = useMemo(() => getTeamStatusCounts(TEAM_MEMBERS), [])
const filteredData = useMemo(() => {
const normalizedSearchQuery = searchQuery.trim().toLowerCase()
return TEAM_MEMBERS.filter((user) => {
const matchesStatus =
!selectedStatuses.length || selectedStatuses.includes(user.status)
const matchesSearch =
normalizedSearchQuery.length === 0 ||
teamSearchBlob(user).includes(normalizedSearchQuery)
return matchesStatus && matchesSearch
})
}, [searchQuery, selectedStatuses])
const hasActiveFilters =
searchQuery.trim().length > 0 || selectedStatuses.length > 0
const handleStatusChange = (checked: boolean, status: TeamMemberStatus) => {
setSelectedStatuses((current) =>
checked ? [...current, status] : current.filter((item) => item !== status)
)
setPagination((current) => ({
...current,
pageIndex: 0,
}))
}
const handleClearFilters = () => {
setSelectedStatuses([])
setSearchQuery("")
setPagination((current) => ({
...current,
pageIndex: 0,
}))
}
const handleSearchChange = (value: string) => {
setSearchQuery(value)
setPagination((current) => ({
...current,
pageIndex: 0,
}))
}
const table = useReactTable({
columns: teamColumns,
data: filteredData,
pageCount: Math.ceil(filteredData.length / pagination.pageSize),
getRowId: (row) => row.id,
state: {
pagination,
sorting,
},
columnResizeMode: "onChange",
onPaginationChange: setPagination,
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
})
return (
<DataGrid
table={table}
recordCount={filteredData.length}
emptyMessage={
filteredData.length === 0 ? "No members match your filters." : undefined
}
tableLayout={{
columnsPinnable: false,
columnsResizable: false,
columnsMovable: false,
columnsVisibility: false,
headerSticky: false,
dense: true,
}}
>
<Frame variant="default" spacing="sm" className="w-full">
<FrameHeader className="flex-row items-center justify-between gap-3">
<div className="flex min-w-0 flex-col gap-0.5">
<FrameTitle id="team-heading" className="text-balance capitalize">
Workspace Team
</FrameTitle>
<FrameDescription className="text-sm">
Manage members and access.
</FrameDescription>
</div>
<Button
type="button"
onClick={() =>
toast.info("Invite member", {
description: "Connect your invite or directory flow.",
})
}
>
<UserPlusIcon aria-hidden="true" />
Invite member
</Button>
</FrameHeader>
<FramePanel className="bg-card p-0! shadow-none!">
<div className="px-4 py-3">
<Toolbar
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
selectedStatuses={selectedStatuses}
onStatusChange={handleStatusChange}
onClearFilters={handleClearFilters}
hasActiveFilters={hasActiveFilters}
statusCounts={statusCounts}
/>
</div>
<Separator />
<DataGridScrollArea>
<DataGridTable />
</DataGridScrollArea>
</FramePanel>
<FrameFooter>
<DataGridPagination />
</FrameFooter>
</Frame>
</DataGrid>
)
}
@@ -1,5 +0,0 @@
import { TeamDataGridView } from "./team-data-grid"
export function TeamTabContent() {
return <TeamDataGridView />
}
@@ -1,156 +0,0 @@
import {
Frame,
FrameDescription,
FrameFooter,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/reui/frame"
import { Button } from "@cfdm/ui/components/button"
import { FieldGroup } from "@cfdm/ui/components/field"
import { Input } from "@cfdm/ui/components/input"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@cfdm/ui/components/input-group"
import { Switch } from "@cfdm/ui/components/switch"
import { BrandAccentPicker } from "./brand-accent-picker"
import {
LANDING_VIEW_OPTIONS,
WORKSPACE_IDENTITY,
WORKSPACE_SIGNALS,
} from "./data"
import { ImageUploadField } from "./image-upload-field"
import { ProfileAdvancedSelectField } from "./profile-form-fields"
import { ProfileSummaryFrame } from "./profile-summary-frames"
import { SettingRow } from "./setting-row"
export function WorkspaceTabContent() {
return (
<div className="space-y-4">
<Frame spacing="sm">
<FrameHeader>
<FrameTitle className="capitalize">Workspace details</FrameTitle>
<FrameDescription>Brand and contact info.</FrameDescription>
</FrameHeader>
<FramePanel className="p-0!">
<FieldGroup className="gap-0">
<SettingRow
title="Workspace logo"
description="Used in navigation and shared links."
labelFor="workspace-logo"
>
<ImageUploadField
ariaLabel="Upload workspace logo"
inputId="workspace-logo"
defaultImage="https://images.unsplash.com/photo-1557683316-973673baf926?w=96&h=96&fit=crop&dpr=2&q=80"
alt={WORKSPACE_IDENTITY.name}
/>
</SettingRow>
<SettingRow title="Workspace name" labelFor="workspace-name">
<Input
id="workspace-name"
defaultValue={WORKSPACE_IDENTITY.name}
/>
</SettingRow>
<SettingRow
title="Workspace URL"
description="Link for teammates and clients."
labelFor="workspace-subdomain"
>
<InputGroup className="w-full">
<InputGroupInput
id="workspace-subdomain"
defaultValue={WORKSPACE_IDENTITY.subdomain}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>
{WORKSPACE_IDENTITY.domainSuffix}
</InputGroupText>
</InputGroupAddon>
</InputGroup>
</SettingRow>
<SettingRow
title="Support email"
description="Shown on help surfaces."
labelFor="workspace-support-email"
>
<Input
id="workspace-support-email"
defaultValue={WORKSPACE_IDENTITY.supportEmail}
/>
</SettingRow>
<SettingRow
title="Accent color"
description="Used for highlights and links. Select one accent."
compact
last
>
<BrandAccentPicker />
</SettingRow>
</FieldGroup>
</FramePanel>
<FrameFooter className="flex-row justify-end gap-2">
<Button type="button" variant="outline">
Preview brand
</Button>
<Button type="button">Save workspace</Button>
</FrameFooter>
</Frame>
<Frame spacing="sm">
<FrameHeader>
<FrameTitle className="capitalize">Workspace preferences</FrameTitle>
<FrameDescription>Sharing and automation defaults.</FrameDescription>
</FrameHeader>
<FramePanel className="p-0!">
<FieldGroup className="gap-0">
<SettingRow title="Default landing view" compact>
<ProfileAdvancedSelectField
id="workspace-landing-view"
options={LANDING_VIEW_OPTIONS}
defaultValue={LANDING_VIEW_OPTIONS[1]}
/>
</SettingRow>
<SettingRow
title="External sharing"
description="Create client-safe links."
labelFor="workspace-external-sharing"
compact
>
<Switch id="workspace-external-sharing" defaultChecked />
</SettingRow>
<SettingRow
title="AI project recaps"
description="Short recaps as work changes."
labelFor="workspace-ai-recaps"
compact
last
>
<Switch id="workspace-ai-recaps" defaultChecked />
</SettingRow>
</FieldGroup>
</FramePanel>
</Frame>
<ProfileSummaryFrame
title="Workspace summary"
description="Brand, routing, and region."
items={WORKSPACE_SIGNALS}
/>
</div>
)
}
@@ -1,9 +0,0 @@
import { Profile } from "./components/profile"
export function Page() {
return (
<div className="flex min-h-svh w-full items-start justify-center p-4 sm:p-8 md:p-12">
<Profile />
</div>
)
}
@@ -1,147 +0,0 @@
import type { ReactNode } from "react"
import type { BadgeProps } from "@/components/reui/badge"
import { ClaudeAiIcon } from "@cfdm/ui/components/svgs/claudeAiIcon"
import { CursorDark } from "@cfdm/ui/components/svgs/cursorDark"
import { CursorLight } from "@cfdm/ui/components/svgs/cursorLight"
import { GoogleCalendar } from "@cfdm/ui/components/svgs/googleCalendar"
import { ResendIconBlack } from "@cfdm/ui/components/svgs/resendIconBlack"
import { ResendIconWhite } from "@cfdm/ui/components/svgs/resendIconWhite"
import { Slack } from "@cfdm/ui/components/svgs/slack"
import { Stripe } from "@cfdm/ui/components/svgs/stripe"
import { Supabase } from "@cfdm/ui/components/svgs/supabase"
export type IntegrationStatus = "connected" | "available" | "preview"
export interface IntegrationStatusMeta {
label: string
variant: BadgeProps["variant"]
}
export interface IntegrationApp {
id: string
name: string
description: string
logo: ReactNode
status: IntegrationStatus
actionLabel: string
}
export const INTEGRATION_STATUS_META: Record<
IntegrationStatus,
IntegrationStatusMeta
> = {
connected: {
label: "Connected",
variant: "success-light",
},
available: {
label: "Available",
variant: "secondary",
},
preview: {
label: "Preview",
variant: "info-light",
},
}
const logoClassName = "block size-5"
function ThemeLogo({ light, dark }: { light: ReactNode; dark: ReactNode }) {
return (
<>
<span
aria-hidden="true"
className="flex size-5 items-center justify-center leading-none dark:hidden"
>
{light}
</span>
<span
aria-hidden="true"
className="hidden size-5 items-center justify-center leading-none dark:flex"
>
{dark}
</span>
</>
)
}
const CURSOR_LOGO = (
<ThemeLogo
light={<CursorLight className={logoClassName} />}
dark={<CursorDark className={logoClassName} />}
/>
)
const RESEND_LOGO = (
<ThemeLogo
light={<ResendIconBlack className={logoClassName} />}
dark={<ResendIconWhite className={logoClassName} />}
/>
)
export const INTEGRATION_APPS: IntegrationApp[] = [
{
id: "supabase",
name: "Supabase",
description:
"Sync workspace records, owners, and lifecycle events from your product database.",
logo: <Supabase className={logoClassName} aria-hidden="true" />,
status: "connected",
actionLabel: "Configure",
},
{
id: "slack",
name: "Slack",
description:
"Route approvals, launch notes, and incident updates into selected team channels.",
logo: <Slack className={logoClassName} aria-hidden="true" />,
status: "connected",
actionLabel: "Configure",
},
{
id: "stripe",
name: "Stripe",
description:
"Mirror subscription, invoice, and renewal signals into account reviews.",
logo: <Stripe className={logoClassName} aria-hidden="true" />,
status: "connected",
actionLabel: "Configure",
},
{
id: "google-calendar",
name: "Google Calendar",
description:
"Create planning reviews from milestones, customer dates, and release windows.",
logo: <GoogleCalendar className={logoClassName} aria-hidden="true" />,
status: "available",
actionLabel: "Install",
},
{
id: "resend",
name: "Resend",
description:
"Send branded transactional updates when workflows change status or ownership.",
logo: RESEND_LOGO,
status: "available",
actionLabel: "Install",
},
{
id: "cursor",
name: "Cursor",
description:
"Attach implementation context and AI coding notes to active product tasks.",
logo: CURSOR_LOGO,
status: "preview",
actionLabel: "Configure",
},
{
id: "claude",
name: "Claude",
description:
"Draft summaries, support notes, and rollout briefs from approved workspace data.",
logo: <ClaudeAiIcon className={logoClassName} aria-hidden="true" />,
status: "preview",
actionLabel: "Install",
},
]
@@ -1,109 +0,0 @@
import { Badge } from "@/components/reui/badge"
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/reui/frame"
import { Button } from "@cfdm/ui/components/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemSeparator,
ItemTitle,
} from "@cfdm/ui/components/item"
import {
INTEGRATION_APPS,
INTEGRATION_STATUS_META,
type IntegrationApp,
} from "./data"
function IntegrationLogo({ app }: { app: IntegrationApp }) {
return (
<Item className="bg-muted/60 border-background flex size-10 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:block [&_svg]:size-5">
<ItemMedia variant="icon" className="size-auto translate-y-0! self-center!">
{app.logo}
</ItemMedia>
</Item>
)
}
function IntegrationRow({ app }: { app: IntegrationApp }) {
const status = INTEGRATION_STATUS_META[app.status]
return (
<Item role="listitem" className="items-center px-3.5 py-2.5 sm:px-4">
{/* Media */}
<ItemMedia className="translate-y-0! self-center!">
<IntegrationLogo app={app} />
</ItemMedia>
{/* Content */}
<ItemContent className="min-w-0 gap-0">
<ItemTitle className="w-full min-w-0 gap-2">
<span className="truncate">{app.name}</span>
<Badge variant={status.variant}>{status.label}</Badge>
</ItemTitle>
<ItemDescription className="line-clamp-1">
{app.description}
</ItemDescription>
</ItemContent>
{/* Actions */}
<ItemActions className="ml-auto shrink-0 justify-end gap-2">
<Button type="button" variant="outline" size="sm">
{app.actionLabel}
</Button>
</ItemActions>
</Item>
)
}
export function IntegrationSettings() {
return (
<div className="flex w-full max-w-4xl flex-col gap-5">
{/* Section */}
<section className="flex flex-col gap-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div className="flex flex-col gap-1">
<h3 className="text-sm font-semibold">Connected Apps</h3>
<p className="text-muted-foreground text-sm">
Manage source systems for automation, scheduling, support, and AI
workflows.
</p>
</div>
<Button type="button" size="sm" className="w-full sm:w-auto">
Build Connector
</Button>
</div>
<Frame>
<FrameHeader className="sr-only">
<FrameTitle>Integration Apps</FrameTitle>
<FrameDescription>
Available workspace apps and configuration actions.
</FrameDescription>
</FrameHeader>
<FramePanel className="p-0!">
<ItemGroup className="gap-0">
{INTEGRATION_APPS.map((app, index) => (
<div key={app.id}>
{index > 0 ? <ItemSeparator className="my-0" /> : null}
<IntegrationRow app={app} />
</div>
))}
</ItemGroup>
</FramePanel>
</Frame>
</section>
</div>
)
}
@@ -1,15 +0,0 @@
import { IntegrationSettings } from "./components/integration-settings"
export function Page() {
return (
<main
className="flex min-h-svh w-full items-start justify-center p-4 sm:p-8 md:p-12"
aria-labelledby="page-heading"
>
<h1 id="page-heading" className="sr-only">
Integration settings
</h1>
<IntegrationSettings />
</main>
)
}
@@ -1,243 +0,0 @@
import { type ReactNode } from "react"
import type { BadgeProps } from "@/components/reui/badge"
import { CreditCardIcon, UsersIcon, RepeatIcon, BanknoteIcon, ShieldAlertIcon } from "lucide-react"
// ── Types ──
export type EndpointStatus = "active" | "failing" | "disabled"
export type EndpointAlertTone = "success" | "warning" | "critical"
export interface EndpointAlert {
id: string
tone: EndpointAlertTone
badgeLabel: string
detail: string
}
export interface WebhookEvent {
id: string
label: string
}
export interface WebhookEndpoint {
id: string
name: string
url: string
description: string
events: WebhookEvent[]
status: EndpointStatus
secret: string
lastDelivery: string | null
secretRotation: string
owner: string
icon: ReactNode
}
export type WebhookEndpointActionHandlers = {
onManage: (endpoint: WebhookEndpoint) => void
onViewDeliveries: (endpoint: WebhookEndpoint) => void
onRotateSecret: (endpoint: WebhookEndpoint) => void
onRemove: (endpoint: WebhookEndpoint) => void
}
// ── Config ──
export const STATUS_CONFIG: Record<
EndpointStatus,
{ label: string; variant: BadgeProps["variant"] }
> = {
active: { label: "Delivering", variant: "success-light" },
failing: { label: "Retrying", variant: "warning-light" },
disabled: { label: "Paused", variant: "outline" },
}
export const ALERT_CONFIG: Record<
EndpointAlertTone,
{
toneClassName: string
badgeVariant: BadgeProps["variant"]
}
> = {
success: {
toneClassName: "text-green-600",
badgeVariant: "success",
},
warning: {
toneClassName: "text-warning",
badgeVariant: "warning",
},
critical: {
toneClassName: "text-destructive",
badgeVariant: "destructive",
},
}
// ── Data ──
export const ENDPOINTS: WebhookEndpoint[] = [
{
id: "endpoint-billing",
name: "Billing Pipeline",
url: "https://events.acme.dev/webhooks/billing",
description: "Invoices and disputes.",
events: [
{ id: "invoice.paid", label: "invoice.paid" },
{ id: "invoice.failed", label: "invoice.failed" },
{ id: "charge.refunded", label: "charge.refunded" },
],
status: "active",
secret: "whsec_1h7qv4r9m2x6k8p3",
lastDelivery: "2m ago",
secretRotation: "12d ago",
owner: "Revenue",
icon: (
<CreditCardIcon aria-hidden="true" />
),
},
{
id: "endpoint-customers",
name: "Customer Ledger",
url: "https://ingest.acme.dev/webhooks/customers",
description: "Customer lifecycle events.",
events: [
{ id: "customer.created", label: "customer.created" },
{ id: "customer.updated", label: "customer.updated" },
],
status: "active",
secret: "whsec_8m1p6q4r2v7x3k9n",
lastDelivery: "14m ago",
secretRotation: "28d ago",
owner: "Data",
icon: (
<UsersIcon aria-hidden="true" />
),
},
{
id: "endpoint-subscriptions",
name: "Subscription Orchestrator",
url: "https://ops.acme.dev/webhooks/subscriptions",
description: "Retries and plan changes.",
events: [
{ id: "subscription.activated", label: "subscription.activated" },
{ id: "subscription.cancelled", label: "subscription.cancelled" },
{ id: "invoice.failed", label: "invoice.failed" },
],
status: "failing",
secret: "whsec_4x8m1r6q3p9k2v7n",
lastDelivery: "9m ago",
secretRotation: "5d left",
owner: "Lifecycle",
icon: (
<RepeatIcon aria-hidden="true" />
),
},
{
id: "endpoint-finance",
name: "Finance Reconciliation",
url: "https://ledger.acme.dev/webhooks/payouts",
description: "Payout sync to ledger.",
events: [
{ id: "payout.completed", label: "payout.completed" },
{ id: "balance.updated", label: "balance.updated" },
],
status: "disabled",
secret: "whsec_7v3q9m2k6r1p4x8n",
lastDelivery: null,
secretRotation: "Paused",
owner: "Finance",
icon: (
<BanknoteIcon aria-hidden="true" />
),
},
{
id: "endpoint-risk",
name: "Risk Intake",
url: "https://risk.acme.dev/webhooks/disputes",
description: "Disputes and refund review.",
events: [
{ id: "dispute.opened", label: "dispute.opened" },
{ id: "charge.refunded", label: "charge.refunded" },
],
status: "active",
secret: "whsec_5q9r2m7x1k4v8p3n",
lastDelivery: "47m ago",
secretRotation: "9d ago",
owner: "Risk",
icon: (
<ShieldAlertIcon aria-hidden="true" />
),
},
]
// ── Helpers ──
export function parseMinutesAgo(label: string | null) {
const match = label?.match(/^(\d+)m ago$/)
return match ? Number(match[1]) : null
}
export function parseRotationDaysAgo(label: string) {
const match = label.match(/^(\d+)d ago$/)
return match ? Number(match[1]) : null
}
export function getEndpointAlerts(endpoint: WebhookEndpoint): EndpointAlert[] {
const alerts: EndpointAlert[] = []
const deliveryMinutes = parseMinutesAgo(endpoint.lastDelivery)
const rotationDaysAgo = parseRotationDaysAgo(endpoint.secretRotation)
if (endpoint.status === "failing") {
alerts.push({
id: "delivery-failure",
tone: "critical",
badgeLabel: "Critical",
detail: `${endpoint.name} has deliveries waiting for retry review.`,
})
} else if (endpoint.status === "disabled") {
alerts.push({
id: "delivery-paused",
tone: "warning",
badgeLabel: "Warning",
detail: `${endpoint.name} is paused and not receiving new events.`,
})
}
if (deliveryMinutes !== null && deliveryMinutes >= 30) {
alerts.push({
id: "delivery-lag",
tone: "warning",
badgeLabel: "Warning",
detail: `Last delivery landed ${endpoint.lastDelivery}. Check if that delay is expected.`,
})
}
if (endpoint.secretRotation === "5d left") {
alerts.push({
id: "rotation-due",
tone: "warning",
badgeLabel: "Warning",
detail: "Signing secret rotation is due within 5 days.",
})
} else if (rotationDaysAgo !== null && rotationDaysAgo >= 21) {
alerts.push({
id: "rotation-stale",
tone: "warning",
badgeLabel: "Warning",
detail: `Signing secret was rotated ${endpoint.secretRotation}. Consider refreshing it soon.`,
})
}
if (alerts.length === 0) {
alerts.push({
id: "healthy",
tone: "success",
badgeLabel: "Healthy",
detail: `${endpoint.name} is delivering subscribed events normally.`,
})
}
return alerts
}
@@ -1,72 +0,0 @@
import { Button } from "@cfdm/ui/components/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@cfdm/ui/components/dropdown-menu"
import type { WebhookEndpoint, WebhookEndpointActionHandlers } from "./data"
import { EllipsisVerticalIcon, Settings2Icon, HistoryIcon, RefreshCwIcon, Trash2Icon } from "lucide-react"
type EndpointActionsMenuProps = WebhookEndpointActionHandlers & {
endpoint: WebhookEndpoint
}
export function EndpointActionsMenu({
endpoint,
onManage,
onViewDeliveries,
onRotateSecret,
onRemove,
}: EndpointActionsMenuProps) {
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label={`Open actions for ${endpoint.name}`}
>
<EllipsisVerticalIcon aria-hidden="true" />
</Button>
}
/>
{/* Content */}
<DropdownMenuContent align="end" className="min-w-44">
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => onManage(endpoint)}>
<Settings2Icon aria-hidden="true" />
{endpoint.status === "failing"
? "Review endpoint"
: "Manage endpoint"}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onViewDeliveries(endpoint)}>
<HistoryIcon aria-hidden="true" />
View deliveries
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onRotateSecret(endpoint)}>
<RefreshCwIcon aria-hidden="true" />
Rotate secret
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={() => onRemove(endpoint)}
>
<Trash2Icon aria-hidden="true" />
Remove endpoint
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -1,49 +0,0 @@
import { Badge } from "@/components/reui/badge"
import { cn } from "@cfdm/ui/lib/utils"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@cfdm/ui/components/tooltip"
import { ALERT_CONFIG, type EndpointAlert } from "./data"
import { CircleCheckIcon, CircleXIcon, TriangleAlertIcon } from "lucide-react"
// ── Endpoint Alert Indicator ──
export function EndpointAlertIndicator({ alert }: { alert: EndpointAlert }) {
const config = ALERT_CONFIG[alert.tone]
return (
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
className={cn(
"focus-visible:ring-ring focus-visible:ring-offset-background inline-flex rounded-sm p-0.5 focus-visible:ring-2 focus-visible:ring-offset-2",
config.toneClassName
)}
aria-label={`${alert.badgeLabel}. ${alert.detail}`}
/>
}
>
{alert.tone === "success" ? (
<CircleCheckIcon className="size-4 shrink-0" aria-hidden="true" />
) : alert.tone === "critical" ? (
<CircleXIcon className="size-4 shrink-0" aria-hidden="true" />
) : (
<TriangleAlertIcon className="size-4 shrink-0" aria-hidden="true" />
)}
</TooltipTrigger>
{/* Content */}
<TooltipContent side="top" className="max-w-xs p-3">
<div className="flex items-center gap-2">
<Badge variant={config.badgeVariant}>{alert.badgeLabel}</Badge>
<p>{alert.detail}</p>
</div>
</TooltipContent>
</Tooltip>
)
}
@@ -1,83 +0,0 @@
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@cfdm/ui/components/item"
import { Switch } from "@cfdm/ui/components/switch"
import {
getEndpointAlerts,
type WebhookEndpoint,
type WebhookEndpointActionHandlers,
} from "./data"
import { EndpointActionsMenu } from "./endpoint-actions-menu"
import { EndpointAlertIndicator } from "./endpoint-alert-indicator"
import { EndpointUrlCopy } from "./endpoint-url-copy"
import { StatusIndicator } from "./status-indicator"
type EndpointRowProps = WebhookEndpointActionHandlers & {
endpoint: WebhookEndpoint
onToggle: (id: string, enabled: boolean) => void
}
// ── Endpoint Row ──
export function EndpointRow({
endpoint,
onToggle,
onManage,
onViewDeliveries,
onRotateSecret,
onRemove,
}: EndpointRowProps) {
const isEnabled = endpoint.status !== "disabled"
const alerts = getEndpointAlerts(endpoint)
return (
<Item variant="outline" className="border-x-0 border-t-0 last:border-b-0">
{/* Media */}
<ItemMedia variant="icon" className="translate-y-0! self-center!">
<Item className="border-border flex size-10 items-center justify-center border p-0 [&_svg]:opacity-60">
{endpoint.icon}
</Item>
</ItemMedia>
{/* Content */}
<ItemContent className="min-w-0 gap-1">
<ItemTitle className="min-w-0 gap-2">
<span className="min-w-0 truncate">{endpoint.name}</span>
<span className="flex shrink-0 items-center gap-1">
{alerts.map((alert) => (
<EndpointAlertIndicator key={alert.id} alert={alert} />
))}
</span>
<StatusIndicator status={endpoint.status} />
</ItemTitle>
<ItemDescription className="min-w-0">
<EndpointUrlCopy endpointName={endpoint.name} url={endpoint.url} />
</ItemDescription>
</ItemContent>
{/* Actions */}
<ItemActions className="gap-2 self-start sm:self-center">
<Switch
checked={isEnabled}
onCheckedChange={(checked) => onToggle(endpoint.id, checked)}
aria-label={`Toggle ${endpoint.url}`}
/>
<EndpointActionsMenu
endpoint={endpoint}
onManage={onManage}
onViewDeliveries={onViewDeliveries}
onRotateSecret={onRotateSecret}
onRemove={onRemove}
/>
</ItemActions>
</Item>
)
}
@@ -1,46 +0,0 @@
import { toast } from "sonner"
import { cn } from "@cfdm/ui/lib/utils"
import { Button } from "@cfdm/ui/components/button"
import { CheckIcon } from "lucide-react"
// ── Show Endpoint Toast ──
export function showEndpointToast({
title,
description,
variant = "info",
}: {
title: string
description: string
variant?: "info" | "success"
}) {
toast.custom((id) => (
<div className="bg-invert text-invert-foreground flex w-[356px] items-start gap-3 rounded-md border border-transparent p-4 shadow-lg">
<span
className={cn(
"flex h-5 shrink-0 items-center",
variant === "success" ? "text-green-500" : "text-info"
)}
>
<CheckIcon className="size-4" aria-hidden="true" />
</span>
<div className="flex flex-1 flex-col gap-1">
<p className="text-sm font-semibold">{title}</p>
<p className="text-invert-foreground/70 text-sm">{description}</p>
<div className="mt-2 flex gap-2">
<Button
size="xs"
variant="outline"
className="bg-background/10 border-border/10 text-invert-foreground"
onClick={() => toast.dismiss(id)}
>
Dismiss
</Button>
</div>
</div>
</div>
))
}
@@ -1,54 +0,0 @@
"use client"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
import { Button } from "@cfdm/ui/components/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@cfdm/ui/components/tooltip"
import { CopyIcon } from "lucide-react"
// ── Endpoint URL Copy ──
export function EndpointUrlCopy({
endpointName,
url,
}: {
endpointName: string
url: string
}) {
const { copyToClipboard, isCopied } = useCopyToClipboard()
return (
<span className="group/url inline-flex max-w-full items-center gap-1 align-top">
<code className="text-foreground/90 max-w-full min-w-0 truncate text-xs">
{url}
</code>
<Tooltip>
<TooltipTrigger
render={
<Button
type="button"
variant="ghost"
size="icon-xs"
className="text-muted-foreground shrink-0 opacity-100 transition-opacity duration-200 focus-visible:opacity-100 sm:opacity-0 sm:group-focus-within/url:opacity-100 sm:group-hover/url:opacity-100"
aria-label={
isCopied
? `Copied endpoint URL for ${endpointName}`
: `Copy endpoint URL for ${endpointName}`
}
onClick={() => copyToClipboard(url)}
/>
}
>
<CopyIcon aria-hidden="true" />
</TooltipTrigger>
<TooltipContent>{isCopied ? "Copied" : "Copy URL"}</TooltipContent>
</Tooltip>
</span>
)
}
@@ -1,11 +0,0 @@
import { Badge } from "@/components/reui/badge"
import { STATUS_CONFIG, type EndpointStatus } from "./data"
// ── Status Indicator ──
export function StatusIndicator({ status }: { status: EndpointStatus }) {
const config = STATUS_CONFIG[status]
return <Badge variant={config.variant}>{config.label}</Badge>
}
@@ -1,128 +0,0 @@
import { useState } from "react"
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/reui/frame"
import { Button } from "@cfdm/ui/components/button"
import { Separator } from "@cfdm/ui/components/separator"
import { ENDPOINTS, type WebhookEndpoint } from "./data"
import { EndpointRow } from "./endpoint-row"
import { showEndpointToast } from "./endpoint-toast"
import { PlusIcon } from "lucide-react"
export function WebhookEndpoints() {
const [endpoints, setEndpoints] = useState(ENDPOINTS)
const handleToggle = (id: string, enabled: boolean) => {
const endpoint = endpoints.find((item) => item.id === id)
setEndpoints((current) =>
current.map((item) =>
item.id === id
? {
...item,
status: enabled ? ("active" as const) : ("disabled" as const),
}
: item
)
)
if (!endpoint) return
showEndpointToast({
title: enabled ? "Endpoint enabled" : "Endpoint paused",
description: enabled
? `${endpoint.name} is live again.`
: `${endpoint.name} is no longer receiving events.`,
variant: enabled ? "success" : "info",
})
}
const handleManage = (endpoint: WebhookEndpoint) => {
showEndpointToast({
title:
endpoint.status === "failing" ? "Review delivery" : "Endpoint settings",
description:
endpoint.status === "failing"
? `Check retries for ${endpoint.name}.`
: `Open rules for ${endpoint.name}.`,
})
}
const handleViewDeliveries = (endpoint: WebhookEndpoint) => {
showEndpointToast({
title: "Delivery history",
description: endpoint.lastDelivery
? `${endpoint.name} delivered ${endpoint.lastDelivery}.`
: `${endpoint.name} has no recent deliveries.`,
})
}
const handleRotateSecret = (endpoint: WebhookEndpoint) => {
setEndpoints((current) =>
current.map((item) =>
item.id === endpoint.id ? { ...item, secretRotation: "Just now" } : item
)
)
showEndpointToast({
title: "Secret rotated",
description: `${endpoint.name} received a new signing secret.`,
variant: "success",
})
}
const handleRemove = (endpoint: WebhookEndpoint) => {
setEndpoints((current) => current.filter((item) => item.id !== endpoint.id))
showEndpointToast({
title: "Endpoint removed",
description: `${endpoint.name} was removed from delivery routes.`,
})
}
return (
<Frame className="w-full max-w-3xl">
{/* Header */}
<FrameHeader className="flex-row items-center justify-between gap-4 px-2! py-2.5!">
<div className="space-y-px">
<FrameTitle>Webhook Endpoints</FrameTitle>
<FrameDescription>Routes and status</FrameDescription>
</div>
<Button
onClick={() =>
showEndpointToast({
title: "Add endpoint",
description: "Add a destination URL and subscribed events.",
})
}
>
<PlusIcon aria-hidden="true" />
Add Endpoint
</Button>
</FrameHeader>
{/* Content */}
<FramePanel className="p-0!">
{endpoints.map((endpoint, index) => (
<div key={endpoint.id}>
{index > 0 ? <Separator /> : null}
<EndpointRow
endpoint={endpoint}
onToggle={handleToggle}
onManage={handleManage}
onViewDeliveries={handleViewDeliveries}
onRotateSecret={handleRotateSecret}
onRemove={handleRemove}
/>
</div>
))}
</FramePanel>
</Frame>
)
}
@@ -1,9 +0,0 @@
import { WebhookEndpoints } from "./components/webhook-endpoints"
export function Page() {
return (
<div className="flex min-h-svh w-full items-start justify-center p-4 sm:p-8 md:p-12">
<WebhookEndpoints />
</div>
)
}
@@ -1,47 +0,0 @@
"use client"
import { ReactNode } from "react"
import { Badge } from "@/components/reui/badge"
import { Headphones, CircleCheckIcon, SmileIcon } from "lucide-react"
// ── Types ──
export interface CardData {
icon: ReactNode
iconBg: string
value: string | number
label: string
info: ReactNode
}
// ── Data ──
export const cards: CardData[] = [
{
icon: (
<Headphones aria-hidden="true" />
),
iconBg: "text-blue-600 dark:text-blue-400",
value: 320,
label: "Support Tickets",
info: <Badge variant="info-light">12 Open, 308 Closed</Badge>,
},
{
icon: (
<CircleCheckIcon aria-hidden="true" />
),
iconBg: "text-emerald-600 dark:text-emerald-400",
value: "98%",
label: "Resolved",
info: <Badge variant="success-light">+2.1% this month</Badge>,
},
{
icon: (
<SmileIcon aria-hidden="true" />
),
iconBg: "text-amber-600 dark:text-amber-400",
value: "4.8",
label: "Satisfaction Rate",
info: <Badge variant="warning-light">Avg. (out of 5)</Badge>,
},
]
@@ -1,43 +0,0 @@
import { Frame, FramePanel } from "@/components/reui/frame"
import { cn } from "@cfdm/ui/lib/utils"
import { Item, ItemMedia } from "@cfdm/ui/components/item"
import { cards } from "./data"
export function Stats() {
return (
<div className="@container w-full grow">
{/* Grid */}
<div className="mx-auto grid max-w-5xl grow grid-cols-1 gap-5 @3xl:grid-cols-3">
{cards.map((card, i) => (
<Frame key={i}>
<FramePanel className="flex flex-col items-start gap-6">
<Item
className={cn(
"border-background bg-muted flex size-10.5 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4",
card.iconBg
)}
>
<ItemMedia variant="icon" className="size-auto">
{card.icon}
</ItemMedia>
</Item>
<div className="space-y-0.5">
<div className="text-foreground text-2xl leading-none font-bold">
{card.value}
</div>
<div className="text-muted-foreground text-sm font-medium">
{card.label}
</div>
</div>
{card.info}
</FramePanel>
</Frame>
))}
</div>
</div>
)
}
@@ -1,9 +0,0 @@
import { Stats } from "./components/stats"
export function Page() {
return (
<div className="flex w-full items-center justify-center p-10 md:p-20">
<Stats />
</div>
)
}
-58
View File
@@ -1,6 +1,4 @@
import type { ReactNode } from 'react'
import type { Control, FieldPath, FieldValues } from 'react-hook-form'
import { Controller } from 'react-hook-form'
import {
Field,
FieldDescription,
@@ -9,62 +7,6 @@ import {
} from '@cfdm/ui/components/field'
import { cn } from '@cfdm/ui/lib/utils'
interface FormFieldProps<T extends FieldValues> {
name: FieldPath<T>
control: Control<T>
label: string
htmlFor?: string
hint?: string
className?: string
children: (props: {
id: string
'aria-invalid': boolean
value: unknown
onChange: (...args: unknown[]) => void
onBlur: () => void
ref: React.Ref<unknown>
}) => ReactNode
}
export function FormField<T extends FieldValues>({
name,
control,
label,
htmlFor,
hint,
className,
children,
}: FormFieldProps<T>) {
const fieldId = htmlFor ?? String(name)
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<Field
data-invalid={!!fieldState.error}
className={cn(className)}
>
<FieldLabel htmlFor={fieldId}>{label}</FieldLabel>
{children({
id: fieldId,
'aria-invalid': !!fieldState.error,
value: field.value,
onChange: field.onChange,
onBlur: field.onBlur,
ref: field.ref,
})}
{hint && !fieldState.error ? (
<FieldDescription>{hint}</FieldDescription>
) : null}
<FieldError errors={[fieldState.error]} />
</Field>
)}
/>
)
}
interface FormFieldSimpleProps {
label: string
htmlFor: string
@@ -31,6 +31,7 @@ export interface HealthCheckConfig {
expected_status: number | null
interval_sec: number
timeout_ms: number
verify_tls: boolean
}
export interface LbAndHealthConfig extends HealthCheckConfig {
@@ -192,33 +193,50 @@ export function HealthCheckConfigFields({
</div>
{isHttp ? (
<div className="grid grid-cols-2 gap-3">
<FormFieldSimple
label="HTTP path"
htmlFor={`${idPrefix}-path`}
hint="По умолчанию /"
>
<AppInput
id={`${idPrefix}-path`}
placeholder="/"
value={value.path ?? ''}
onChange={(e) =>
patch({ path: e.target.value === '' ? null : e.target.value })
}
/>
</FormFieldSimple>
<>
<div className="grid grid-cols-2 gap-3">
<FormFieldSimple
label="HTTP path"
htmlFor={`${idPrefix}-path`}
hint="По умолчанию /"
>
<AppInput
id={`${idPrefix}-path`}
placeholder="/"
value={value.path ?? ''}
onChange={(e) =>
patch({ path: e.target.value === '' ? null : e.target.value })
}
/>
</FormFieldSimple>
<FormFieldSimple label="HTTP-статус" htmlFor={`${idPrefix}-status`}>
<CompactNumberField
id={`${idPrefix}-status`}
value={value.expected_status}
min={100}
max={599}
placeholder="200"
onValueChange={(expected_status) => patch({ expected_status })}
<FormFieldSimple label="HTTP-статус" htmlFor={`${idPrefix}-status`}>
<CompactNumberField
id={`${idPrefix}-status`}
value={value.expected_status}
min={100}
max={599}
placeholder="200"
onValueChange={(expected_status) => patch({ expected_status })}
/>
</FormFieldSimple>
</div>
<SettingRow
title="Проверять сертификат"
description="HTTPS (:443). Выключите для self-signed или IP-pin без валидной цепочки."
labelFor={`${idPrefix}-verify-tls`}
compact
last
className={rowClass}
>
<Switch
id={`${idPrefix}-verify-tls`}
checked={value.verify_tls}
onCheckedChange={(checked) => patch({ verify_tls: checked })}
/>
</FormFieldSimple>
</div>
</SettingRow>
</>
) : null}
<div className="grid grid-cols-2 gap-3">
@@ -1,120 +0,0 @@
import { useFieldArray, useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { PlusIcon, Trash2Icon } from 'lucide-react'
import { appSwitcherConfigSchema, type AppSwitcherConfig } from '@cfdm/shared'
import { Button } from '@cfdm/ui/components/button'
import { FieldGroup } from '@cfdm/ui/components/field'
import { Input } from '@cfdm/ui/components/input'
import { ItemSeparator } from '@cfdm/ui/components/item'
import { FormFieldSimple } from '@/components/form-field'
import { SelectField } from '@/components/select-field'
import { LoadingButton } from '@/components/loading-button'
import { APP_SWITCHER_ICONS, type AppSwitcherIconName } from '@/lib/app-switcher-config'
const ICON_OPTIONS = (Object.keys(APP_SWITCHER_ICONS) as AppSwitcherIconName[]).map((icon) => ({
value: icon,
label: icon,
}))
export type AppSwitcherFormValues = AppSwitcherConfig
interface AppSwitcherEditorProps {
defaultValues: AppSwitcherFormValues
onSave: (values: AppSwitcherFormValues) => void
isSaving?: boolean
}
export function AppSwitcherEditor({ defaultValues, onSave, isSaving }: AppSwitcherEditorProps) {
const form = useForm({
resolver: zodResolver(appSwitcherConfigSchema),
defaultValues,
})
const { fields, append, remove } = useFieldArray({ control: form.control, name: 'apps' })
return (
<form
className="flex flex-col gap-4"
onSubmit={(e) => void form.handleSubmit((values) => onSave(values))(e)}
>
<FieldGroup>
<FormFieldSimple label="Заголовок меню" htmlFor="menu-label">
<Input id="menu-label" {...form.register('menuLabel')} />
</FormFieldSimple>
<div className="flex flex-col gap-3">
{fields.map((field, index) => (
<div key={field.id}>
{index > 0 ? <ItemSeparator className="my-3" /> : null}
<div className="grid gap-3 sm:grid-cols-2">
<FormFieldSimple label="ID" htmlFor={`app-id-${index}`}>
<Input id={`app-id-${index}`} {...form.register(`apps.${index}.id`)} />
</FormFieldSimple>
<FormFieldSimple label="Название" htmlFor={`app-name-${index}`}>
<Input id={`app-name-${index}`} {...form.register(`apps.${index}.name`)} />
</FormFieldSimple>
<FormFieldSimple
label="URL"
htmlFor={`app-url-${index}`}
className="sm:col-span-2"
>
<Input id={`app-url-${index}`} {...form.register(`apps.${index}.url`)} />
</FormFieldSimple>
<FormFieldSimple label="Иконка" htmlFor={`app-icon-${index}`}>
<SelectField
triggerId={`app-icon-${index}`}
value={form.watch(`apps.${index}.icon`)}
onValueChange={(v: string | null) =>
form.setValue(`apps.${index}.icon`, (v ?? 'server') as AppSwitcherIconName, {
shouldDirty: true,
})
}
options={ICON_OPTIONS}
/>
</FormFieldSimple>
<div className="flex items-end justify-end">
<Button
type="button"
variant="outline"
size="icon"
disabled={fields.length <= 1}
onClick={() => remove(index)}
aria-label="Удалить приложение"
>
<Trash2Icon className="size-4" />
</Button>
</div>
</div>
</div>
))}
</div>
<Button
type="button"
variant="outline"
className="w-fit"
onClick={() =>
append({
id: `app-${fields.length + 1}`,
name: 'Приложение',
url: 'http://localhost:3000',
icon: 'server',
})
}
>
<PlusIcon data-icon="inline-start" />
Добавить приложение
</Button>
</FieldGroup>
<LoadingButton
type="submit"
className="w-fit"
isLoading={isSaving}
disabled={!form.formState.isDirty}
>
Сохранить приложения
</LoadingButton>
</form>
)
}

Some files were not shown because too many files have changed in this diff Show More