User list view

This commit is contained in:
2025-10-11 23:14:33 +02:00
parent 205f294e78
commit 8c05c0e825
10 changed files with 449 additions and 5 deletions

View File

@@ -59,7 +59,7 @@ export const handle = implement(api, {
let query = db
.selectFrom("User")
.leftJoin("UserRole", "User.userId", "UserRole.role")
.leftJoin("UserRole", "User.userId", "UserRole.userId")
.select("User.userId")
.groupBy("User.userId")
.orderBy("displayName")

View File

@@ -21,6 +21,7 @@
"@radix-ui/react-dialog": "catalog:",
"@radix-ui/react-dropdown-menu": "catalog:",
"@radix-ui/react-label": "catalog:",
"@radix-ui/react-select": "catalog:",
"@radix-ui/react-slot": "catalog:",
"clsx": "catalog:",
"common": "workspace:^",

View File

@@ -1,3 +1,4 @@
import { Admin } from "@/routes/Admin";
import { Home } from "@/routes/Home";
import { Login } from "@/routes/Login";
import { Piece } from "@/routes/Piece";
@@ -61,6 +62,10 @@ const router = createBrowserRouter([
path: "settings",
Component: Settings,
},
{
path: "admin",
Component: Admin,
}
],
},
{

View File

@@ -0,0 +1,156 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};

View File

@@ -0,0 +1,183 @@
import { userCache } from "@/cache";
import { client } from "@/client";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useBreakpoint } from "@/hooks/useBreakpoint";
import { useCache } from "@/hooks/useCache";
import { useLoading } from "@/hooks/useLoading";
import { DEBOUNCE } from "@/snippets";
import { UserId } from "common";
import { Role } from "common/the_api";
import { Array, Effect, HashSet, Match, Option, pipe } from "effect";
import { Loader2 } from "lucide-react";
import { useRef, useState } from "react";
export function Admin() {
const [displayName, setDisplayName] = useState("");
const [role, setRole] = useState<Role | "">("");
const debounce = useRef(Effect.void);
const breakpoint = useBreakpoint();
const columns = breakpoint ? 4 : 1;
const { isLoading, error, data: userIds } = useLoading(Effect.gen(function* () {
yield* debounce.current;
const data = yield* client.queryUsers({
displayName: displayName !== "" ? Option.some(displayName) : Option.none(),
role: role !== "" ? Option.some(role) : Option.none(),
offset: 0,
limit: 100,
});
return data;
}), [displayName, role]);
return (
<div className="p-4 overflow-y-auto flex flex-col gap-4">
<div className="flex flex-row flex-wrap items-baseline gap-4">
<Input
className="w-full md:w-[32ch]"
type="text"
placeholder="Nazwa wyświetlana"
value={displayName}
onChange={(e) => {
setDisplayName(e.target.value);
debounce.current = DEBOUNCE;
}}
/>
<Select
value={role}
onValueChange={(role) => setRole(role === "None" ? "" : role as Role)}
>
<SelectTrigger className="w-full md:w-[24ch]">
<SelectValue placeholder="Rola" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Role</SelectLabel>
<SelectItem value="None"><em>Dowolna</em></SelectItem>
<SelectItem value={Role.Viewer}>Przeglądanie</SelectItem>
<SelectItem value={Role.Editor}>Edytor</SelectItem>
<SelectItem value={Role.Admin}>Administrator</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<Table>
{breakpoint && (
<TableHeader className="bg-white sticky top-0">
<TableRow>
<TableHead className="w-10" />
<TableHead>Nazwa wyświetlana</TableHead>
<TableHead>ID</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
)}
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns} >
<div className="flex items-center justify-center gap-2">
<Loader2 className="animate-spin" />
Ładowanie
</div>
</TableCell>
</TableRow>
) : error !== null ? (
<TableRow>
<TableCell colSpan={columns} >
<div className="flex items-center justify-center gap-2">
Wystąpił błąd: {Match.value(error).pipe(
Match.tag("FetchError", () => "Nie można połączyć się z serwerem"),
Match.tag("Unauthenticated", () => "Zaloguj się, aby kontynuować"),
Match.exhaustive,
)}
</div>
</TableCell>
</TableRow>
) : (
userIds.map((userId) => <UserRow key={userId} userId={userId} />)
)}
</TableBody>
</Table>
</div>
);
}
namespace UserRow {
export interface Props {
readonly userId: UserId;
}
}
function UserRow(props: UserRow.Props) {
const { isLoading, error, data: user } = useCache(userCache, props.userId);
const breakpoint = useBreakpoint();
const columns = breakpoint ? 4 : 1;
if (isLoading) {
return (
<TableRow>
<TableCell colSpan={columns}>Ładowanie</TableCell>
</TableRow>
);
}
if (error !== null) {
return (
<TableRow>
<TableCell colSpan={columns}>
Wystąpił błąd: {Match.value(error).pipe(
Match.tag("FetchError", () => "Nie można połączyć się z serwerem"),
Match.tag("NotFound", () => "Użytkownik nie istnieje"),
Match.tag("Unauthenticated", () => "Zaloguj się, aby kontynuować"),
Match.exhaustive,
)}
</TableCell>
</TableRow>
);
}
const rolesText = pipe(
user.roles,
HashSet.values,
Array.join(", "),
);
return (
<TableRow>
{breakpoint ? (<>
<TableCell>
<Avatar className="shadow">
<AvatarImage src={Option.getOrUndefined(user.avatarUrl)} />
</Avatar>
</TableCell>
<TableCell>
{Option.getOrUndefined(user.displayName)}
</TableCell>
<TableCell className="font-mono">
{user.userId}
</TableCell>
<TableCell>
{rolesText}
</TableCell>
</>) : (
<TableCell className="flex flex-col">
<div className="flex flex-row items-center gap-2 mb-2">
<Avatar className="shadow">
<AvatarImage src={Option.getOrUndefined(user.avatarUrl)} />
</Avatar>
<div>{Option.getOrUndefined(user.displayName)}</div>
</div>
<div className="text-xs font-mono">{user.userId}</div>
<div className="text-xs">{rolesText}</div>
</TableCell>
)}
</TableRow>
);
}

View File

@@ -41,7 +41,7 @@ export function Pieces() {
}), [name, author]);
return (
<div className="p-4 overflow-y-auto flex flex-col items-start gap-4">
<div className="p-4 overflow-y-auto flex flex-col gap-4">
<div className="flex flex-row flex-wrap gap-4">
<Dialog>
<DialogTrigger asChild>

View File

@@ -34,7 +34,7 @@ export function Repertoires() {
}), [name]);
return (
<div className="p-4 overflow-y-auto flex flex-col items-start gap-4">
<div className="p-4 overflow-y-auto flex flex-col gap-4">
<div className="flex flex-row flex-wrap gap-4">
<Dialog>
<DialogTrigger asChild>

View File

@@ -3,8 +3,9 @@ import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { buttonVariants } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { setUser, useStore } from "@/hooks/useStore";
import { Effect, Option, pipe } from "effect";
import { LogOut, Settings } from "lucide-react";
import { Role } from "common/the_api";
import { Effect, HashSet, Option, pipe } from "effect";
import { LogOut, Settings, Users } from "lucide-react";
import { useEffect } from "react";
import { Link, Outlet, useNavigate } from "react-router-dom";
@@ -63,6 +64,13 @@ export function Root() {
<Settings />Ustawienia
</Link>
</DropdownMenuItem>
{HashSet.has(user.roles, Role.Admin) && (
<DropdownMenuItem asChild>
<Link to="/admin">
<Users />Administracja
</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={onLogoutClick}>
<LogOut />Wyloguj się
</DropdownMenuItem>

90
pnpm-lock.yaml generated
View File

@@ -24,6 +24,9 @@ catalogs:
'@radix-ui/react-label':
specifier: ^2.1.7
version: 2.1.7
'@radix-ui/react-select':
specifier: ^2.2.6
version: 2.2.6
'@radix-ui/react-slot':
specifier: ^1.2.3
version: 1.2.3
@@ -184,6 +187,9 @@ importers:
'@radix-ui/react-label':
specifier: 'catalog:'
version: 2.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-select':
specifier: 'catalog:'
version: 2.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-slot':
specifier: 'catalog:'
version: 1.2.3(@types/react@19.2.2)(react@19.2.0)
@@ -708,6 +714,9 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
@@ -938,6 +947,19 @@ packages:
'@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-slot@1.2.3':
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies:
@@ -1001,6 +1023,15 @@ packages:
'@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:
@@ -1019,6 +1050,19 @@ packages:
'@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/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
@@ -3108,6 +3152,8 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
@@ -3335,6 +3381,35 @@ snapshots:
'@types/react': 19.2.2
'@types/react-dom': 19.2.1(@types/react@19.2.2)
'@radix-ui/react-select@2.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
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.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
aria-hidden: 1.2.6
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.1(@types/react@19.2.2)
'@radix-ui/react-slot@1.2.3(@types/react@19.2.2)(react@19.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
@@ -3383,6 +3458,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-previous@1.1.1(@types/react@19.2.2)(react@19.2.0)':
dependencies:
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-rect@1.1.1(@types/react@19.2.2)(react@19.2.0)':
dependencies:
'@radix-ui/rect': 1.1.1
@@ -3397,6 +3478,15 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.1(@types/react@19.2.2)
'@radix-ui/rect@1.1.1': {}
'@rolldown/pluginutils@1.0.0-beta.27': {}

View File

@@ -16,6 +16,7 @@ catalog:
'@radix-ui/react-dialog': '^1.1.15'
'@radix-ui/react-dropdown-menu': '^2.1.16'
'@radix-ui/react-label': '^2.1.7'
'@radix-ui/react-select': '^2.2.6'
'@radix-ui/react-slot': '^1.2.3'
'@stylistic/eslint-plugin': '^4.4.1'
'@tailwindcss/vite': '^4.1.14'