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 ( -
+
-
+
- - {/* 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 ( + + ); +} 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 ( +
+ +
+ ); + } + + 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 ( +
+ +
+ ); + } + + return ( +
+ {loading ? ( + + ) : ( +
+
+ {/* Header content */} + {headerContent && ( +
+ {headerContent} +
+ )} + +
+ {/* Item count */} +
+ Viewing + + {start}-{end} + + of + {totalCount} + {itemLabel} +
+ + {/* Pagination controls */} + +
+
+
+ )} +
+ ); +}); 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 ( + + ); +} + +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. + + + + + +
+ )} +
+ ); +}; 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) => ( + + ))} + + ))} + + + + + + {/* 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) => ( + + ))} + , + ); + } 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) => ( + + ))} + + , + ); + } + + return elements; + }); + })()} + +
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} +
+
+
+
+
+ +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ + {/* 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