refactor(web): удалить мёртвый код (~5.9К строк)

- демо-блоки blocks/ (sheet-9, solution-users-1/6, settings-5, auth-18) —
  не импортировались; auth-logo перенесён в components/auth-logo.tsx
- варианты грида data-grid-table-dnd/-dnd-rows/-virtual и ui/svgs — не импортировались
- убраны исключения blocks из tsconfig и eslint
- AGENTS.md: добавить apps/api/db/shared в описание стека
This commit is contained in:
Denozordec
2026-09-23 14:51:25 +07:00
parent 15073f53d9
commit e107af8e43
39 changed files with 5 additions and 6717 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
## Стек
- Monorepo: `apps/web` + `packages/ui` (`@authportal/ui`)
- Monorepo: `apps/web` + `apps/api` (`@authportal/api`, Fastify + SQLite/better-sqlite3) + `packages/ui` (`@authportal/ui`) + `packages/db` + `packages/shared`
- Vite + React + TanStack + shadcn **base-nova** + ReUI Frame
- App Switcher: CFDM / vps / bgp / fw / **dns** (Technitium)
- Auth: JWT fragment SSO для своих SPA **и** OIDC IdP для внешних RP (Technitium)
+1 -1
View File
@@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist', 'src/routeTree.gen.ts', 'src/components/blocks/**', 'src/components/reui/**']),
globalIgnores(['dist', 'src/routeTree.gen.ts', 'src/components/reui/**']),
{
files: ['**/*.{ts,tsx}'],
extends: [
@@ -32,4 +32,4 @@ export function AuthLogo({ className }: { className?: string }) {
</ItemMedia>
</Item>
)
}
}
@@ -1,208 +0,0 @@
"use client"
import {
useCallback,
useEffect,
useId,
useRef,
useState,
type ComponentPropsWithoutRef,
} from "react"
import { motion } from "motion/react"
import { cn } from "@authportal/ui/lib/utils"
export interface AnimatedGridPatternProps extends ComponentPropsWithoutRef<"svg"> {
width?: number
height?: number
x?: number
y?: number
strokeDasharray?: number
numSquares?: number
maxOpacity?: number
duration?: number
repeatDelay?: number
}
type Square = {
id: number
pos: [number, number]
iteration: number
}
// Deterministic 0..1 hash so cell placement stays stable across renders without
// Math.random, so SSR and client agree and the no-randomness gate stays green.
function pseudoRandom(seed: number): number {
const value = Math.sin(seed * 12.9898) * 43758.5453
return value - Math.floor(value)
}
export function AnimatedGridPattern({
width = 40,
height = 40,
x = -1,
y = -1,
strokeDasharray = 0,
numSquares = 30,
className,
maxOpacity = 0.1,
duration = 3,
repeatDelay = 1,
...props
}: AnimatedGridPatternProps) {
const id = useId()
const containerRef = useRef<SVGSVGElement | null>(null)
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
const [squares, setSquares] = useState<Array<Square>>([])
const getPos = useCallback(
(seed: number): [number, number] => {
const cols = Math.max(1, Math.floor(dimensions.width / width))
const rows = Math.max(1, Math.floor(dimensions.height / height))
return [
Math.floor(pseudoRandom(seed) * cols),
Math.floor(pseudoRandom(seed + 0.5) * rows),
]
},
[dimensions.height, dimensions.width, height, width]
)
const generateSquares = useCallback(
(count: number) => {
return Array.from({ length: count }, (_, index) => ({
id: index,
pos: getPos(index + 1),
iteration: 0,
}))
},
[getPos]
)
const updateSquarePosition = useCallback(
(squareId: number) => {
setSquares((currentSquares) => {
const current = currentSquares[squareId]
if (!current || current.id !== squareId) {
return currentSquares
}
const nextSquares = currentSquares.slice()
const nextIteration = current.iteration + 1
nextSquares[squareId] = {
...current,
pos: getPos((squareId + 1) * 97 + nextIteration * 13),
iteration: nextIteration,
}
return nextSquares
})
},
[getPos]
)
useEffect(() => {
if (dimensions.width && dimensions.height) {
setSquares(generateSquares(numSquares))
}
}, [dimensions.width, dimensions.height, generateSquares, numSquares])
useEffect(() => {
const element = containerRef.current
if (!element) {
return
}
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
setDimensions((currentDimensions) => {
const nextWidth = entry.contentRect.width
const nextHeight = entry.contentRect.height
if (
currentDimensions.width === nextWidth &&
currentDimensions.height === nextHeight
) {
return currentDimensions
}
return { width: nextWidth, height: nextHeight }
})
}
})
resizeObserver.observe(element)
return () => {
resizeObserver.disconnect()
}
}, [])
return (
<svg
ref={containerRef}
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/12 stroke-gray-400/9 dark:fill-gray-500/9 dark:stroke-gray-500/8",
className
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path
d={`M.5 ${height}V.5H${width}`}
fill="none"
strokeDasharray={strokeDasharray}
/>
</pattern>
</defs>
<rect width="100%" height="100%" fill={`url(#${id})`} />
<svg x={x} y={y} className="overflow-visible">
{squares.map(({ pos: [squareX, squareY], id, iteration }, index) => (
<motion.rect
key={`${id}-${iteration}`}
initial={{ opacity: 0 }}
animate={{ opacity: maxOpacity }}
transition={{
duration,
repeat: 1,
delay: index * 0.1,
repeatType: "reverse",
repeatDelay,
}}
onAnimationComplete={() => updateSquarePosition(id)}
width={width - 1}
height={height - 1}
x={squareX * width + 1}
y={squareY * height + 1}
fill="currentColor"
strokeWidth="0"
/>
))}
</svg>
</svg>
)
}
export function AuthGridBackground() {
return (
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<AnimatedGridPattern
numSquares={30}
maxOpacity={0.06}
duration={3}
repeatDelay={1}
className="inset-x-0 inset-y-[-30%] h-[200%] skew-y-12 mask-[radial-gradient(460px_circle_at_center,white,transparent)]"
/>
</div>
)
}
@@ -1,108 +0,0 @@
import { useState, type ComponentProps } from "react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@authportal/ui/components/avatar"
import { Item, ItemMedia } from "@authportal/ui/components/item"
import { AuthGridBackground } from "./auth-grid-background"
import { AUTH18_TESTIMONIAL, AUTH18_TRUST_BRANDS } from "./data"
import { LoginForm } from "./login-form"
import { StarIcon } from "lucide-react"
type FormSubmitHandler = NonNullable<ComponentProps<"form">["onSubmit"]>
type FormSubmitEvent = Parameters<FormSubmitHandler>[0]
function SidebarBackground() {
return <AuthGridBackground />
}
function Sidebar() {
return (
<aside className="bg-muted/25 border-border/70 relative flex min-h-[34rem] overflow-hidden px-8 py-10 sm:px-12 lg:min-h-svh lg:border-r lg:px-14 lg:py-12">
{/* Sidebar */}
<SidebarBackground />
<div className="relative z-10 flex min-h-full w-full flex-col justify-between gap-10">
<div className="mx-auto flex max-w-md flex-1 flex-col items-center justify-center text-center">
<div className="text-primary flex items-center gap-1">
{Array.from({ length: AUTH18_TESTIMONIAL.stars }).map((_, index) => (
<StarIcon aria-hidden="true" className="size-4 fill-current" key={index} />
))}
</div>
<blockquote className="mt-6 max-w-[28rem] text-[1.625rem] leading-[1.34] font-semibold text-balance">
{AUTH18_TESTIMONIAL.quote}
</blockquote>
<div className="mt-5 flex w-full justify-center">
<div className="flex min-w-0 items-center gap-1.5">
<Avatar className="size-8">
<AvatarImage
src={AUTH18_TESTIMONIAL.avatar}
alt={AUTH18_TESTIMONIAL.name}
/>
<AvatarFallback>{AUTH18_TESTIMONIAL.fallback}</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-col items-start gap-0 text-left">
<div className="text-sm font-medium">
{AUTH18_TESTIMONIAL.name}
</div>
<div className="text-muted-foreground flex items-center gap-1.5 text-xs">
<span>{AUTH18_TESTIMONIAL.role}</span>
<span
aria-hidden="true"
className="bg-muted-foreground/40 size-1 shrink-0 rounded-full"
/>
<span>{AUTH18_TESTIMONIAL.company}</span>
</div>
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-4">
<div className="text-foreground text-sm font-medium">
Trusted by leading teams
</div>
<div className="flex flex-wrap items-center gap-x-6 gap-y-3">
{AUTH18_TRUST_BRANDS.map((brand) => (
<Item
key={brand.id}
className="text-foreground/90 flex w-auto items-center border-0 p-0"
>
<ItemMedia variant="icon" className="size-auto">
{brand.logo}
</ItemMedia>
</Item>
))}
</div>
</div>
</div>
</aside>
)
}
export function Auth() {
const [showPassword, setShowPassword] = useState(false)
function handleSubmit(event: FormSubmitEvent) {
event.preventDefault()
}
return (
<div className="w-full lg:grid lg:min-h-svh lg:grid-cols-[600px_minmax(0,1fr)]">
{/* Sidebar */}
<Sidebar />
{/* Form */}
<LoginForm
showPassword={showPassword}
onTogglePassword={() => setShowPassword((current) => !current)}
onSubmit={handleSubmit}
/>
</div>
)
}
@@ -1,118 +0,0 @@
import type { ReactNode } from "react"
import { Apple } from "@authportal/ui/components/svgs/apple"
import { AppleDark } from "@authportal/ui/components/svgs/appleDark"
import { Google } from "@authportal/ui/components/svgs/google"
import { OpenaiWordmarkDark } from "@authportal/ui/components/svgs/openaiWordmarkDark"
import { OpenaiWordmarkLight } from "@authportal/ui/components/svgs/openaiWordmarkLight"
import { SlackWordmark } from "@authportal/ui/components/svgs/slackWordmark"
import { StripeWordmark } from "@authportal/ui/components/svgs/stripeWordmark"
import { SupabaseWordmarkDark } from "@authportal/ui/components/svgs/supabaseWordmarkDark"
import { SupabaseWordmarkLight } from "@authportal/ui/components/svgs/supabaseWordmarkLight"
export type AuthProvider = {
id: string
label: string
logo: ReactNode
}
export type TrustBrand = {
id: string
logo: ReactNode
}
export type Testimonial = {
quote: string
name: string
role: string
company: string
avatar: string
fallback: string
stars: number
}
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>
</>
)
}
const providerLogoClassName = "size-4 shrink-0"
export const AUTH18_PROVIDERS: AuthProvider[] = [
{
id: "google",
label: "Google",
logo: <Google className={providerLogoClassName} aria-hidden="true" />,
},
{
id: "apple",
label: "Apple",
logo: (
<ThemeLogo
light={<Apple className={providerLogoClassName} aria-hidden="true" />}
dark={
<AppleDark className={providerLogoClassName} aria-hidden="true" />
}
/>
),
},
]
export const AUTH18_TRUST_BRANDS: TrustBrand[] = [
{
id: "openai",
logo: (
<ThemeLogo
light={
<OpenaiWordmarkLight aria-hidden="true" className="h-4 w-auto" />
}
dark={<OpenaiWordmarkDark aria-hidden="true" className="h-4 w-auto" />}
/>
),
},
{
id: "stripe",
logo: <StripeWordmark aria-hidden="true" className="h-4 w-auto" />,
},
{
id: "supabase",
logo: (
<ThemeLogo
light={
<SupabaseWordmarkLight aria-hidden="true" className="h-4 w-auto" />
}
dark={
<SupabaseWordmarkDark aria-hidden="true" className="h-4 w-auto" />
}
/>
),
},
{
id: "slack",
logo: (
<SlackWordmark
aria-hidden="true"
className="text-foreground h-4 w-auto"
/>
),
},
]
export const AUTH18_TESTIMONIAL: Testimonial = {
quote: "The best login pages disappear. This one already feels fast.",
name: "Sean Bold",
role: "Co-founder",
company: "ReUI",
avatar:
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=96&h=96&dpr=2&q=80",
fallback: "SB",
stars: 5,
}
@@ -1,140 +0,0 @@
import { type ComponentProps } from "react"
import { Button } from "@authportal/ui/components/button"
import { Field, FieldGroup, FieldLabel } from "@authportal/ui/components/field"
import { Input } from "@authportal/ui/components/input"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@authportal/ui/components/input-group"
import { Separator } from "@authportal/ui/components/separator"
import { AuthLogo } from "./auth-logo"
import { AUTH18_PROVIDERS } from "./data"
import { EyeOffIcon, EyeIcon } from "lucide-react"
type FormSubmitHandler = NonNullable<ComponentProps<"form">["onSubmit"]>
type FormSubmitEvent = Parameters<FormSubmitHandler>[0]
export function LoginForm({
showPassword,
onTogglePassword,
onSubmit,
}: {
showPassword: boolean
onTogglePassword: () => void
onSubmit: (event: FormSubmitEvent) => void
}) {
return (
<section className="flex min-w-0 flex-col justify-between py-4 sm:py-6 lg:py-8">
{/* Heading */}
<div className="flex flex-1 flex-col justify-center">
<div className="mx-auto flex w-full max-w-90 flex-col gap-6">
<div className="flex flex-col items-center gap-3 text-center">
<AuthLogo />
<div className="flex flex-col gap-1">
<h1 className="text-xl font-semibold tracking-tight">
Sign in to ReUI
</h1>
<p className="text-muted-foreground text-sm">Welcome back.</p>
</div>
</div>
<form className="flex flex-col gap-4" onSubmit={onSubmit}>
<FieldGroup className="gap-3.5">
<Field className="gap-2">
<FieldLabel htmlFor="auth-18-identifier">
Email or username
</FieldLabel>
<Input
id="auth-18-identifier"
type="text"
autoComplete="username"
placeholder="Email or username"
className="bg-background"
/>
</Field>
<Field className="gap-2">
<div className="flex items-center justify-between gap-3">
<FieldLabel htmlFor="auth-18-password">
Password
</FieldLabel>
<Button
type="button"
variant="link"
className="text-muted-foreground h-auto p-0 text-xs font-normal"
>
Forgot password?
</Button>
</div>
<InputGroup className="bg-background w-full">
<InputGroupInput
id="auth-18-password"
type={showPassword ? "text" : "password"}
autoComplete="current-password"
placeholder="Enter your password"
/>
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
size="icon-xs"
className="text-muted-foreground hover:text-foreground"
aria-label={
showPassword ? "Hide password" : "Show password"
}
aria-pressed={showPassword}
onClick={onTogglePassword}
>
{showPassword ? (
<EyeOffIcon aria-hidden="true" className="size-4" />
) : (
<EyeIcon aria-hidden="true" className="size-4" />
)}
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
</FieldGroup>
<Button type="submit" className="w-full">
Sign in
</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>
<div className="grid gap-3 sm:grid-cols-2">
{AUTH18_PROVIDERS.map((provider) => (
<Button
key={provider.id}
type="button"
variant="outline"
className="w-full"
>
{provider.logo}
{provider.label}
</Button>
))}
</div>
<p className="text-muted-foreground text-center text-sm">
Need an account?{" "}
<Button type="button" variant="link" className="h-auto p-0">
Sign up
</Button>
</p>
</div>
</div>
</section>
)
}
@@ -1,9 +0,0 @@
import { Auth } from "./components/auth"
export function Page() {
return (
<div className="bg-background min-h-svh w-full">
<Auth />
</div>
)
}
@@ -1,77 +0,0 @@
export interface PermissionItem {
id: string
title: string
description: string
defaultChecked: boolean
}
// ── Data ──
export const permissions: PermissionItem[] = [
{
id: "workspace-settings",
title: "Workspace Settings",
description:
"Review workspace details, team defaults, and operational preferences.",
defaultChecked: true,
},
{
id: "billing-management",
title: "Billing Management",
description: "Access plan details, invoices, and subscription adjustments.",
defaultChecked: false,
},
{
id: "integration-setup",
title: "Integration Setup",
description: "Configure apps, credentials, and automation entry points.",
defaultChecked: true,
},
{
id: "permissions-control",
title: "Permissions Control",
description: "Grant, revoke, and review access scopes for collaborators.",
defaultChecked: false,
},
{
id: "map-creation",
title: "Map Creation",
description: "Create new workspace maps and maintain location structure.",
defaultChecked: false,
},
{
id: "data-export",
title: "Data Export",
description:
"Download structured workspace reports for analysis and audits.",
defaultChecked: true,
},
{
id: "user-roles",
title: "User Roles",
description:
"Edit role assignments and keep team responsibility lines clear.",
defaultChecked: true,
},
{
id: "security-settings",
title: "Security Settings",
description:
"Adjust workspace protection controls and policy requirements.",
defaultChecked: true,
},
{
id: "insights-access",
title: "Insights Access",
description:
"View performance dashboards, usage trends, and reporting panels.",
defaultChecked: false,
},
{
id: "merchant-list",
title: "Merchant List",
description:
"Maintain merchant records and workspace-linked account mappings.",
defaultChecked: false,
},
]
@@ -1,108 +0,0 @@
import { useState } from "react"
import { Badge } from "@/components/reui/badge"
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/reui/frame"
import { Button } from "@authportal/ui/components/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemTitle,
} from "@authportal/ui/components/item"
import { Switch } from "@authportal/ui/components/switch"
import { permissions } from "./data"
// ── Permission Card ──
function PermissionCard({
checked,
description,
id,
onCheckedChange,
title,
}: {
checked: boolean
description: string
id: string
onCheckedChange: (checked: boolean) => void
title: string
}) {
return (
<Item variant="outline" className="items-start gap-3">
{/* Content */}
<ItemContent className="gap-1.5">
<ItemTitle>{title}</ItemTitle>
<ItemDescription>{description}</ItemDescription>
</ItemContent>
{/* Actions */}
<ItemActions className="ml-auto self-center">
<Switch
checked={checked}
onCheckedChange={onCheckedChange}
aria-label={title}
id={id}
/>
</ItemActions>
</Item>
)
}
export function RolePermissions() {
const [values, setValues] = useState<Record<string, boolean>>(() =>
Object.fromEntries(
permissions.map((permission) => [
permission.id,
permission.defaultChecked,
])
)
)
return (
<Frame className="w-full max-w-5xl">
{/* Header */}
<FrameHeader className="flex-row items-center justify-between gap-5">
<div className="space-y-px">
<FrameTitle>Role Permissions for Project Manager</FrameTitle>
<FrameDescription>
Control the workspace capabilities this role can manage.
</FrameDescription>
</div>
<Button aria-label="Updates (new)">
Permission
<Badge variant="success" size="xs" aria-hidden="true">
New
</Badge>
</Button>
</FrameHeader>
{/* Content */}
<FramePanel className="grid grid-cols-1 gap-3 lg:grid-cols-2">
{permissions.map((permission) => (
<PermissionCard
key={permission.id}
id={permission.id}
title={permission.title}
description={permission.description}
checked={values[permission.id]}
onCheckedChange={(checked) =>
setValues((current) => ({
...current,
[permission.id]: checked,
}))
}
/>
))}
</FramePanel>
</Frame>
)
}
@@ -1,9 +0,0 @@
import { RolePermissions } from "./components/role-permissions"
export function Page() {
return (
<div className="flex min-h-svh w-full max-w-4xl items-start justify-center p-4 sm:p-8 md:p-12">
<RolePermissions />
</div>
)
}
@@ -1,165 +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 TicketDetailTip = {
text: 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",
}
export const TICKET_DETAIL_TIPS: TicketDetailTip[] = [
{
text: "Press Enter while editing a text value to save the row.",
},
{
text: "Use row actions for quick updates without leaving the queue.",
},
]
@@ -1,224 +0,0 @@
"use client"
import { useEffect, useRef, type ReactNode } from "react"
import { cn } from "@authportal/ui/lib/utils"
import { Button } from "@authportal/ui/components/button"
import { Field, FieldTitle } from "@authportal/ui/components/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
} from "@authportal/ui/components/input-group"
import { Item, ItemMedia } from "@authportal/ui/components/item"
import { Spinner } from "@authportal/ui/components/spinner"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@authportal/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-0.5 px-4 py-0.5 sm:grid-cols-[minmax(7.75rem,0.5fr)_minmax(0,1.5fr)] sm:gap-x-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,15 +0,0 @@
import { TicketDetailsSheet } from "./components/ticket-details-sheet"
export function Page() {
return (
<main
className="flex min-h-svh w-full items-center justify-center p-6 sm:p-10 md:p-12"
aria-labelledby="page-heading"
>
<h1 id="page-heading" className="sr-only">
Editable ticket details sheet
</h1>
<TicketDetailsSheet />
</main>
)
}
@@ -1,98 +0,0 @@
import { Button } from "@authportal/ui/components/button"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@authportal/ui/components/select"
import { BULK_ROLE_OPTIONS, type MemberRole } from "./data"
import { PencilIcon, SendIcon, PauseCircleIcon } from "lucide-react"
interface BulkActionBarProps {
selectedCount: number
roleValue: MemberRole
onRoleChange: (value: MemberRole) => void
onChangeRole: () => void
onResendInvite: () => void
onDeactivate: () => void
onClear: () => void
}
export function BulkActionBar({
selectedCount,
roleValue,
onRoleChange,
onChangeRole,
onResendInvite,
onDeactivate,
onClear,
}: BulkActionBarProps) {
return (
<div className="bg-muted/25 flex flex-col gap-3 px-(--frame-panel-header-px) py-(--frame-panel-header-py) lg:flex-row lg:items-center lg:justify-between">
<div className="flex min-w-0 flex-col gap-0.5">
<span className="text-sm font-medium">
{selectedCount} member{selectedCount === 1 ? "" : "s"} selected
</span>
<span className="text-muted-foreground text-xs">
Change role, resend invites, or revoke access in one step.
</span>
</div>
{/* Actions */}
<div className="flex flex-wrap items-center gap-2">
<Select
value={roleValue}
onValueChange={(value) => {
if (!value) return
onRoleChange(value as MemberRole)
}}
items={BULK_ROLE_OPTIONS}
>
<SelectTrigger size="sm" className="w-[164px]">
<SelectValue />
</SelectTrigger>
<SelectContent align="end">
<SelectGroup>
{BULK_ROLE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<Button
type="button"
size="sm"
variant="outline"
onClick={onChangeRole}
>
<PencilIcon data-icon="inline-start" aria-hidden="true" />
Change role
</Button>
<Button
type="button"
size="sm"
variant="outline"
onClick={onResendInvite}
>
<SendIcon data-icon="inline-start" aria-hidden="true" />
Resend invite
</Button>
<Button type="button" size="sm" onClick={onDeactivate}>
<PauseCircleIcon data-icon="inline-start" aria-hidden="true" />
Deactivate
</Button>
<Button type="button" size="sm" variant="ghost" onClick={onClear}>
Clear
</Button>
</div>
</div>
)
}
@@ -1,540 +0,0 @@
import { memo, useState } 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 { Row, type ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner"
import { cn } from "@authportal/ui/lib/utils"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@authportal/ui/components/alert-dialog"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@authportal/ui/components/avatar"
import { Button } from "@authportal/ui/components/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@authportal/ui/components/dropdown-menu"
import { Skeleton } from "@authportal/ui/components/skeleton"
import {
MemberRole,
MemberStatus,
TEAM_LABELS,
type AuthMethod,
type IMember,
type TeamLabel,
type TwoFactor,
} from "./data"
import { ShieldCheckIcon, LockIcon, TriangleAlertIcon, KeyRoundIcon, MoreHorizontalIcon, EyeIcon, PencilIcon, SendIcon, PauseCircleIcon, Trash2Icon } from "lucide-react"
// ── Team tag colors (light + dark) ──
const teamBadgeClass: Record<TeamLabel, string> = {
Engineering:
"bg-indigo-100 text-indigo-800 dark:bg-indigo-950/50 dark:text-indigo-300",
Product:
"bg-violet-100 text-violet-800 dark:bg-violet-950/50 dark:text-violet-300",
Design: "bg-sky-100 text-sky-800 dark:bg-sky-950/50 dark:text-sky-300",
Sales:
"bg-emerald-100 text-emerald-800 dark:bg-emerald-950/50 dark:text-emerald-300",
Marketing:
"bg-amber-100 text-amber-800 dark:bg-amber-950/50 dark:text-amber-300",
"Customer Success":
"bg-cyan-100 text-cyan-800 dark:bg-cyan-950/50 dark:text-cyan-300",
Finance:
"bg-yellow-100 text-yellow-800 dark:bg-yellow-950/50 dark:text-yellow-300",
"IT/Security":
"bg-rose-100 text-rose-800 dark:bg-rose-950/50 dark:text-rose-300",
}
function getTeamClasses(tag: string): string {
if (TEAM_LABELS.includes(tag as TeamLabel)) {
return teamBadgeClass[tag as TeamLabel]
}
return "bg-muted text-muted-foreground"
}
export const TeamTags = memo(function TeamTags({
teams,
}: {
teams: TeamLabel[]
}) {
return (
<div className="flex flex-wrap items-center gap-1">
{teams.map((team) => (
<Badge
key={team}
variant="secondary"
className={cn("border-0", getTeamClasses(team))}
>
{team}
</Badge>
))}
</div>
)
})
// ── Status badge ──
const statusConfig: Record<MemberStatus, { dot: string }> = {
Active: { dot: "bg-emerald-500" },
Invited: { dot: "bg-amber-500" },
Suspended: { dot: "bg-red-500" },
Deactivated: { dot: "bg-muted-foreground" },
}
export function StatusBadge({ status }: { status: MemberStatus }) {
return (
<Badge variant="outline">
<span
className={cn(
"size-1.5 shrink-0 rounded-full!",
statusConfig[status].dot
)}
/>
{status}
</Badge>
)
}
// ── Role badge ──
const roleConfig: Record<
MemberRole,
{ variant: React.ComponentProps<typeof Badge>["variant"] }
> = {
Owner: { variant: "primary-outline" },
Admin: { variant: "info-outline" },
Member: { variant: "secondary" },
Billing: { variant: "warning-outline" },
Guest: { variant: "outline" },
"Support Agent": { variant: "secondary" },
}
export function RoleBadge({ role }: { role: MemberRole }) {
return <Badge variant={roleConfig[role].variant}>{role}</Badge>
}
// ── Member cell (same layout as the donor ContactCell) ──
const MemberCell = memo(function MemberCell({ row }: { row: Row<IMember> }) {
const o = row.original
return (
<div className="flex items-center gap-2">
<Avatar className="size-8 shrink-0">
<AvatarImage src={o.avatar} alt="" />
<AvatarFallback>
{o.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="min-w-0">
<div className="text-foreground line-clamp-1 font-medium">{o.name}</div>
<div
className="text-muted-foreground line-clamp-1 text-xs"
title={o.email}
>
{o.email}
</div>
</div>
</div>
)
})
// ── Auth cell (SSO / 2FA badges) ──
const authBadgeVariant: Record<
AuthMethod,
React.ComponentProps<typeof Badge>["variant"]
> = {
SSO: "success-outline",
Password: "warning-outline",
}
const twoFactorBadgeVariant: Record<
TwoFactor,
React.ComponentProps<typeof Badge>["variant"]
> = {
Authenticator: "info-outline",
Passkey: "success-outline",
"Security key": "success-outline",
SMS: "warning-outline",
}
function AuthMethodIcon({ auth }: { auth: AuthMethod }) {
if (auth === "SSO") {
return (
<ShieldCheckIcon aria-hidden="true" />
)
}
return (
<LockIcon aria-hidden="true" />
)
}
function TwoFactorIcon({ factor }: { factor: TwoFactor | null }) {
if (!factor) {
return (
<TriangleAlertIcon aria-hidden="true" />
)
}
return (
<KeyRoundIcon aria-hidden="true" />
)
}
function AuthCell({ row }: { row: Row<IMember> }) {
const { auth, ssoProvider, twoFactor } = row.original
return (
<div className="flex flex-wrap items-center gap-1">
<Badge variant={authBadgeVariant[auth]}>
<AuthMethodIcon auth={auth} />
{auth === "SSO" ? (ssoProvider ?? "SSO") : "Password"}
</Badge>
<Badge
variant={
twoFactor ? twoFactorBadgeVariant[twoFactor] : "destructive-outline"
}
>
<TwoFactorIcon factor={twoFactor} />
{twoFactor ?? "No 2FA"}
</Badge>
</div>
)
}
// ── Actions cell ──
export function ActionsCell({
row,
onEditRole,
onView,
}: {
row: Row<IMember>
onEditRole: (member: IMember) => void
onView: (member: IMember) => void
}) {
const [removeOpen, setRemoveOpen] = useState(false)
const member = row.original
const handleRemoveConfirm = () => {
setRemoveOpen(false)
toast.success("Member removed", {
description: `${member.name} loses access to Acme Cloud immediately.`,
})
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
size="icon"
variant="ghost"
className="size-7"
aria-label="Row actions"
/>
}
>
<MoreHorizontalIcon aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-44">
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => onView(member)}>
<EyeIcon className="size-4" aria-hidden="true" />
View Profile
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onEditRole(member)}>
<PencilIcon className="size-4" aria-hidden="true" />
Edit Role
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
toast.info("Invite resent", {
description: `New link sent to ${member.email}. Expires in 7 days.`,
})
}
>
<SendIcon className="size-4" aria-hidden="true" />
Resend Invite
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
toast.message("Member suspended", {
description: `${member.name} can no longer sign in until reinstated.`,
})
}
>
<PauseCircleIcon className="size-4" aria-hidden="true" />
Suspend
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={() => setRemoveOpen(true)}
>
<Trash2Icon className="size-4" aria-hidden="true" />
Remove
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<AlertDialog open={removeOpen} onOpenChange={setRemoveOpen}>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogTitle>Remove member?</AlertDialogTitle>
<AlertDialogDescription>
This revokes access for{""}
<span className="text-foreground font-medium">{member.name}</span>
{""}
and frees their seat. Connect your API to persist changes.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
onClick={handleRemoveConfirm}
>
Remove
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}
// ── Column definitions ──
interface ColumnHandlers {
onEditRole: (member: IMember) => void
onView: (member: IMember) => void
}
export function createMemberColumns({
onEditRole,
onView,
}: ColumnHandlers): ColumnDef<IMember>[] {
return [
{
id: "select",
header: () => <DataGridTableRowSelectAll />,
cell: ({ row }) => <DataGridTableRowSelect row={row} />,
enableSorting: false,
size: 40,
enableResizing: false,
enableHiding: false,
meta: {
skeleton: <Skeleton className="mx-auto size-5" />,
headerClassName:
"[--data-grid-header-cell-ps:var(--frame-panel-header-px)]",
cellClassName: "[--data-grid-body-cell-ps:var(--frame-panel-px)]",
},
},
{
accessorKey: "name",
id: "name",
header: ({ column }) => (
<DataGridColumnHeader
title="Member"
visibility={true}
column={column}
/>
),
cell: ({ row }) => <MemberCell row={row} />,
enableSorting: true,
enableHiding: false,
enableResizing: true,
minSize: 220,
meta: {
headerTitle: "Member",
autoSize: true,
skeleton: (
<div className="flex min-w-0 items-center gap-2">
<Skeleton className="size-8 shrink-0 rounded-full" />
<div className="flex min-w-0 flex-col gap-0.5">
<Skeleton className="h-3.5 w-32" />
<Skeleton className="h-3 w-40" />
</div>
</div>
),
},
},
{
accessorKey: "role",
id: "role",
header: ({ column }) => (
<DataGridColumnHeader title="Role" visibility={true} column={column} />
),
cell: ({ row }) => <RoleBadge role={row.original.role} />,
size: 120,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Role",
skeleton: <Skeleton className="h-6 w-16 rounded-full" />,
},
},
{
accessorKey: "teams",
id: "teams",
header: ({ column }) => (
<DataGridColumnHeader title="Teams" visibility={true} column={column} />
),
cell: ({ row }) => <TeamTags teams={row.original.teams} />,
size: 200,
enableSorting: false,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Teams",
skeleton: (
<div className="flex flex-wrap items-center gap-1">
<Skeleton className="h-5 w-20 rounded-full" />
<Skeleton className="h-5 w-16 rounded-full" />
</div>
),
},
},
{
accessorKey: "status",
id: "status",
header: ({ column }) => (
<DataGridColumnHeader
title="Status"
visibility={true}
column={column}
/>
),
cell: ({ row }) => <StatusBadge status={row.original.status} />,
size: 120,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Status",
skeleton: <Skeleton className="h-6 w-24 rounded-full" />,
},
},
{
accessorKey: "auth",
id: "auth",
header: ({ column }) => (
<DataGridColumnHeader title="Auth" visibility={true} column={column} />
),
cell: ({ row }) => <AuthCell row={row} />,
size: 220,
enableSorting: false,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Auth",
skeleton: (
<div className="flex items-center gap-1">
<Skeleton className="h-5 w-20 rounded-full" />
<Skeleton className="h-5 w-24 rounded-full" />
</div>
),
},
},
{
accessorKey: "lastActiveIso",
id: "lastActive",
header: ({ column }) => (
<DataGridColumnHeader
title="Last Active"
visibility={true}
column={column}
/>
),
cell: ({ row }) => (
<span className="text-muted-foreground text-sm">
{row.original.lastActive}
</span>
),
size: 130,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Last Active",
skeleton: <Skeleton className="h-4 w-28" />,
},
},
{
accessorKey: "seatLabel",
id: "seat",
header: ({ column }) => (
<DataGridColumnHeader title="Seat" visibility={true} column={column} />
),
cell: ({ row }) => (
<div className="min-w-0">
<div className="text-foreground truncate text-sm font-medium">
{row.original.seatLabel}
</div>
<div className="text-muted-foreground truncate text-xs">
{row.original.provisioning}
</div>
</div>
),
size: 140,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Seat",
skeleton: (
<div className="flex min-w-0 flex-col gap-0.5">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-16" />
</div>
),
},
},
{
id: "actions",
header: "",
cell: ({ row }) => (
<ActionsCell row={row} onEditRole={onEditRole} onView={onView} />
),
size: 60,
enableSorting: false,
enableHiding: false,
enableResizing: false,
meta: {
skeleton: <Skeleton className="mx-auto size-7 rounded-md" />,
headerClassName:
"[--data-grid-header-cell-pe:var(--frame-panel-header-px)]",
cellClassName: "[--data-grid-body-cell-pe:var(--frame-panel-px)]",
},
},
]
}
@@ -1,306 +0,0 @@
export type MemberStatus = "Active" | "Invited" | "Suspended" | "Deactivated"
export type MemberRole =
| "Owner"
| "Admin"
| "Member"
| "Billing"
| "Guest"
| "Support Agent"
export type AuthMethod = "SSO" | "Password"
export type SsoProvider = "Okta" | "Microsoft Entra ID" | "Google Workspace"
export type TwoFactor = "Authenticator" | "Passkey" | "Security key" | "SMS"
export type Provisioning = "SCIM" | "JIT" | "Manual"
export type Scope = "None" | "Read" | "Write" | "Admin"
/** Closed vocabulary for team tags (filters + badge colors). */
export const TEAM_LABELS = [
"Engineering",
"Product",
"Design",
"Sales",
"Marketing",
"Customer Success",
"Finance",
"IT/Security",
] as const
export type TeamLabel = (typeof TEAM_LABELS)[number]
export interface IMember {
id: string
name: string
avatar: string
email: string
role: MemberRole
title: string
scope: Scope
teams: TeamLabel[]
status: MemberStatus
auth: AuthMethod
ssoProvider: SsoProvider | null
twoFactor: TwoFactor | null
provisioning: Provisioning
lastActive: string
lastActiveIso: string
seatLabel: string
}
// ── Status + role order (filter + bulk options) ──
export const STATUS_ORDER: MemberStatus[] = [
"Active",
"Invited",
"Suspended",
"Deactivated",
]
export const ROLE_ORDER: MemberRole[] = [
"Owner",
"Admin",
"Member",
"Billing",
"Guest",
"Support Agent",
]
/** Roles offered in the bulk change-role control. */
export const BULK_ROLE_OPTIONS: { value: MemberRole; label: string }[] = [
{ value: "Member", label: "Member" },
{ value: "Admin", label: "Admin" },
{ value: "Billing", label: "Billing" },
{ value: "Guest", label: "Guest" },
{ value: "Support Agent", label: "Support Agent" },
]
// ── Data (12 members) ──
export const MEMBERS: IMember[] = [
{
id: "usr_a1b2c3d4",
name: "Mira Stone",
avatar:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=96&h=96&dpr=2&q=80",
email: "[email protected]",
role: "Owner",
title: "Head of Product",
scope: "Admin",
teams: ["Product", "IT/Security"],
status: "Active",
auth: "SSO",
ssoProvider: "Okta",
twoFactor: "Passkey",
provisioning: "SCIM",
lastActive: "2 min ago",
lastActiveIso: "2026-06-17T14:12:00Z",
seatLabel: "Seat 1 of 80",
},
{
id: "usr_b2c3d4e5",
name: "Leo Grant",
avatar:
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=96&h=96&dpr=2&q=80",
email: "[email protected]",
role: "Admin",
title: "Eng Manager",
scope: "Admin",
teams: ["Engineering", "IT/Security"],
status: "Active",
auth: "SSO",
ssoProvider: "Microsoft Entra ID",
twoFactor: "Authenticator",
provisioning: "SCIM",
lastActive: "18 min ago",
lastActiveIso: "2026-06-17T13:56:00Z",
seatLabel: "Seat 4 of 80",
},
{
id: "usr_c3d4e5f6",
name: "Sarah Chen",
avatar:
"https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
email: "[email protected]",
role: "Admin",
title: "Eng Manager",
scope: "Admin",
teams: ["Engineering"],
status: "Active",
auth: "SSO",
ssoProvider: "Okta",
twoFactor: "Security key",
provisioning: "SCIM",
lastActive: "1 hour ago",
lastActiveIso: "2026-06-17T13:09:00Z",
seatLabel: "Seat 7 of 80",
},
{
id: "usr_d4e5f6a7",
name: "Nora Vale",
avatar:
"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=96&h=96&dpr=2&q=80",
email: "[email protected]",
role: "Member",
title: "Product Designer",
scope: "Write",
teams: ["Design", "Product"],
status: "Active",
auth: "SSO",
ssoProvider: "Google Workspace",
twoFactor: "Authenticator",
provisioning: "JIT",
lastActive: "3 hours ago",
lastActiveIso: "2026-06-17T11:05:00Z",
seatLabel: "Seat 12 of 80",
},
{
id: "usr_e5f6a7b8",
name: "Sana Qureshi",
avatar:
"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=96&h=96&dpr=2&q=80",
email: "[email protected]",
role: "Member",
title: "Backend Engineer",
scope: "Write",
teams: ["Engineering"],
status: "Active",
auth: "SSO",
ssoProvider: "Okta",
twoFactor: "Authenticator",
provisioning: "SCIM",
lastActive: "5 hours ago",
lastActiveIso: "2026-06-17T09:21:00Z",
seatLabel: "Seat 18 of 80",
},
{
id: "usr_f6a7b8c9",
name: "David Kim",
avatar:
"https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
email: "[email protected]",
role: "Member",
title: "DevOps",
scope: "Write",
teams: ["Engineering", "IT/Security"],
status: "Active",
auth: "SSO",
ssoProvider: "Microsoft Entra ID",
twoFactor: "Security key",
provisioning: "SCIM",
lastActive: "Yesterday",
lastActiveIso: "2026-06-16T17:40:00Z",
seatLabel: "Seat 23 of 80",
},
{
id: "usr_a7b8c9d0",
name: "Michael Rodriguez",
avatar:
"https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
email: "[email protected]",
role: "Member",
title: "Account Executive",
scope: "Read",
teams: ["Sales"],
status: "Active",
auth: "Password",
ssoProvider: null,
twoFactor: "SMS",
provisioning: "Manual",
lastActive: "Yesterday",
lastActiveIso: "2026-06-16T15:18:00Z",
seatLabel: "Seat 31 of 80",
},
{
id: "usr_b8c9d0e1",
name: "Priya Patel",
avatar:
"https://images.unsplash.com/photo-1488426862026-3ee34a7d66df?w=96&h=96&dpr=2&q=80",
email: "[email protected]",
role: "Billing",
title: "Finance",
scope: "Read",
teams: ["Finance"],
status: "Active",
auth: "SSO",
ssoProvider: "Google Workspace",
twoFactor: "Authenticator",
provisioning: "JIT",
lastActive: "2 days ago",
lastActiveIso: "2026-06-15T10:02:00Z",
seatLabel: "Seat 38 of 80",
},
{
id: "usr_c9d0e1f2",
name: "Emma Wilson",
avatar:
"https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
email: "[email protected]",
role: "Member",
title: "Marketing",
scope: "Read",
teams: ["Marketing"],
status: "Invited",
auth: "Password",
ssoProvider: null,
twoFactor: null,
provisioning: "Manual",
lastActive: "Invite sent",
lastActiveIso: "2026-06-14T09:30:00Z",
seatLabel: "Pending seat",
},
{
id: "usr_d0e1f2a3",
name: "Omar Haddad",
avatar:
"https://images.unsplash.com/photo-1507591064344-4c6ce005b128?w=96&h=96&dpr=2&q=80",
email: "[email protected]",
role: "Member",
title: "Data Analyst",
scope: "Read",
teams: ["Product"],
status: "Invited",
auth: "Password",
ssoProvider: null,
twoFactor: null,
provisioning: "Manual",
lastActive: "Expires in 3 days",
lastActiveIso: "2026-06-13T16:45:00Z",
seatLabel: "Pending seat",
},
{
id: "usr_e1f2a3b4",
name: "Kenji Tan",
avatar:
"https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=96&h=96&dpr=2&q=80",
email: "[email protected]",
role: "Guest",
title: "Contractor",
scope: "Read",
teams: ["Design"],
status: "Suspended",
auth: "SSO",
ssoProvider: "Google Workspace",
twoFactor: "SMS",
provisioning: "JIT",
lastActive: "14 days ago",
lastActiveIso: "2026-06-03T12:00:00Z",
seatLabel: "Seat 52 of 80",
},
{
id: "usr_f2a3b4c5",
name: "Alex Johnson",
avatar:
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
email: "[email protected]",
role: "Member",
title: "QA Engineer",
scope: "None",
teams: ["Engineering"],
status: "Deactivated",
auth: "Password",
ssoProvider: null,
twoFactor: null,
provisioning: "Manual",
lastActive: "97 days ago",
lastActiveIso: "2026-03-12T08:15:00Z",
seatLabel: "Seat released",
},
]
@@ -1,181 +0,0 @@
"use client"
import { type ReactNode } from "react"
import { toast } from "sonner"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@authportal/ui/components/avatar"
import { Button } from "@authportal/ui/components/button"
import { ScrollArea } from "@authportal/ui/components/scroll-area"
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from "@authportal/ui/components/sheet"
import { RoleBadge, StatusBadge } from "./columns"
import { type IMember } from "./data"
import { XIcon, PencilIcon } from "lucide-react"
const mutedIconButtonClassName = "text-muted-foreground hover:text-foreground"
function DetailRow({
label,
children,
}: {
label: string
children: ReactNode
}) {
return (
<div className="flex min-h-9 items-center justify-between gap-3 px-4 py-1">
<span className="text-muted-foreground shrink-0 text-sm">{label}</span>
<span className="flex min-w-0 items-center justify-end gap-1.5 text-sm font-medium">
{children}
</span>
</div>
)
}
export function MemberDetailSheet({
member,
open,
onOpenChange,
}: {
member: IMember | null
open: boolean
onOpenChange: (open: boolean) => void
}) {
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
side="right"
showCloseButton={false}
className="inset-y-4 right-4 left-auto h-[calc(100svh-2rem)] w-[min(30rem,calc(100vw-2rem))] max-w-none overflow-hidden rounded-xl p-0 outline-none"
>
{/* Header */}
<SheetHeader className="shrink-0 p-0">
<div className="flex min-h-12 items-center justify-between gap-2 border-b px-4">
<SheetTitle className="min-w-0 truncate text-base font-semibold">
Member Profile
</SheetTitle>
<SheetClose
render={
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label="Close sheet"
className={mutedIconButtonClassName}
>
<XIcon aria-hidden="true" />
</Button>
}
/>
</div>
<SheetDescription className="sr-only">
Review role, teams, and authentication for this member.
</SheetDescription>
</SheetHeader>
{/* Content */}
<div className="min-h-0 flex-1">
<ScrollArea className="h-full">
{member ? (
<div className="flex min-h-full flex-col pb-6">
<div className="flex items-center gap-3 px-4 py-5">
<Avatar className="size-12 shrink-0">
<AvatarImage src={member.avatar} alt="" />
<AvatarFallback>
{member.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="min-w-0">
<div className="text-foreground truncate text-sm font-semibold">
{member.name}
</div>
<div className="text-muted-foreground truncate text-xs">
{member.title}
</div>
<div className="text-muted-foreground truncate text-xs">
{member.email}
</div>
</div>
</div>
<div className="flex flex-col gap-0 border-t pt-1">
<DetailRow label="Role">
<RoleBadge role={member.role} />
</DetailRow>
<DetailRow label="Status">
<StatusBadge status={member.status} />
</DetailRow>
<DetailRow label="Permission scope">{member.scope}</DetailRow>
<DetailRow label="Teams">
<span className="truncate">{member.teams.join(", ")}</span>
</DetailRow>
<DetailRow label="Sign-in">
{member.auth === "SSO"
? (member.ssoProvider ?? "SSO")
: "Password"}
</DetailRow>
<DetailRow label="Two-factor">
{member.twoFactor ?? "Not enrolled"}
</DetailRow>
<DetailRow label="Provisioning">
{member.provisioning}
</DetailRow>
<DetailRow label="Seat">{member.seatLabel}</DetailRow>
<DetailRow label="Last active">{member.lastActive}</DetailRow>
<DetailRow label="Member ID">
<span className="truncate font-mono text-xs">
{member.id}
</span>
</DetailRow>
</div>
</div>
) : null}
</ScrollArea>
</div>
{/* Footer */}
<SheetFooter className="bg-background shrink-0 border-t">
<div className="flex w-full gap-2">
<Button
type="button"
className="min-w-0 flex-1"
onClick={() => {
if (!member) return
toast.info("Role editor", {
description: `Update ${member.name} from ${member.role}. Demo only.`,
})
}}
>
<PencilIcon aria-hidden="true" />
Edit role
</Button>
<SheetClose
render={
<Button
type="button"
variant="outline"
className="min-w-0 flex-1"
>
Close
</Button>
}
/>
</div>
</SheetFooter>
</SheetContent>
</Sheet>
)
}
@@ -1,550 +0,0 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { DataGrid } from "@/components/reui/data-grid/data-grid"
import { DataGridColumnVisibility } from "@/components/reui/data-grid/data-grid-column-visibility"
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,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
PaginationState,
RowSelectionState,
SortingState,
useReactTable,
type VisibilityState,
} from "@tanstack/react-table"
import { toast } from "sonner"
import { Button } from "@authportal/ui/components/button"
import { Separator } from "@authportal/ui/components/separator"
import { TooltipProvider } from "@authportal/ui/components/tooltip"
import { BulkActionBar } from "./bulk-action-bar"
import { createMemberColumns, RoleBadge, StatusBadge } from "./columns"
import {
MEMBERS,
ROLE_ORDER,
STATUS_ORDER,
TEAM_LABELS,
type IMember,
type MemberRole,
type MemberStatus,
type TeamLabel,
} from "./data"
import { MemberDetailSheet } from "./member-detail-sheet"
import { UserIcon, MailIcon, ShieldCheckIcon, CircleDotIcon, UsersIcon, UserPlusIcon, FilterIcon, FunnelXIcon, Settings2Icon } from "lucide-react"
// ── Helpers ──
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 serializeActiveFiltersKey(active: Filter[]) {
return JSON.stringify(
active.map((f) => ({
field: f.field,
operator: f.operator,
values: f.values,
}))
)
}
function filterFieldValue(item: IMember, field: string): unknown {
if (field === "teams") return item.teams.join(" ")
return item[field as keyof IMember]
}
function applyFiltersToData(data: IMember[], filters: Filter[]): IMember[] {
const active = getActiveFilters(filters)
let result = [...data]
active.forEach((filter) => {
const { field, operator, values } = filter
result = result.filter((item) => {
if (field === "teams") {
const selected = values.map(String)
switch (operator) {
case "is":
return (
selected.length > 0 &&
item.teams.includes(selected[0] as TeamLabel)
)
case "is_not":
return !selected.some((v) => item.teams.includes(v as TeamLabel))
case "is_any_of":
return selected.some((v) => item.teams.includes(v as TeamLabel))
case "is_not_any_of":
return !selected.some((v) => item.teams.includes(v as TeamLabel))
case "contains": {
const tokens = values.map((v) => String(v).trim()).filter(Boolean)
if (tokens.length === 0) return true
return tokens.some((token) =>
item.teams.some((t) =>
t.toLowerCase().includes(token.toLowerCase())
)
)
}
case "not_contains":
return !values.some((v) =>
item.teams.some((t) =>
t.toLowerCase().includes(String(v).toLowerCase())
)
)
default:
return true
}
}
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((v) => fieldValue === v)
case "is_not_any_of":
return !values.some((v) => fieldValue === v)
case "contains": {
const tokens = values.map((v) => String(v).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((v) =>
String(fieldValue).toLowerCase().includes(String(v).toLowerCase())
)
case "starts_with":
return values.some((v) =>
String(fieldValue).toLowerCase().startsWith(String(v).toLowerCase())
)
case "ends_with":
return values.some((v) =>
String(fieldValue).toLowerCase().endsWith(String(v).toLowerCase())
)
case "empty":
return fieldValue === "" || fieldValue == null
case "not_empty":
return fieldValue !== "" && fieldValue != null
default:
return true
}
})
})
return result
}
const STATUS_OPTIONS: { value: MemberStatus; label: string }[] =
STATUS_ORDER.map((status) => ({ value: status, label: status }))
const ROLE_OPTIONS: { value: MemberRole; label: string }[] = ROLE_ORDER.map(
(role) => ({ value: role, label: role })
)
function renderSelectedCount(values: unknown[]) {
if (values.length === 0) return "Select..."
if (values.length > 1) return `${values.length} selected`
return null
}
function createDefaultMemberFilters(): Filter[] {
return [createFilter("name", "contains", [""])]
}
function DotSeparator() {
return (
<span
className="bg-muted-foreground/40 size-1 shrink-0 rounded-full"
aria-hidden="true"
/>
)
}
// ── Main ──
export function MembersGrid() {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const [sorting, setSorting] = useState<SortingState>([
{ id: "name", desc: false },
])
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
seat: false,
})
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [filters, setFilters] = useState<Filter[]>(createDefaultMemberFilters)
const [bulkRole, setBulkRole] = useState<MemberRole>("Member")
const [activeMember, setActiveMember] = useState<IMember | null>(null)
const [sheetOpen, setSheetOpen] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [filteredData, setFilteredData] = useState<IMember[]>(MEMBERS)
const isInitialLoad = useRef(true)
const lastAppliedActiveKey = useRef<string>(
serializeActiveFiltersKey(getActiveFilters(createDefaultMemberFilters()))
)
const filterFields: FilterFieldConfig[] = useMemo(
() => [
{
key: "name",
label: "Member",
icon: (
<UserIcon className="size-3.5" aria-hidden />
),
type: "text",
className: "w-44",
placeholder: "Search...",
},
{
key: "email",
label: "Email",
icon: (
<MailIcon className="size-3.5" aria-hidden />
),
type: "text",
className: "w-48",
placeholder: "Search...",
},
{
key: "role",
label: "Role",
icon: (
<ShieldCheckIcon className="size-3.5" aria-hidden />
),
type: "select",
searchable: false,
className: "w-[160px]",
options: ROLE_OPTIONS,
customValueRenderer: (values) => {
const state = renderSelectedCount(values)
if (state) return state
return <RoleBadge role={values[0] as MemberRole} />
},
},
{
key: "status",
label: "Status",
icon: (
<CircleDotIcon className="size-3.5" aria-hidden />
),
type: "select",
searchable: false,
className: "w-[150px]",
options: STATUS_OPTIONS,
customValueRenderer: (values) => {
const state = renderSelectedCount(values)
if (state) return state
return <StatusBadge status={values[0] as MemberStatus} />
},
},
{
key: "teams",
label: "Team",
icon: (
<UsersIcon className="size-3.5" aria-hidden />
),
type: "select",
searchable: true,
className: "w-[190px]",
options: TEAM_LABELS.map((team) => ({
value: team,
label: team,
})),
customValueRenderer: (values) => {
const state = renderSelectedCount(values)
if (state) return state
return String(values[0])
},
},
],
[]
)
const applyFilters = useCallback((newFilters: Filter[]) => {
return applyFiltersToData(MEMBERS, newFilters)
}, [])
const simulateAsyncFiltering = useCallback(
async (newFilters: Filter[]) => {
setIsLoading(true)
await new Promise((resolve) => setTimeout(resolve, 400))
setFilteredData(applyFilters(newFilters))
setIsLoading(false)
},
[applyFilters]
)
const handleFiltersChange = useCallback(
(newFilters: Filter[]) => {
setFilters(newFilters)
const newActive = getActiveFilters(newFilters)
const nextKey = serializeActiveFiltersKey(newActive)
if (nextKey === lastAppliedActiveKey.current) return
lastAppliedActiveKey.current = nextKey
setPagination((prev) => ({ ...prev, pageIndex: 0 }))
setRowSelection({})
simulateAsyncFiltering(newFilters)
},
[simulateAsyncFiltering]
)
useEffect(() => {
if (isInitialLoad.current) {
setFilteredData(applyFilters(filters))
isInitialLoad.current = false
}
}, [filters, applyFilters])
const handleView = useCallback((member: IMember) => {
setActiveMember(member)
setSheetOpen(true)
}, [])
const handleEditRole = useCallback((member: IMember) => {
toast.info("Role editor", {
description: `Update ${member.name} from ${member.role}. Demo only.`,
})
}, [])
const columns = useMemo(
() =>
createMemberColumns({ onEditRole: handleEditRole, onView: handleView }),
[handleEditRole, handleView]
)
const [columnOrder, setColumnOrder] = useState<string[]>(
columns.map((c) => c.id as string)
)
const table = useReactTable({
columns,
data: filteredData,
pageCount: Math.ceil(filteredData.length / pagination.pageSize),
getRowId: (row) => row.id,
state: {
pagination,
sorting,
columnOrder,
columnVisibility,
rowSelection,
},
enableRowSelection: true,
columnResizeMode: "onChange",
onColumnOrderChange: setColumnOrder,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: setPagination,
onSortingChange: setSorting,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
})
const selectedCount = table.getSelectedRowModel().rows.length
const handleClearSelection = useCallback(() => setRowSelection({}), [])
const handleChangeRole = useCallback(() => {
if (selectedCount === 0) return
setRowSelection({})
toast.success("Role updated", {
description: `${selectedCount} member${selectedCount === 1 ? "" : "s"} moved to ${bulkRole}.`,
})
}, [bulkRole, selectedCount])
const handleBulkResend = useCallback(() => {
if (selectedCount === 0) return
setRowSelection({})
toast.info("Invites resent", {
description: `${selectedCount} link${selectedCount === 1 ? "" : "s"} sent. Each expires in 7 days.`,
})
}, [selectedCount])
const handleBulkDeactivate = useCallback(() => {
if (selectedCount === 0) return
setRowSelection({})
toast.message("Members deactivated", {
description: `${selectedCount} member${selectedCount === 1 ? "" : "s"} can no longer sign in.`,
})
}, [selectedCount])
const showClearButton = filters.length > 0
return (
<TooltipProvider delay={200}>
{/* Table */}
<DataGrid
table={table}
isLoading={isLoading}
loadingMode="skeleton"
recordCount={filteredData.length}
emptyMessage={
!isLoading && filteredData.length === 0
? "No members match your filters. Clear filters or adjust operators."
: undefined
}
tableLayout={{
columnsResizable: true,
columnsMovable: true,
columnsVisibility: true,
headerSticky: true,
dense: true,
}}
>
<Frame spacing="sm" className="w-full">
<FrameHeader className="flex-row items-center justify-between gap-3">
<div className="flex flex-col gap-0.5">
<FrameTitle id="page-heading" className="text-balance">
Members
</FrameTitle>
<FrameDescription className="flex items-center gap-1.5 text-xs text-pretty">
<span>68 of 80 seats</span>
<DotSeparator />
<span>5 pending invites</span>
</FrameDescription>
</div>
<Button
type="button"
size="default"
onClick={() =>
toast.info("Invite people", {
description: "Send to acmecloud.com addresses. Demo only.",
})
}
>
<UserPlusIcon aria-hidden="true" />
Invite people
</Button>
</FrameHeader>
<FramePanel className="p-0 shadow-none">
<div className="flex flex-wrap items-center justify-between gap-2 px-(--frame-panel-header-px) py-(--frame-panel-header-py)">
<Filters
filters={filters}
fields={filterFields}
onChange={handleFiltersChange}
size="default"
trigger={
<Button
type="button"
size="default"
variant="outline"
aria-label="Filters"
>
<FilterIcon aria-hidden />
Filters
</Button>
}
/>
<div className="flex flex-wrap items-center gap-2">
{showClearButton && (
<Button
type="button"
size="default"
variant="outline"
className="shrink-0"
onClick={() => {
const next = createDefaultMemberFilters()
lastAppliedActiveKey.current = serializeActiveFiltersKey(
getActiveFilters(next)
)
setFilters(next)
setRowSelection({})
simulateAsyncFiltering(next)
}}
disabled={isLoading}
>
<FunnelXIcon aria-hidden />
Clear
</Button>
)}
<DataGridColumnVisibility
table={table}
trigger={
<Button
type="button"
size="default"
variant="outline"
aria-label="View settings"
>
<Settings2Icon aria-hidden="true" />
View settings
</Button>
}
/>
</div>
</div>
<Separator />
{selectedCount > 0 ? (
<>
<BulkActionBar
selectedCount={selectedCount}
roleValue={bulkRole}
onRoleChange={setBulkRole}
onChangeRole={handleChangeRole}
onResendInvite={handleBulkResend}
onDeactivate={handleBulkDeactivate}
onClear={handleClearSelection}
/>
<Separator />
</>
) : null}
<DataGridScrollArea>
<DataGridTable />
</DataGridScrollArea>
<Separator />
<FrameFooter>
<DataGridPagination sizes={[10, 20, 30]} />
</FrameFooter>
</FramePanel>
</Frame>
</DataGrid>
<MemberDetailSheet
member={activeMember}
open={sheetOpen}
onOpenChange={setSheetOpen}
/>
</TooltipProvider>
)
}
export { MembersGrid as DataGridView }
@@ -1,27 +0,0 @@
"use client"
import { useEffect, useState } from "react"
import { MembersGrid } from "./components/members-grid"
export function Page() {
const [isReady, setIsReady] = useState(false)
useEffect(() => setIsReady(true), [])
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">
Members directory data grid
</h1>
{isReady ? (
<MembersGrid />
) : (
<div className="bg-background min-h-svh w-full" aria-hidden="true" />
)}
</main>
)
}
@@ -1,356 +0,0 @@
"use client"
import * as React from "react"
import { Badge } from "@/components/reui/badge"
import {
Frame,
FrameHeader,
FramePanel,
} from "@/components/reui/frame"
import {
Timeline,
TimelineContent,
TimelineHeader,
TimelineIndicator,
TimelineItem,
TimelineSeparator,
TimelineTitle,
} from "@/components/reui/timeline"
import { toast } from "sonner"
import { cn } from "@authportal/ui/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@authportal/ui/components/avatar"
import { Button } from "@authportal/ui/components/button"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@authportal/ui/components/collapsible"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@authportal/ui/components/empty"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@authportal/ui/components/select"
import { Tabs, TabsList, TabsTrigger } from "@authportal/ui/components/tabs"
import {
AUDIT_DAYS,
FILTER_OPTIONS,
RANGE_OPTIONS,
severityDotClass,
severityLabel,
severityVariant,
type AuditEvent,
type EventType,
} from "./data"
import { ChevronRightIcon, CopyIcon, CalendarIcon, DownloadIcon, FilterIcon } from "lucide-react"
const TOTAL_EVENTS = AUDIT_DAYS.reduce((sum, day) => sum + day.events.length, 0)
function copyValue(value: string) {
if (typeof navigator !== "undefined" && navigator.clipboard) {
void navigator.clipboard.writeText(value).catch(() => undefined)
}
}
// ── Single audit event row (reuses timeline-1 Collapsible-in-Frame grammar) ──
function EventRow({
event,
isLast,
step,
defaultOpen,
}: {
event: AuditEvent
isLast: boolean
step: number
defaultOpen: boolean
}) {
const [open, setOpen] = React.useState(defaultOpen)
return (
<TimelineItem step={step} className={cn("ms-10", isLast ? "pb-0" : "pb-6")}>
<TimelineHeader className="flex min-w-0 items-center justify-between gap-2.5">
<TimelineSeparator className="bg-border! group-data-[orientation=vertical]/timeline:-left-7 group-data-[orientation=vertical]/timeline:h-[calc(100%-1.5rem-0.5rem)] group-data-[orientation=vertical]/timeline:translate-y-7" />
<div className="flex flex-wrap items-center gap-2">
<TimelineTitle className="text-sm font-semibold">
{event.action}
</TimelineTitle>
<Badge variant={severityVariant[event.severity]} className="gap-1.5">
<span
className={cn(
"size-1.5 shrink-0 rounded-full",
severityDotClass[event.severity]
)}
aria-hidden="true"
/>
{severityLabel[event.severity]}
</Badge>
<span className="text-muted-foreground text-xs">{event.time}</span>
</div>
<TimelineIndicator className="border-border bg-background text-muted-foreground flex size-6 items-center justify-center border shadow-xs group-data-[orientation=vertical]/timeline:-left-7 [&_svg]:size-3.5">
{event.icon}
</TimelineIndicator>
</TimelineHeader>
<TimelineContent className="mt-2">
<Frame stacked dense spacing="sm">
<Collapsible
open={open}
onOpenChange={(nextOpen) => setOpen(nextOpen)}
className="group/collapsible"
>
<CollapsibleTrigger
type="button"
className="flex w-full"
aria-label={`Toggle ${event.action} details`}
>
<FrameHeader className="flex grow flex-row items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
<Avatar className="size-5">
<AvatarImage
src={event.actor.avatar}
alt={event.actor.name}
/>
<AvatarFallback className="text-[10px]">
{event.actor.initials}
</AvatarFallback>
</Avatar>
<span className="text-muted-foreground min-w-0 truncate text-sm font-medium">
{event.actor.name}, {event.label}
</span>
</div>
<ChevronRightIcon className="text-muted-foreground size-4 shrink-0 transition-transform duration-200 group-data-open/collapsible:rotate-90" aria-hidden="true" />
</FrameHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<FramePanel className="space-y-3">
<dl className="grid grid-cols-1 gap-2.5 sm:grid-cols-2">
<DetailRow label="Target">
<span className="text-foreground truncate font-medium">
{event.target}
</span>
</DetailRow>
<DetailRow label="Actor">
<span className="text-foreground truncate font-medium">
{event.actor.email}
</span>
</DetailRow>
<DetailRow label="Source IP">
<span className="text-foreground inline-flex min-w-0 items-center gap-2 font-medium tabular-nums">
<span className="truncate">{event.ip}</span>
<span className="text-muted-foreground truncate">
{event.location}
</span>
</span>
</DetailRow>
<DetailRow label="Session">
<span className="text-foreground truncate font-mono text-xs">
{event.detail.sessionId}
</span>
</DetailRow>
</dl>
<p className="text-muted-foreground text-xs leading-5">
{event.detail.reason}
</p>
<div className="flex flex-wrap items-center justify-between gap-2.5 border-t pt-2.5">
<Badge variant="outline" className="gap-1.5 font-mono">
{event.ref}
</Badge>
<Button
type="button"
size="sm"
variant="ghost"
onClick={() => {
copyValue(event.ref)
toast.success("Reference copied", {
description: `${event.ref} is on your clipboard.`,
})
}}
>
<CopyIcon className="opacity-60" aria-hidden="true" />
Copy reference
</Button>
</div>
</FramePanel>
</CollapsibleContent>
</Collapsible>
</Frame>
</TimelineContent>
</TimelineItem>
)
}
function DetailRow({
label,
children,
}: {
label: string
children: React.ReactNode
}) {
return (
<div className="flex min-w-0 flex-col gap-0.5">
<dt className="text-muted-foreground text-xs">{label}</dt>
<dd className="flex min-w-0 items-center text-sm">{children}</dd>
</div>
)
}
export function AuditLogTimeline() {
const [filter, setFilter] = React.useState<string[]>(["All"])
const [range, setRange] = React.useState("24h")
const activeFilter = (filter[0] ?? "All") as EventType | "All"
const visibleDays = React.useMemo(() => {
if (activeFilter === "All") return AUDIT_DAYS
return AUDIT_DAYS.map((day) => ({
...day,
events: day.events.filter((event) => event.type === activeFilter),
})).filter((day) => day.events.length > 0)
}, [activeFilter])
const visibleCount = visibleDays.reduce(
(sum, day) => sum + day.events.length,
0
)
const handleExport = () => {
toast.success("Export ready", {
description: `${visibleCount} events queued as CSV. Link valid for 24 hours.`,
})
}
return (
<section
className="mx-auto w-full max-w-2xl"
aria-labelledby="audit-log-title"
>
{/* ── Content header (title + filter chips + range + export) ── */}
<div className="mb-6 flex flex-col gap-4">
<div className="flex flex-wrap items-end justify-between gap-3">
<div className="min-w-0 space-y-1">
<h1
id="audit-log-title"
className="text-xl font-semibold tracking-tight"
>
Audit Log
</h1>
<p className="text-muted-foreground text-sm leading-5">
{TOTAL_EVENTS} events in Acme Cloud workspace
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<Select
value={range}
onValueChange={(value) => value && setRange(value)}
items={RANGE_OPTIONS}
>
<SelectTrigger size="sm" className="w-40">
<CalendarIcon className="text-muted-foreground size-4" aria-hidden="true" />
<SelectValue />
</SelectTrigger>
<SelectContent align="end" alignItemWithTrigger={false}>
<SelectGroup>
{RANGE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<Button size="sm" type="button" onClick={handleExport}>
<DownloadIcon aria-hidden="true" />
<span className="hidden sm:block">Export CSV</span>
</Button>
</div>
</div>
<Tabs
value={activeFilter}
onValueChange={(value) => value && setFilter([value])}
>
<TabsList
variant="line"
aria-label="Filter by event type"
className="h-10! w-full justify-start gap-6 overflow-x-auto border-b"
>
{FILTER_OPTIONS.map((option) => (
<TabsTrigger
key={option.value}
value={option.value}
className="px-1 text-sm after:-bottom-px!"
>
{option.label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
</div>
{visibleDays.length === 0 ? (
<Empty className="min-h-[280px] border-0 bg-transparent">
<EmptyHeader>
<EmptyMedia variant="icon">
<FilterIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>No {activeFilter} events</EmptyTitle>
<EmptyDescription>
No {activeFilter} events in the last 24 hours. Try another type or
widen the range.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button
type="button"
variant="outline"
onClick={() => setFilter(["All"])}
>
Clear filter
</Button>
</EmptyContent>
</Empty>
) : (
<div className="space-y-8">
{visibleDays.map((day) => (
<div key={day.id} className="space-y-4">
<h2 className="text-muted-foreground text-xs font-semibold tracking-wide uppercase">
{day.date}
</h2>
<Timeline>
{day.events.map((event, index) => (
<EventRow
key={event.id}
event={event}
step={index + 1}
isLast={index === day.events.length - 1}
defaultOpen={day.id === 1 && index < 2}
/>
))}
</Timeline>
</div>
))}
</div>
)}
</section>
)
}
@@ -1,407 +0,0 @@
import type { BadgeProps } from "@/components/reui/badge"
import { CircleCheckIcon, TriangleAlertIcon, ArrowLeftRightIcon, MailIcon, ShieldCheckIcon, UsersIcon, RefreshCwIcon, LogOutIcon, KeyRoundIcon, DatabaseIcon } from "lucide-react"
// ── Audit log world (Acme Cloud workspace) ──
// Severity drives the timeline indicator + the inline severity badge. Event
// type drives the filter chips. Each event carries an actor (avatar + email +
// IP), a target, and an expandable detail block (session id, reason).
export type EventSeverity = "info" | "notice" | "critical"
export type EventType = "Auth" | "Roles" | "SSO/SCIM" | "Sessions" | "API"
export type AuditActor = {
name: string
email: string
avatar: string
initials: string
}
export type AuditEvent = {
id: string
ref: string
type: EventType
action: string
label: string
target: string
severity: EventSeverity
time: string
actor: AuditActor
ip: string
location: string
icon: React.ReactNode
detail: { sessionId: string; reason: string }
}
export type AuditDay = {
id: number
date: string
events: AuditEvent[]
}
export type FilterOption = { value: EventType | "All"; label: string }
export type RangeOption = { value: string; label: string }
// ── Filter chips (event-type) ──
export const FILTER_OPTIONS: FilterOption[] = [
{ value: "All", label: "All" },
{ value: "Auth", label: "Auth" },
{ value: "Roles", label: "Roles" },
{ value: "SSO/SCIM", label: "SSO/SCIM" },
{ value: "Sessions", label: "Sessions" },
{ value: "API", label: "API" },
]
// ── Date-range select ──
export const RANGE_OPTIONS: RangeOption[] = [
{ value: "24h", label: "Last 24 hours" },
{ value: "7d", label: "Last 7 days" },
{ value: "30d", label: "Last 30 days" },
{ value: "90d", label: "Last 90 days" },
]
// ── Severity → badge variant + indicator dot ──
export const severityVariant: Record<EventSeverity, BadgeProps["variant"]> = {
info: "success-outline",
notice: "warning-outline",
critical: "destructive-outline",
}
export const severityLabel: Record<EventSeverity, string> = {
info: "Info",
notice: "Notice",
critical: "Critical",
}
export const severityDotClass: Record<EventSeverity, string> = {
info: "bg-success",
notice: "bg-warning",
critical: "bg-destructive",
}
const MIRA: AuditActor = {
name: "Mira Stone",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=96&h=96&dpr=2&q=80",
initials: "MS",
}
const LEO: AuditActor = {
name: "Leo Grant",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=96&h=96&dpr=2&q=80",
initials: "LG",
}
const SANA: AuditActor = {
name: "Sana Qureshi",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=96&h=96&dpr=2&q=80",
initials: "SQ",
}
const SARAH: AuditActor = {
name: "Sarah Chen",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
initials: "SC",
}
const DAVID: AuditActor = {
name: "David Kim",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
initials: "DK",
}
const KENJI: AuditActor = {
name: "Kenji Tan",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=96&h=96&dpr=2&q=80",
initials: "KT",
}
const OMAR: AuditActor = {
name: "Omar Haddad",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1507591064344-4c6ce005b128?w=96&h=96&dpr=2&q=80",
initials: "OH",
}
const NORA: AuditActor = {
name: "Nora Vale",
email: "[email protected]",
avatar:
"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=96&h=96&dpr=2&q=80",
initials: "NV",
}
const authIcon = (
<CircleCheckIcon className="size-3.5" aria-hidden="true" />
)
const authFailIcon = (
<TriangleAlertIcon className="size-3.5" aria-hidden="true" />
)
const roleIcon = (
<ArrowLeftRightIcon className="size-3.5" aria-hidden="true" />
)
const inviteIcon = (
<MailIcon className="size-3.5" aria-hidden="true" />
)
const ssoIcon = (
<ShieldCheckIcon className="size-3.5" aria-hidden="true" />
)
const scimIcon = (
<UsersIcon className="size-3.5" aria-hidden="true" />
)
const mfaIcon = (
<RefreshCwIcon className="size-3.5" aria-hidden="true" />
)
const sessionIcon = (
<LogOutIcon className="size-3.5" aria-hidden="true" />
)
const apiIcon = (
<KeyRoundIcon className="size-3.5" aria-hidden="true" />
)
const exportIcon = (
<DatabaseIcon className="size-3.5" aria-hidden="true" />
)
// ── Audit events grouped by day (newest first) ──
export const AUDIT_DAYS: AuditDay[] = [
{
id: 1,
date: "Today, Jun 17",
events: [
{
id: "e1",
ref: "evt_9f3a21c8",
type: "Auth",
action: "Login failed",
label: "Password rejected",
target: "[email protected]",
severity: "critical",
time: "2:14 PM",
actor: KENJI,
ip: "192.0.2.51",
location: "Berlin",
icon: authFailIcon,
detail: {
sessionId: "sess_b71e0d44",
reason: "3 failed attempts in 5 minutes, account temporarily locked",
},
},
{
id: "e2",
ref: "evt_71b0a9d2",
type: "Roles",
action: "Role changed",
label: "Member to Admin",
target: "Sana Qureshi",
severity: "notice",
time: "1:02 PM",
actor: LEO,
ip: "192.0.2.14",
location: "San Francisco",
icon: roleIcon,
detail: {
sessionId: "sess_c98a2f10",
reason: "Promotion approved by Mira Stone, scope raised to Write",
},
},
{
id: "e3",
ref: "evt_4c2d80ae",
type: "Sessions",
action: "Session revoked",
label: "Chrome on Windows",
target: "David Kim",
severity: "notice",
time: "11:48 AM",
actor: SARAH,
ip: "192.0.2.22",
location: "Seattle",
icon: sessionIcon,
detail: {
sessionId: "sess_5d1c6b09",
reason: "Revoked from a stale device, last active 14 days ago",
},
},
{
id: "e4",
ref: "evt_2a6f13bb",
type: "Auth",
action: "Login success",
label: "SSO via Okta",
target: "[email protected]",
severity: "info",
time: "9:05 AM",
actor: MIRA,
ip: "192.0.2.14",
location: "San Francisco",
icon: authIcon,
detail: {
sessionId: "sess_a02d7e58",
reason: "Passkey verified, session valid for 12 hours",
},
},
],
},
{
id: 2,
date: "Yesterday, Jun 16",
events: [
{
id: "e5",
ref: "evt_88e1c5f0",
type: "API",
action: "API key created",
label: "Production, ci-deploy",
target: "key_3f9a...c712",
severity: "notice",
time: "6:21 PM",
actor: DAVID,
ip: "192.0.2.31",
location: "Seattle",
icon: apiIcon,
detail: {
sessionId: "sess_7b40e1aa",
reason: "Scopes: deployments:write, logs:read, expires in 90 days",
},
},
{
id: "e6",
ref: "evt_15d7a3e9",
type: "SSO/SCIM",
action: "SSO config changed",
label: "Okta to Microsoft Entra ID",
target: "Acme Cloud workspace",
severity: "critical",
time: "4:37 PM",
actor: MIRA,
ip: "192.0.2.14",
location: "San Francisco",
icon: ssoIcon,
detail: {
sessionId: "sess_e21f9c03",
reason: "Default identity provider switched, 68 members affected",
},
},
{
id: "e7",
ref: "evt_6b094d27",
type: "SSO/SCIM",
action: "SCIM provision",
label: "4 members imported",
target: "Engineering team",
severity: "info",
time: "4:30 PM",
actor: LEO,
ip: "192.0.2.14",
location: "San Francisco",
icon: scimIcon,
detail: {
sessionId: "sess_d4c7b210",
reason: "JIT provisioning from Entra ID, 80 of 80 seats reconciled",
},
},
{
id: "e8",
ref: "evt_33a8e0c1",
type: "Auth",
action: "MFA reset",
label: "Authenticator re-enrolled",
target: "Omar Haddad",
severity: "notice",
time: "2:10 PM",
actor: SARAH,
ip: "192.0.2.22",
location: "Seattle",
icon: mfaIcon,
detail: {
sessionId: "sess_9f0b2d6e",
reason: "Lost device reported, TOTP factor reset by admin",
},
},
{
id: "e9",
ref: "evt_07c4f2a5",
type: "Roles",
action: "Member invited",
label: "Guest, Support Agent",
target: "[email protected]",
severity: "info",
time: "10:55 AM",
actor: MIRA,
ip: "192.0.2.14",
location: "San Francisco",
icon: inviteIcon,
detail: {
sessionId: "sess_1ab39e7c",
reason: "Invite expires in 7 days, scope set to Read",
},
},
],
},
{
id: 3,
date: "Jun 15",
events: [
{
id: "e10",
ref: "evt_5e2b9114",
type: "API",
action: "Data export",
label: "Audit log, CSV",
target: "8,420 events",
severity: "notice",
time: "5:42 PM",
actor: OMAR,
ip: "192.0.2.40",
location: "Toronto",
icon: exportIcon,
detail: {
sessionId: "sess_4c8d1f93",
reason: "Export covered 90 days, download link valid for 24 hours",
},
},
{
id: "e11",
ref: "evt_9012ad6f",
type: "Sessions",
action: "Session revoked",
label: "Safari on iOS",
target: "Nora Vale",
severity: "info",
time: "3:18 PM",
actor: NORA,
ip: "192.0.2.47",
location: "Austin",
icon: sessionIcon,
detail: {
sessionId: "sess_2f7a0c61",
reason: "Signed out of all other devices from account settings",
},
},
{
id: "e12",
ref: "evt_a4f60b38",
type: "Auth",
action: "Login success",
label: "Password, 2FA passed",
target: "[email protected]",
severity: "info",
time: "8:47 AM",
actor: SANA,
ip: "192.0.2.33",
location: "London",
icon: authIcon,
detail: {
sessionId: "sess_88be4d02",
reason: "Security key verified, new device added to trusted list",
},
},
],
},
]
@@ -1,9 +0,0 @@
import { AuditLogTimeline } from "./components/audit-log-timeline"
export function Page() {
return (
<div className="flex min-h-svh w-full items-start justify-center p-4 sm:p-8 md:p-12">
<AuditLogTimeline />
</div>
)
}
-82
View File
@@ -1,82 +0,0 @@
/**
* ReUI Empty + IconStack — Frame-friendly (empty-state-14 / empty-state-12).
* Preview: https://reui.io/preview/base/empty-state-14 · https://reui.io/preview/base/empty-state-12
* Docs: https://reui.io/docs/components/base/icon-stack
*/
import { InboxIcon, type LucideIcon } from 'lucide-react'
import { IconStack } from '@/components/reui/icon-stack'
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@authportal/ui/components/empty'
import { cn } from '@authportal/ui/lib/utils'
import type { ReactNode } from 'react'
interface EmptyStateProps {
icon?: LucideIcon
title: string
description?: string
action?: ReactNode
className?: string
stackedIcon?: boolean
centered?: boolean
}
export function EmptyState({
icon: Icon = InboxIcon,
title,
description,
action,
className,
stackedIcon = true,
centered = true,
}: EmptyStateProps) {
const body = (
<Empty
className={cn(
'max-w-md flex-none border-0 bg-transparent p-0',
!centered && className,
)}
>
<EmptyHeader className="gap-5 text-center">
<EmptyMedia className="mb-0">
{stackedIcon ? (
<IconStack aria-hidden="true" className="h-14 w-12">
<Icon strokeWidth={1.9} aria-hidden="true" className="size-5" />
</IconStack>
) : (
<span className="bg-muted text-muted-foreground flex size-10 items-center justify-center rounded-lg [&_svg]:size-5">
<Icon aria-hidden="true" />
</span>
)}
</EmptyMedia>
<div className="flex flex-col items-center gap-2">
<EmptyTitle className="text-base font-semibold tracking-tight">{title}</EmptyTitle>
{description ? (
<EmptyDescription className="max-w-sm text-sm/relaxed">{description}</EmptyDescription>
) : null}
</div>
</EmptyHeader>
{action ? (
<EmptyContent className="mt-1 items-center justify-center">{action}</EmptyContent>
) : null}
</Empty>
)
if (!centered) return body
return (
<div
className={cn(
'flex w-full flex-1 items-center justify-center py-14 sm:py-16',
className,
)}
>
{body}
</div>
)
}
@@ -34,7 +34,7 @@ import { ensureAuthConfig, setToken } from '@/lib/auth'
import { ApiError } from '@/lib/api-client'
import { login, meQueryKey } from '@/queries/auth'
import { webauthnLogin, webauthnLoginOptions } from '@/queries/webauthn'
import { AuthLogo } from '@/components/blocks/auth-18/components/auth-logo'
import { AuthLogo } from '@/components/auth-logo'
export function PortalLoginForm() {
const navigate = useNavigate()
@@ -1,302 +0,0 @@
import {
createContext,
CSSProperties,
ReactNode,
useContext,
useEffect,
useId,
useMemo,
useRef,
useState,
} from "react"
import { useDataGrid } from "@/components/reui/data-grid/data-grid"
import {
DataGridTableBase,
DataGridTableBody,
DataGridTableBodyRow,
DataGridTableBodyRowCell,
DataGridTableBodyRowSkeleton,
DataGridTableBodyRowSkeletonCell,
DataGridTableEmpty,
DataGridTableFoot,
DataGridTableHead,
DataGridTableHeadRow,
DataGridTableHeadRowCell,
DataGridTableHeadRowCellResize,
DataGridTableRowSpacer,
DataGridTableViewport,
} from "@/components/reui/data-grid/data-grid-table"
import {
closestCenter,
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
UniqueIdentifier,
useSensor,
useSensors,
type DragEndEvent,
type Modifier,
} from "@dnd-kit/core"
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
import {
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import { Cell, flexRender, HeaderGroup, Row } from "@tanstack/react-table"
import { cn } from "@authportal/ui/lib/utils"
import { Button } from "@authportal/ui/components/button"
import { GripHorizontalIcon } from "lucide-react"
// Context to share sortable listeners from row to handle
type SortableContextValue = ReturnType<typeof useSortable>
const SortableRowContext = createContext<Pick<
SortableContextValue,
"attributes" | "listeners"
> | null>(null)
function DataGridTableDndRowHandle({ className }: { className?: string }) {
const context = useContext(SortableRowContext)
if (!context) {
// Fallback if context is not available (shouldn't happen in normal usage)
return (
<Button
variant="ghost"
size="icon-sm"
className={cn(
"size-7 cursor-grab opacity-70 hover:bg-transparent hover:opacity-100 active:cursor-grabbing",
className
)}
aria-label="Drag to reorder row"
disabled
>
<GripHorizontalIcon aria-hidden="true" />
</Button>
)
}
return (
<Button
variant="ghost"
size="icon-sm"
className={cn(
"size-7 cursor-grab opacity-70 hover:bg-transparent hover:opacity-100 active:cursor-grabbing",
className
)}
aria-label="Drag to reorder row"
{...context.attributes}
{...context.listeners}
>
<GripHorizontalIcon aria-hidden="true" />
</Button>
)
}
function DataGridTableDndRow<TData>({ row }: { row: Row<TData> }) {
const {
transform,
transition,
setNodeRef,
isDragging,
attributes,
listeners,
} = useSortable({
id: row.id,
})
const style: CSSProperties = {
transform: CSS.Transform.toString(transform),
transition: transition,
opacity: isDragging ? 0.8 : 1,
zIndex: isDragging ? 1 : 0,
position: "relative",
cursor: isDragging ? "grabbing" : undefined,
}
return (
<SortableRowContext.Provider value={{ attributes, listeners }}>
<DataGridTableBodyRow row={row} dndRef={setNodeRef} dndStyle={style}>
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => {
return (
<DataGridTableBodyRowCell cell={cell} key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</DataGridTableBodyRowCell>
)
})}
</DataGridTableBodyRow>
</SortableRowContext.Provider>
)
}
function DataGridTableDndRows<TData>({
handleDragEnd,
dataIds,
footerContent,
}: {
handleDragEnd: (event: DragEndEvent) => void
dataIds: UniqueIdentifier[]
footerContent?: ReactNode
}) {
const { table, isLoading, props } = useDataGrid()
const pagination = table.getState().pagination
const tableContainerRef = useRef<HTMLDivElement>(null)
const [isDraggingRow, setIsDraggingRow] = useState(false)
const sensors = useSensors(
useSensor(MouseSensor, {}),
useSensor(TouchSensor, {}),
useSensor(KeyboardSensor, {})
)
useEffect(() => {
if (!isDraggingRow) return
const { body, documentElement } = document
const previousBodyCursor = body.style.cursor
const previousDocumentCursor = documentElement.style.cursor
body.style.cursor = "grabbing"
documentElement.style.cursor = "grabbing"
return () => {
body.style.cursor = previousBodyCursor
documentElement.style.cursor = previousDocumentCursor
}
}, [isDraggingRow])
const modifiers = useMemo(() => {
const restrictToTableContainer: Modifier = ({
transform,
draggingNodeRect,
}) => {
if (!tableContainerRef.current || !draggingNodeRect) {
return transform
}
const containerRect = tableContainerRef.current.getBoundingClientRect()
const { x, y } = transform
const minX = containerRect.left - draggingNodeRect.left
const maxX = containerRect.right - draggingNodeRect.right
const minY = containerRect.top - draggingNodeRect.top
const maxY = containerRect.bottom - draggingNodeRect.bottom
return {
...transform,
x: Math.max(minX, Math.min(maxX, x)),
y: Math.max(minY, Math.min(maxY, y)),
}
}
return [restrictToVerticalAxis, restrictToTableContainer]
}, [])
return (
<DndContext
id={useId()}
collisionDetection={closestCenter}
modifiers={modifiers}
onDragCancel={() => setIsDraggingRow(false)}
onDragEnd={(event) => {
setIsDraggingRow(false)
handleDragEnd(event)
}}
onDragStart={() => setIsDraggingRow(true)}
sensors={sensors}
>
<DataGridTableViewport
viewportRef={tableContainerRef}
className={
isDraggingRow
? "relative cursor-grabbing [&_*]:cursor-grabbing!"
: "relative"
}
>
<DataGridTableBase>
<DataGridTableHead>
{table
.getHeaderGroups()
.map((headerGroup: HeaderGroup<TData>, index) => {
return (
<DataGridTableHeadRow key={index} rowId={headerGroup.id}>
{headerGroup.headers.map((header, index) => {
const { column } = header
return (
<DataGridTableHeadRowCell header={header} key={index}>
{header.isPlaceholder ? null : props.tableLayout
?.columnsResizable && column.getCanResize() ? (
<div className="truncate">
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</div>
) : (
flexRender(
header.column.columnDef.header,
header.getContext()
)
)}
{props.tableLayout?.columnsResizable &&
column.getCanResize() && (
<DataGridTableHeadRowCellResize header={header} />
)}
</DataGridTableHeadRowCell>
)
})}
</DataGridTableHeadRow>
)
})}
</DataGridTableHead>
{(props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && (
<DataGridTableRowSpacer />
)}
<DataGridTableBody>
{props.loadingMode === "skeleton" &&
isLoading &&
pagination?.pageSize ? (
Array.from({ length: pagination.pageSize }).map((_, rowIndex) => (
<DataGridTableBodyRowSkeleton key={rowIndex}>
{table.getVisibleFlatColumns().map((column, colIndex) => {
return (
<DataGridTableBodyRowSkeletonCell
column={column}
key={colIndex}
>
{column.columnDef.meta?.skeleton}
</DataGridTableBodyRowSkeletonCell>
)
})}
</DataGridTableBodyRowSkeleton>
))
) : table.getRowModel().rows.length ? (
<SortableContext
items={dataIds}
strategy={verticalListSortingStrategy}
>
{table.getRowModel().rows.map((row: Row<TData>) => {
return <DataGridTableDndRow row={row} key={row.id} />
})}
</SortableContext>
) : (
<DataGridTableEmpty />
)}
</DataGridTableBody>
{footerContent && (
<DataGridTableFoot>{footerContent}</DataGridTableFoot>
)}
</DataGridTableBase>
</DataGridTableViewport>
</DndContext>
)
}
export { DataGridTableDndRowHandle, DataGridTableDndRows }
@@ -1,319 +0,0 @@
"use client"
import {
CSSProperties,
Fragment,
ReactNode,
useEffect,
useId,
useMemo,
useRef,
useState,
} from "react"
import { useDataGrid } from "@/components/reui/data-grid/data-grid"
import {
DataGridTableBase,
DataGridTableBody,
DataGridTableBodyRow,
DataGridTableBodyRowCell,
DataGridTableBodyRowExpandded,
DataGridTableBodyRowSkeleton,
DataGridTableBodyRowSkeletonCell,
DataGridTableEmpty,
DataGridTableFoot,
DataGridTableHead,
DataGridTableHeadRow,
DataGridTableHeadRowCell,
DataGridTableHeadRowCellResize,
DataGridTableRowSpacer,
DataGridTableViewport,
} from "@/components/reui/data-grid/data-grid-table"
import {
closestCenter,
DndContext,
KeyboardSensor,
Modifier,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
type DragEndEvent,
} from "@dnd-kit/core"
import {
horizontalListSortingStrategy,
SortableContext,
useSortable,
} from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import {
Cell,
flexRender,
Header,
HeaderGroup,
Row,
} from "@tanstack/react-table"
import { Button } from "@authportal/ui/components/button"
import { GripVerticalIcon } from "lucide-react"
function DataGridTableDndHeader<TData>({
header,
}: {
header: Header<TData, unknown>
}) {
const { props } = useDataGrid()
const { column } = header
// Check if column ordering is enabled for this column
const canOrder =
(column.columnDef as { enableColumnOrdering?: boolean })
.enableColumnOrdering !== false
const {
attributes,
isDragging,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({
id: header.column.id,
})
const style: CSSProperties = {
opacity: isDragging ? 0.8 : 1,
position: "relative",
transform: CSS.Translate.toString(transform),
transition,
cursor: isDragging ? "grabbing" : undefined,
whiteSpace: "nowrap",
width: props.tableLayout?.columnsResizable
? `calc(var(--header-${header.id}-size) * 1px)`
: header.column.getSize(),
zIndex: isDragging ? 1 : 0,
}
return (
<DataGridTableHeadRowCell
header={header}
dndStyle={style}
dndRef={setNodeRef}
>
<div className="flex items-center justify-start gap-0.5">
{canOrder && (
<Button
size="icon-sm"
variant="ghost"
className={`-ms-2 size-6 ${isDragging ? "cursor-grabbing" : "cursor-grab active:cursor-grabbing"}`}
{...attributes}
{...listeners}
aria-label="Drag to reorder"
>
<GripVerticalIcon className="opacity-60 hover:opacity-100" aria-hidden="true" />
</Button>
)}
<div className="grow">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</div>
{props.tableLayout?.columnsResizable && column.getCanResize() && (
<DataGridTableHeadRowCellResize header={header} />
)}
</div>
</DataGridTableHeadRowCell>
)
}
function DataGridTableDndCell<TData>({ cell }: { cell: Cell<TData, unknown> }) {
const { props } = useDataGrid()
const { isDragging, setNodeRef, transform, transition } = useSortable({
id: cell.column.id,
})
const style: CSSProperties = {
opacity: isDragging ? 0.8 : 1,
position: "relative",
transform: CSS.Translate.toString(transform),
transition,
cursor: isDragging ? "grabbing" : undefined,
width: props.tableLayout?.columnsResizable
? `calc(var(--col-${cell.column.id}-size) * 1px)`
: cell.column.getSize(),
zIndex: isDragging ? 1 : 0,
}
return (
<DataGridTableBodyRowCell cell={cell} dndStyle={style} dndRef={setNodeRef}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</DataGridTableBodyRowCell>
)
}
function DataGridTableDnd<TData>({
handleDragEnd,
footerContent,
}: {
handleDragEnd: (event: DragEndEvent) => void
footerContent?: ReactNode
}) {
const { table, isLoading, props } = useDataGrid()
const pagination = table.getState().pagination
const containerRef = useRef<HTMLDivElement>(null)
const [isDraggingColumn, setIsDraggingColumn] = useState(false)
const sensors = useSensors(
useSensor(MouseSensor, {}),
useSensor(TouchSensor, {}),
useSensor(KeyboardSensor, {})
)
useEffect(() => {
if (!isDraggingColumn) return
const { body, documentElement } = document
const previousBodyCursor = body.style.cursor
const previousDocumentCursor = documentElement.style.cursor
body.style.cursor = "grabbing"
documentElement.style.cursor = "grabbing"
return () => {
body.style.cursor = previousBodyCursor
documentElement.style.cursor = previousDocumentCursor
}
}, [isDraggingColumn])
// Custom modifier to restrict dragging within table bounds with edge offset
const modifiers = useMemo(() => {
const restrictToTableBounds: Modifier = ({
draggingNodeRect,
transform,
}) => {
if (!draggingNodeRect || !containerRef.current) {
return { ...transform, y: 0 }
}
const containerRect = containerRef.current.getBoundingClientRect()
const edgeOffset = 0
const minX = containerRect.left - draggingNodeRect.left - edgeOffset
const maxX =
containerRect.right -
draggingNodeRect.left -
draggingNodeRect.width +
edgeOffset
return {
...transform,
x: Math.min(Math.max(transform.x, minX), maxX),
y: 0, // Lock vertical movement
}
}
return [restrictToTableBounds]
}, [])
return (
<DndContext
collisionDetection={closestCenter}
id={useId()}
modifiers={modifiers}
onDragCancel={() => setIsDraggingColumn(false)}
onDragEnd={(event) => {
setIsDraggingColumn(false)
handleDragEnd(event)
}}
onDragStart={() => setIsDraggingColumn(true)}
sensors={sensors}
>
<DataGridTableViewport
viewportRef={containerRef}
className={
isDraggingColumn
? "relative cursor-grabbing [&_*]:cursor-grabbing!"
: "relative"
}
>
<DataGridTableBase>
<DataGridTableHead>
{table
.getHeaderGroups()
.map((headerGroup: HeaderGroup<TData>, index) => {
return (
<DataGridTableHeadRow key={index} rowId={headerGroup.id}>
<SortableContext
items={table.getState().columnOrder}
strategy={horizontalListSortingStrategy}
>
{headerGroup.headers.map((header) => (
<DataGridTableDndHeader
header={header}
key={header.id}
/>
))}
</SortableContext>
</DataGridTableHeadRow>
)
})}
</DataGridTableHead>
{(props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && (
<DataGridTableRowSpacer />
)}
<DataGridTableBody>
{props.loadingMode === "skeleton" &&
isLoading &&
pagination?.pageSize ? (
Array.from({ length: pagination.pageSize }).map((_, rowIndex) => (
<DataGridTableBodyRowSkeleton key={rowIndex}>
{table.getVisibleFlatColumns().map((column, colIndex) => {
return (
<DataGridTableBodyRowSkeletonCell
column={column}
key={colIndex}
>
{column.columnDef.meta?.skeleton}
</DataGridTableBodyRowSkeletonCell>
)
})}
</DataGridTableBodyRowSkeleton>
))
) : table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row: Row<TData>) => {
return (
<Fragment key={row.id}>
<DataGridTableBodyRow row={row}>
<SortableContext
items={table.getState().columnOrder}
strategy={horizontalListSortingStrategy}
>
{row
.getVisibleCells()
.map((cell: Cell<TData, unknown>) => (
<DataGridTableDndCell cell={cell} key={cell.id} />
))}
</SortableContext>
</DataGridTableBodyRow>
{row.getIsExpanded() && (
<DataGridTableBodyRowExpandded row={row} />
)}
</Fragment>
)
})
) : (
<DataGridTableEmpty />
)}
</DataGridTableBody>
{footerContent && (
<DataGridTableFoot>{footerContent}</DataGridTableFoot>
)}
</DataGridTableBase>
</DataGridTableViewport>
</DndContext>
)
}
export { DataGridTableDnd }
@@ -1,597 +0,0 @@
import {
CSSProperties,
memo,
ReactNode,
useCallback,
useEffect,
useState,
} from "react"
import { useDataGrid } from "@/components/reui/data-grid/data-grid"
import {
DataGridTableBase,
DataGridTableBody,
DataGridTableEmpty,
DataGridTableFillBodyCell,
DataGridTableFillHeadCell,
DataGridTableFoot,
DataGridTableHead,
DataGridTableHeadRow,
DataGridTableHeadRowCell,
DataGridTableHeadRowCellResize,
DataGridTableRenderedRow,
DataGridTableRowSpacer,
DataGridTableViewport,
getDataGridTableMergedHeaderGroups,
getDataGridTableRowSections,
getPinningStyles,
hasDataGridTableRightPinnedColumns,
} from "@/components/reui/data-grid/data-grid-table"
import { Column, flexRender, Row, Table } from "@tanstack/react-table"
import {
useVirtualizer,
VirtualItem,
Virtualizer,
VirtualizerOptions,
} from "@tanstack/react-virtual"
import { cn } from "@authportal/ui/lib/utils"
import { Spinner } from "@authportal/ui/components/spinner"
type DataGridTableVirtualScrollElements = {
containerElement: HTMLDivElement | null
scrollElement: HTMLElement | null
}
type DataGridTableVirtualizerInstance = Virtualizer<
HTMLElement,
HTMLTableRowElement
>
type DataGridTableVirtualizerOptions<TData> = Omit<
VirtualizerOptions<HTMLElement, HTMLTableRowElement>,
"count" | "estimateSize" | "getItemKey" | "getScrollElement"
> & {
estimateSize?: (index: number, row: Row<TData>) => number
getItemKey?: (index: number, row: Row<TData>) => string | number
getScrollElement?: (
elements: DataGridTableVirtualScrollElements
) => HTMLElement | null
}
interface DataGridTableVirtualProps<TData> {
height?: number | string
estimateSize?: number
overscan?: number
footerContent?: ReactNode
renderHeader?: boolean
onFetchMore?: () => void
isFetchingMore?: boolean
hasMore?: boolean
fetchMoreOffset?: number
virtualizerOptions?: DataGridTableVirtualizerOptions<TData>
}
interface VirtualBodyProps<TData> {
table: Table<TData>
topRows: Row<TData>[]
centerRows: Row<TData>[]
bottomRows: Row<TData>[]
virtualItems: VirtualItem[]
totalSize: number
isVirtualizationEnabled: boolean
isInfiniteMode: boolean
isFetchingMore: boolean
hasMore?: boolean
loadingMoreMessage: ReactNode
allRowsLoadedMessage: ReactNode
measureRowRef?: (element: HTMLTableRowElement | null) => void
}
function DataGridTableVirtualPinnedPlaceholderCell<TData>({
column,
}: {
column: Column<TData>
}) {
const { props } = useDataGrid()
const isPinned = column.getIsPinned()
const isLastLeftPinned = isPinned === "left" && column.getIsLastColumn("left")
const isFirstRightPinned =
isPinned === "right" && column.getIsFirstColumn("right")
return (
<td
aria-hidden="true"
style={{
...(props.tableLayout?.columnsPinnable &&
column.getCanPin() &&
getPinningStyles(column)),
...(props.tableLayout?.columnsResizable && {
width: `calc(var(--col-${column.id}-size) * 1px)`,
}),
}}
data-pinned={isPinned || undefined}
data-last-col={
isLastLeftPinned ? "left" : isFirstRightPinned ? "right" : undefined
}
className={cn(
"p-0",
props.tableLayout?.cellBorder && "border-e",
props.tableLayout?.columnsPinnable &&
column.getCanPin() &&
"data-pinned:bg-background data-pinned:isolate [&[data-pinned=left][data-last-col=left]]:shadow-[inset_-1px_0_0_0_var(--border)] [&[data-pinned=right][data-last-col=right]]:shadow-[inset_1px_0_0_0_var(--border)]"
)}
/>
)
}
function DataGridTableVirtualUtilityRow<TData>({
table,
children,
centerCellClassName,
centerCellStyle,
rowClassName,
ariaHidden,
}: {
table: Table<TData>
children: ReactNode
centerCellClassName?: string
centerCellStyle?: CSSProperties
rowClassName?: string
ariaHidden?: boolean
}) {
const { props } = useDataGrid()
const leftVisibleColumns = table.getLeftVisibleLeafColumns()
const centerVisibleColumns = table.getCenterVisibleLeafColumns()
const rightVisibleColumns = table.getRightVisibleLeafColumns()
const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)
return (
<tr aria-hidden={ariaHidden || undefined} className={rowClassName}>
{leftVisibleColumns.map((column) => (
<DataGridTableVirtualPinnedPlaceholderCell
column={column}
key={column.id}
/>
))}
<td
colSpan={Math.max(centerVisibleColumns.length, 1)}
className={centerCellClassName}
style={centerCellStyle}
>
{children}
</td>
{props.tableLayout?.columnsResizable && hasRightPinnedColumns ? (
<DataGridTableFillBodyCell />
) : null}
{rightVisibleColumns.map((column) => (
<DataGridTableVirtualPinnedPlaceholderCell
column={column}
key={column.id}
/>
))}
{props.tableLayout?.columnsResizable && !hasRightPinnedColumns ? (
<DataGridTableFillBodyCell />
) : null}
</tr>
)
}
function DataGridTableVirtualSpacer<TData>({
table,
height,
}: {
table: Table<TData>
height: number
}) {
if (height <= 0) return null
return (
<DataGridTableVirtualUtilityRow
table={table}
ariaHidden
centerCellClassName="p-0"
centerCellStyle={{ height, padding: 0 }}
>
{null}
</DataGridTableVirtualUtilityRow>
)
}
function DataGridTableVirtualStatusRow<TData>({
table,
children,
className,
}: {
table: Table<TData>
children: ReactNode
className?: string
}) {
return (
<DataGridTableVirtualUtilityRow
table={table}
centerCellClassName={cn(
"text-muted-foreground py-4 text-center text-sm",
className
)}
>
{children}
</DataGridTableVirtualUtilityRow>
)
}
function DataGridTableVirtualBody<TData>({
table,
topRows,
centerRows,
bottomRows,
virtualItems,
totalSize,
isVirtualizationEnabled,
isInfiniteMode,
isFetchingMore,
hasMore,
loadingMoreMessage,
allRowsLoadedMessage,
measureRowRef,
}: VirtualBodyProps<TData>) {
const totalRows = topRows.length + centerRows.length + bottomRows.length
if (!totalRows) return <DataGridTableEmpty />
const hasCenterRows = centerRows.length > 0
const showFetchingRow = isInfiniteMode && isFetchingMore
const showCompleteRow = isInfiniteMode && hasMore === false && totalRows > 0
const hasMiddleSection = hasCenterRows || showFetchingRow || showCompleteRow
const leadingSpacerHeight =
isVirtualizationEnabled && hasCenterRows && virtualItems.length > 0
? (virtualItems[0]?.start ?? 0)
: 0
const trailingSpacerHeight =
isVirtualizationEnabled && hasCenterRows && virtualItems.length > 0
? Math.max(
0,
totalSize - (virtualItems[virtualItems.length - 1]?.end ?? 0)
)
: 0
const renderedRows: ReactNode[] = []
topRows.forEach((row, index) => {
renderedRows.push(
<DataGridTableRenderedRow
key={row.id}
row={row}
pinnedBoundary={
index === topRows.length - 1 && hasMiddleSection ? "top" : undefined
}
/>
)
})
if (isVirtualizationEnabled) {
if (leadingSpacerHeight > 0) {
renderedRows.push(
<DataGridTableVirtualSpacer
key="virtual-spacer-start"
table={table}
height={leadingSpacerHeight}
/>
)
}
virtualItems.forEach((virtualRow) => {
const row = centerRows[virtualRow.index]
if (!row) return
renderedRows.push(
<DataGridTableRenderedRow
key={row.id}
row={row}
rowRef={measureRowRef}
/>
)
})
if (trailingSpacerHeight > 0) {
renderedRows.push(
<DataGridTableVirtualSpacer
key="virtual-spacer-end"
table={table}
height={trailingSpacerHeight}
/>
)
}
} else {
centerRows.forEach((row) => {
renderedRows.push(<DataGridTableRenderedRow key={row.id} row={row} />)
})
}
if (showFetchingRow) {
renderedRows.push(
<DataGridTableVirtualStatusRow key="virtual-status-loading" table={table}>
<div className="flex items-center justify-center gap-2">
<Spinner className="size-4 opacity-60" />
{loadingMoreMessage}
</div>
</DataGridTableVirtualStatusRow>
)
}
if (showCompleteRow) {
renderedRows.push(
<DataGridTableVirtualStatusRow
key="virtual-status-complete"
table={table}
className="py-3 text-xs"
>
{allRowsLoadedMessage}
</DataGridTableVirtualStatusRow>
)
}
bottomRows.forEach((row, index) => {
renderedRows.push(
<DataGridTableRenderedRow
key={row.id}
row={row}
pinnedBoundary={
index === 0 && (topRows.length > 0 || hasMiddleSection)
? "bottom"
: undefined
}
/>
)
})
return <>{renderedRows}</>
}
/**
* Memoized virtual body: skip re-renders during active column resize.
* Column widths update via CSS variables on the <table> element,
* so the browser handles width changes without React re-renders.
*/
const MemoizedVirtualBody = memo(
DataGridTableVirtualBody,
(_prev, next) => !!next.table.getState().columnSizingInfo.isResizingColumn
) as typeof DataGridTableVirtualBody
function DataGridTableVirtual<TData>({
height,
estimateSize = 48,
overscan = 10,
footerContent,
renderHeader = true,
onFetchMore,
isFetchingMore = false,
hasMore,
fetchMoreOffset = 0,
virtualizerOptions,
}: DataGridTableVirtualProps<TData>) {
const { table, props } = useDataGrid()
const mergedHeaderGroups = getDataGridTableMergedHeaderGroups(table)
const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)
const { topRows, centerRows, bottomRows } = getDataGridTableRowSections(
table,
props.tableLayout?.rowsPinnable
)
const isInfiniteMode = typeof onFetchMore === "function"
const [viewportElements, setViewportElements] =
useState<DataGridTableVirtualScrollElements>({
containerElement: null,
scrollElement: null,
})
const {
estimateSize: customEstimateSize,
getItemKey: customGetItemKey,
getScrollElement: customGetScrollElement,
measureElement: customMeasureElement,
overscan: customOverscan,
...virtualizerOptionsRest
} = virtualizerOptions ?? {}
const isVirtualizationEnabled = virtualizerOptions?.enabled !== false
const loadingMoreMessage =
props.fetchingMoreMessage || props.loadingMessage || "Loading..."
const allRowsLoadedMessage =
props.allRowsLoadedMessage || "All records loaded"
const handleViewportRef = useCallback((node: HTMLDivElement | null) => {
setViewportElements({
containerElement: node,
scrollElement:
(node?.closest(
'[data-slot="scroll-area-viewport"]'
) as HTMLElement | null) ?? node,
})
}, [])
const usesExternalScrollArea =
viewportElements.scrollElement !== null &&
viewportElements.scrollElement !== viewportElements.containerElement
const resolveScrollElement = useCallback(() => {
if (customGetScrollElement) {
return customGetScrollElement(viewportElements)
}
return viewportElements.scrollElement
}, [customGetScrollElement, viewportElements])
const resolveItemKey = useCallback(
(index: number) => {
const row = centerRows[index]
if (!row) return index
return customGetItemKey?.(index, row) ?? row.id ?? index
},
[centerRows, customGetItemKey]
)
const resolveEstimateSize = useCallback(
(index: number) => {
const row = centerRows[index]
return row
? (customEstimateSize?.(index, row) ?? estimateSize)
: estimateSize
},
[centerRows, customEstimateSize, estimateSize]
)
const virtualizer = useVirtualizer({
count: centerRows.length,
getScrollElement: resolveScrollElement,
getItemKey: resolveItemKey,
estimateSize: resolveEstimateSize,
overscan: customOverscan ?? overscan,
measureElement: customMeasureElement,
...virtualizerOptionsRest,
}) as DataGridTableVirtualizerInstance
const virtualItems = isVirtualizationEnabled
? virtualizer.getVirtualItems()
: []
const totalSize = isVirtualizationEnabled ? virtualizer.getTotalSize() : 0
const measureRowRef =
isVirtualizationEnabled && customMeasureElement
? virtualizer.measureElement
: undefined
const resolvedFetchMoreOffset = Math.max(0, fetchMoreOffset)
useEffect(() => {
if (
!isVirtualizationEnabled ||
!isInfiniteMode ||
hasMore === false ||
isFetchingMore
) {
return
}
const lastItem = virtualItems[virtualItems.length - 1]
if (!lastItem) return
if (lastItem.index >= centerRows.length - 1 - resolvedFetchMoreOffset) {
onFetchMore?.()
}
}, [
centerRows.length,
hasMore,
isFetchingMore,
isInfiniteMode,
isVirtualizationEnabled,
onFetchMore,
resolvedFetchMoreOffset,
virtualItems,
])
return (
<DataGridTableViewport
viewportRef={handleViewportRef}
className={!usesExternalScrollArea ? "block" : undefined}
style={
usesExternalScrollArea
? undefined
: { height, overflow: "auto", position: "relative" }
}
>
<DataGridTableBase>
{renderHeader && (
<DataGridTableHead>
{mergedHeaderGroups.map((headerGroup) => (
<DataGridTableHeadRow key={headerGroup.id} rowId={headerGroup.id}>
{headerGroup.headers
.filter((header) => header.column.getIsPinned() !== "right")
.map((header) => {
const { column } = header
return (
<DataGridTableHeadRowCell header={header} key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
{props.tableLayout?.columnsResizable &&
column.getCanResize() && (
<DataGridTableHeadRowCellResize header={header} />
)}
</DataGridTableHeadRowCell>
)
})}
{props.tableLayout?.columnsResizable &&
hasRightPinnedColumns ? (
<DataGridTableFillHeadCell />
) : null}
{headerGroup.headers
.filter((header) => header.column.getIsPinned() === "right")
.map((header) => {
const { column } = header
return (
<DataGridTableHeadRowCell header={header} key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
{props.tableLayout?.columnsResizable &&
column.getCanResize() && (
<DataGridTableHeadRowCellResize header={header} />
)}
</DataGridTableHeadRowCell>
)
})}
{props.tableLayout?.columnsResizable &&
!hasRightPinnedColumns ? (
<DataGridTableFillHeadCell />
) : null}
</DataGridTableHeadRow>
))}
</DataGridTableHead>
)}
{renderHeader &&
(props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && (
<DataGridTableRowSpacer />
)}
<DataGridTableBody>
<MemoizedVirtualBody
table={table}
topRows={topRows}
centerRows={centerRows}
bottomRows={bottomRows}
virtualItems={virtualItems}
totalSize={totalSize}
isVirtualizationEnabled={isVirtualizationEnabled}
isInfiniteMode={isInfiniteMode}
isFetchingMore={isFetchingMore}
hasMore={hasMore}
loadingMoreMessage={loadingMoreMessage}
allRowsLoadedMessage={allRowsLoadedMessage}
measureRowRef={measureRowRef}
/>
</DataGridTableBody>
{footerContent && (
<DataGridTableFoot>{footerContent}</DataGridTableFoot>
)}
</DataGridTableBase>
</DataGridTableViewport>
)
}
export { DataGridTableVirtual }
export type {
DataGridTableVirtualProps,
DataGridTableVirtualScrollElements,
DataGridTableVirtualizerOptions,
}
@@ -1,9 +0,0 @@
import type { SVGProps } from "react";
const Apple = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} xmlSpace="preserve" viewBox="0 0 814 1000">
<path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z" />
</svg>
);
export { Apple };
@@ -1,12 +0,0 @@
import type { SVGProps } from "react";
const AppleDark = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} xmlSpace="preserve" viewBox="0 0 814 1000">
<path
fill="#fff"
d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"
/>
</svg>
);
export { AppleDark };
-243
View File
@@ -1,243 +0,0 @@
import type { SVGProps } from "react";
const Google = (props: SVGProps<SVGSVGElement>) => (
<svg
{...props}
xmlnsXlink="http://www.w3.org/1999/xlink"
xmlSpace="preserve"
overflow="hidden"
viewBox="0 0 268.152 273.883"
>
<defs>
<linearGradient id="a">
<stop offset="0" stopColor="#0fbc5c" />
<stop offset="1" stopColor="#0cba65" />
</linearGradient>
<linearGradient id="g">
<stop offset=".231" stopColor="#0fbc5f" />
<stop offset=".312" stopColor="#0fbc5f" />
<stop offset=".366" stopColor="#0fbc5e" />
<stop offset=".458" stopColor="#0fbc5d" />
<stop offset=".54" stopColor="#12bc58" />
<stop offset=".699" stopColor="#28bf3c" />
<stop offset=".771" stopColor="#38c02b" />
<stop offset=".861" stopColor="#52c218" />
<stop offset=".915" stopColor="#67c30f" />
<stop offset="1" stopColor="#86c504" />
</linearGradient>
<linearGradient id="h">
<stop offset=".142" stopColor="#1abd4d" />
<stop offset=".248" stopColor="#6ec30d" />
<stop offset=".312" stopColor="#8ac502" />
<stop offset=".366" stopColor="#a2c600" />
<stop offset=".446" stopColor="#c8c903" />
<stop offset=".54" stopColor="#ebcb03" />
<stop offset=".616" stopColor="#f7cd07" />
<stop offset=".699" stopColor="#fdcd04" />
<stop offset=".771" stopColor="#fdce05" />
<stop offset=".861" stopColor="#ffce0a" />
</linearGradient>
<linearGradient id="f">
<stop offset=".316" stopColor="#ff4c3c" />
<stop offset=".604" stopColor="#ff692c" />
<stop offset=".727" stopColor="#ff7825" />
<stop offset=".885" stopColor="#ff8d1b" />
<stop offset="1" stopColor="#ff9f13" />
</linearGradient>
<linearGradient id="b">
<stop offset=".231" stopColor="#ff4541" />
<stop offset=".312" stopColor="#ff4540" />
<stop offset=".458" stopColor="#ff4640" />
<stop offset=".54" stopColor="#ff473f" />
<stop offset=".699" stopColor="#ff5138" />
<stop offset=".771" stopColor="#ff5b33" />
<stop offset=".861" stopColor="#ff6c29" />
<stop offset="1" stopColor="#ff8c18" />
</linearGradient>
<linearGradient id="d">
<stop offset=".408" stopColor="#fb4e5a" />
<stop offset="1" stopColor="#ff4540" />
</linearGradient>
<linearGradient id="c">
<stop offset=".132" stopColor="#0cba65" />
<stop offset=".21" stopColor="#0bb86d" />
<stop offset=".297" stopColor="#09b479" />
<stop offset=".396" stopColor="#08ad93" />
<stop offset=".477" stopColor="#0aa6a9" />
<stop offset=".568" stopColor="#0d9cc6" />
<stop offset=".667" stopColor="#1893dd" />
<stop offset=".769" stopColor="#258bf1" />
<stop offset=".859" stopColor="#3086ff" />
</linearGradient>
<linearGradient id="e">
<stop offset=".366" stopColor="#ff4e3a" />
<stop offset=".458" stopColor="#ff8a1b" />
<stop offset=".54" stopColor="#ffa312" />
<stop offset=".616" stopColor="#ffb60c" />
<stop offset=".771" stopColor="#ffcd0a" />
<stop offset=".861" stopColor="#fecf0a" />
<stop offset=".915" stopColor="#fecf08" />
<stop offset="1" stopColor="#fdcd01" />
</linearGradient>
<linearGradient
xlinkHref="#a"
id="s"
x1="219.7"
x2="254.467"
y1="329.535"
y2="329.535"
gradientUnits="userSpaceOnUse"
/>
<radialGradient
xlinkHref="#b"
id="m"
cx="109.627"
cy="135.862"
r="71.46"
fx="109.627"
fy="135.862"
gradientTransform="matrix(-1.93688 1.043 1.45573 2.55542 290.525 -400.634)"
gradientUnits="userSpaceOnUse"
/>
<radialGradient
xlinkHref="#c"
id="n"
cx="45.259"
cy="279.274"
r="71.46"
fx="45.259"
fy="279.274"
gradientTransform="matrix(-3.5126 -4.45809 -1.69255 1.26062 870.8 191.554)"
gradientUnits="userSpaceOnUse"
/>
<radialGradient
xlinkHref="#d"
id="l"
cx="304.017"
cy="118.009"
r="47.854"
fx="304.017"
fy="118.009"
gradientTransform="matrix(2.06435 0 0 2.59204 -297.679 -151.747)"
gradientUnits="userSpaceOnUse"
/>
<radialGradient
xlinkHref="#e"
id="o"
cx="181.001"
cy="177.201"
r="71.46"
fx="181.001"
fy="177.201"
gradientTransform="matrix(-.24858 2.08314 2.96249 .33417 -255.146 -331.164)"
gradientUnits="userSpaceOnUse"
/>
<radialGradient
xlinkHref="#f"
id="p"
cx="207.673"
cy="108.097"
r="41.102"
fx="207.673"
fy="108.097"
gradientTransform="matrix(-1.2492 1.34326 -3.89684 -3.4257 880.501 194.905)"
gradientUnits="userSpaceOnUse"
/>
<radialGradient
xlinkHref="#g"
id="r"
cx="109.627"
cy="135.862"
r="71.46"
fx="109.627"
fy="135.862"
gradientTransform="matrix(-1.93688 -1.043 1.45573 -2.55542 290.525 838.683)"
gradientUnits="userSpaceOnUse"
/>
<radialGradient
xlinkHref="#h"
id="j"
cx="154.87"
cy="145.969"
r="71.46"
fx="154.87"
fy="145.969"
gradientTransform="matrix(-.0814 -1.93722 2.92674 -.11625 -215.135 632.86)"
gradientUnits="userSpaceOnUse"
/>
<filter
id="q"
width="1.097"
height="1.116"
x="-.048"
y="-.058"
colorInterpolationFilters="sRGB"
>
<feGaussianBlur stdDeviation="1.701" />
</filter>
<filter
id="k"
width="1.033"
height="1.02"
x="-.017"
y="-.01"
colorInterpolationFilters="sRGB"
>
<feGaussianBlur stdDeviation=".242" />
</filter>
<clipPath id="i" clipPathUnits="userSpaceOnUse">
<path d="M371.378 193.24H237.083v53.438h77.167c-1.241 7.563-4.026 15.003-8.105 21.786-4.674 7.773-10.451 13.69-16.373 18.196-17.74 13.498-38.42 16.258-52.783 16.258-36.283 0-67.283-23.286-79.285-54.928-.484-1.149-.805-2.335-1.197-3.507a81.115 81.115 0 0 1-4.101-25.448c0-9.226 1.569-18.057 4.43-26.398 11.285-32.897 42.985-57.467 80.179-57.467 7.481 0 14.685.884 21.517 2.648a77.668 77.668 0 0 1 33.425 18.25l40.834-39.712c-24.839-22.616-57.219-36.32-95.844-36.32-30.878 0-59.386 9.553-82.748 25.7-18.945 13.093-34.483 30.625-44.97 50.985-9.753 18.879-15.094 39.8-15.094 62.294 0 22.495 5.35 43.633 15.103 62.337v.126c10.302 19.857 25.368 36.954 43.678 49.988 15.997 11.386 44.68 26.551 84.031 26.551 22.63 0 42.687-4.051 60.375-11.644 12.76-5.478 24.065-12.622 34.301-21.804 13.525-12.132 24.117-27.139 31.347-44.404 7.23-17.265 11.097-36.79 11.097-57.957 0-9.858-.998-19.87-2.689-28.968Z" />
</clipPath>
</defs>
<g clipPath="url(#i)" transform="matrix(.95792 0 0 .98525 -90.174 -78.856)">
<path
fill="url(#j)"
d="M92.076 219.958c.148 22.14 6.501 44.983 16.117 63.424v.127c6.949 13.392 16.445 23.97 27.26 34.452l65.327-23.67c-12.36-6.235-14.246-10.055-23.105-17.026-9.054-9.066-15.802-19.473-20.004-31.677h-.17l.17-.127c-2.765-8.058-3.037-16.613-3.14-25.503Z"
filter="url(#k)"
/>
<path
fill="url(#l)"
d="M237.083 79.025c-6.456 22.526-3.988 44.421 0 57.161 7.457.006 14.64.888 21.45 2.647a77.662 77.662 0 0 1 33.424 18.25l41.88-40.726c-24.81-22.59-54.667-37.297-96.754-37.332Z"
filter="url(#k)"
/>
<path
fill="url(#m)"
d="M236.943 78.847c-31.67 0-60.91 9.798-84.871 26.359a145.533 145.533 0 0 0-24.332 21.15c-1.904 17.744 14.257 39.551 46.262 39.37 15.528-17.936 38.495-29.542 64.056-29.542l.07.002-1.044-57.335c-.048 0-.093-.004-.14-.004Z"
filter="url(#k)"
/>
<path
fill="url(#n)"
d="m341.475 226.379-28.268 19.285c-1.24 7.562-4.028 15.002-8.107 21.786-4.674 7.772-10.45 13.69-16.373 18.196-17.702 13.47-38.328 16.244-52.687 16.255-14.842 25.102-17.444 37.675 1.043 57.934 22.877-.016 43.157-4.117 61.046-11.796 12.931-5.551 24.388-12.792 34.761-22.097 13.706-12.295 24.442-27.503 31.769-45 7.327-17.497 11.245-37.282 11.245-58.734Z"
filter="url(#k)"
/>
<path
fill="#3086ff"
d="M234.996 191.21v57.498h136.006c1.196-7.874 5.152-18.064 5.152-26.5 0-9.858-.996-21.899-2.687-30.998Z"
filter="url(#k)"
/>
<path
fill="url(#o)"
d="M128.39 124.327c-8.394 9.119-15.564 19.326-21.249 30.364-9.753 18.879-15.094 41.83-15.094 64.324 0 .317.026.627.029.944 4.32 8.224 59.666 6.649 62.456 0-.004-.31-.039-.613-.039-.924 0-9.226 1.57-16.026 4.43-24.367 3.53-10.289 9.056-19.763 16.123-27.926 1.602-2.031 5.875-6.397 7.121-9.016.475-.997-.862-1.557-.937-1.908-.083-.393-1.876-.077-2.277-.37-1.275-.929-3.8-1.414-5.334-1.845-3.277-.921-8.708-2.953-11.725-5.06-9.536-6.658-24.417-14.612-33.505-24.216Z"
filter="url(#k)"
/>
<path
fill="url(#p)"
d="M162.099 155.857c22.112 13.301 28.471-6.714 43.173-12.977l-25.574-52.664a144.74 144.74 0 0 0-26.543 14.504c-12.316 8.512-23.192 18.9-32.176 30.72Z"
filter="url(#q)"
/>
<path
fill="url(#r)"
d="M171.099 290.222c-29.683 10.641-34.33 11.023-37.062 29.29a144.806 144.806 0 0 0 16.792 13.984c15.996 11.386 46.766 26.551 86.118 26.551.046 0 .09-.004.137-.004v-59.157l-.094.002c-14.736 0-26.512-3.843-38.585-10.527-2.977-1.648-8.378 2.777-11.123.799-3.786-2.729-12.9 2.35-16.183-.938Z"
filter="url(#k)"
/>
<path
fill="url(#s)"
d="M219.7 299.023v59.996c5.506.64 11.236 1.028 17.247 1.028 6.026 0 11.855-.307 17.52-.872v-59.748a105.119 105.119 0 0 1-17.477 1.461c-5.932 0-11.7-.686-17.29-1.865Z"
filter="url(#k)"
opacity=".5"
/>
</g>
</svg>
);
export { Google };
@@ -1,37 +0,0 @@
import type { SVGProps } from "react"
const OPENAI_WORDMARK_DARK_PATH = [
"M367.44 153.84c0 52.32 33.6 88.8 80.16 88.8 46.56 0 80.16-36.48 80.16-88.8s-33.6-88.8-80.16-88.8c-46.56 0-80.16 36.48-80.16 88.8Z",
"m129.6 0c0 37.44-20.4 61.68-49.44 61.68s-49.44-24.24-49.44-61.68 20.4-61.68 49.44-61.68 49.44 24.24 49.44 61.68Z",
"M614.27 242.64c35.28 0 55.44-29.76 55.44-65.52 0-35.76-20.16-65.52-55.44-65.52-16.32 0-28.32 6.48-36.24 15.84V114h-28.8v169.2h28.8v-56.4c7.92 9.36 19.92 15.84 36.24 15.84Z",
"m-36.96-69.12c0-23.76 13.44-36.72 31.2-36.72 20.88 0 32.16 16.32 32.16 40.32s-11.28 40.32-32.16 40.32c-17.76 0-31.2-13.2-31.2-36.48v-7.44Z",
"M747.65 242.64c25.2 0 45.12-13.2 54-35.28L776.93 198c-3.84 12.96-15.12 20.16-29.28 20.16-18.48 0-31.44-13.2-33.6-34.8h88.32v-9.6c0-34.56-19.44-62.16-55.92-62.16-36.48 0-60 28.56-60 65.52 0 38.88 25.2 65.52 61.2 65.52Z",
"m-1.44-106.8c18.24 0 26.88 12 27.12 25.92h-57.84c4.32-17.04 15.84-25.92 30.72-25.92Z",
"M823.98 240h28.8v-73.92c0-18 13.2-27.6 26.16-27.6 15.84 0 22.08 11.28 22.08 26.88V240h28.8v-83.04c0-27.12-15.84-45.36-42.24-45.36-16.32 0-27.6 7.44-34.8 15.84V114h-28.8v126Z",
"M1014.17 67.68 948.89 240h30.48l14.64-39.36h74.4l14.88 39.36h30.96l-65.28-172.32h-34.8Z",
"m16.8 34.08 27.36 72h-54.24l26.88-72Z",
"M1163.69 68.18h-30.72V240.5h30.72V68.18Z",
"M297.06 130.97a79.712 79.712 0 0 0-6.85-65.48c-17.46-30.4-52.56-46.04-86.84-38.68A79.747 79.747 0 0 0 143.24 0C108.2-.08 77.11 22.48 66.33 55.82a79.754 79.754 0 0 0-53.31 38.67c-17.59 30.32-13.58 68.54 9.92 94.54a79.712 79.712 0 0 0 6.85 65.48c17.46 30.4 52.56 46.04 86.84 38.68a79.687 79.687 0 0 0 60.13 26.8c35.06.09 66.16-22.49 76.94-55.86a79.754 79.754 0 0 0 53.31-38.67c17.57-30.32 13.55-68.51-9.94-94.51l-.01.02Z",
"M176.78 299.08a59.77 59.77 0 0 1-38.39-13.88c.49-.26 1.34-.73 1.89-1.07l63.72-36.8a10.36 10.36 0 0 0 5.24-9.07v-89.83l26.93 15.55c.29.14.48.42.52.74v74.39c-.04 33.08-26.83 59.9-59.91 59.97Z",
"M47.94 244.05a59.71 59.71 0 0 1-7.15-40.18c.47.28 1.3.79 1.89 1.13l63.72 36.8c3.23 1.89 7.23 1.89 10.47 0l77.79-44.92v31.1c.02.32-.13.63-.38.83L129.87 266c-28.69 16.52-65.33 6.7-81.92-21.95h-.01Z",
"M31.17 104.96c7-12.16 18.05-21.46 31.21-26.29 0 .55-.03 1.52-.03 2.2v73.61c-.02 3.74 1.98 7.21 5.23 9.06l77.79 44.91L118.44 224c-.27.18-.61.21-.91.08l-64.42-37.22c-28.63-16.58-38.45-53.21-21.95-81.89l.01-.01Z",
"m221.26 51.49-77.79-44.92 26.93-15.54c.27-.18.61-.21.91-.08l64.42 37.19c28.68 16.57 38.51 53.26 21.94 81.94a59.94 59.94 0 0 1-31.2 26.28v-75.81c.03-3.74-1.96-7.2-5.2-9.06h-.01Z",
"m26.8-40.34c-.47-.29-1.3-.79-1.89-1.13l-63.72-36.8a10.375 10.375 0 0 0-10.47 0l-77.79 44.92V92c-.02-.32.13-.63.38-.83l64.41-37.16c28.69-16.55 65.37-6.7 81.91 22a59.95 59.95 0 0 1 7.15 40.1h.02Z",
"m-168.51 55.43-26.94-15.55a.943.943 0 0 1-.52-.74V80.86c.02-33.12 26.89-59.96 60.01-59.94 14.01 0 27.57 4.92 38.34 13.88-.49.26-1.33.73-1.89 1.07L116 72.67a10.344 10.344 0 0 0-5.24 9.06l-.04 89.79v.02Z",
"M125.35 140 160 119.99l34.65 20V180L160 200l-34.65-20v-40Z",
].join(" ")
const OpenaiWordmarkDark = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} fill="none" viewBox="0 0 1180 320">
<g fill="#fff" clipPath="url(#a)">
<path d={OPENAI_WORDMARK_DARK_PATH} />
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h1180v320H0z" />
</clipPath>
</defs>
</svg>
)
export { OpenaiWordmarkDark }
@@ -1,30 +0,0 @@
import type { SVGProps } from "react"
const OPENAI_WORDMARK_LIGHT_PATH = [
"M367.44 153.84c0 52.32 33.6 88.8 80.16 88.8s80.16-36.48 80.16-88.8-33.6-88.8-80.16-88.8-80.16 36.48-80.16 88.8z",
"m129.6 0c0 37.44-20.4 61.68-49.44 61.68s-49.44-24.24-49.44-61.68 20.4-61.68 49.44-61.68 49.44 24.24 49.44 61.68z",
"M614.27 242.64c35.28 0 55.44-29.76 55.44-65.52s-20.16-65.52-55.44-65.52c-16.32 0-28.32 6.48-36.24 15.84V114h-28.8v169.2h28.8v-56.4c7.92 9.36 19.92 15.84 36.24 15.84z",
"m-36.96-69.12c0-23.76 13.44-36.72 31.2-36.72 20.88 0 32.16 16.32 32.16 40.32s-11.28 40.32-32.16 40.32c-17.76 0-31.2-13.2-31.2-36.48z",
"M747.65 242.64c25.2 0 45.12-13.2 54-35.28L776.93 198c-3.84 12.96-15.12 20.16-29.28 20.16-18.48 0-31.44-13.2-33.6-34.8h88.32v-9.6c0-34.56-19.44-62.16-55.92-62.16s-60 28.56-60 65.52c0 38.88 25.2 65.52 61.2 65.52z",
"m-1.44-106.8c18.24 0 26.88 12 27.12 25.92h-57.84c4.32-17.04 15.84-25.92 30.72-25.92z",
"M823.98 240h28.8v-73.92c0-18 13.2-27.6 26.16-27.6 15.84 0 22.08 11.28 22.08 26.88V240h28.8v-83.04c0-27.12-15.84-45.36-42.24-45.36-16.32 0-27.6 7.44-34.8 15.84V114h-28.8z",
"M1014.17 67.68 948.89 240h30.48l14.64-39.36h74.4l14.88 39.36h30.96l-65.28-172.32z",
"m16.8 34.08 27.36 72h-54.24z",
"M1163.69 68.18h-30.72V240.5h30.72z",
"M297.06 130.97a79.712 79.712 0 0 0-6.85-65.48c-17.46-30.4-52.56-46.04-86.84-38.68A79.747 79.747 0 0 0 143.24 0C108.2-.08 77.11 22.48 66.33 55.82a79.754 79.754 0 0 0-53.31 38.67c-17.59 30.32-13.58 68.54 9.92 94.54a79.712 79.712 0 0 0 6.85 65.48c17.46 30.4 52.56 46.04 86.84 38.68a79.687 79.687 0 0 0 60.13 26.8c35.06.09 66.16-22.49 76.94-55.86a79.754 79.754 0 0 0 53.31-38.67c17.57-30.32 13.55-68.51-9.94-94.51z",
"M176.78 299.08a59.77 59.77 0 0 1-38.39-13.88c.49-.26 1.34-.73 1.89-1.07l63.72-36.8a10.36 10.36 0 0 0 5.24-9.07v-89.83l26.93 15.55c.29.14.48.42.52.74v74.39c-.04 33.08-26.83 59.9-59.91 59.97z",
"M47.94 244.05a59.71 59.71 0 0 1-7.15-40.18c.47.28 1.3.79 1.89 1.13l63.72 36.8c3.23 1.89 7.23 1.89 10.47 0l77.79-44.92v31.1c.02.32-.13.63-.38.83L129.87 266c-28.69 16.52-65.33 6.7-81.92-21.95z",
"M31.17 104.96c7-12.16 18.05-21.46 31.21-26.29 0 .55-.03 1.52-.03 2.2v73.61c-.02 3.74 1.98 7.21 5.23 9.06l77.79 44.91L118.44 224c-.27.18-.61.21-.91.08l-64.42-37.22c-28.63-16.58-38.45-53.21-21.95-81.89z",
"m221.26 51.49-77.79-44.92 26.93-15.54c.27-.18.61-.21.91-.08l64.42 37.19c28.68 16.57 38.51 53.26 21.94 81.94a59.94 59.94 0 0 1-31.2 26.28v-75.81c.03-3.74-1.96-7.2-5.2-9.06z",
"m26.8-40.34c-.47-.29-1.3-.79-1.89-1.13l-63.72-36.8a10.375 10.375 0 0 0-10.47 0l-77.79 44.92V92c-.02-.32.13-.63.38-.83l64.41-37.16c28.69-16.55 65.37-6.7 81.91 22a59.95 59.95 0 0 1 7.15 40.1z",
"m-168.51 55.43-26.94-15.55a.943.943 0 0 1-.52-.74V80.86c.02-33.12 26.89-59.96 60.01-59.94 14.01 0 27.57 4.92 38.34 13.88-.49.26-1.33.73-1.89 1.07L116 72.67a10.344 10.344 0 0 0-5.24 9.06l-.04 89.79z",
"M125.35 140 160 119.99l34.65 20V180L160 200l-34.65-20z",
].join(" ")
const OpenaiWordmarkLight = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} viewBox="0 0 1180 320">
<path d={OPENAI_WORDMARK_LIGHT_PATH} />
</svg>
)
export { OpenaiWordmarkLight }
@@ -1,32 +0,0 @@
import type { SVGProps } from "react"
const SlackWordmark = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} fill="currentColor" viewBox="0 0 2500 632.6">
<path
fillRule="evenodd"
d="m799.8 498.1 31.2-72.5c33.7 25.2 78.6 38.3 122.9 38.3 32.7 0 53.4-12.6 53.4-31.7-.5-53.4-195.9-11.6-197.4-145.5-.5-68 59.9-120.4 145.5-120.4 50.9 0 101.7 12.6 138 41.3l-29.2 74c-33.2-21.2-74.5-36.3-113.8-36.3-26.7 0-44.3 12.6-44.3 28.7.5 52.4 197.4 23.7 199.4 151.6 0 69.5-58.9 118.4-143.5 118.4-62-.1-118.9-14.7-162.2-45.9m1198.1-98.7c-15.6 27.2-44.8 45.8-78.6 45.8-49.9 0-90.1-40.3-90.1-90.1s40.3-90.1 90.1-90.1c33.7 0 63 18.6 78.6 45.8L2084 263c-32.2-57.4-94.2-96.7-164.7-96.7-104.3 0-188.9 84.6-188.9 188.9s84.6 188.9 188.9 188.9c71 0 132.5-38.8 164.7-96.7zM1148.8 9.6h107.8v527.3h-107.8zm977.5 0v527.3h107.8V378.7L2362 536.9h138L2337.3 349l150.6-175.3h-132L2234 319.2V9.6z"
clipRule="evenodd"
/>
<path d="M1576.9 400.4c-15.6 25.7-47.8 44.8-84.1 44.8-49.9 0-90.1-40.3-90.1-90.1s40.3-90.1 90.1-90.1c36.3 0 68.5 20.1 84.1 46.3zm0-226.6v42.8c-17.6-29.7-61.4-50.4-107.3-50.4-94.7 0-169.2 83.6-169.2 188.4S1374.9 544 1469.6 544c45.8 0 89.6-20.6 107.3-50.4v42.8h107.8V173.8z" />
<g fillRule="evenodd" clipRule="evenodd">
<path
fill="#e01e5a"
d="M133.5 399.9c0 36.8-29.7 66.5-66.5 66.5S.5 436.6.5 399.9s29.7-66.5 66.5-66.5h66.5zm33.2 0c0-36.8 29.7-66.5 66.5-66.5s66.5 29.7 66.5 66.5v166.2c0 36.8-29.7 66.5-66.5 66.5s-66.5-29.7-66.5-66.5z"
/>
<path
fill="#36c5f0"
d="M233.2 133c-36.8 0-66.5-29.7-66.5-66.5S196.4 0 233.2 0s66.5 29.7 66.5 66.5V133zm0 33.7c36.8 0 66.5 29.7 66.5 66.5s-29.7 66.5-66.5 66.5H66.5C29.7 299.7 0 269.9 0 233.2s29.7-66.5 66.5-66.5z"
/>
<path
fill="#2eb67d"
d="M499.6 233.2c0-36.8 29.7-66.5 66.5-66.5s66.5 29.7 66.5 66.5-29.7 66.5-66.5 66.5h-66.5zm-33.2 0c0 36.8-29.7 66.5-66.5 66.5s-66.5-29.7-66.5-66.5V66.5c0-36.8 29.7-66.5 66.5-66.5s66.5 29.7 66.5 66.5z"
/>
<path
fill="#ecb22e"
d="M399.9 499.6c36.8 0 66.5 29.7 66.5 66.5s-29.7 66.5-66.5 66.5-66.5-29.7-66.5-66.5v-66.5zm0-33.2c-36.8 0-66.5-29.7-66.5-66.5s29.7-66.5 66.5-66.5h166.7c36.8 0 66.5 29.7 66.5 66.5s-29.7 66.5-66.5 66.5z"
/>
</g>
</svg>
)
export { SlackWordmark }
@@ -1,12 +0,0 @@
import type { SVGProps } from "react";
const StripeWordmark = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} viewBox="0 0 512 214">
<path
fill="#635bff"
d="M512 110.08c0-36.409-17.636-65.138-51.342-65.138-33.85 0-54.33 28.73-54.33 64.854 0 42.808 24.179 64.426 58.88 64.426 16.925 0 29.725-3.84 39.396-9.244v-28.445c-9.67 4.836-20.764 7.823-34.844 7.823-13.796 0-26.027-4.836-27.591-21.618h69.547c0-1.85.284-9.245.284-12.658m-70.258-13.511c0-16.071 9.814-22.756 18.774-22.756 8.675 0 17.92 6.685 17.92 22.756zm-90.31-51.627c-13.939 0-22.899 6.542-27.876 11.094l-1.85-8.818h-31.288v165.83l35.555-7.537.143-40.249c5.12 3.698 12.657 8.96 25.173 8.96 25.458 0 48.64-20.48 48.64-65.564-.142-41.245-23.609-63.716-48.498-63.716m-8.534 97.991c-8.391 0-13.37-2.986-16.782-6.684l-.143-52.765c3.698-4.124 8.818-6.968 16.925-6.968 12.942 0 21.902 14.506 21.902 33.137 0 19.058-8.818 33.28-21.902 33.28M241.493 36.551l35.698-7.68V0l-35.698 7.538zm0 10.809h35.698v124.444h-35.698zm-38.257 10.524L200.96 47.36h-30.72v124.444h35.556V87.467c8.39-10.951 22.613-8.96 27.022-7.396V47.36c-4.551-1.707-21.191-4.836-29.582 10.524m-71.112-41.386-34.702 7.395-.142 113.92c0 21.05 15.787 36.551 36.836 36.551 11.662 0 20.195-2.133 24.888-4.693V140.8c-4.55 1.849-27.022 8.391-27.022-12.658V77.653h27.022V47.36h-27.022zM35.982 83.484c0-5.546 4.551-7.68 12.09-7.68 10.808 0 24.461 3.272 35.27 9.103V51.484c-11.804-4.693-23.466-6.542-35.27-6.542C19.2 44.942 0 60.018 0 85.192c0 39.252 54.044 32.995 54.044 49.92 0 6.541-5.688 8.675-13.653 8.675-11.804 0-26.88-4.836-38.827-11.378v33.849c13.227 5.689 26.596 8.106 38.827 8.106 29.582 0 49.92-14.648 49.92-40.106-.142-42.382-54.329-34.845-54.329-50.774"
/>
</svg>
);
export { StripeWordmark };
@@ -1,77 +0,0 @@
import type { SVGProps } from "react"
const SupabaseWordmarkDark = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} viewBox="0 0 581 113" fill="none">
<path
d="M151.397 66.7608C151.996 72.3621 157.091 81.9642 171.877 81.9642C184.764 81.9642 190.959 73.7624 190.959 65.7607C190.959 58.559 186.063 52.6577 176.373 50.6571L169.379 49.1569C166.682 48.6568 164.884 47.1565 164.884 44.7559C164.884 41.9552 167.681 39.8549 171.178 39.8549C176.772 39.8549 178.87 43.5556 179.27 46.4564L190.359 43.9558C189.76 38.6546 185.064 29.7527 171.078 29.7527C160.488 29.7527 152.696 37.0543 152.696 45.8561C152.696 52.7576 156.991 58.4591 166.482 60.5594L172.976 62.0598C176.772 62.8599 178.271 64.6605 178.271 66.8609C178.271 69.4615 176.173 71.762 171.777 71.762C165.983 71.762 163.085 68.1611 162.786 64.2602L151.397 66.7608Z"
fill="white"
/>
<path
d="M233.421 80.4639H246.109C245.909 78.7635 245.609 75.3628 245.609 71.5618V31.2529H232.321V59.8592C232.321 65.5606 228.925 69.5614 223.031 69.5614C216.837 69.5614 214.039 65.1604 214.039 59.6592V31.2529H200.752V62.3599C200.752 73.0622 207.545 81.7642 219.434 81.7642C224.628 81.7642 230.325 79.7638 233.022 75.1627C233.022 77.1631 233.221 79.4636 233.421 80.4639Z"
fill="white"
/>
<path
d="M273.076 99.4682V75.663C275.473 78.9636 280.469 81.6644 287.263 81.6644C301.149 81.6644 310.439 70.6617 310.439 55.7584C310.439 41.1553 302.148 30.1528 287.762 30.1528C280.37 30.1528 274.875 33.4534 272.677 37.2544V31.253H259.79V99.4682H273.076ZM297.352 55.8585C297.352 64.6606 291.958 69.7616 285.164 69.7616C278.372 69.7616 272.877 64.5605 272.877 55.8585C272.877 47.1566 278.372 42.0554 285.164 42.0554C291.958 42.0554 297.352 47.1566 297.352 55.8585Z"
fill="white"
/>
<path
d="M317.964 67.0609C317.964 74.7627 324.357 81.8643 334.848 81.8643C342.139 81.8643 346.835 78.4634 349.332 74.5625C349.332 76.463 349.532 79.1635 349.832 80.4639H362.02C361.72 78.7635 361.422 75.2627 361.422 72.6622V48.4567C361.422 38.5545 355.627 29.7527 340.043 29.7527C326.855 29.7527 319.761 38.2544 318.963 45.9562L330.751 48.4567C331.151 44.1558 334.348 40.455 340.141 40.455C345.737 40.455 348.434 43.3556 348.434 46.8564C348.434 48.5568 347.536 49.9572 344.738 50.3572L332.65 52.1576C324.458 53.3579 317.964 58.2589 317.964 67.0609ZM337.644 71.962C333.349 71.962 331.25 69.1614 331.25 66.2608C331.25 62.4599 333.947 60.5594 337.345 60.0594L348.434 58.359V60.5594C348.434 69.2615 343.239 71.962 337.644 71.962Z"
fill="white"
/>
<path
d="M387.703 80.4641V74.4627C390.299 78.6637 395.494 81.6644 402.288 81.6644C416.276 81.6644 425.467 70.5618 425.467 55.6585C425.467 41.0552 417.174 29.9528 402.788 29.9528C395.494 29.9528 390.1 33.1535 387.902 36.6541V8.04785H374.815V80.4641H387.703ZM412.178 55.7584C412.178 64.7605 406.784 69.7616 399.99 69.7616C393.297 69.7616 387.703 64.6606 387.703 55.7584C387.703 46.7564 393.297 41.8554 399.99 41.8554C406.784 41.8554 412.178 46.7564 412.178 55.7584Z"
fill="white"
/>
<path
d="M432.99 67.0609C432.99 74.7627 439.383 81.8643 449.873 81.8643C457.165 81.8643 461.862 78.4634 464.358 74.5625C464.358 76.463 464.559 79.1635 464.858 80.4639H477.046C476.748 78.7635 476.448 75.2627 476.448 72.6622V48.4567C476.448 38.5545 470.653 29.7527 455.068 29.7527C441.881 29.7527 434.788 38.2544 433.989 45.9562L445.776 48.4567C446.177 44.1558 449.374 40.455 455.167 40.455C460.763 40.455 463.46 43.3556 463.46 46.8564C463.46 48.5568 462.561 49.9572 459.763 50.3572L447.676 52.1576C439.484 53.3579 432.99 58.2589 432.99 67.0609ZM452.671 71.962C448.375 71.962 446.276 69.1614 446.276 66.2608C446.276 62.4599 448.973 60.5594 452.371 60.0594L463.46 58.359V60.5594C463.46 69.2615 458.265 71.962 452.671 71.962Z"
fill="white"
/>
<path
d="M485.645 66.7608C486.243 72.3621 491.339 81.9642 506.124 81.9642C519.012 81.9642 525.205 73.7624 525.205 65.7607C525.205 58.559 520.311 52.6577 510.62 50.6571L503.626 49.1569C500.929 48.6568 499.132 47.1565 499.132 44.7559C499.132 41.9552 501.928 39.8549 505.425 39.8549C511.021 39.8549 513.118 43.5556 513.519 46.4564L524.607 43.9558C524.007 38.6546 519.312 29.7527 505.326 29.7527C494.735 29.7527 486.944 37.0543 486.944 45.8561C486.944 52.7576 491.238 58.4591 500.73 60.5594L507.224 62.0598C511.021 62.8599 512.519 64.6605 512.519 66.8609C512.519 69.4615 510.421 71.762 506.025 71.762C500.23 71.762 497.334 68.1611 497.034 64.2602L485.645 66.7608Z"
fill="white"
/>
<path
d="M545.385 50.2571C545.685 45.7562 549.482 40.5549 556.375 40.5549C563.967 40.5549 567.165 45.3561 567.365 50.2571H545.385ZM568.664 63.0601C567.065 67.4609 563.668 70.5617 557.474 70.5617C550.88 70.5617 545.385 65.8606 545.087 59.3593H580.252C580.252 59.159 580.451 57.1587 580.451 55.2582C580.451 39.4547 571.361 29.7527 556.175 29.7527C543.588 29.7527 531.998 39.9548 531.998 55.6584C531.998 72.262 543.886 81.9642 557.374 81.9642C569.462 81.9642 577.255 74.8626 579.753 66.3607L568.664 63.0601Z"
fill="white"
/>
<path
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
fill="url(#paint0_linear)"
/>
<path
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
fill="url(#paint1_linear)"
fillOpacity="0.2"
/>
<path
d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z"
fill="#3ECF8E"
/>
<defs>
<linearGradient
id="paint0_linear"
x1="53.9738"
y1="54.974"
x2="94.1635"
y2="71.8295"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#249361" />
<stop offset="1" stopColor="#3ECF8E" />
</linearGradient>
<linearGradient
id="paint1_linear"
x1="36.1558"
y1="30.578"
x2="54.4844"
y2="65.0806"
gradientUnits="userSpaceOnUse"
>
<stop />
<stop offset="1" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
)
export { SupabaseWordmarkDark }
@@ -1,77 +0,0 @@
import type { SVGProps } from "react"
const SupabaseWordmarkLight = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} viewBox="0 0 581 113" fill="none">
<path
d="M151.397 66.7608C151.996 72.3621 157.091 81.9642 171.877 81.9642C184.764 81.9642 190.959 73.7624 190.959 65.7607C190.959 58.559 186.063 52.6577 176.373 50.6571L169.379 49.1569C166.682 48.6568 164.884 47.1565 164.884 44.7559C164.884 41.9552 167.681 39.8549 171.178 39.8549C176.772 39.8549 178.87 43.5556 179.27 46.4564L190.359 43.9558C189.76 38.6546 185.064 29.7527 171.078 29.7527C160.488 29.7527 152.696 37.0543 152.696 45.8561C152.696 52.7576 156.991 58.4591 166.482 60.5594L172.976 62.0598C176.772 62.8599 178.271 64.6605 178.271 66.8609C178.271 69.4615 176.173 71.762 171.777 71.762C165.983 71.762 163.085 68.1611 162.786 64.2602L151.397 66.7608Z"
fill="#1F1F1F"
/>
<path
d="M233.421 80.4639H246.109C245.909 78.7635 245.609 75.3628 245.609 71.5618V31.2529H232.321V59.8592C232.321 65.5606 228.925 69.5614 223.031 69.5614C216.837 69.5614 214.039 65.1604 214.039 59.6592V31.2529H200.752V62.3599C200.752 73.0622 207.545 81.7642 219.434 81.7642C224.628 81.7642 230.325 79.7638 233.022 75.1627C233.022 77.1631 233.221 79.4636 233.421 80.4639Z"
fill="#1F1F1F"
/>
<path
d="M273.076 99.4682V75.663C275.473 78.9636 280.469 81.6644 287.263 81.6644C301.149 81.6644 310.439 70.6617 310.439 55.7584C310.439 41.1553 302.148 30.1528 287.762 30.1528C280.37 30.1528 274.875 33.4534 272.677 37.2544V31.253H259.79V99.4682H273.076ZM297.352 55.8585C297.352 64.6606 291.958 69.7616 285.164 69.7616C278.372 69.7616 272.877 64.5605 272.877 55.8585C272.877 47.1566 278.372 42.0554 285.164 42.0554C291.958 42.0554 297.352 47.1566 297.352 55.8585Z"
fill="#1F1F1F"
/>
<path
d="M317.964 67.0609C317.964 74.7627 324.357 81.8643 334.848 81.8643C342.139 81.8643 346.835 78.4634 349.332 74.5625C349.332 76.463 349.532 79.1635 349.832 80.4639H362.02C361.72 78.7635 361.422 75.2627 361.422 72.6622V48.4567C361.422 38.5545 355.627 29.7527 340.043 29.7527C326.855 29.7527 319.761 38.2544 318.963 45.9562L330.751 48.4567C331.151 44.1558 334.348 40.455 340.141 40.455C345.737 40.455 348.434 43.3556 348.434 46.8564C348.434 48.5568 347.536 49.9572 344.738 50.3572L332.65 52.1576C324.458 53.3579 317.964 58.2589 317.964 67.0609ZM337.644 71.962C333.349 71.962 331.25 69.1614 331.25 66.2608C331.25 62.4599 333.947 60.5594 337.345 60.0594L348.434 58.359V60.5594C348.434 69.2615 343.239 71.962 337.644 71.962Z"
fill="#1F1F1F"
/>
<path
d="M387.703 80.4641V74.4627C390.299 78.6637 395.494 81.6644 402.288 81.6644C416.276 81.6644 425.467 70.5618 425.467 55.6585C425.467 41.0552 417.174 29.9528 402.788 29.9528C395.494 29.9528 390.1 33.1535 387.902 36.6541V8.04785H374.815V80.4641H387.703ZM412.178 55.7584C412.178 64.7605 406.784 69.7616 399.99 69.7616C393.297 69.7616 387.703 64.6606 387.703 55.7584C387.703 46.7564 393.297 41.8554 399.99 41.8554C406.784 41.8554 412.178 46.7564 412.178 55.7584Z"
fill="#1F1F1F"
/>
<path
d="M432.99 67.0609C432.99 74.7627 439.383 81.8643 449.873 81.8643C457.165 81.8643 461.862 78.4634 464.358 74.5625C464.358 76.463 464.559 79.1635 464.858 80.4639H477.046C476.748 78.7635 476.448 75.2627 476.448 72.6622V48.4567C476.448 38.5545 470.653 29.7527 455.068 29.7527C441.881 29.7527 434.788 38.2544 433.989 45.9562L445.776 48.4567C446.177 44.1558 449.374 40.455 455.167 40.455C460.763 40.455 463.46 43.3556 463.46 46.8564C463.46 48.5568 462.561 49.9572 459.763 50.3572L447.676 52.1576C439.484 53.3579 432.99 58.2589 432.99 67.0609ZM452.671 71.962C448.375 71.962 446.276 69.1614 446.276 66.2608C446.276 62.4599 448.973 60.5594 452.371 60.0594L463.46 58.359V60.5594C463.46 69.2615 458.265 71.962 452.671 71.962Z"
fill="#1F1F1F"
/>
<path
d="M485.645 66.7608C486.243 72.3621 491.339 81.9642 506.124 81.9642C519.012 81.9642 525.205 73.7624 525.205 65.7607C525.205 58.559 520.311 52.6577 510.62 50.6571L503.626 49.1569C500.929 48.6568 499.132 47.1565 499.132 44.7559C499.132 41.9552 501.928 39.8549 505.425 39.8549C511.021 39.8549 513.118 43.5556 513.519 46.4564L524.607 43.9558C524.007 38.6546 519.312 29.7527 505.326 29.7527C494.735 29.7527 486.944 37.0543 486.944 45.8561C486.944 52.7576 491.238 58.4591 500.73 60.5594L507.224 62.0598C511.021 62.8599 512.519 64.6605 512.519 66.8609C512.519 69.4615 510.421 71.762 506.025 71.762C500.23 71.762 497.334 68.1611 497.034 64.2602L485.645 66.7608Z"
fill="#1F1F1F"
/>
<path
d="M545.385 50.2571C545.685 45.7562 549.482 40.5549 556.375 40.5549C563.967 40.5549 567.165 45.3561 567.365 50.2571H545.385ZM568.664 63.0601C567.065 67.4609 563.668 70.5617 557.474 70.5617C550.88 70.5617 545.385 65.8606 545.087 59.3593H580.252C580.252 59.159 580.451 57.1587 580.451 55.2582C580.451 39.4547 571.361 29.7527 556.175 29.7527C543.588 29.7527 531.998 39.9548 531.998 55.6584C531.998 72.262 543.886 81.9642 557.374 81.9642C569.462 81.9642 577.255 74.8626 579.753 66.3607L568.664 63.0601Z"
fill="#1F1F1F"
/>
<path
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
fill="url(#paint0_linear)"
/>
<path
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
fill="url(#paint1_linear)"
fillOpacity="0.2"
/>
<path
d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z"
fill="#3ECF8E"
/>
<defs>
<linearGradient
id="paint0_linear"
x1="53.9738"
y1="54.974"
x2="94.1635"
y2="71.8295"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#249361" />
<stop offset="1" stopColor="#3ECF8E" />
</linearGradient>
<linearGradient
id="paint1_linear"
x1="36.1558"
y1="30.578"
x2="54.4844"
y2="65.0806"
gradientUnits="userSpaceOnUse"
>
<stop />
<stop offset="1" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
)
export { SupabaseWordmarkLight }
+1 -2
View File
@@ -25,6 +25,5 @@
"@authportal/shared": ["../../packages/shared/src/index.ts"]
}
},
"include": ["src"],
"exclude": ["src/components/blocks/**"]
"include": ["src"]
}