diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
index f38a443305..074b3c2590 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
@@ -5,13 +5,13 @@ import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation";
import { shortenId } from "@/lib/shorten-id";
import type { KeyDetails } from "@/lib/trpc/routers/api/keys/query-api-keys/schema";
import { BookBookmark, Dots, Focus, Key } from "@unkey/icons";
+import { HiddenValueCell } from "@unkey/ui";
import { Button, Checkbox, Empty, InfoTooltip, Loading } from "@unkey/ui";
import { cn } from "@unkey/ui/src/lib/utils";
import dynamic from "next/dynamic";
import Link from "next/link";
import React, { useCallback, useMemo, useState } from "react";
import { VerificationBarChart } from "./components/bar-chart";
-import { HiddenValueCell } from "./components/hidden-value";
import { LastUsedCell } from "./components/last-used";
import { SelectionControls } from "./components/selection-controls";
import {
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/README.md b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/README.md
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/README.md
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/README.md
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/expandable-category.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/expandable-category.tsx
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/expandable-category.tsx
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/expandable-category.tsx
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/highlighted-text.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/highlighted-text.tsx
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/highlighted-text.tsx
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/highlighted-text.tsx
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/permission-badge-list.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/permission-badge-list.tsx
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/permission-badge-list.tsx
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/permission-badge-list.tsx
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/permission-list.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/permission-list.tsx
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/permission-list.tsx
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/permission-list.tsx
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/permission-sheet.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/permission-sheet.tsx
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/permission-sheet.tsx
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/permission-sheet.tsx
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/permission-toggle.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/permission-toggle.tsx
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/permission-toggle.tsx
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/permission-toggle.tsx
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/search-input.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/search-input.tsx
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/search-input.tsx
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/search-input.tsx
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/search-permissions.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/search-permissions.tsx
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/components/search-permissions.tsx
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/components/search-permissions.tsx
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/constants.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/constants.ts
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/constants.ts
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/constants.ts
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/create-rootkey-button.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/create-rootkey-button.tsx
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/create-rootkey-button.tsx
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/create-rootkey-button.tsx
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/hooks/use-permission-sheet.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/hooks/use-permission-sheet.ts
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/hooks/use-permission-sheet.ts
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/hooks/use-permission-sheet.ts
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/hooks/use-permissions.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/hooks/use-permissions.ts
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/hooks/use-permissions.ts
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/hooks/use-permissions.ts
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/hooks/use-root-key-dialog.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/hooks/use-root-key-dialog.ts
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/hooks/use-root-key-dialog.ts
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/hooks/use-root-key-dialog.ts
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/hooks/use-root-key-success.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/hooks/use-root-key-success.ts
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/hooks/use-root-key-success.ts
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/hooks/use-root-key-success.ts
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/permissions.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/permissions.ts
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/permissions.ts
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/permissions.ts
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/root-key-dialog.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/root-key-dialog.tsx
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/root-key-dialog.tsx
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/root-key-dialog.tsx
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/root-key-success.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/root-key-success.tsx
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/root-key-success.tsx
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/root-key-success.tsx
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/utils/permissions.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/utils/permissions.ts
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/root-key/utils/permissions.ts
rename to web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/dialog/utils/permissions.ts
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/hooks/use-root-keys-list-query.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/hooks/use-root-keys-list-query.ts
deleted file mode 100644
index 6a2e3816d5..0000000000
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/hooks/use-root-keys-list-query.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { trpc } from "@/lib/trpc/client";
-import type { RootKey } from "@/lib/trpc/routers/settings/root-keys/query";
-import { useEffect, useMemo, useState } from "react";
-import { rootKeysFilterFieldConfig, rootKeysListFilterFieldNames } from "../../../filters.schema";
-import { useFilters } from "../../../hooks/use-filters";
-import type { RootKeysQueryPayload } from "../query-logs.schema";
-
-export function useRootKeysListQuery() {
- const [totalCount, setTotalCount] = useState(0);
- const [rootKeysMap, setRootKeysMap] = useState(() => new Map());
- const { filters } = useFilters();
-
- const rootKeys = useMemo(() => Array.from(rootKeysMap.values()), [rootKeysMap]);
-
- const queryParams = useMemo(() => {
- const params: RootKeysQueryPayload = {
- ...Object.fromEntries(rootKeysListFilterFieldNames.map((field) => [field, []])),
- };
-
- filters.forEach((filter) => {
- if (!rootKeysListFilterFieldNames.includes(filter.field) || !params[filter.field]) {
- return;
- }
-
- const fieldConfig = rootKeysFilterFieldConfig[filter.field];
- const validOperators = fieldConfig.operators;
- if (!validOperators.includes(filter.operator)) {
- throw new Error("Invalid operator");
- }
-
- if (typeof filter.value === "string") {
- params[filter.field]?.push({
- operator: filter.operator,
- value: filter.value,
- });
- }
- });
-
- return params;
- }, [filters]);
-
- const {
- data: rootKeyData,
- hasNextPage,
- fetchNextPage,
- isFetchingNextPage,
- isLoading: isLoadingInitial,
- } = trpc.settings.rootKeys.query.useInfiniteQuery(queryParams, {
- getNextPageParam: (lastPage) => lastPage.nextCursor,
- staleTime: Number.POSITIVE_INFINITY,
- refetchOnMount: false,
- refetchOnWindowFocus: false,
- });
-
- useEffect(() => {
- if (rootKeyData) {
- const newMap = new Map();
-
- rootKeyData.pages.forEach((page) => {
- page.keys.forEach((rootKey) => {
- // Use slug as the unique identifier
- newMap.set(rootKey.id, rootKey);
- });
- });
-
- if (rootKeyData.pages.length > 0) {
- setTotalCount(rootKeyData.pages[0].total);
- }
-
- setRootKeysMap(newMap);
- }
- }, [rootKeyData]);
-
- return {
- rootKeys,
- isLoading: isLoadingInitial,
- hasMore: hasNextPage,
- loadMore: fetchNextPage,
- isLoadingMore: isFetchingNextPage,
- totalCount,
- };
-}
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/root-keys-list.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/root-keys-list.tsx
index 0f4a4b81b8..5ac45a16e2 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/root-keys-list.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/root-keys-list.tsx
@@ -1,58 +1,43 @@
"use client";
-import { HiddenValueCell } from "@/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/hidden-value";
-import { VirtualTable } from "@/components/virtual-table/index";
-import type { Column } from "@/components/virtual-table/types";
+import {
+ createRootKeyColumns,
+ getRowClassName,
+ renderRootKeySkeletonRow,
+ useRootKeysListPaginated,
+} from "@/components/root-keys-table";
import type { RootKey } from "@/lib/trpc/routers/settings/root-keys/query";
-import { cn } from "@/lib/utils";
-import { BookBookmark, Dots, Key2 } from "@unkey/icons";
import type { UnkeyPermission } from "@unkey/rbac";
import { unkeyPermissionValidation } from "@unkey/rbac";
-import { Empty, InfoTooltip, TimestampInfo, buttonVariants } from "@unkey/ui";
-import dynamic from "next/dynamic";
+import { DataTable, EmptyRootKeys, PaginationFooter } from "@unkey/ui";
import { useCallback, useMemo, useState } from "react";
-import { RootKeyDialog } from "../root-key/root-key-dialog";
+import { RootKeyDialog } from "../dialog/root-key-dialog";
// Type guard function to check if a string is a valid UnkeyPermission
const isUnkeyPermission = (permissionName: string): permissionName is UnkeyPermission => {
const result = unkeyPermissionValidation.safeParse(permissionName);
return result.success;
};
-import { AssignedItemsCell } from "./components/assigned-items-cell";
-import { LastUpdated } from "./components/last-updated";
-import {
- ActionColumnSkeleton,
- CreatedAtColumnSkeleton,
- KeyColumnSkeleton,
- LastUpdatedColumnSkeleton,
- PermissionsColumnSkeleton,
- RootKeyColumnSkeleton,
-} from "./components/skeletons";
-import { useRootKeysListQuery } from "./hooks/use-root-keys-list-query";
-import { getRowClassName } from "./utils/get-row-class";
-const RootKeysTableActions = dynamic(
- () =>
- import("./components/actions/root-keys-table-action.popover.constants").then(
- (mod) => mod.RootKeysTableActions,
- ),
- {
- loading: () => (
-
-
-
- ),
- },
-);
+const TABLE_CONFIG = {
+ rowHeight: 40,
+ layout: "grid" as const,
+ rowBorders: true,
+ containerPadding: "px-0",
+};
export const RootKeysList = () => {
- const { rootKeys, isLoading, isLoadingMore, loadMore, totalCount, hasMore } =
- useRootKeysListQuery();
+ const {
+ rootKeys,
+ isInitialLoading,
+ isFetching,
+ totalCount,
+ onPageChange,
+ page,
+ pageSize,
+ totalPages,
+ sorting,
+ onSortingChange,
+ } = useRootKeysListPaginated();
const [selectedRootKey, setSelectedRootKey] = useState(null);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [editingKey, setEditingKey] = useState(null);
@@ -62,116 +47,23 @@ export const RootKeysList = () => {
setEditDialogOpen(true);
}, []);
- // Memoize the selected root key ID to prevent unnecessary re-renders
const selectedRootKeyId = selectedRootKey?.id;
- // Memoize the row click handler
- const handleRowClick = useCallback((rootKey: RootKey) => {
- setEditingKey(rootKey);
- setSelectedRootKey(rootKey);
- setEditDialogOpen(true);
+ const handleRowClick = useCallback((rootKey: RootKey | null) => {
+ if (rootKey) {
+ setEditingKey(rootKey);
+ setSelectedRootKey(rootKey);
+ setEditDialogOpen(true);
+ } else {
+ setSelectedRootKey(null);
+ }
}, []);
- // Memoize the row className function
const getRowClassNameMemoized = useCallback(
(rootKey: RootKey) => getRowClassName(rootKey, selectedRootKey),
[selectedRootKey],
);
- // Memoize the loadMoreFooterProps to prevent unnecessary re-renders
- const loadMoreFooterProps = useMemo(
- () => ({
- hide: isLoading,
- buttonText: "Load more root keys",
- hasMore,
- countInfoText: (
-
- Showing {" "}
- {new Intl.NumberFormat().format(rootKeys.length)}
- of
- {new Intl.NumberFormat().format(totalCount)}
- root keys
-
- ),
- }),
- [isLoading, hasMore, rootKeys.length, totalCount],
- );
-
- // Memoize the emptyState to prevent unnecessary re-renders
- const emptyState = useMemo(
- () => (
-
-
-
- No Root Keys Found
-
- There are no root keys configured yet. Create your first root key to start managing
- permissions and access control.
-
-
-
-
-
- Learn about Root Keys
-
-
-
-
-
- ),
- [],
- );
-
- // Memoize the config to prevent unnecessary re-renders
- const config = useMemo(
- () => ({
- rowHeight: 52,
- layoutMode: "grid" as const,
- rowBorders: true,
- containerPadding: "px-0",
- }),
- [],
- );
-
- // Memoize the keyExtractor to prevent unnecessary re-renders
- const keyExtractor = useCallback((rootKey: RootKey) => rootKey.id, []);
-
- // Memoize the renderSkeletonRow function to prevent unnecessary re-renders
- const renderSkeletonRow = useCallback(
- ({
- columns,
- rowHeight,
- }: {
- columns: Column[];
- rowHeight: number;
- }) =>
- columns.map((column) => (
-
- {column.key === "root_key" && }
- {column.key === "key" && }
- {column.key === "created_at" && }
- {column.key === "permissions" && }
- {column.key === "last_updated" && }
- {column.key === "action" && }
-
- )),
- [],
- );
-
- // Memoize the existingKey object to prevent unnecessary re-renders
const existingKey = useMemo(() => {
if (!editingKey) {
return null;
@@ -188,131 +80,38 @@ export const RootKeysList = () => {
};
}, [editingKey]);
- const columns: Column[] = useMemo(
- () => [
- {
- key: "root_key",
- header: "Name",
- width: "15%",
- headerClassName: "pl-[18px]",
- render: (rootKey) => {
- const isSelected = rootKey.id === selectedRootKeyId;
- const iconContainer = (
-
-
-
- );
- return (
-
-
- {iconContainer}
-
-
- {rootKey.name ?? "Unnamed Root Key"}
-
-
-
-
- );
- },
- },
- {
- key: "key",
- header: "Key",
- width: "15%",
- render: (rootKey) => (
-
- This is the first part of the key to visually match it. We don't store the full key
- for security reasons.
-
- }
- >
-
-
- ),
- },
- {
- key: "permissions",
- header: "Permissions",
- width: "15%",
- render: (rootKey) => (
-
- ),
- },
- {
- key: "created_at",
- header: "Created At",
- width: "20%",
- render: (rootKey) => {
- return (
-
- );
- },
- },
- {
- key: "last_updated",
- header: "Last Updated",
- width: "20%",
- render: (rootKey) => {
- return (
-
- );
- },
- },
- {
- key: "action",
- header: "",
- width: "auto",
- render: (rootKey) => {
- return ;
- },
- },
- ],
+ const columns = useMemo(
+ () => createRootKeyColumns({ selectedRootKeyId, onEditKey: handleEditKey }),
[selectedRootKeyId, handleEditKey],
);
+ const isNavigating = isFetching && !isInitialLoading;
+
return (
<>
- rootKey.id}
+ isLoading={isInitialLoading}
onRowClick={handleRowClick}
selectedItem={selectedRootKey}
- keyExtractor={keyExtractor}
rowClassName={getRowClassNameMemoized}
- loadMoreFooterProps={loadMoreFooterProps}
- emptyState={emptyState}
- config={config}
- renderSkeletonRow={renderSkeletonRow}
+ emptyState={ }
+ config={TABLE_CONFIG}
+ renderSkeletonRow={renderRootKeySkeletonRow}
+ sorting={sorting}
+ onSortingChange={onSortingChange}
+ />
+
{editingKey && existingKey && (
{
- const style = STATUS_STYLES;
- const isSelected = log.id === selectedRow?.id;
-
- return cn(
- style.base,
- style.hover,
- "group rounded-sm",
- "focus:outline-hidden focus:ring-1 focus:ring-opacity-40",
- style.focusRing,
- isSelected && style.selected,
- );
-};
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/navigation.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/navigation.tsx
index d0ed54c1db..6c93d91937 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/navigation.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/navigation.tsx
@@ -4,7 +4,7 @@ import { Navbar } from "@/components/navigation/navbar";
import { ChevronExpandY, Gear } from "@unkey/icons";
import { Badge, Button, CopyButton } from "@unkey/ui";
import Link from "next/link";
-import { CreateRootKeyButton } from "./components/root-key/create-rootkey-button";
+import { CreateRootKeyButton } from "./components/dialog/create-rootkey-button";
const settingsNavbar = [
{
diff --git a/web/apps/dashboard/components/logs/checkbox/filter-item.tsx b/web/apps/dashboard/components/logs/checkbox/filter-item.tsx
index 62308d6ac3..158f560f69 100644
--- a/web/apps/dashboard/components/logs/checkbox/filter-item.tsx
+++ b/web/apps/dashboard/components/logs/checkbox/filter-item.tsx
@@ -33,11 +33,6 @@ export const FilterItem = ({
const itemRef = useRef(null); // Ref for the trigger div
const contentRef = useRef(null); // Ref for the DroverContent
- // Synchronize internal open state with the parent's isActive prop
- useEffect(() => {
- setOpen(isActive ?? false);
- }, [isActive]);
-
// Focus the trigger div when parent indicates it's focused in the main list
// biome-ignore lint/correctness/useExhaustiveDependencies: no need to react for label
useEffect(() => {
@@ -129,7 +124,6 @@ export const FilterItem = ({
)}
diff --git a/web/apps/dashboard/components/root-keys-table/columns/create-root-key-columns.tsx b/web/apps/dashboard/components/root-keys-table/columns/create-root-key-columns.tsx
new file mode 100644
index 0000000000..4da49d9de9
--- /dev/null
+++ b/web/apps/dashboard/components/root-keys-table/columns/create-root-key-columns.tsx
@@ -0,0 +1,160 @@
+import type { RootKey } from "@/lib/trpc/routers/settings/root-keys/query";
+import { cn } from "@/lib/utils";
+import type { DataTableColumnDef } from "@unkey/ui";
+import {
+ AssignedItemsCell,
+ HiddenValueCell,
+ LastUpdatedCell,
+ RootKeyNameCell,
+ RowActionSkeleton,
+ SortableHeader,
+} from "@unkey/ui";
+import { InfoTooltip, TimestampInfo } from "@unkey/ui";
+import dynamic from "next/dynamic";
+
+const RootKeysTableActions = dynamic(
+ () =>
+ import("../components/settings-root-keys/root-keys-table-action.popover").then(
+ (mod) => mod.RootKeysTableActions,
+ ),
+ {
+ loading: () => ,
+ },
+);
+
+export const ROOT_KEY_COLUMN_IDS = {
+ ROOT_KEY: "root_key",
+ KEY: "key",
+ PERMISSIONS: "permissions",
+ CREATED_AT: "created_at",
+ LAST_UPDATED: "last_updated",
+ ACTION: "action",
+} as const;
+
+type CreateRootKeyColumnsOptions = {
+ selectedRootKeyId?: string;
+ onEditKey: (rootKey: RootKey) => void;
+};
+
+export const createRootKeyColumns = ({
+ selectedRootKeyId,
+ onEditKey,
+}: CreateRootKeyColumnsOptions): DataTableColumnDef[] => [
+ {
+ id: ROOT_KEY_COLUMN_IDS.ROOT_KEY,
+ accessorKey: "name",
+ header: ({ header }) => (
+
+ Name
+
+ ),
+ meta: {
+ width: "20%",
+ headerClassName: "pl-[18px]",
+ },
+ cell: ({ row }) => {
+ const rootKey = row.original;
+ const isSelected = rootKey.id === selectedRootKeyId;
+ return ;
+ },
+ },
+ {
+ id: ROOT_KEY_COLUMN_IDS.KEY,
+ accessorKey: "start",
+ header: "Key",
+ enableSorting: false,
+ meta: {
+ width: "18%",
+ },
+ cell: ({ row }) => {
+ const rootKey = row.original;
+ return (
+
+ This is the first part of the key to visually match it. We don't store the full
+ key for security reasons.
+
+ }
+ >
+
+
+ );
+ },
+ },
+ {
+ id: ROOT_KEY_COLUMN_IDS.PERMISSIONS,
+ header: "Permissions",
+ enableSorting: false,
+ meta: {
+ width: "15%",
+ },
+ cell: ({ row }) => {
+ const rootKey = row.original;
+ return (
+
+ );
+ },
+ },
+ {
+ id: ROOT_KEY_COLUMN_IDS.CREATED_AT,
+ accessorKey: "createdAt",
+ sortDescFirst: true,
+ header: ({ header }) => (
+
+ Created At
+
+ ),
+ meta: {
+ width: "14%",
+ },
+ cell: ({ row }) => {
+ const rootKey = row.original;
+ return (
+
+ );
+ },
+ },
+ {
+ id: ROOT_KEY_COLUMN_IDS.LAST_UPDATED,
+ accessorKey: "lastUpdatedAt",
+ header: ({ header }) => (
+
+ Last Updated
+
+ ),
+ meta: {
+ width: "20%",
+ },
+ cell: ({ row }) => {
+ const rootKey = row.original;
+ return (
+
+ );
+ },
+ },
+ {
+ id: ROOT_KEY_COLUMN_IDS.ACTION,
+ header: "",
+ meta: {
+ width: "auto",
+ },
+ cell: ({ row }) => {
+ const rootKey = row.original;
+ return ;
+ },
+ },
+];
diff --git a/web/apps/dashboard/components/root-keys-table/columns/index.ts b/web/apps/dashboard/components/root-keys-table/columns/index.ts
new file mode 100644
index 0000000000..3dd5d2a816
--- /dev/null
+++ b/web/apps/dashboard/components/root-keys-table/columns/index.ts
@@ -0,0 +1 @@
+export { createRootKeyColumns, ROOT_KEY_COLUMN_IDS } from "./create-root-key-columns";
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/actions/components/delete-root-key.tsx b/web/apps/dashboard/components/root-keys-table/components/settings-root-keys/delete-root-key.tsx
similarity index 98%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/actions/components/delete-root-key.tsx
rename to web/apps/dashboard/components/root-keys-table/components/settings-root-keys/delete-root-key.tsx
index 3d22e116f3..8b7e23ae4d 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/actions/components/delete-root-key.tsx
+++ b/web/apps/dashboard/components/root-keys-table/components/settings-root-keys/delete-root-key.tsx
@@ -6,7 +6,7 @@ import { Button, ConfirmPopover, DialogContainer, FormCheckbox } from "@unkey/ui
import { useRef, useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
-import { useDeleteRootKey } from "./hooks/use-delete-root-key";
+import { useDeleteRootKey } from "../../hooks/use-delete-root-key";
import { RootKeyInfo } from "./root-key-info";
const deleteRootKeyFormSchema = z.object({
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/actions/components/root-key-info.tsx b/web/apps/dashboard/components/root-keys-table/components/settings-root-keys/root-key-info.tsx
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/actions/components/root-key-info.tsx
rename to web/apps/dashboard/components/root-keys-table/components/settings-root-keys/root-key-info.tsx
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/actions/root-keys-table-action.popover.constants.tsx b/web/apps/dashboard/components/root-keys-table/components/settings-root-keys/root-keys-table-action.popover.tsx
similarity index 94%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/actions/root-keys-table-action.popover.constants.tsx
rename to web/apps/dashboard/components/root-keys-table/components/settings-root-keys/root-keys-table-action.popover.tsx
index 6cf8ae5111..7b34b0a480 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/actions/root-keys-table-action.popover.constants.tsx
+++ b/web/apps/dashboard/components/root-keys-table/components/settings-root-keys/root-keys-table-action.popover.tsx
@@ -2,7 +2,7 @@
import { type MenuItem, TableActionPopover } from "@/components/logs/table-action.popover";
import type { RootKey } from "@/lib/trpc/routers/settings/root-keys/query";
import { PenWriting3, Trash } from "@unkey/icons";
-import { DeleteRootKey } from "./components/delete-root-key";
+import { DeleteRootKey } from "./delete-root-key";
type RootKeysTableActionsProps = {
rootKey: RootKey;
diff --git a/web/apps/dashboard/components/root-keys-table/components/skeletons/index.ts b/web/apps/dashboard/components/root-keys-table/components/skeletons/index.ts
new file mode 100644
index 0000000000..1d1ab6b337
--- /dev/null
+++ b/web/apps/dashboard/components/root-keys-table/components/skeletons/index.ts
@@ -0,0 +1 @@
+export { renderRootKeySkeletonRow } from "./render-root-key-skeleton-row";
diff --git a/web/apps/dashboard/components/root-keys-table/components/skeletons/render-root-key-skeleton-row.tsx b/web/apps/dashboard/components/root-keys-table/components/skeletons/render-root-key-skeleton-row.tsx
new file mode 100644
index 0000000000..a818b52ba8
--- /dev/null
+++ b/web/apps/dashboard/components/root-keys-table/components/skeletons/render-root-key-skeleton-row.tsx
@@ -0,0 +1,32 @@
+import type { RootKey } from "@/lib/trpc/routers/settings/root-keys/query";
+import { cn } from "@/lib/utils";
+import type { DataTableColumnDef } from "@unkey/ui";
+import {
+ ActionColumnSkeleton,
+ CreatedAtColumnSkeleton,
+ KeyColumnSkeleton,
+ LastUpdatedColumnSkeleton,
+ PermissionsColumnSkeleton,
+ RootKeyColumnSkeleton,
+} from "@unkey/ui";
+import { ROOT_KEY_COLUMN_IDS } from "../../../root-keys-table/columns/create-root-key-columns";
+
+type RenderRootKeySkeletonRowProps = {
+ columns: DataTableColumnDef[];
+ rowHeight: number;
+};
+
+export const renderRootKeySkeletonRow = ({ columns }: RenderRootKeySkeletonRowProps) =>
+ columns.map((column) => (
+
+ {column.id === ROOT_KEY_COLUMN_IDS.ROOT_KEY && }
+ {column.id === ROOT_KEY_COLUMN_IDS.KEY && }
+ {column.id === ROOT_KEY_COLUMN_IDS.CREATED_AT && }
+ {column.id === ROOT_KEY_COLUMN_IDS.PERMISSIONS && }
+ {column.id === ROOT_KEY_COLUMN_IDS.LAST_UPDATED && }
+ {column.id === ROOT_KEY_COLUMN_IDS.ACTION && }
+
+ ));
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/actions/components/hooks/use-delete-root-key.ts b/web/apps/dashboard/components/root-keys-table/hooks/use-delete-root-key.ts
similarity index 100%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/actions/components/hooks/use-delete-root-key.ts
rename to web/apps/dashboard/components/root-keys-table/hooks/use-delete-root-key.ts
diff --git a/web/apps/dashboard/components/root-keys-table/hooks/use-root-keys-list-query.ts b/web/apps/dashboard/components/root-keys-table/hooks/use-root-keys-list-query.ts
new file mode 100644
index 0000000000..7d3e2ac7ae
--- /dev/null
+++ b/web/apps/dashboard/components/root-keys-table/hooks/use-root-keys-list-query.ts
@@ -0,0 +1,195 @@
+import {
+ rootKeysFilterFieldConfig,
+ rootKeysListFilterFieldNames,
+} from "@/app/(app)/[workspaceSlug]/settings/root-keys/filters.schema";
+import type { RootKeysFilterValue } from "@/app/(app)/[workspaceSlug]/settings/root-keys/filters.schema";
+import { useFilters } from "@/app/(app)/[workspaceSlug]/settings/root-keys/hooks/use-filters";
+import { parseAsSortArray } from "@/components/logs/validation/utils/nuqs-parsers";
+import { trpc } from "@/lib/trpc/client";
+import type { SortingState } from "@tanstack/react-table";
+import { parseAsInteger, useQueryState } from "nuqs";
+import { useCallback, useEffect, useMemo, useRef } from "react";
+import type { RootKeysQueryPayload, RootKeysSortField } from "../schema/query-logs.schema";
+
+const PREFETCH_PAGES_AHEAD = 2;
+
+type RootKeysFilterParams = Pick;
+
+// Mirrors LIMIT in query.ts — kept here to avoid importing the server-side router into the client bundle
+const DEFAULT_PAGE_SIZE = 50;
+
+// Maps TanStack column IDs → server sort field names (and reverse)
+const COLUMN_ID_TO_SORT_FIELD: Record = {
+ root_key: "name",
+ created_at: "createdAt",
+ last_updated: "lastUpdatedAt",
+};
+const SORT_FIELD_TO_COLUMN_ID: Record = {
+ name: "root_key",
+ createdAt: "created_at",
+ lastUpdatedAt: "last_updated",
+};
+
+function buildQueryParams(filters: RootKeysFilterValue[]): RootKeysFilterParams {
+ const params: RootKeysFilterParams = {
+ name: [],
+ start: [],
+ permission: [],
+ };
+
+ for (const filter of filters) {
+ if (!rootKeysListFilterFieldNames.includes(filter.field) || !params[filter.field]) {
+ continue;
+ }
+
+ const fieldConfig = rootKeysFilterFieldConfig[filter.field];
+ if (!fieldConfig || !fieldConfig.operators.includes(filter.operator)) {
+ continue;
+ }
+
+ if (typeof filter.value === "string") {
+ params[filter.field]?.push({
+ operator: filter.operator,
+ value: filter.value,
+ });
+ }
+ }
+
+ return params;
+}
+
+const MAX_PAGE_SIZE = 200;
+
+export function useRootKeysListPaginated(pageSize = DEFAULT_PAGE_SIZE) {
+ const normalizedPageSize =
+ Number.isFinite(pageSize) && pageSize > 0
+ ? Math.min(Math.floor(pageSize), MAX_PAGE_SIZE)
+ : DEFAULT_PAGE_SIZE;
+
+ const { filters } = useFilters();
+ const [page, setPage] = useQueryState("page", parseAsInteger.withDefault(1));
+ const normalizedPage = Math.max(1, page);
+ const [sortParams, setSortParams] = useQueryState("sort", parseAsSortArray());
+
+ const sorting: SortingState = useMemo(() => {
+ if (!sortParams || sortParams.length === 0) {
+ return [{ id: "created_at", desc: true }];
+ }
+ return sortParams.map((s) => ({
+ id: SORT_FIELD_TO_COLUMN_ID[s.column] ?? s.column,
+ desc: s.direction === "desc",
+ }));
+ }, [sortParams]);
+
+ const onSortingChange = useCallback(
+ (updater: SortingState | ((old: SortingState) => SortingState)) => {
+ const next = typeof updater === "function" ? updater(sorting) : updater;
+ setSortParams(
+ next.length === 0
+ ? null
+ : next
+ .filter((s) => COLUMN_ID_TO_SORT_FIELD[s.id] !== undefined)
+ .map((s) => ({
+ column: COLUMN_ID_TO_SORT_FIELD[s.id],
+ direction: s.desc ? "desc" : "asc",
+ })),
+ );
+ setPage(1);
+ },
+ [sorting, setSortParams, setPage],
+ );
+
+ // Stable string key derived from filter content — avoids resetting page when
+ // useQueryStates returns a new array reference for the same filter values
+ // (which happens on every URL change, including page navigation).
+ const filtersKey = useMemo(
+ () => filters.map((f) => `${f.field}:${f.operator}:${f.value}`).join("|"),
+ [filters],
+ );
+
+ // Reset to page 1 only when filter content actually changes (not on initial mount).
+ const prevFiltersKeyRef = useRef(null);
+ useEffect(() => {
+ if (prevFiltersKeyRef.current === null) {
+ prevFiltersKeyRef.current = filtersKey;
+ return;
+ }
+ if (filtersKey !== prevFiltersKeyRef.current) {
+ prevFiltersKeyRef.current = filtersKey;
+ setPage(1);
+ }
+ }, [filtersKey, setPage]);
+
+ const baseParams = useMemo(() => buildQueryParams(filters), [filters]);
+
+ const queryParams = useMemo(
+ () => ({
+ ...baseParams,
+ page: normalizedPage,
+ limit: normalizedPageSize,
+ sortBy: sortParams?.[0]?.column ?? "createdAt",
+ sortOrder: sortParams?.[0]?.direction ?? "desc",
+ }),
+ [baseParams, normalizedPage, normalizedPageSize, sortParams],
+ );
+
+ const utils = trpc.useUtils();
+
+ const { data, isLoading, isFetching } = trpc.settings.rootKeys.query.useQuery(queryParams, {
+ staleTime: Number.POSITIVE_INFINITY,
+ refetchOnMount: false,
+ refetchOnWindowFocus: false,
+ keepPreviousData: true,
+ });
+
+ const isInitialLoading = isLoading && !data;
+
+ const totalCount = data?.total ?? 0;
+ const totalPages = Math.max(1, Math.ceil(totalCount / normalizedPageSize));
+
+ // Clamp page to valid range after data/totalPages updates.
+ useEffect(() => {
+ if (normalizedPage > totalPages) {
+ setPage(totalPages);
+ }
+ }, [normalizedPage, totalPages, setPage]);
+
+ // Prefetch the next few pages so navigation feels instant.
+ useEffect(() => {
+ for (let i = 1; i <= PREFETCH_PAGES_AHEAD; i++) {
+ const nextPage = normalizedPage + i;
+ if (nextPage > totalPages) {
+ break;
+ }
+ utils.settings.rootKeys.query.prefetch(
+ { ...queryParams, page: nextPage },
+ { staleTime: Number.POSITIVE_INFINITY },
+ );
+ }
+ }, [normalizedPage, totalPages, queryParams, utils.settings.rootKeys.query]);
+
+ const onPageChange = useCallback(
+ (newPage: number) => {
+ if (newPage < 1 || newPage > totalPages) {
+ return;
+ }
+ setPage(newPage);
+ },
+ [totalPages, setPage],
+ );
+
+ return {
+ rootKeys: data?.keys ?? [],
+ isLoading,
+ isInitialLoading,
+ isPending: isFetching,
+ isFetching,
+ page: normalizedPage,
+ pageSize: normalizedPageSize,
+ totalPages,
+ totalCount,
+ onPageChange,
+ sorting,
+ onSortingChange,
+ };
+}
diff --git a/web/apps/dashboard/components/root-keys-table/index.ts b/web/apps/dashboard/components/root-keys-table/index.ts
new file mode 100644
index 0000000000..56850a3dcc
--- /dev/null
+++ b/web/apps/dashboard/components/root-keys-table/index.ts
@@ -0,0 +1,4 @@
+export { createRootKeyColumns, ROOT_KEY_COLUMN_IDS } from "./columns";
+export { renderRootKeySkeletonRow } from "./components/skeletons";
+export { getRowClassName, STATUS_STYLES } from "./utils/get-row-class";
+export { useRootKeysListPaginated } from "./hooks/use-root-keys-list-query";
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/query-logs.schema.ts b/web/apps/dashboard/components/root-keys-table/schema/query-logs.schema.ts
similarity index 57%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/query-logs.schema.ts
rename to web/apps/dashboard/components/root-keys-table/schema/query-logs.schema.ts
index 30128456e7..da00a759b9 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/query-logs.schema.ts
+++ b/web/apps/dashboard/components/root-keys-table/schema/query-logs.schema.ts
@@ -1,5 +1,8 @@
+import {
+ rootKeysFilterOperatorEnum,
+ rootKeysListFilterFieldNames,
+} from "@/app/(app)/[workspaceSlug]/settings/root-keys/filters.schema";
import { z } from "zod";
-import { rootKeysFilterOperatorEnum, rootKeysListFilterFieldNames } from "../../filters.schema";
const filterItemSchema = z.object({
operator: rootKeysFilterOperatorEnum,
@@ -21,7 +24,12 @@ const filterFieldsSchema = rootKeysListFilterFieldNames.reduce(
const baseRootKeysSchema = z.object(filterFieldsSchema);
export const rootKeysQueryPayload = baseRootKeysSchema.extend({
- cursor: z.number().nullish(),
+ limit: z.number().min(20).optional(),
+ page: z.number().int().min(1).optional().default(1),
+ sortBy: z.enum(["name", "createdAt", "lastUpdatedAt"]).optional().default("createdAt"),
+ sortOrder: z.enum(["asc", "desc"]).optional().default("desc"),
});
+export type RootKeysSortField = "name" | "createdAt" | "lastUpdatedAt";
+export type RootKeysSortOrder = "asc" | "desc";
export type RootKeysQueryPayload = z.infer;
diff --git a/web/apps/dashboard/components/root-keys-table/utils/get-row-class.ts b/web/apps/dashboard/components/root-keys-table/utils/get-row-class.ts
new file mode 100644
index 0000000000..765d7fe39d
--- /dev/null
+++ b/web/apps/dashboard/components/root-keys-table/utils/get-row-class.ts
@@ -0,0 +1,19 @@
+import type { RootKey } from "@/lib/trpc/routers/settings/root-keys/query";
+import { cn } from "@/lib/utils";
+import { STATUS_STYLES } from "@unkey/ui";
+
+export { STATUS_STYLES };
+
+export const getRowClassName = (log: RootKey, selectedRow: RootKey | null) => {
+ const style = STATUS_STYLES;
+ const isSelected = log.id === selectedRow?.id;
+
+ return cn(
+ style.base,
+ style.hover,
+ "group rounded",
+ "focus:outline-none focus:ring-1 focus:ring-opacity-40",
+ style.focusRing,
+ isSelected && style.selected,
+ );
+};
diff --git a/web/apps/dashboard/components/virtual-table/components/loading-indicator.tsx b/web/apps/dashboard/components/virtual-table/components/loading-indicator.tsx
index d46f8bcbb9..4cc38bddfe 100644
--- a/web/apps/dashboard/components/virtual-table/components/loading-indicator.tsx
+++ b/web/apps/dashboard/components/virtual-table/components/loading-indicator.tsx
@@ -1,3 +1,4 @@
+import { cn } from "@/lib/utils";
import { ArrowsToAllDirections, ArrowsToCenter } from "@unkey/icons";
import { Button } from "@unkey/ui";
import { useCallback, useState } from "react";
@@ -47,12 +48,7 @@ export const LoadMoreFooter = ({
// Minimized state - parked at right side
if (!isOpen) {
return (
-
+
{/* Header content */}
{headerContent && (
-
+
{headerContent}
)}
-
+
{countInfoText &&
{countInfoText}
}
{!countInfoText && (
@@ -136,12 +122,7 @@ export const LoadMoreFooter = ({
>
{buttonText}
-
-
- {/* CSS Keyframes */}
-
);
};
diff --git a/web/apps/dashboard/lib/trpc/routers/settings/root-keys/query.ts b/web/apps/dashboard/lib/trpc/routers/settings/root-keys/query.ts
index 5c03ccefca..30d61d9c7a 100644
--- a/web/apps/dashboard/lib/trpc/routers/settings/root-keys/query.ts
+++ b/web/apps/dashboard/lib/trpc/routers/settings/root-keys/query.ts
@@ -1,5 +1,5 @@
-import { rootKeysQueryPayload } from "@/app/(app)/[workspaceSlug]/settings/root-keys/components/table/query-logs.schema";
-import { and, count, db, desc, eq, exists, isNull, like, lt, or, schema } from "@/lib/db";
+import { rootKeysQueryPayload } from "@/components/root-keys-table/schema/query-logs.schema";
+import { and, asc, count, db, desc, eq, exists, isNull, like, or, schema } from "@/lib/db";
import { ratelimit, withRatelimit, workspaceProcedure } from "@/lib/trpc/trpc";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
@@ -27,7 +27,6 @@ const RootKeysResponse = z.object({
keys: z.array(RootKeyResponse),
hasMore: z.boolean(),
total: z.number(),
- nextCursor: z.int().optional(),
});
type PermissionResponse = z.infer
;
@@ -35,23 +34,19 @@ type RootKeysResponse = z.infer;
export type RootKey = z.infer;
export const LIMIT = 50;
+export const MAX_LIMIT = 200;
export const queryRootKeys = workspaceProcedure
.use(withRatelimit(ratelimit.read))
.input(rootKeysQueryPayload)
.output(RootKeysResponse)
.query(async ({ ctx, input }) => {
- // Build base conditions
+ // Build base conditions (used for both count and fetch)
const baseConditions = [
eq(schema.keys.forWorkspaceId, ctx.workspace.id),
isNull(schema.keys.deletedAtM),
];
- // Add cursor condition for pagination
- if (input.cursor && typeof input.cursor === "number") {
- baseConditions.push(lt(schema.keys.createdAtM, input.cursor));
- }
-
// Build filter conditions
const filterConditions = [];
@@ -132,20 +127,37 @@ export const queryRootKeys = workspaceProcedure
}
}
- // Combine all conditions
- const allConditions =
+ // Count conditions: base + filters only (total must reflect all matching keys)
+ const countConditions =
+ filterConditions.length > 0 ? [...baseConditions, ...filterConditions] : baseConditions;
+
+ // Fetch conditions: base + filters
+ const fetchConditions =
filterConditions.length > 0 ? [...baseConditions, ...filterConditions] : baseConditions;
+ // Build ORDER BY based on sort input
+ const SORT_COLUMN_MAP = {
+ name: schema.keys.name,
+ createdAt: schema.keys.createdAtM,
+ lastUpdatedAt: schema.keys.updatedAtM,
+ } as const;
+ const sortColumn = SORT_COLUMN_MAP[input.sortBy ?? "createdAt"];
+ const sortFn = input.sortOrder === "asc" ? asc : desc;
+
+ const page = input.page ?? 1;
+ const pageSize = Math.min(input.limit ?? LIMIT, MAX_LIMIT);
+
try {
const [totalResult, keysResult] = await Promise.all([
db
.select({ count: count() })
.from(schema.keys)
- .where(and(...allConditions)),
+ .where(and(...countConditions)),
db.query.keys.findMany({
- where: and(...allConditions),
- orderBy: [desc(schema.keys.createdAtM)],
- limit: LIMIT + 1, // Get one extra to check if there are more
+ where: and(...fetchConditions),
+ orderBy: [sortFn(sortColumn), sortFn(schema.keys.id)],
+ limit: pageSize,
+ offset: (page - 1) * pageSize,
columns: {
id: true,
start: true,
@@ -171,12 +183,8 @@ export const queryRootKeys = workspaceProcedure
}),
]);
- // Check if we have more results
- const hasMore = keysResult.length > LIMIT;
- const keysWithoutExtra = hasMore ? keysResult.slice(0, LIMIT) : keysResult;
-
// Transform the data to flatten permissions and add summary
- const keys = keysWithoutExtra.map((key) => {
+ const keys = keysResult.map((key) => {
const permissions = key.permissions
.map((p) => p.permission)
.filter(Boolean)
@@ -198,11 +206,11 @@ export const queryRootKeys = workspaceProcedure
};
});
+ const totalCount = totalResult[0]?.count ?? 0;
const response: RootKeysResponse = {
keys,
- hasMore,
- total: totalResult[0]?.count ?? 0,
- nextCursor: keys.length > 0 ? keys[keys.length - 1].createdAt : undefined,
+ hasMore: page * pageSize < totalCount,
+ total: totalCount,
};
return response;
diff --git a/web/internal/ui/package.json b/web/internal/ui/package.json
index da94877625..588c23ca24 100644
--- a/web/internal/ui/package.json
+++ b/web/internal/ui/package.json
@@ -19,6 +19,7 @@
"typescript": "5.9.3"
},
"dependencies": {
+ "@tanstack/react-table": "8.21.3",
"@radix-ui/colors": "3.0.0",
"@radix-ui/react-checkbox": "1.3.3",
"@radix-ui/react-dialog": "1.1.15",
diff --git a/web/internal/ui/pnpm-lock.yaml b/web/internal/ui/pnpm-lock.yaml
new file mode 100644
index 0000000000..567567d478
--- /dev/null
+++ b/web/internal/ui/pnpm-lock.yaml
@@ -0,0 +1,1875 @@
+lockfileVersion: "9.0"
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+ .:
+ dependencies:
+ "@radix-ui/colors":
+ specifier: 3.0.0
+ version: 3.0.0
+ "@radix-ui/react-checkbox":
+ specifier: 1.3.3
+ version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-dialog":
+ specifier: 1.1.15
+ version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-popover":
+ specifier: 1.1.15
+ version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-select":
+ specifier: 2.2.6
+ version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-separator":
+ specifier: 1.1.8
+ version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-slider":
+ specifier: ^1.3.6
+ version: 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-slot":
+ specifier: 1.2.4
+ version: 1.2.4(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-tabs":
+ specifier: 1.1.0
+ version: 1.1.0(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-tooltip":
+ specifier: 1.2.8
+ version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-use-controllable-state":
+ specifier: 1.2.2
+ version: 1.2.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-visually-hidden":
+ specifier: ^1.2.4
+ version: 1.2.4(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@tanstack/react-table":
+ specifier: 8.21.3
+ version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@unkey/icons":
+ specifier: workspace:^
+ version: link:../icons
+ class-variance-authority:
+ specifier: 0.7.1
+ version: 0.7.1
+ clsx:
+ specifier: 2.1.1
+ version: 2.1.1
+ date-fns:
+ specifier: 4.1.0
+ version: 4.1.0
+ next-themes:
+ specifier: 0.4.6
+ version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ nuqs:
+ specifier: 2.8.6
+ version: 2.8.6(react@19.2.3)
+ react:
+ specifier: 19.2.3
+ version: 19.2.3
+ react-day-picker:
+ specifier: 9.13.0
+ version: 9.13.0(react@19.2.3)
+ react-dom:
+ specifier: 19.2.3
+ version: 19.2.3(react@19.2.3)
+ react-hook-form:
+ specifier: 7.71.1
+ version: 7.71.1(react@19.2.3)
+ sonner:
+ specifier: 2.0.7
+ version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ tailwind-merge:
+ specifier: 3.4.0
+ version: 3.4.0
+ vaul:
+ specifier: 1.1.2
+ version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ zod:
+ specifier: 4.3.5
+ version: 4.3.5
+ devDependencies:
+ "@testing-library/react":
+ specifier: 16.3.2
+ version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@testing-library/react-hooks":
+ specifier: 8.0.1
+ version: 8.0.1(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@types/node":
+ specifier: 25.0.10
+ version: 25.0.10
+ "@types/react":
+ specifier: 19.2.3
+ version: 19.2.3
+ "@types/react-dom":
+ specifier: 19.2.3
+ version: 19.2.3(@types/react@19.2.3)
+ "@unkey/tsconfig":
+ specifier: workspace:^
+ version: link:../tsconfig
+ tailwindcss:
+ specifier: 4.1.18
+ version: 4.1.18
+ tailwindcss-animate:
+ specifier: 1.0.7
+ version: 1.0.7(tailwindcss@4.1.18)
+ typescript:
+ specifier: 5.9.3
+ version: 5.9.3
+
+packages:
+ "@babel/code-frame@7.29.0":
+ resolution: {
+ integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==,
+ }
+ engines: { node: ">=6.9.0" }
+
+ "@babel/helper-validator-identifier@7.28.5":
+ resolution: {
+ integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==,
+ }
+ engines: { node: ">=6.9.0" }
+
+ "@babel/runtime@7.28.6":
+ resolution: {
+ integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==,
+ }
+ engines: { node: ">=6.9.0" }
+
+ "@date-fns/tz@1.4.1":
+ resolution: {
+ integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==,
+ }
+
+ "@floating-ui/core@1.7.4":
+ resolution: {
+ integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==,
+ }
+
+ "@floating-ui/dom@1.7.5":
+ resolution: {
+ integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==,
+ }
+
+ "@floating-ui/react-dom@2.1.7":
+ resolution: {
+ integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==,
+ }
+ peerDependencies:
+ react: ">=16.8.0"
+ react-dom: ">=16.8.0"
+
+ "@floating-ui/utils@0.2.10":
+ resolution: {
+ integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==,
+ }
+
+ "@radix-ui/colors@3.0.0":
+ resolution: {
+ integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==,
+ }
+
+ "@radix-ui/number@1.1.1":
+ resolution: {
+ integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==,
+ }
+
+ "@radix-ui/primitive@1.1.0":
+ resolution: {
+ integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==,
+ }
+
+ "@radix-ui/primitive@1.1.3":
+ resolution: {
+ integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==,
+ }
+
+ "@radix-ui/react-arrow@1.1.7":
+ resolution: {
+ integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-checkbox@1.3.3":
+ resolution: {
+ integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-collection@1.1.0":
+ resolution: {
+ integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-collection@1.1.7":
+ resolution: {
+ integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-compose-refs@1.1.0":
+ resolution: {
+ integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-compose-refs@1.1.2":
+ resolution: {
+ integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-context@1.1.0":
+ resolution: {
+ integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-context@1.1.2":
+ resolution: {
+ integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-dialog@1.1.15":
+ resolution: {
+ integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-direction@1.1.0":
+ resolution: {
+ integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-direction@1.1.1":
+ resolution: {
+ integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-dismissable-layer@1.1.11":
+ resolution: {
+ integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-focus-guards@1.1.3":
+ resolution: {
+ integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-focus-scope@1.1.7":
+ resolution: {
+ integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-id@1.1.0":
+ resolution: {
+ integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-id@1.1.1":
+ resolution: {
+ integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-popover@1.1.15":
+ resolution: {
+ integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-popper@1.2.8":
+ resolution: {
+ integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-portal@1.1.9":
+ resolution: {
+ integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-presence@1.1.0":
+ resolution: {
+ integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-presence@1.1.5":
+ resolution: {
+ integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-primitive@2.0.0":
+ resolution: {
+ integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-primitive@2.1.3":
+ resolution: {
+ integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-primitive@2.1.4":
+ resolution: {
+ integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-roving-focus@1.1.0":
+ resolution: {
+ integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-select@2.2.6":
+ resolution: {
+ integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-separator@1.1.8":
+ resolution: {
+ integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-slider@1.3.6":
+ resolution: {
+ integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-slot@1.1.0":
+ resolution: {
+ integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-slot@1.2.3":
+ resolution: {
+ integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-slot@1.2.4":
+ resolution: {
+ integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-tabs@1.1.0":
+ resolution: {
+ integrity: sha512-bZgOKB/LtZIij75FSuPzyEti/XBhJH52ExgtdVqjCIh+Nx/FW+LhnbXtbCzIi34ccyMsyOja8T0thCzoHFXNKA==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-tooltip@1.2.8":
+ resolution: {
+ integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-use-callback-ref@1.1.0":
+ resolution: {
+ integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-use-callback-ref@1.1.1":
+ resolution: {
+ integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-use-controllable-state@1.1.0":
+ resolution: {
+ integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-use-controllable-state@1.2.2":
+ resolution: {
+ integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-use-effect-event@0.0.2":
+ resolution: {
+ integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-use-escape-keydown@1.1.1":
+ resolution: {
+ integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-use-layout-effect@1.1.0":
+ resolution: {
+ integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-use-layout-effect@1.1.1":
+ resolution: {
+ integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-use-previous@1.1.1":
+ resolution: {
+ integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-use-rect@1.1.1":
+ resolution: {
+ integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-use-size@1.1.1":
+ resolution: {
+ integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ "@radix-ui/react-visually-hidden@1.2.3":
+ resolution: {
+ integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-visually-hidden@1.2.4":
+ resolution: {
+ integrity: sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==,
+ }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/rect@1.1.1":
+ resolution: {
+ integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==,
+ }
+
+ "@standard-schema/spec@1.0.0":
+ resolution: {
+ integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==,
+ }
+
+ "@tanstack/react-table@8.21.3":
+ resolution: {
+ integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==,
+ }
+ engines: { node: ">=12" }
+ peerDependencies:
+ react: ">=16.8"
+ react-dom: ">=16.8"
+
+ "@tanstack/table-core@8.21.3":
+ resolution: {
+ integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==,
+ }
+ engines: { node: ">=12" }
+
+ "@testing-library/dom@10.4.1":
+ resolution: {
+ integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==,
+ }
+ engines: { node: ">=18" }
+
+ "@testing-library/react-hooks@8.0.1":
+ resolution: {
+ integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==,
+ }
+ engines: { node: ">=12" }
+ peerDependencies:
+ "@types/react": ^16.9.0 || ^17.0.0
+ react: ^16.9.0 || ^17.0.0
+ react-dom: ^16.9.0 || ^17.0.0
+ react-test-renderer: ^16.9.0 || ^17.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ react-dom:
+ optional: true
+ react-test-renderer:
+ optional: true
+
+ "@testing-library/react@16.3.2":
+ resolution: {
+ integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==,
+ }
+ engines: { node: ">=18" }
+ peerDependencies:
+ "@testing-library/dom": ^10.0.0
+ "@types/react": ^18.0.0 || ^19.0.0
+ "@types/react-dom": ^18.0.0 || ^19.0.0
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@types/aria-query@5.0.4":
+ resolution: {
+ integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==,
+ }
+
+ "@types/node@25.0.10":
+ resolution: {
+ integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==,
+ }
+
+ "@types/react-dom@19.2.3":
+ resolution: {
+ integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==,
+ }
+ peerDependencies:
+ "@types/react": ^19.2.0
+
+ "@types/react@19.2.3":
+ resolution: {
+ integrity: sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg==,
+ }
+
+ ansi-regex@5.0.1:
+ resolution: {
+ integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==,
+ }
+ engines: { node: ">=8" }
+
+ ansi-styles@5.2.0:
+ resolution: {
+ integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==,
+ }
+ engines: { node: ">=10" }
+
+ aria-hidden@1.2.6:
+ resolution: {
+ integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==,
+ }
+ engines: { node: ">=10" }
+
+ aria-query@5.3.0:
+ resolution: {
+ integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==,
+ }
+
+ class-variance-authority@0.7.1:
+ resolution: {
+ integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==,
+ }
+
+ clsx@2.1.1:
+ resolution: {
+ integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==,
+ }
+ engines: { node: ">=6" }
+
+ csstype@3.2.3:
+ resolution: {
+ integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==,
+ }
+
+ date-fns-jalali@4.1.0-0:
+ resolution: {
+ integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==,
+ }
+
+ date-fns@4.1.0:
+ resolution: {
+ integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==,
+ }
+
+ dequal@2.0.3:
+ resolution: {
+ integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==,
+ }
+ engines: { node: ">=6" }
+
+ detect-node-es@1.1.0:
+ resolution: {
+ integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==,
+ }
+
+ dom-accessibility-api@0.5.16:
+ resolution: {
+ integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==,
+ }
+
+ get-nonce@1.0.1:
+ resolution: {
+ integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==,
+ }
+ engines: { node: ">=6" }
+
+ js-tokens@4.0.0:
+ resolution: {
+ integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==,
+ }
+
+ lz-string@1.5.0:
+ resolution: {
+ integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==,
+ }
+ hasBin: true
+
+ next-themes@0.4.6:
+ resolution: {
+ integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==,
+ }
+ peerDependencies:
+ react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+
+ nuqs@2.8.6:
+ resolution: {
+ integrity: sha512-aRxeX68b4ULmhio8AADL2be1FWDy0EPqaByPvIYWrA7Pm07UjlrICp/VPlSnXJNAG0+3MQwv3OporO2sOXMVGA==,
+ }
+ peerDependencies:
+ "@remix-run/react": ">=2"
+ "@tanstack/react-router": ^1
+ next: ">=14.2.0"
+ react: ">=18.2.0 || ^19.0.0-0"
+ react-router: ^5 || ^6 || ^7
+ react-router-dom: ^5 || ^6 || ^7
+ peerDependenciesMeta:
+ "@remix-run/react":
+ optional: true
+ "@tanstack/react-router":
+ optional: true
+ next:
+ optional: true
+ react-router:
+ optional: true
+ react-router-dom:
+ optional: true
+
+ picocolors@1.1.1:
+ resolution: {
+ integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==,
+ }
+
+ pretty-format@27.5.1:
+ resolution: {
+ integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==,
+ }
+ engines: { node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0 }
+
+ react-day-picker@9.13.0:
+ resolution: {
+ integrity: sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==,
+ }
+ engines: { node: ">=18" }
+ peerDependencies:
+ react: ">=16.8.0"
+
+ react-dom@19.2.3:
+ resolution: {
+ integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==,
+ }
+ peerDependencies:
+ react: ^19.2.3
+
+ react-error-boundary@3.1.4:
+ resolution: {
+ integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==,
+ }
+ engines: { node: ">=10", npm: ">=6" }
+ peerDependencies:
+ react: ">=16.13.1"
+
+ react-hook-form@7.71.1:
+ resolution: {
+ integrity: sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==,
+ }
+ engines: { node: ">=18.0.0" }
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
+ react-is@17.0.2:
+ resolution: {
+ integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==,
+ }
+
+ react-remove-scroll-bar@2.3.8:
+ resolution: {
+ integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==,
+ }
+ engines: { node: ">=10" }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ react-remove-scroll@2.7.2:
+ resolution: {
+ integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==,
+ }
+ engines: { node: ">=10" }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ react-style-singleton@2.2.3:
+ resolution: {
+ integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==,
+ }
+ engines: { node: ">=10" }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ react@19.2.3:
+ resolution: {
+ integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==,
+ }
+ engines: { node: ">=0.10.0" }
+
+ scheduler@0.27.0:
+ resolution: {
+ integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==,
+ }
+
+ sonner@2.0.7:
+ resolution: {
+ integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==,
+ }
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
+ tailwind-merge@3.4.0:
+ resolution: {
+ integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==,
+ }
+
+ tailwindcss-animate@1.0.7:
+ resolution: {
+ integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==,
+ }
+ peerDependencies:
+ tailwindcss: ">=3.0.0 || insiders"
+
+ tailwindcss@4.1.18:
+ resolution: {
+ integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==,
+ }
+
+ tslib@2.8.1:
+ resolution: {
+ integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==,
+ }
+
+ typescript@5.9.3:
+ resolution: {
+ integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==,
+ }
+ engines: { node: ">=14.17" }
+ hasBin: true
+
+ undici-types@7.16.0:
+ resolution: {
+ integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==,
+ }
+
+ use-callback-ref@1.3.3:
+ resolution: {
+ integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==,
+ }
+ engines: { node: ">=10" }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ use-sidecar@1.1.3:
+ resolution: {
+ integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==,
+ }
+ engines: { node: ">=10" }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
+ vaul@1.1.2:
+ resolution: {
+ integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==,
+ }
+ peerDependencies:
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+
+ zod@4.3.5:
+ resolution: {
+ integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==,
+ }
+
+snapshots:
+ "@babel/code-frame@7.29.0":
+ dependencies:
+ "@babel/helper-validator-identifier": 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ "@babel/helper-validator-identifier@7.28.5": {}
+
+ "@babel/runtime@7.28.6": {}
+
+ "@date-fns/tz@1.4.1": {}
+
+ "@floating-ui/core@1.7.4":
+ dependencies:
+ "@floating-ui/utils": 0.2.10
+
+ "@floating-ui/dom@1.7.5":
+ dependencies:
+ "@floating-ui/core": 1.7.4
+ "@floating-ui/utils": 0.2.10
+
+ "@floating-ui/react-dom@2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@floating-ui/dom": 1.7.5
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+
+ "@floating-ui/utils@0.2.10": {}
+
+ "@radix-ui/colors@3.0.0": {}
+
+ "@radix-ui/number@1.1.1": {}
+
+ "@radix-ui/primitive@1.1.0": {}
+
+ "@radix-ui/primitive@1.1.3": {}
+
+ "@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.3
+ "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-context": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-presence": 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-use-controllable-state": 1.2.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-previous": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-size": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-collection@1.1.0(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-compose-refs": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-context": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-primitive": 2.0.0(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-slot": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-context": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-slot": 1.2.3(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-compose-refs@1.1.0(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-context@1.1.0(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-context@1.1.2(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.3
+ "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-context": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-dismissable-layer": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-focus-guards": 1.1.3(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-focus-scope": 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-id": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-portal": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-presence": 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-slot": 1.2.3(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-controllable-state": 1.2.2(@types/react@19.2.3)(react@19.2.3)
+ aria-hidden: 1.2.6
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ react-remove-scroll: 2.7.2(@types/react@19.2.3)(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-direction@1.1.0(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-direction@1.1.1(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.3
+ "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-use-callback-ref": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-escape-keydown": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-use-callback-ref": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-id@1.1.0(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-use-layout-effect": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-id@1.1.1(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-use-layout-effect": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.3
+ "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-context": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-dismissable-layer": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-focus-guards": 1.1.3(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-focus-scope": 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-id": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-popper": 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-portal": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-presence": 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-slot": 1.2.3(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-controllable-state": 1.2.2(@types/react@19.2.3)(react@19.2.3)
+ aria-hidden: 1.2.6
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ react-remove-scroll: 2.7.2(@types/react@19.2.3)(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@floating-ui/react-dom": 2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-arrow": 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-context": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-use-callback-ref": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-layout-effect": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-rect": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-size": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/rect": 1.1.1
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-use-layout-effect": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-presence@1.1.0(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-compose-refs": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-layout-effect": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-layout-effect": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-primitive@2.0.0(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-slot": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-slot": 1.2.3(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-slot": 1.2.4(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-roving-focus@1.1.0(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.0
+ "@radix-ui/react-collection": 1.1.0(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-compose-refs": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-context": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-direction": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-id": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-primitive": 2.0.0(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-use-callback-ref": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-controllable-state": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/number": 1.1.1
+ "@radix-ui/primitive": 1.1.3
+ "@radix-ui/react-collection": 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-context": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-direction": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-dismissable-layer": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-focus-guards": 1.1.3(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-focus-scope": 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-id": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-popper": 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-portal": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-slot": 1.2.3(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-callback-ref": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-controllable-state": 1.2.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-layout-effect": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-previous": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-visually-hidden": 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ aria-hidden: 1.2.6
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ react-remove-scroll: 2.7.2(@types/react@19.2.3)(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-primitive": 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/number": 1.1.1
+ "@radix-ui/primitive": 1.1.3
+ "@radix-ui/react-collection": 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-context": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-direction": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-use-controllable-state": 1.2.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-layout-effect": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-previous": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-size": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-slot@1.1.0(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-compose-refs": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-slot@1.2.3(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-slot@1.2.4(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-tabs@1.1.0(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.0
+ "@radix-ui/react-context": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-direction": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-id": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-presence": 1.1.0(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-primitive": 2.0.0(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-roving-focus": 1.1.0(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-use-controllable-state": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.3
+ "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-context": 1.1.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-dismissable-layer": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-id": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-popper": 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-portal": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-presence": 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ "@radix-ui/react-slot": 1.2.3(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-controllable-state": 1.2.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-visually-hidden": 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-use-callback-ref": 1.1.0(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-use-effect-event": 0.0.2(@types/react@19.2.3)(react@19.2.3)
+ "@radix-ui/react-use-layout-effect": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-use-layout-effect": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-use-callback-ref": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-use-previous@1.1.1(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-use-rect@1.1.1(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ "@radix-ui/rect": 1.1.1
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-use-size@1.1.1(@types/react@19.2.3)(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-use-layout-effect": 1.1.1(@types/react@19.2.3)(react@19.2.3)
+ react: 19.2.3
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ "@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/react-visually-hidden@1.2.4(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@radix-ui/react-primitive": 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@radix-ui/rect@1.1.1": {}
+
+ "@standard-schema/spec@1.0.0": {}
+
+ "@tanstack/react-table@8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@tanstack/table-core": 8.21.3
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+
+ "@tanstack/table-core@8.21.3": {}
+
+ "@testing-library/dom@10.4.1":
+ dependencies:
+ "@babel/code-frame": 7.29.0
+ "@babel/runtime": 7.28.6
+ "@types/aria-query": 5.0.4
+ aria-query: 5.3.0
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ picocolors: 1.1.1
+ pretty-format: 27.5.1
+
+ "@testing-library/react-hooks@8.0.1(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@babel/runtime": 7.28.6
+ react: 19.2.3
+ react-error-boundary: 3.1.4(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+
+ "@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
+ dependencies:
+ "@babel/runtime": 7.28.6
+ "@testing-library/dom": 10.4.1
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+ "@types/react-dom": 19.2.3(@types/react@19.2.3)
+
+ "@types/aria-query@5.0.4": {}
+
+ "@types/node@25.0.10":
+ dependencies:
+ undici-types: 7.16.0
+
+ "@types/react-dom@19.2.3(@types/react@19.2.3)":
+ dependencies:
+ "@types/react": 19.2.3
+
+ "@types/react@19.2.3":
+ dependencies:
+ csstype: 3.2.3
+
+ ansi-regex@5.0.1: {}
+
+ ansi-styles@5.2.0: {}
+
+ aria-hidden@1.2.6:
+ dependencies:
+ tslib: 2.8.1
+
+ aria-query@5.3.0:
+ dependencies:
+ dequal: 2.0.3
+
+ class-variance-authority@0.7.1:
+ dependencies:
+ clsx: 2.1.1
+
+ clsx@2.1.1: {}
+
+ csstype@3.2.3: {}
+
+ date-fns-jalali@4.1.0-0: {}
+
+ date-fns@4.1.0: {}
+
+ dequal@2.0.3: {}
+
+ detect-node-es@1.1.0: {}
+
+ dom-accessibility-api@0.5.16: {}
+
+ get-nonce@1.0.1: {}
+
+ js-tokens@4.0.0: {}
+
+ lz-string@1.5.0: {}
+
+ next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+
+ nuqs@2.8.6(react@19.2.3):
+ dependencies:
+ "@standard-schema/spec": 1.0.0
+ react: 19.2.3
+
+ picocolors@1.1.1: {}
+
+ pretty-format@27.5.1:
+ dependencies:
+ ansi-regex: 5.0.1
+ ansi-styles: 5.2.0
+ react-is: 17.0.2
+
+ react-day-picker@9.13.0(react@19.2.3):
+ dependencies:
+ "@date-fns/tz": 1.4.1
+ date-fns: 4.1.0
+ date-fns-jalali: 4.1.0-0
+ react: 19.2.3
+
+ react-dom@19.2.3(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+ scheduler: 0.27.0
+
+ react-error-boundary@3.1.4(react@19.2.3):
+ dependencies:
+ "@babel/runtime": 7.28.6
+ react: 19.2.3
+
+ react-hook-form@7.71.1(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+
+ react-is@17.0.2: {}
+
+ react-remove-scroll-bar@2.3.8(@types/react@19.2.3)(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+ react-style-singleton: 2.2.3(@types/react@19.2.3)(react@19.2.3)
+ tslib: 2.8.1
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ react-remove-scroll@2.7.2(@types/react@19.2.3)(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+ react-remove-scroll-bar: 2.3.8(@types/react@19.2.3)(react@19.2.3)
+ react-style-singleton: 2.2.3(@types/react@19.2.3)(react@19.2.3)
+ tslib: 2.8.1
+ use-callback-ref: 1.3.3(@types/react@19.2.3)(react@19.2.3)
+ use-sidecar: 1.1.3(@types/react@19.2.3)(react@19.2.3)
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ react-style-singleton@2.2.3(@types/react@19.2.3)(react@19.2.3):
+ dependencies:
+ get-nonce: 1.0.1
+ react: 19.2.3
+ tslib: 2.8.1
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ react@19.2.3: {}
+
+ scheduler@0.27.0: {}
+
+ sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+
+ tailwind-merge@3.4.0: {}
+
+ tailwindcss-animate@1.0.7(tailwindcss@4.1.18):
+ dependencies:
+ tailwindcss: 4.1.18
+
+ tailwindcss@4.1.18: {}
+
+ tslib@2.8.1: {}
+
+ typescript@5.9.3: {}
+
+ undici-types@7.16.0: {}
+
+ use-callback-ref@1.3.3(@types/react@19.2.3)(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+ tslib: 2.8.1
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ use-sidecar@1.1.3(@types/react@19.2.3)(react@19.2.3):
+ dependencies:
+ detect-node-es: 1.1.0
+ react: 19.2.3
+ tslib: 2.8.1
+ optionalDependencies:
+ "@types/react": 19.2.3
+
+ vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ dependencies:
+ "@radix-ui/react-dialog": 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ transitivePeerDependencies:
+ - "@types/react"
+ - "@types/react-dom"
+
+ zod@4.3.5: {}
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/assigned-items-cell.tsx b/web/internal/ui/src/components/data-table/components/cells/assigned-items-cell.tsx
similarity index 92%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/assigned-items-cell.tsx
rename to web/internal/ui/src/components/data-table/components/cells/assigned-items-cell.tsx
index 24588a74fb..b292287c68 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/assigned-items-cell.tsx
+++ b/web/internal/ui/src/components/data-table/components/cells/assigned-items-cell.tsx
@@ -1,16 +1,18 @@
-import { cn } from "@/lib/utils";
import { Page2 } from "@unkey/icons";
+import { cn } from "../../../../lib/utils";
-export const AssignedItemsCell = ({
- permissionSummary,
- isSelected = false,
-}: {
+export interface AssignedItemsCellProps {
permissionSummary: {
total: number;
categories: Record;
};
isSelected?: boolean;
-}) => {
+}
+
+export const AssignedItemsCell = ({
+ permissionSummary,
+ isSelected = false,
+}: AssignedItemsCellProps) => {
const { total } = permissionSummary;
const itemClassName = cn(
diff --git a/web/internal/ui/src/components/data-table/components/cells/badge-cell.tsx b/web/internal/ui/src/components/data-table/components/cells/badge-cell.tsx
new file mode 100644
index 0000000000..12acf26d97
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/cells/badge-cell.tsx
@@ -0,0 +1,18 @@
+import { Badge } from "../../../badge";
+
+export interface BadgeCellProps {
+ children: React.ReactNode;
+ variant?: "primary" | "secondary" | "success" | "warning" | "error" | "blocked";
+ className?: string;
+}
+
+/**
+ * Generic badge cell component
+ */
+export function BadgeCell({ children, variant = "primary", className }: BadgeCellProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/web/internal/ui/src/components/data-table/components/cells/checkbox-cell.tsx b/web/internal/ui/src/components/data-table/components/cells/checkbox-cell.tsx
new file mode 100644
index 0000000000..cac91fdf86
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/cells/checkbox-cell.tsx
@@ -0,0 +1,47 @@
+import type { Row, Table } from "@tanstack/react-table";
+import { Checkbox } from "../../../form/checkbox";
+
+interface CheckboxCellProps {
+ row: Row;
+}
+
+/**
+ * Checkbox cell for row selection
+ * Supports individual row selection and indeterminate state
+ */
+export function CheckboxCell({ row }: CheckboxCellProps) {
+ return (
+
+ row.toggleSelected(!!value)}
+ aria-label="Select row"
+ />
+
+ );
+}
+
+interface CheckboxHeaderCellProps {
+ table: Table;
+}
+
+/**
+ * Checkbox header cell for select-all functionality
+ */
+export function CheckboxHeaderCell({ table }: CheckboxHeaderCellProps) {
+ return (
+
+ table.toggleAllRowsSelected(!!value)}
+ aria-label="Select all rows"
+ />
+
+ );
+}
diff --git a/web/internal/ui/src/components/data-table/components/cells/copy-cell.tsx b/web/internal/ui/src/components/data-table/components/cells/copy-cell.tsx
new file mode 100644
index 0000000000..8ad1cfd1a2
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/cells/copy-cell.tsx
@@ -0,0 +1,47 @@
+"use client";
+import { Clipboard } from "@unkey/icons";
+import { useState } from "react";
+import { cn } from "../../../../lib/utils";
+
+export interface CopyCellProps {
+ value: string;
+ displayValue?: string;
+ className?: string;
+ monospace?: boolean;
+}
+
+/**
+ * Copyable cell with click-to-copy functionality
+ */
+export function CopyCell({ value, displayValue, className, monospace = false }: CopyCellProps) {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = async () => {
+ await navigator.clipboard.writeText(value);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ return (
+
+ {displayValue || value}
+
+ {copied && Copied! }
+
+ );
+}
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/hidden-value.tsx b/web/internal/ui/src/components/data-table/components/cells/hidden-value-cell.tsx
similarity index 83%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/hidden-value.tsx
rename to web/internal/ui/src/components/data-table/components/cells/hidden-value-cell.tsx
index 467fb024f5..89d02d8ea1 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/hidden-value.tsx
+++ b/web/internal/ui/src/components/data-table/components/cells/hidden-value-cell.tsx
@@ -1,16 +1,14 @@
-import { cn } from "@/lib/utils";
import { CircleLock } from "@unkey/icons";
-import { toast } from "@unkey/ui";
+import { cn } from "../../../../lib/utils";
+import { toast } from "../../../toaster";
-export const HiddenValueCell = ({
- value,
- title = "Value",
- selected,
-}: {
+export interface HiddenValueCellProps {
value: string;
title: string;
selected: boolean;
-}) => {
+}
+
+export const HiddenValueCell = ({ value, title = "Value", selected }: HiddenValueCellProps) => {
// Show only first 4 characters, then dots
const displayValue = value.padEnd(16, "•");
diff --git a/web/internal/ui/src/components/data-table/components/cells/index.ts b/web/internal/ui/src/components/data-table/components/cells/index.ts
new file mode 100644
index 0000000000..53a6f741a6
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/cells/index.ts
@@ -0,0 +1,18 @@
+export { CheckboxCell, CheckboxHeaderCell } from "./checkbox-cell";
+export { StatusCell } from "./status-cell";
+export type { StatusCellProps } from "./status-cell";
+export { TimestampCell } from "./timestamp-cell";
+export type { TimestampCellProps } from "./timestamp-cell";
+export { BadgeCell } from "./badge-cell";
+export type { BadgeCellProps } from "./badge-cell";
+export { CopyCell } from "./copy-cell";
+export type { CopyCellProps } from "./copy-cell";
+export { AssignedItemsCell } from "./assigned-items-cell";
+export type { AssignedItemsCellProps } from "./assigned-items-cell";
+export { HiddenValueCell } from "./hidden-value-cell";
+export type { HiddenValueCellProps } from "./hidden-value-cell";
+export { LastUpdatedCell } from "./last-updated-cell";
+export type { LastUpdatedCellProps } from "./last-updated-cell";
+export { RootKeyNameCell } from "./root-key-name-cell";
+export type { RootKeyNameCellProps } from "./root-key-name-cell";
+export { RowActionSkeleton } from "./row-action-skeleton";
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/last-updated.tsx b/web/internal/ui/src/components/data-table/components/cells/last-updated-cell.tsx
similarity index 72%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/last-updated.tsx
rename to web/internal/ui/src/components/data-table/components/cells/last-updated-cell.tsx
index 7c78431e51..04442639e5 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/last-updated.tsx
+++ b/web/internal/ui/src/components/data-table/components/cells/last-updated-cell.tsx
@@ -1,17 +1,18 @@
-import { cn } from "@/lib/utils";
+"use client";
import { ChartActivity2 } from "@unkey/icons";
-import { Badge, TimestampInfo } from "@unkey/ui";
import { useRef, useState } from "react";
-import { STATUS_STYLES } from "../utils/get-row-class";
+import { cn } from "../../../../lib/utils";
+import { Badge } from "../../../badge";
+import { TimestampInfo } from "../../../timestamp-info";
+import { STATUS_STYLES } from "../../constants/constants";
-export const LastUpdated = ({
- isSelected,
- lastUpdated,
-}: {
+export interface LastUpdatedCellProps {
isSelected: boolean;
lastUpdated?: number | null;
-}) => {
- const badgeRef = useRef(null) as React.RefObject;
+}
+
+export const LastUpdatedCell = ({ isSelected, lastUpdated }: LastUpdatedCellProps) => {
+ const badgeRef = useRef(null);
const [showTooltip, setShowTooltip] = useState(false);
return (
diff --git a/web/internal/ui/src/components/data-table/components/cells/root-key-name-cell.tsx b/web/internal/ui/src/components/data-table/components/cells/root-key-name-cell.tsx
new file mode 100644
index 0000000000..49c5f0ebdc
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/cells/root-key-name-cell.tsx
@@ -0,0 +1,35 @@
+import { Key2 } from "@unkey/icons";
+import { cn } from "../../../../lib/utils";
+
+export type RootKeyNameCellProps = {
+ name?: string;
+ isSelected?: boolean;
+};
+
+export const RootKeyNameCell = ({ name, isSelected = false }: RootKeyNameCellProps) => {
+ return (
+
+
+
+
+
+
+
+ {name ?? "Unnamed Root Key"}
+
+
+
+
+ );
+};
diff --git a/web/internal/ui/src/components/data-table/components/cells/row-action-skeleton.tsx b/web/internal/ui/src/components/data-table/components/cells/row-action-skeleton.tsx
new file mode 100644
index 0000000000..1a9752cd82
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/cells/row-action-skeleton.tsx
@@ -0,0 +1,17 @@
+import { Dots } from "@unkey/icons";
+import { cn } from "../../../../lib/utils";
+
+export const RowActionSkeleton = () => (
+
+
+
+);
diff --git a/web/internal/ui/src/components/data-table/components/cells/status-cell.tsx b/web/internal/ui/src/components/data-table/components/cells/status-cell.tsx
new file mode 100644
index 0000000000..c85eb4cfc5
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/cells/status-cell.tsx
@@ -0,0 +1,53 @@
+import { Check, Clock, XMark } from "@unkey/icons";
+import { Badge } from "../../../badge";
+
+export interface StatusCellProps {
+ status: "success" | "pending" | "error" | "warning" | "active" | "inactive";
+ label?: string;
+ showIcon?: boolean;
+}
+
+/**
+ * Status cell with badge and optional icon
+ */
+export function StatusCell({ status, label, showIcon = true }: StatusCellProps) {
+ const config = getStatusConfig(status);
+
+ return (
+
+ {showIcon && config.icon && }
+ {label || config.label}
+
+ );
+}
+
+function getStatusConfig(status: StatusCellProps["status"]) {
+ switch (status) {
+ case "success":
+ case "active":
+ return {
+ variant: "success" as const,
+ label: status === "success" ? "Success" : "Active",
+ icon: Check,
+ };
+ case "pending":
+ return {
+ variant: "primary" as const,
+ label: "Pending",
+ icon: Clock,
+ };
+ case "warning":
+ return {
+ variant: "warning" as const,
+ label: "Warning",
+ icon: Clock,
+ };
+ case "error":
+ case "inactive":
+ return {
+ variant: "error" as const,
+ label: status === "error" ? "Error" : "Inactive",
+ icon: XMark,
+ };
+ }
+}
diff --git a/web/internal/ui/src/components/data-table/components/cells/timestamp-cell.tsx b/web/internal/ui/src/components/data-table/components/cells/timestamp-cell.tsx
new file mode 100644
index 0000000000..053ffd3c7d
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/cells/timestamp-cell.tsx
@@ -0,0 +1,39 @@
+import { formatDistanceToNow } from "date-fns";
+
+export interface TimestampCellProps {
+ timestamp: number | Date;
+ format?: "relative" | "absolute" | "both";
+}
+
+/**
+ * Timestamp cell with relative or absolute time display
+ */
+export function TimestampCell({ timestamp, format = "relative" }: TimestampCellProps) {
+ const date = typeof timestamp === "number" ? new Date(timestamp) : timestamp;
+
+ if (Number.isNaN(date.getTime())) {
+ return — ;
+ }
+
+ if (format === "relative") {
+ return (
+
+ {formatDistanceToNow(date, { addSuffix: true })}
+
+ );
+ }
+
+ if (format === "absolute") {
+ return {date.toLocaleString()} ;
+ }
+
+ // Both
+ return (
+
+
+ {formatDistanceToNow(date, { addSuffix: true })}
+
+ {date.toLocaleString()}
+
+ );
+}
diff --git a/web/internal/ui/src/components/data-table/components/empty/empty-root-keys.tsx b/web/internal/ui/src/components/data-table/components/empty/empty-root-keys.tsx
new file mode 100644
index 0000000000..cb945af90e
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/empty/empty-root-keys.tsx
@@ -0,0 +1,31 @@
+import { BookBookmark } from "@unkey/icons";
+import { buttonVariants } from "../../../buttons/button";
+import { Empty } from "../../../empty";
+
+export function EmptyRootKeys() {
+ return (
+
+
+
+ No Root Keys Found
+
+ There are no root keys configured yet. Create your first root key to start managing
+ permissions and access control.
+
+
+
+
+
+ Learn about Root Keys
+
+
+
+
+
+ );
+}
diff --git a/web/internal/ui/src/components/data-table/components/empty/index.ts b/web/internal/ui/src/components/data-table/components/empty/index.ts
new file mode 100644
index 0000000000..b887cfbcde
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/empty/index.ts
@@ -0,0 +1 @@
+export { EmptyRootKeys } from "./empty-root-keys";
diff --git a/web/internal/ui/src/components/data-table/components/footer/index.ts b/web/internal/ui/src/components/data-table/components/footer/index.ts
new file mode 100644
index 0000000000..682fe992e0
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/footer/index.ts
@@ -0,0 +1,2 @@
+export { LoadMoreFooter } from "./load-more-footer";
+export { PaginationFooter } from "./pagination-footer";
diff --git a/web/internal/ui/src/components/data-table/components/footer/load-more-footer.tsx b/web/internal/ui/src/components/data-table/components/footer/load-more-footer.tsx
new file mode 100644
index 0000000000..623cec239e
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/footer/load-more-footer.tsx
@@ -0,0 +1,156 @@
+"use client";
+import { ArrowsToAllDirections, ArrowsToCenter } from "@unkey/icons";
+import { useCallback, useState } from "react";
+import { cn } from "../../../../lib/utils";
+import { Button } from "../../../buttons/button";
+
+export interface LoadMoreFooterComponentProps {
+ onLoadMore?: () => void;
+ isFetchingNextPage?: boolean;
+ totalVisible: number;
+ totalCount: number;
+ className?: string;
+ itemLabel?: string;
+ buttonText?: string;
+ hasMore: boolean;
+ hide?: boolean;
+ countInfoText?: React.ReactNode;
+ headerContent?: React.ReactNode;
+}
+
+/**
+ * Load more footer component with collapsible design
+ * Preserves exact design from virtual-table
+ */
+export function LoadMoreFooter({
+ onLoadMore,
+ isFetchingNextPage = false,
+ totalVisible,
+ totalCount,
+ itemLabel = "items",
+ buttonText = "Load more",
+ hasMore,
+ countInfoText,
+ hide,
+ headerContent,
+}: LoadMoreFooterComponentProps) {
+ const [isOpen, setIsOpen] = useState(true);
+
+ const shouldShow = !!onLoadMore;
+
+ const handleClose = useCallback(() => {
+ setIsOpen(false);
+ }, []);
+
+ const handleOpen = useCallback(() => {
+ setIsOpen(true);
+ }, []);
+
+ if (hide) {
+ return null;
+ }
+
+ // Minimized state - parked at right side
+ if (!isOpen) {
+ return (
+
+
+
+
+ {countInfoText}
+
+
+
+ {buttonText}
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {/* Header content */}
+ {headerContent && (
+
+ {headerContent}
+
+ )}
+
+
+ {countInfoText &&
{countInfoText}
}
+ {!countInfoText && (
+
+ Viewing
+
+ {totalVisible}
+
+ of
+ {totalCount}
+ {itemLabel}
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/web/internal/ui/src/components/data-table/components/footer/pagination-footer.tsx b/web/internal/ui/src/components/data-table/components/footer/pagination-footer.tsx
new file mode 100644
index 0000000000..36da63dfad
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/footer/pagination-footer.tsx
@@ -0,0 +1,207 @@
+"use client";
+import { ArrowsToAllDirections, ArrowsToCenter, ChevronLeft, ChevronRight } from "@unkey/icons";
+import { memo, useMemo, useState } from "react";
+import { cn } from "../../../../lib/utils";
+import { Button } from "../../../buttons/button";
+import { getPageNumbers } from "../../utils/get-page-numbers";
+import { PaginationFooterSkeleton } from "../skeletons/pagination-footer-skeleton";
+
+export interface PaginationFooterProps {
+ page: number;
+ pageSize: number;
+ totalPages: number;
+ totalCount: number;
+ onPageChange: (page: number) => void;
+ itemLabel?: string;
+ hide?: boolean;
+ loading?: boolean;
+ disabled?: boolean;
+ headerContent?: React.ReactNode;
+}
+
+export const PaginationFooter = memo(function PaginationFooter({
+ page,
+ pageSize,
+ totalPages,
+ totalCount,
+ onPageChange,
+ itemLabel = "items",
+ hide,
+ loading,
+ disabled,
+ headerContent,
+}: PaginationFooterProps) {
+ const [isOpen, setIsOpen] = useState(true);
+
+ const start = (page - 1) * pageSize + 1;
+ const end = Math.min(page * pageSize, totalCount);
+ const pageNumbers = useMemo(() => getPageNumbers(page, totalPages), [page, totalPages]);
+
+ if (hide) {
+ return null;
+ }
+
+ // Minimized state - parked at right side
+ if (!isOpen) {
+ return (
+
+
setIsOpen(true)}
+ className="bg-gray-1 dark:bg-black border border-gray-6 rounded-lg shadow-lg p-3 duration-200 hover:shadow-xl hover:scale-105 group"
+ title={`Page ${page} of ${totalPages} • ${start}-${end} of ${totalCount} ${itemLabel}`}
+ >
+
+
+ {start}-{end} of {totalCount}
+
+ {totalPages === 1 ? null : (
+ <>
+
+
+ Page {page}/{totalPages}
+
+ >
+ )}
+
+
+
+
+ );
+ }
+
+ return (
+
+ {loading ? (
+
+ ) : (
+
+
+ {/* Header content */}
+ {headerContent && (
+
+ {headerContent}
+
+ )}
+
+
+ {/* Item count */}
+
+ Viewing
+
+ {start}-{end}
+
+ of
+ {totalCount}
+ {itemLabel}
+
+
+ {/* Pagination controls */}
+
+ {/* Previous button */}
+ {totalPages === 1 ? null : (
+ onPageChange(page - 1)}
+ disabled={disabled || page === 1}
+ aria-label="Go to previous page"
+ className="border-none disabled:pointer-events-none disabled:opacity-30 focus:ring-0"
+ >
+
+
+ )}
+ {/* Page number segmented group */}
+ {totalPages === 1 ? null : (
+
+ {pageNumbers.map((pageNum, idx) => {
+ if (pageNum === "ellipsis") {
+ return (
+
+ ···
+
+ );
+ }
+
+ const isCurrentPage = pageNum === page;
+ return (
+ {
+ if (!isCurrentPage && !disabled) {
+ onPageChange(pageNum);
+ }
+ }}
+ disabled={disabled && !isCurrentPage}
+ aria-label={`Page ${pageNum}`}
+ aria-current={isCurrentPage ? "page" : undefined}
+ className={cn(
+ "w-7 h-7 flex items-center justify-center rounded-md text-xs font-medium cursor-pointer",
+ isCurrentPage
+ ? "text-accent-12 shadow- pointer-events-none ring-0 border border-grayA-4 text-sm transition-all duration-300"
+ : "text-gray-11 hover:text-gray-12 hover:bg-grayA-3",
+ disabled && !isCurrentPage && "opacity-30 pointer-events-none",
+ )}
+ >
+ {pageNum}
+
+ );
+ })}
+
+ )}
+ {/* Next button */}
+ {totalPages === 1 ? null : (
+ onPageChange(page + 1)}
+ disabled={disabled || page === totalPages}
+ aria-label="Go to next page"
+ className="border-none disabled:pointer-events-none disabled:opacity-30 focus:ring-0"
+ >
+
+
+ )}
+ {/* Minimize button */}
+
+
setIsOpen(false)}
+ aria-label="Minimize"
+ title="Minimize"
+ >
+
+
+
+
+
+
+
+ )}
+
+ );
+});
diff --git a/web/internal/ui/src/components/data-table/components/headers/index.ts b/web/internal/ui/src/components/data-table/components/headers/index.ts
new file mode 100644
index 0000000000..526fdee95b
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/headers/index.ts
@@ -0,0 +1,2 @@
+export { SortableHeader } from "./sortable-header";
+export type { SortableHeaderProps } from "./sortable-header";
diff --git a/web/internal/ui/src/components/data-table/components/headers/sortable-header.tsx b/web/internal/ui/src/components/data-table/components/headers/sortable-header.tsx
new file mode 100644
index 0000000000..d320577e75
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/headers/sortable-header.tsx
@@ -0,0 +1,61 @@
+import type { Header } from "@tanstack/react-table";
+import { flexRender } from "@tanstack/react-table";
+import { ChevronDown, ChevronUp } from "@unkey/icons";
+import type { ReactNode } from "react";
+import { cn } from "../../../../lib/utils";
+
+export interface SortableHeaderProps {
+ header: Header;
+ children?: ReactNode;
+}
+
+/**
+ * Sortable header component with 3-state sorting
+ * States: null → asc → desc → null
+ */
+export function SortableHeader({ header, children }: SortableHeaderProps) {
+ const { column } = header;
+ const canSort = column.getCanSort();
+ const isSorted = column.getIsSorted();
+
+ if (!canSort) {
+ return (
+
+ {children || flexRender(column.columnDef.header, header.getContext())}
+
+ );
+ }
+
+ return (
+
+ {children || flexRender(column.columnDef.header, header.getContext())}
+
+
+
+
+ );
+}
+
+function SortIcon({ sorted }: { sorted: false | "asc" | "desc" }) {
+ return (
+
+
+
+
+ );
+}
diff --git a/web/internal/ui/src/components/data-table/components/rows/index.ts b/web/internal/ui/src/components/data-table/components/rows/index.ts
new file mode 100644
index 0000000000..dc18e07f56
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/rows/index.ts
@@ -0,0 +1 @@
+export { SkeletonRow } from "./skeleton-row";
diff --git a/web/internal/ui/src/components/data-table/components/rows/skeleton-row.tsx b/web/internal/ui/src/components/data-table/components/rows/skeleton-row.tsx
new file mode 100644
index 0000000000..2017af6d8b
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/rows/skeleton-row.tsx
@@ -0,0 +1,31 @@
+import { cn } from "../../../../lib/utils";
+import type { DataTableColumnDef } from "../../types";
+
+interface SkeletonRowProps {
+ columns: DataTableColumnDef[];
+ rowHeight: number;
+ className?: string;
+}
+
+/**
+ * Skeleton row component for loading states
+ * Supports custom skeleton renderers per column
+ */
+export function SkeletonRow({ columns, rowHeight, className }: SkeletonRowProps) {
+ return (
+ <>
+ {columns.map((column) => (
+
+ {column.meta?.skeleton ? (
+ column.meta.skeleton()
+ ) : (
+
+ )}
+
+ ))}
+ >
+ );
+}
diff --git a/web/internal/ui/src/components/data-table/components/skeletons/index.ts b/web/internal/ui/src/components/data-table/components/skeletons/index.ts
new file mode 100644
index 0000000000..d99c837d7f
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/skeletons/index.ts
@@ -0,0 +1,9 @@
+export {
+ ActionColumnSkeleton,
+ CreatedAtColumnSkeleton,
+ KeyColumnSkeleton,
+ LastUpdatedColumnSkeleton,
+ PermissionsColumnSkeleton,
+ RootKeyColumnSkeleton,
+} from "./root-key-skeletons";
+export { PaginationFooterSkeleton } from "./pagination-footer-skeleton";
diff --git a/web/internal/ui/src/components/data-table/components/skeletons/pagination-footer-skeleton.tsx b/web/internal/ui/src/components/data-table/components/skeletons/pagination-footer-skeleton.tsx
new file mode 100644
index 0000000000..846a958e39
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/skeletons/pagination-footer-skeleton.tsx
@@ -0,0 +1,40 @@
+export const PaginationFooterSkeleton = () => {
+ return (
+
+
+ {/* Item count skeleton */}
+
+
+ {/* Pagination controls skeleton */}
+
+ {/* Prev button */}
+
+
+ {/* Page pill group */}
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+
+ {/* Next button */}
+
+
+ {/* Minimize button */}
+
+
+
+
+ );
+};
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/skeletons.tsx b/web/internal/ui/src/components/data-table/components/skeletons/root-key-skeletons.tsx
similarity index 89%
rename from web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/skeletons.tsx
rename to web/internal/ui/src/components/data-table/components/skeletons/root-key-skeletons.tsx
index bb5b74b9f0..db1644b7d2 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/components/skeletons.tsx
+++ b/web/internal/ui/src/components/data-table/components/skeletons/root-key-skeletons.tsx
@@ -1,8 +1,8 @@
-import { cn } from "@/lib/utils";
import { ChartActivity2, Dots, Key2, Page2 } from "@unkey/icons";
+import { cn } from "../../../../lib/utils";
export const RootKeyColumnSkeleton = () => (
-
+
@@ -13,7 +13,7 @@ export const RootKeyColumnSkeleton = () => (
);
export const CreatedAtColumnSkeleton = () => (
-
+
);
@@ -25,7 +25,7 @@ export const KeyColumnSkeleton = () => (
);
export const PermissionsColumnSkeleton = () => (
-
+
diff --git a/web/internal/ui/src/components/data-table/components/utils/empty-state.tsx b/web/internal/ui/src/components/data-table/components/utils/empty-state.tsx
new file mode 100644
index 0000000000..6a10d0dcc1
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/utils/empty-state.tsx
@@ -0,0 +1,41 @@
+import { BookBookmark } from "@unkey/icons";
+import type React from "react";
+import { Button } from "../../../buttons/button";
+import { Empty } from "../../../empty";
+
+export interface EmptyStateProps {
+ content?: React.ReactNode;
+}
+
+/**
+ * Empty state component for tables with no data
+ */
+export const EmptyState = ({ content }: EmptyStateProps) => {
+ return (
+
+ {content ?? (
+
+
+
+ Nothing here yet
+
+ Ready to get started? Check our documentation for a step-by-step guide.
+
+
+
+
+
+ Documentation
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/web/internal/ui/src/components/data-table/components/utils/realtime-separator.tsx b/web/internal/ui/src/components/data-table/components/utils/realtime-separator.tsx
new file mode 100644
index 0000000000..0e1e040714
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/components/utils/realtime-separator.tsx
@@ -0,0 +1,14 @@
+import { CircleCaretRight } from "@unkey/icons";
+
+/**
+ * Separator component for real-time data boundary
+ * Preserves exact design from virtual-table
+ */
+export const RealtimeSeparator = () => {
+ return (
+
+
+ Live
+
+ );
+};
diff --git a/web/internal/ui/src/components/data-table/constants/constants.ts b/web/internal/ui/src/components/data-table/constants/constants.ts
new file mode 100644
index 0000000000..81bae385e1
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/constants/constants.ts
@@ -0,0 +1,49 @@
+import type { DataTableConfig } from "../types";
+
+/**
+ * Default configuration for DataTable
+ */
+export const DEFAULT_CONFIG: DataTableConfig = {
+ // Dimensions
+ rowHeight: 36,
+ headerHeight: 40,
+ rowSpacing: 4,
+
+ // Layout
+ layout: "classic",
+ rowBorders: false,
+ containerPadding: "px-2",
+ tableLayout: "fixed",
+
+ // Loading
+ loadingRows: 10,
+} as const;
+
+/**
+ * Mobile table height constant
+ */
+export const MOBILE_TABLE_HEIGHT = 400;
+
+/**
+ * Breathing space for table height calculation
+ */
+export const BREATHING_SPACE = 10;
+
+export type StatusStyle = {
+ base: string;
+ hover: string;
+ selected: string;
+ badge: { default: string; selected: string };
+ focusRing: string;
+};
+
+export const STATUS_STYLES: StatusStyle = {
+ base: "text-grayA-9",
+ hover: "hover:text-accent-11 dark:hover:text-accent-12 hover:bg-grayA-2",
+ selected: "text-accent-12 bg-grayA-2 hover:text-accent-12",
+ badge: {
+ default: "bg-grayA-3 text-grayA-11 group-hover:bg-grayA-5 border-transparent",
+ selected: "bg-grayA-5 text-grayA-12 hover:bg-grayA-5 border-grayA-3",
+ },
+ focusRing: "focus:ring-accent-7",
+};
diff --git a/web/internal/ui/src/components/data-table/constants/index.ts b/web/internal/ui/src/components/data-table/constants/index.ts
new file mode 100644
index 0000000000..0402ac0e3c
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/constants/index.ts
@@ -0,0 +1,2 @@
+export { BREATHING_SPACE, DEFAULT_CONFIG, MOBILE_TABLE_HEIGHT, STATUS_STYLES } from "./constants";
+export type { StatusStyle } from "./constants";
diff --git a/web/internal/ui/src/components/data-table/data-table.tsx b/web/internal/ui/src/components/data-table/data-table.tsx
new file mode 100644
index 0000000000..23fc3079d0
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/data-table.tsx
@@ -0,0 +1,443 @@
+"use client";
+import { flexRender } from "@tanstack/react-table";
+import {
+ Fragment,
+ type KeyboardEvent,
+ type ReactElement,
+ type ReactNode,
+ type Ref,
+ forwardRef,
+ useCallback,
+ useImperativeHandle,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import { useIsMobile } from "../../hooks/use-mobile";
+import { cn } from "../../lib/utils";
+import { LoadMoreFooter } from "./components/footer/load-more-footer";
+import { SkeletonRow } from "./components/rows/skeleton-row";
+import { EmptyState } from "./components/utils/empty-state";
+import { RealtimeSeparator } from "./components/utils/realtime-separator";
+import { DEFAULT_CONFIG, MOBILE_TABLE_HEIGHT } from "./constants/constants";
+import { useDataTable } from "./hooks/use-data-table";
+import { useRealtimeData } from "./hooks/use-realtime-data";
+import { useTableHeight } from "./hooks/use-table-height";
+import type { DataTableProps } from "./types";
+import { calculateColumnWidth } from "./utils/column-width";
+
+export type DataTableRef = {
+ parentRef: HTMLDivElement | null;
+ containerRef: HTMLDivElement | null;
+};
+
+/**
+ * Main DataTable component with TanStack Table + TanStack Virtual
+ */
+function DataTableInner
(props: DataTableProps, ref: Ref) {
+ const {
+ data: historicData,
+ realtimeData = [],
+ columns,
+ getRowId,
+ sorting,
+ onSortingChange,
+ rowSelection,
+ onRowSelectionChange,
+ onRowClick,
+ onRowMouseEnter,
+ onRowMouseLeave,
+ selectedItem,
+ config: userConfig,
+ emptyState,
+ loadMoreFooterProps,
+ rowClassName,
+ selectedClassName,
+ fixedHeight: fixedHeightProp,
+ enableKeyboardNav = true,
+ enableSorting = true,
+ enableRowSelection = false,
+ manualSorting = false,
+ isLoading = false,
+ renderSkeletonRow,
+ } = props;
+
+ // Merge configs
+ const config = useMemo(() => ({ ...DEFAULT_CONFIG, ...userConfig }), [userConfig]);
+ const isGridLayout = config.layout === "grid";
+
+ // Refs
+ const parentRef = useRef(null);
+ const containerRef = useRef(null);
+
+ // Roving tabindex: track which row is the current tab stop
+ const [focusedRowIndex, setFocusedRowIndex] = useState(0);
+
+ // Mobile detection
+ const isMobile = useIsMobile({ defaultValue: false });
+
+ // Height calculation
+ const calculatedHeight = useTableHeight(containerRef);
+ const fixedHeight = fixedHeightProp ?? calculatedHeight;
+
+ // Real-time data merging
+ const tableDataHelper = useRealtimeData(getRowId, realtimeData, historicData);
+
+ // TanStack Table
+ const table = useDataTable({
+ data: tableDataHelper.data,
+ columns,
+ getRowId,
+ enableSorting,
+ enableRowSelection,
+ manualSorting,
+ sorting,
+ onSortingChange,
+ rowSelection,
+ onRowSelectionChange,
+ });
+
+ // Expose refs
+ useImperativeHandle(
+ ref,
+ () => ({
+ parentRef: parentRef.current,
+ containerRef: containerRef.current,
+ }),
+ [],
+ );
+
+ // Calculate column widths
+ const colWidths = useMemo(
+ () => columns.map((col) => calculateColumnWidth(col.meta?.width)),
+ [columns],
+ );
+
+ // Build a Set of realtime row IDs for separator boundary detection
+ const realtimeIds = useMemo(() => new Set(realtimeData.map(getRowId)), [realtimeData, getRowId]);
+
+ // Keyboard navigation handler
+ const handleKeyDown = useCallback(
+ (event: KeyboardEvent, rowIndex: number) => {
+ if (!enableKeyboardNav) {
+ return;
+ }
+
+ if (event.key === "Escape") {
+ event.preventDefault();
+ onRowClick?.(null);
+ const activeElement = document.activeElement as HTMLElement;
+ activeElement?.blur();
+ }
+
+ if (event.key === "ArrowDown" || event.key === "j") {
+ event.preventDefault();
+ const nextIndex = rowIndex + 1;
+ const nextElement = parentRef.current?.querySelector(
+ `[data-row-index="${nextIndex}"]`,
+ ) as HTMLElement | null;
+
+ if (nextElement) {
+ setFocusedRowIndex(nextIndex);
+ nextElement.focus();
+ }
+ }
+
+ if (event.key === "ArrowUp" || event.key === "k") {
+ event.preventDefault();
+ const prevIndex = rowIndex - 1;
+ const prevElement = parentRef.current?.querySelector(
+ `[data-row-index="${prevIndex}"]`,
+ ) as HTMLElement | null;
+ if (prevElement) {
+ setFocusedRowIndex(prevIndex);
+ prevElement.focus();
+ }
+ }
+ },
+ [enableKeyboardNav, onRowClick],
+ );
+
+ // CSS classes
+ const hasPadding = config.containerPadding !== "px-0";
+
+ const tableClassName = cn(
+ "w-full",
+ isGridLayout ? "border-collapse" : "border-separate border-spacing-0",
+ config.tableLayout === "fixed" ? "table-fixed" : "table-auto",
+ );
+
+ const containerClassName = cn(
+ "overflow-auto relative pb-4 bg-white dark:bg-black",
+ config.containerPadding || "px-2",
+ );
+
+ // Empty state
+ if (!isLoading && historicData.length === 0 && realtimeData.length === 0) {
+ return (
+
+
+
+ {columns.map((col, idx) => (
+
+ ))}
+
+
+
+ {table.getHeaderGroups()[0]?.headers.map((header) => (
+
+ {header.isPlaceholder ? null : (
+
+ {flexRender(header.column.columnDef.header, header.getContext())}
+
+ )}
+
+ ))}
+
+
+
+
+
+
+
+
+ {emptyState ? (
+
{emptyState}
+ ) : (
+
+ )}
+
+ );
+ }
+
+ // Main render
+ return (
+
+
+
+
+ {columns.map((col, idx) => (
+
+ ))}
+
+
+ {/* Header */}
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(header.column.columnDef.header, header.getContext())}
+
+ ))}
+
+ ))}
+
+
+
+
+
+
+
+ {/* Body */}
+
+ {/* Render all rows directly without virtualization */}
+ {isLoading
+ ? // Loading skeleton rows
+ Array.from({ length: config.loadingRows }).map((_, index) => (
+
+ {renderSkeletonRow ? (
+ renderSkeletonRow({
+ columns,
+ rowHeight: config.rowHeight,
+ })
+ ) : (
+
+ )}
+
+ ))
+ : // Data rows — iterate TanStack's sorted row model for correct ordering
+ (() => {
+ let separatorInserted = realtimeData.length === 0;
+ return table.getRowModel().rows.flatMap((tableRow, index) => {
+ const typedItem = tableRow.original;
+ const rowId = tableRow.id;
+ const isSelected = selectedItem ? getRowId(selectedItem) === rowId : false;
+ const elements: ReactNode[] = [];
+
+ // Insert separator at boundary between realtime and historic items
+ if (!separatorInserted && !realtimeIds.has(rowId)) {
+ separatorInserted = true;
+ elements.push(
+
+
+
+
+
+
+
+ ,
+ );
+ }
+
+ const visibleCells = tableRow.getVisibleCells();
+
+ // Grid layout (no spacing)
+ if (isGridLayout) {
+ elements.push(
+ {
+ setFocusedRowIndex(index);
+ onRowClick?.(typedItem);
+ }}
+ onMouseEnter={() => onRowMouseEnter?.(typedItem)}
+ onMouseLeave={() => onRowMouseLeave?.()}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ onRowClick?.(typedItem);
+ } else {
+ handleKeyDown(e, index);
+ }
+ }}
+ className={cn(
+ "cursor-pointer transition-colors hover:bg-accent/50 focus:outline-none focus:ring-1 focus:ring-opacity-40",
+ config.rowBorders && "border-b border-gray-4",
+ rowClassName?.(typedItem),
+ selectedClassName?.(typedItem, isSelected),
+ )}
+ style={{ height: `${config.rowHeight}px` }}
+ >
+ {visibleCells.map((cell, idx) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+ ,
+ );
+ } else {
+ // Classic layout (with spacing)
+ elements.push(
+
+ {(config.rowSpacing ?? 4) > 0 && (
+
+ )}
+ {
+ setFocusedRowIndex(index);
+ onRowClick?.(typedItem);
+ }}
+ onMouseEnter={() => onRowMouseEnter?.(typedItem)}
+ onMouseLeave={() => onRowMouseLeave?.()}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ onRowClick?.(typedItem);
+ } else {
+ handleKeyDown(e, index);
+ }
+ }}
+ className={cn(
+ "cursor-pointer transition-colors hover:bg-accent/50 focus:outline-none focus:ring-1 focus:ring-opacity-40",
+ config.rowBorders && "border-b border-gray-4",
+ rowClassName?.(typedItem),
+ selectedClassName?.(typedItem, isSelected),
+ )}
+ style={{ height: `${config.rowHeight}px` }}
+ >
+ {visibleCells.map((cell, idx) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+
+ ,
+ );
+ }
+
+ return elements;
+ });
+ })()}
+
+
+
+
+ {/* Legacy load more footer */}
+ {loadMoreFooterProps && (
+
+ )}
+
+ );
+}
+
+/**
+ * Exported DataTable component with proper generic type support
+ */
+export const DataTable = forwardRef(DataTableInner) as (
+ props: DataTableProps & { ref?: Ref },
+) => ReactElement;
diff --git a/web/internal/ui/src/components/data-table/hooks/use-data-table.ts b/web/internal/ui/src/components/data-table/hooks/use-data-table.ts
new file mode 100644
index 0000000000..516203a77f
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/hooks/use-data-table.ts
@@ -0,0 +1,77 @@
+"use client";
+import {
+ type OnChangeFn,
+ type RowSelectionState,
+ type SortingState,
+ getCoreRowModel,
+ getSortedRowModel,
+ useReactTable,
+} from "@tanstack/react-table";
+import { useState } from "react";
+import type { DataTableColumnDef } from "../types";
+
+interface UseDataTableProps {
+ data: TData[];
+ columns: DataTableColumnDef[];
+ getRowId: (row: TData) => string;
+ enableSorting?: boolean;
+ enableRowSelection?: boolean;
+ manualSorting?: boolean;
+ sorting?: SortingState;
+ onSortingChange?: OnChangeFn;
+ rowSelection?: RowSelectionState;
+ onRowSelectionChange?: OnChangeFn;
+}
+
+/**
+ * TanStack Table wrapper with default configuration
+ */
+export const useDataTable = ({
+ data,
+ columns,
+ getRowId,
+ enableSorting = true,
+ enableRowSelection = false,
+ manualSorting = false,
+ sorting: controlledSorting,
+ onSortingChange: controlledOnSortingChange,
+ rowSelection: controlledRowSelection,
+ onRowSelectionChange: controlledOnRowSelectionChange,
+}: UseDataTableProps) => {
+ // Internal state for uncontrolled mode
+ const [internalSorting, setInternalSorting] = useState([]);
+ const [internalRowSelection, setInternalRowSelection] = useState({});
+
+ // Use controlled state if provided, otherwise use internal state
+ const sorting = controlledSorting ?? internalSorting;
+ const onSortingChange = controlledOnSortingChange ?? setInternalSorting;
+ const rowSelection = controlledRowSelection ?? internalRowSelection;
+ const onRowSelectionChange = controlledOnRowSelectionChange ?? setInternalRowSelection;
+
+ // Create table instance
+ const table = useReactTable({
+ data,
+ columns,
+ getRowId,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: enableSorting && !manualSorting ? getSortedRowModel() : undefined,
+ manualSorting,
+
+ // Sorting state
+ state: {
+ sorting,
+ rowSelection,
+ },
+ onSortingChange,
+ onRowSelectionChange,
+
+ // Enable features
+ enableSorting,
+ enableRowSelection,
+ enableSortingRemoval: true, // Enable 3-state sorting
+ enableMultiSort: false, // Single column sort only
+ sortDescFirst: false, // First click sorts ascending (A→Z / oldest→newest)
+ });
+
+ return table;
+};
diff --git a/web/internal/ui/src/components/data-table/hooks/use-realtime-data.ts b/web/internal/ui/src/components/data-table/hooks/use-realtime-data.ts
new file mode 100644
index 0000000000..bb9e6fc687
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/hooks/use-realtime-data.ts
@@ -0,0 +1,51 @@
+import { useMemo } from "react";
+import type { TableDataItem } from "../types";
+
+/**
+ * Merges realtime and historic data with separator
+ * Deduplicates by ID and inserts separator at boundary
+ */
+export const useRealtimeData = (
+ getRowId: (row: TData) => string,
+ realtimeData: TData[] = [],
+ historicData: TData[] = [],
+) => {
+ return useMemo(() => {
+ // If no realtime data, return historic data as-is
+ if (realtimeData.length === 0) {
+ return {
+ data: historicData,
+ getTotalLength: () => historicData.length,
+ getItemAt: (index: number): TableDataItem | undefined => historicData[index],
+ };
+ }
+
+ // Create ID set from realtime data for deduplication
+ const realtimeIds = new Set(realtimeData.map(getRowId));
+
+ // Filter out historic items that exist in realtime
+ const filteredHistoric = historicData.filter((item) => !realtimeIds.has(getRowId(item)));
+
+ // Total length: realtime + separator + deduplicated historic
+ const totalLength = realtimeData.length + 1 + filteredHistoric.length;
+
+ return {
+ data: [...realtimeData, ...filteredHistoric],
+ getTotalLength: () => totalLength,
+ getItemAt: (index: number): TableDataItem | undefined => {
+ // Realtime data
+ if (index < realtimeData.length) {
+ return realtimeData[index];
+ }
+
+ // Separator
+ if (index === realtimeData.length) {
+ return { isSeparator: true };
+ }
+
+ // Historic data (offset by realtime length + separator)
+ return filteredHistoric[index - realtimeData.length - 1];
+ },
+ };
+ }, [realtimeData, historicData, getRowId]);
+};
diff --git a/web/internal/ui/src/components/data-table/hooks/use-table-height.ts b/web/internal/ui/src/components/data-table/hooks/use-table-height.ts
new file mode 100644
index 0000000000..4baa4cf899
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/hooks/use-table-height.ts
@@ -0,0 +1,38 @@
+"use client";
+import { type RefObject, useEffect, useState } from "react";
+import { BREATHING_SPACE } from "../constants/constants";
+
+/**
+ * Calculate dynamic table height based on viewport
+ * Adds breathing space to prevent table from extending to viewport edge
+ */
+export const useTableHeight = (containerRef: RefObject) => {
+ const [fixedHeight, setFixedHeight] = useState(0);
+
+ useEffect(() => {
+ const calculateHeight = () => {
+ if (!containerRef.current) {
+ return;
+ }
+ const rect = containerRef.current.getBoundingClientRect();
+ const availableHeight = window.innerHeight - rect.top - BREATHING_SPACE;
+ setFixedHeight(Math.max(availableHeight, 0));
+ };
+
+ calculateHeight();
+
+ const resizeObserver = new ResizeObserver(calculateHeight);
+ window.addEventListener("resize", calculateHeight);
+
+ if (containerRef.current) {
+ resizeObserver.observe(containerRef.current);
+ }
+
+ return () => {
+ resizeObserver.disconnect();
+ window.removeEventListener("resize", calculateHeight);
+ };
+ }, [containerRef]);
+
+ return fixedHeight;
+};
diff --git a/web/internal/ui/src/components/data-table/index.ts b/web/internal/ui/src/components/data-table/index.ts
new file mode 100644
index 0000000000..781f3c5746
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/index.ts
@@ -0,0 +1,76 @@
+// Main component
+export { DataTable } from "./data-table";
+export type { DataTableRef } from "./data-table";
+
+// Types
+export type {
+ DataTableProps,
+ DataTableConfig,
+ DataTableColumnDef,
+ DataTableColumnMeta,
+ ColumnWidth,
+ LayoutMode,
+ LoadMoreFooterProps,
+ SeparatorItem,
+ TableDataItem,
+} from "./types";
+
+// Constants
+export { DEFAULT_CONFIG, MOBILE_TABLE_HEIGHT, BREATHING_SPACE, STATUS_STYLES } from "./constants";
+export type { StatusStyle } from "./constants";
+
+// Hooks
+export { useDataTable } from "./hooks/use-data-table";
+export { useRealtimeData } from "./hooks/use-realtime-data";
+export { useTableHeight } from "./hooks/use-table-height";
+
+// Cell components
+export { CheckboxCell, CheckboxHeaderCell, RowActionSkeleton } from "./components/cells";
+export { StatusCell } from "./components/cells";
+export type { StatusCellProps } from "./components/cells";
+export { TimestampCell } from "./components/cells";
+export type { TimestampCellProps } from "./components/cells";
+export { BadgeCell } from "./components/cells";
+export type { BadgeCellProps } from "./components/cells";
+export { CopyCell } from "./components/cells";
+export type { CopyCellProps } from "./components/cells";
+export { AssignedItemsCell } from "./components/cells";
+export type { AssignedItemsCellProps } from "./components/cells";
+export { HiddenValueCell } from "./components/cells";
+export type { HiddenValueCellProps } from "./components/cells";
+export { LastUpdatedCell } from "./components/cells";
+export type { LastUpdatedCellProps } from "./components/cells";
+export { RootKeyNameCell } from "./components/cells";
+export type { RootKeyNameCellProps } from "./components/cells";
+
+// Skeletons
+export {
+ ActionColumnSkeleton,
+ CreatedAtColumnSkeleton,
+ KeyColumnSkeleton,
+ LastUpdatedColumnSkeleton,
+ PermissionsColumnSkeleton,
+ RootKeyColumnSkeleton,
+} from "./components/skeletons";
+
+// Header components
+export { SortableHeader } from "./components/headers";
+export type { SortableHeaderProps } from "./components/headers";
+
+// Row components
+export { SkeletonRow } from "./components/rows";
+
+// Footer components
+export { LoadMoreFooter, PaginationFooter } from "./components/footer";
+export type { LoadMoreFooterComponentProps } from "./components/footer/load-more-footer";
+export type { PaginationFooterProps } from "./components/footer/pagination-footer";
+
+// Utility components
+export { EmptyState } from "./components/utils/empty-state";
+export type { EmptyStateProps } from "./components/utils/empty-state";
+export { EmptyRootKeys } from "./components/empty/empty-root-keys";
+export { RealtimeSeparator } from "./components/utils/realtime-separator";
+
+// Utils
+export { calculateColumnWidth } from "./utils/column-width";
+export { getPageNumbers } from "./utils/get-page-numbers";
diff --git a/web/internal/ui/src/components/data-table/types.ts b/web/internal/ui/src/components/data-table/types.ts
new file mode 100644
index 0000000000..1457abb865
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/types.ts
@@ -0,0 +1,152 @@
+import type { ColumnDef, RowSelectionState, SortingState } from "@tanstack/react-table";
+import type { ReactNode } from "react";
+
+/**
+ * Column width configuration options
+ */
+export type ColumnWidth =
+ | number // Fixed pixels
+ | string // CSS: "165px", "15%"
+ | "auto"
+ | "min"
+ | "1fr" // Keywords
+ | { min: number; max: number } // Range
+ | { flex: number }; // Flex ratio
+
+/**
+ * Custom column metadata
+ */
+export interface DataTableColumnMeta {
+ // Styling
+ headerClassName?: string;
+ cellClassName?: string;
+
+ // Width configuration
+ width?: ColumnWidth;
+
+ // Display options
+ isCopyable?: boolean;
+ isMonospace?: boolean;
+
+ // Loading state
+ skeleton?: () => ReactNode;
+}
+
+/**
+ * Extended column definition with custom metadata
+ */
+export type DataTableColumnDef = ColumnDef & {
+ meta?: DataTableColumnMeta;
+};
+
+// Extend TanStack Table's module to include custom meta
+declare module "@tanstack/react-table" {
+ // biome-ignore lint/correctness/noUnusedVariables: Module augmentation requires these type parameters
+ interface ColumnMeta extends DataTableColumnMeta {}
+}
+
+/**
+ * Table layout mode
+ */
+export type LayoutMode = "grid" | "classic";
+
+/**
+ * Table configuration options
+ */
+export interface DataTableConfig {
+ // Dimensions
+ rowHeight: number; // Default: 36
+ headerHeight: number; // Default: 40
+ rowSpacing: number; // Default: 4 (classic mode)
+
+ // Layout
+ layout: LayoutMode; // Default: "classic"
+ rowBorders: boolean; // Default: false
+ containerPadding: string; // Default: "px-2"
+ tableLayout: "fixed" | "auto"; // Default: "fixed"
+
+ // Loading
+ loadingRows: number; // Default: 10
+}
+
+/**
+ * Load more footer configuration
+ */
+export interface LoadMoreFooterProps {
+ itemLabel?: string;
+ buttonText?: string;
+ countInfoText?: ReactNode;
+ headerContent?: ReactNode;
+ hasMore: boolean;
+ hide?: boolean;
+}
+
+/**
+ * Main DataTable component props
+ */
+export interface DataTableProps {
+ // Data (required)
+ data: TData[];
+ columns: DataTableColumnDef[];
+ getRowId: (row: TData) => string;
+
+ // Real-time data (optional)
+ realtimeData?: TData[];
+
+ // State management (optional, controlled)
+ sorting?: SortingState;
+ onSortingChange?: (sorting: SortingState | ((old: SortingState) => SortingState)) => void;
+ manualSorting?: boolean;
+ rowSelection?: RowSelectionState;
+ onRowSelectionChange?: (
+ selection: RowSelectionState | ((old: RowSelectionState) => RowSelectionState),
+ ) => void;
+
+ // Interaction (optional)
+ onRowClick?: (row: TData | null) => void;
+ onRowMouseEnter?: (row: TData) => void;
+ onRowMouseLeave?: () => void;
+ selectedItem?: TData | null;
+
+ // Pagination (optional)
+ page?: number;
+ pageSize?: number;
+ totalPages?: number;
+ onPageChange?: (page: number) => void;
+
+ // Configuration (optional)
+ config?: Partial;
+
+ // UI customization (optional)
+ emptyState?: ReactNode;
+ loadMoreFooterProps?: LoadMoreFooterProps;
+ rowClassName?: (row: TData) => string;
+ selectedClassName?: (row: TData, isSelected: boolean) => string;
+ fixedHeight?: number;
+
+ // Features (optional)
+ enableKeyboardNav?: boolean;
+ enableSorting?: boolean;
+ enableRowSelection?: boolean;
+
+ // Loading state
+ isLoading?: boolean;
+
+ // Custom skeleton renderer
+ renderSkeletonRow?: (props: {
+ columns: DataTableColumnDef[];
+ rowHeight: number;
+ }) => ReactNode;
+}
+
+/**
+ * Internal separator item type for real-time data boundary
+ */
+export type SeparatorItem = {
+ isSeparator: true;
+};
+
+/**
+ * Combined data type including separator
+ */
+export type TableDataItem = TData | SeparatorItem;
diff --git a/web/internal/ui/src/components/data-table/utils/column-width.ts b/web/internal/ui/src/components/data-table/utils/column-width.ts
new file mode 100644
index 0000000000..d7e8214228
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/utils/column-width.ts
@@ -0,0 +1,29 @@
+import type { ColumnWidth } from "../types";
+
+/**
+ * Convert ColumnWidth to CSS width string
+ */
+export const calculateColumnWidth = (width?: ColumnWidth): string => {
+ if (!width) {
+ return "auto";
+ }
+
+ if (typeof width === "number") {
+ return `${width}px`;
+ }
+
+ if (typeof width === "string") {
+ return width;
+ }
+
+ if (typeof width === "object") {
+ if ("min" in width && "max" in width) {
+ return `${width.min}px`;
+ }
+ if ("flex" in width) {
+ return "auto";
+ }
+ }
+
+ return "auto";
+};
diff --git a/web/internal/ui/src/components/data-table/utils/get-page-numbers.ts b/web/internal/ui/src/components/data-table/utils/get-page-numbers.ts
new file mode 100644
index 0000000000..d446c2581c
--- /dev/null
+++ b/web/internal/ui/src/components/data-table/utils/get-page-numbers.ts
@@ -0,0 +1,36 @@
+/**
+ * Generates an array of exactly 7 page slots (or fewer when totalPages ≤ 7)
+ * for pagination UI. When only one ellipsis is needed, extra page numbers
+ * fill the remaining slots so the output length stays constant.
+ *
+ * @param page - Current page (1-indexed)
+ * @param totalPages - Total number of pages
+ */
+export function getPageNumbers(page: number, totalPages: number): Array {
+ const TOTAL_SLOTS = 7;
+
+ if (totalPages <= TOTAL_SLOTS) {
+ return Array.from({ length: totalPages }, (_, i) => i + 1);
+ }
+
+ // Near start — no leading ellipsis, show first 5 pages + trailing ellipsis + last
+ if (page < 5) {
+ return [1, 2, 3, 4, 5, "ellipsis", totalPages];
+ }
+
+ // Near end — leading ellipsis + last 5 pages
+ if (page > totalPages - 4) {
+ return [
+ 1,
+ "ellipsis",
+ totalPages - 4,
+ totalPages - 3,
+ totalPages - 2,
+ totalPages - 1,
+ totalPages,
+ ];
+ }
+
+ // Middle — both ellipses with a 3-page window around current
+ return [1, "ellipsis", page - 1, page, page + 1, "ellipsis", totalPages];
+}
diff --git a/web/internal/ui/src/components/timestamp-info.tsx b/web/internal/ui/src/components/timestamp-info.tsx
index dca22c0b35..fcdae8d818 100644
--- a/web/internal/ui/src/components/timestamp-info.tsx
+++ b/web/internal/ui/src/components/timestamp-info.tsx
@@ -47,7 +47,7 @@ const TimestampInfo: React.FC<{
value: string | number;
className?: string;
displayType?: DisplayType;
- triggerRef?: React.RefObject;
+ triggerRef?: React.RefObject;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}> = ({
@@ -61,7 +61,7 @@ const TimestampInfo: React.FC<{
className?: string;
value: string | number;
displayType?: DisplayType;
- triggerRef?: React.RefObject;
+ triggerRef?: React.RefObject;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}) => {
diff --git a/web/internal/ui/src/index.ts b/web/internal/ui/src/index.ts
index 861efce9c7..53a792e13b 100644
--- a/web/internal/ui/src/index.ts
+++ b/web/internal/ui/src/index.ts
@@ -36,4 +36,5 @@ export * from "./components/visually-hidden";
export * from "./components/slider";
export * from "./components/step-wizard";
export * from "./hooks/use-mobile";
+export * from "./components/data-table";
export * from "../css";
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index 3e40d81896..611ae898c6 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -714,6 +714,9 @@ importers:
'@radix-ui/react-visually-hidden':
specifier: ^1.2.4
version: 1.2.4(@types/react-dom@19.2.3)(@types/react@19.2.4)(react-dom@19.2.3)(react@19.2.3)
+ '@tanstack/react-table':
+ specifier: 8.21.3
+ version: 8.21.3(react-dom@19.2.3)(react@19.2.3)
'@unkey/icons':
specifier: workspace:^
version: link:../icons
@@ -8529,6 +8532,18 @@ packages:
react-dom: 19.2.4(react@19.2.4)
dev: false
+ /@tanstack/react-table@8.21.3(react-dom@19.2.3)(react@19.2.3):
+ resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ react: '>=16.8'
+ react-dom: '>=16.8'
+ dependencies:
+ '@tanstack/table-core': 8.21.3
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ dev: false
+
/@tanstack/react-virtual@3.10.9(react-dom@19.2.4)(react@19.2.4):
resolution: {integrity: sha512-OXO2uBjFqA4Ibr2O3y0YMnkrRWGVNqcvHQXmGvMu6IK8chZl3PrDxFXdGZ2iZkSrKh3/qUYoFqYe+Rx23RoU0g==}
peerDependencies:
@@ -8545,6 +8560,11 @@ packages:
engines: {node: '>=12'}
dev: false
+ /@tanstack/table-core@8.21.3:
+ resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
+ engines: {node: '>=12'}
+ dev: false
+
/@tanstack/virtual-core@3.10.9:
resolution: {integrity: sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw==}
dev: false