Compare commits
7 Commits
2c44df3a5c
...
45fe122580
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45fe122580 | ||
|
|
969165f4a7 | ||
|
|
e7be0dbeca | ||
|
|
322ae1e7c8 | ||
|
|
46b57d2a73 | ||
|
|
99ded43483 | ||
|
|
1394c0496a |
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 disabled:cursor-default [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
29
frontend/src/components/ui/button.test.tsx
Normal file
29
frontend/src/components/ui/button.test.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { Button } from "./button"
|
||||
|
||||
describe("Button component", () => {
|
||||
it("renders with a pointer cursor", () => {
|
||||
render(<Button>Click me</Button>)
|
||||
const button = screen.getByRole("button", { name: /click me/i })
|
||||
expect(button).toHaveClass("cursor-pointer")
|
||||
})
|
||||
|
||||
it("renders different variants with a pointer cursor", () => {
|
||||
const { rerender } = render(<Button variant="outline">Outline</Button>)
|
||||
let button = screen.getByRole("button", { name: /outline/i })
|
||||
expect(button).toHaveClass("cursor-pointer")
|
||||
|
||||
rerender(<Button variant="destructive">Destructive</Button>)
|
||||
button = screen.getByRole("button", { name: /destructive/i })
|
||||
expect(button).toHaveClass("cursor-pointer")
|
||||
})
|
||||
|
||||
it("renders with a default cursor when disabled", () => {
|
||||
render(<Button disabled>Disabled Button</Button>)
|
||||
const button = screen.getByRole("button", { name: /disabled button/i })
|
||||
expect(button).toBeDisabled()
|
||||
expect(button).toHaveClass("disabled:pointer-events-none")
|
||||
expect(button).toHaveClass("disabled:cursor-default")
|
||||
})
|
||||
})
|
||||
74
frontend/src/components/ui/dropdown-menu.test.tsx
Normal file
74
frontend/src/components/ui/dropdown-menu.test.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuPortal
|
||||
} from "./dropdown-menu"
|
||||
|
||||
describe("DropdownMenu components", () => {
|
||||
it("DropdownMenuItem should have cursor-pointer when enabled", () => {
|
||||
render(
|
||||
<DropdownMenu open={true}>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Enabled Item</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
const item = screen.getByText("Enabled Item")
|
||||
expect(item).toHaveClass("cursor-pointer")
|
||||
})
|
||||
|
||||
it("DropdownMenuSubTrigger should have cursor-pointer when enabled", () => {
|
||||
render(
|
||||
<DropdownMenu open={true}>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Sub Trigger</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent />
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
const trigger = screen.getByText("Sub Trigger")
|
||||
expect(trigger).toHaveClass("cursor-pointer")
|
||||
})
|
||||
|
||||
it("DropdownMenuItem should show default cursor when disabled", () => {
|
||||
render(
|
||||
<DropdownMenu open={true}>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem disabled>Disabled Item</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
const item = screen.getByText("Disabled Item")
|
||||
// Radix sets data-disabled attribute
|
||||
expect(item).toHaveAttribute("data-disabled")
|
||||
expect(item).toHaveClass("data-[disabled]:cursor-default")
|
||||
})
|
||||
|
||||
it("DropdownMenuSubTrigger should show default cursor when disabled", () => {
|
||||
render(
|
||||
<DropdownMenu open={true}>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger disabled>Disabled Sub Trigger</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent />
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
const trigger = screen.getByText("Disabled Sub Trigger")
|
||||
expect(trigger).toHaveAttribute("data-disabled")
|
||||
expect(trigger).toHaveClass("data-[disabled]:cursor-default")
|
||||
})
|
||||
})
|
||||
@@ -27,7 +27,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"focus:bg-accent data-[state=open]:bg-accent flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none",
|
||||
"focus:bg-accent data-[state=open]:bg-accent flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:cursor-default data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
@@ -81,7 +81,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 select-none",
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default select-none",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
@@ -99,7 +99,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 select-none",
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default select-none",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
@@ -122,7 +122,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 select-none",
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default select-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
83
frontend/src/components/ui/sidebar.test.tsx
Normal file
83
frontend/src/components/ui/sidebar.test.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import {
|
||||
SidebarProvider,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuAction,
|
||||
SidebarGroupAction,
|
||||
SidebarMenu,
|
||||
SidebarMenuItem,
|
||||
SidebarGroup
|
||||
} from "./sidebar"
|
||||
|
||||
describe("Sidebar components", () => {
|
||||
it("SidebarMenuButton should have cursor-pointer", () => {
|
||||
render(
|
||||
<SidebarProvider>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>Menu Button</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarProvider>
|
||||
)
|
||||
const button = screen.getByRole("button", { name: /menu button/i })
|
||||
expect(button).toHaveClass("cursor-pointer")
|
||||
})
|
||||
|
||||
it("SidebarMenuAction should have cursor-pointer", () => {
|
||||
render(
|
||||
<SidebarProvider>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>Button</SidebarMenuButton>
|
||||
<SidebarMenuAction>Action</SidebarMenuAction>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarProvider>
|
||||
)
|
||||
const action = screen.getByRole("button", { name: /action/i })
|
||||
expect(action).toHaveClass("cursor-pointer")
|
||||
})
|
||||
|
||||
it("SidebarGroupAction should have cursor-pointer", () => {
|
||||
render(
|
||||
<SidebarProvider>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupAction>Group Action</SidebarGroupAction>
|
||||
</SidebarGroup>
|
||||
</SidebarProvider>
|
||||
)
|
||||
const action = screen.getByRole("button", { name: /group action/i })
|
||||
expect(action).toHaveClass("cursor-pointer")
|
||||
})
|
||||
|
||||
it("SidebarGroupAction should have cursor-default when disabled", () => {
|
||||
render(
|
||||
<SidebarProvider>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupAction disabled>Disabled Group Action</SidebarGroupAction>
|
||||
</SidebarGroup>
|
||||
</SidebarProvider>
|
||||
)
|
||||
const action = screen.getByRole("button", { name: /disabled group action/i })
|
||||
expect(action).toHaveClass("disabled:cursor-default")
|
||||
expect(action).toBeDisabled()
|
||||
})
|
||||
|
||||
it("SidebarMenuAction should not show pointer when disabled", () => {
|
||||
render(
|
||||
<SidebarProvider>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>Button</SidebarMenuButton>
|
||||
<SidebarMenuAction disabled>Disabled Action</SidebarMenuAction>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarProvider>
|
||||
)
|
||||
const action = screen.getByRole("button", { name: /disabled action/i })
|
||||
expect(action).toHaveClass("disabled:cursor-default")
|
||||
expect(action).toBeDisabled()
|
||||
})
|
||||
})
|
||||
@@ -405,7 +405,7 @@ function SidebarGroupAction({
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 cursor-pointer disabled:cursor-default disabled:pointer-events-none disabled:opacity-50",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
@@ -477,7 +477,7 @@ function SidebarMenuButton({
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 cursor-pointer disabled:cursor-default",
|
||||
variant === "default" && "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
variant === "outline" && "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
size === "default" && "h-8 text-sm",
|
||||
@@ -528,7 +528,7 @@ function SidebarMenuAction({
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 cursor-pointer disabled:cursor-default disabled:pointer-events-none disabled:opacity-50",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
|
||||
Reference in New Issue
Block a user