refactor: Align form validation UX across management dialogs

This commit is contained in:
2026-06-18 11:05:31 +05:30
parent d7c4027328
commit 9695c80e20
23 changed files with 1458 additions and 1017 deletions

View File

@@ -0,0 +1,85 @@
import { cn } from '@/lib/utils';
const BOKEH_ORBS = [
'top-[20%] left-[10%] size-[min(18vw,140px)] opacity-[0.14]',
'top-[-18%] right-[-20%] size-[min(42vw,380px)] opacity-[0.1]',
'bottom-[-17%] left-[-15%] size-[min(34vw,300px)] opacity-[0.12]',
] as const;
const PARTICLES = [
'top-[30%] left-[45%] opacity-15',
'top-[43%] right-[20%] opacity-20',
'top-[57%] left-[10%] opacity-20',
'bottom-[28%] left-[45%] opacity-20',
'bottom-[14%] right-[20%] opacity-20',
] as const;
type AuthBackgroundProps = {
className?: string;
};
export function AuthBackground({ className }: AuthBackgroundProps) {
return (
<div
className={cn(
'relative hidden h-screen w-1/2 shrink-0 overflow-hidden md:block',
className,
)}
aria-hidden
>
<div
className="absolute inset-0"
style={{
background:
'linear-gradient(155deg, color-mix(in oklch, var(--primary) 92%, white 8%) 0%, var(--primary) 32%, color-mix(in oklch, var(--primary) 72%, black 28%) 66%, color-mix(in oklch, var(--primary) 48%, black 52%) 100%)',
}}
/>
<div
className="absolute inset-0"
style={{
background:
'radial-gradient(circle at 50% 43%, color-mix(in oklch, var(--primary) 78%, white 22% / 34%), transparent 42%), radial-gradient(circle at 18% 8%, color-mix(in oklch, var(--primary) 70%, white 30% / 26%), transparent 36%)',
}}
/>
<svg
className="pointer-events-none absolute inset-0 size-full text-primary-foreground/8"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid slice"
>
{Array.from({ length: 14 }, (_, index) => (
<circle
key={index}
cx="50"
cy="45"
r={8 + index * 5.2}
fill="none"
stroke="currentColor"
strokeWidth="0.08"
/>
))}
</svg>
{BOKEH_ORBS.map((position, index) => (
<div
key={index}
className={cn(
'absolute rounded-full bg-primary-foreground/16',
position,
)}
/>
))}
{PARTICLES.map((position, index) => (
<div
key={index}
className={cn(
'absolute size-1.5 rounded-full bg-primary-foreground/35',
position,
)}
/>
))}
</div>
);
}

View File

@@ -52,7 +52,7 @@ export function FilterSelector({
onValueChange={onProjectChange}
disabled={isLoading || projects.length === 0}
>
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60 shadow-xs">
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60">
<SelectValue placeholder="Select project" />
</SelectTrigger>
<SelectContent>
@@ -81,7 +81,7 @@ export function FilterSelector({
onValueChange={onPackageChange}
disabled={isLoading}
>
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60 shadow-xs">
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60">
<SelectValue placeholder="All packages" />
</SelectTrigger>
<SelectContent>
@@ -112,7 +112,7 @@ export function FilterSelector({
onValueChange={onChainageChange}
disabled={isLoading}
>
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60 shadow-xs">
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60">
<SelectValue placeholder="All segments" />
</SelectTrigger>
<SelectContent>

View File

@@ -88,14 +88,14 @@ export function MultiSelectPopover({
return (
<Popover>
<PopoverTrigger asChild>
<Button type="button" variant="outline" size="sm">
<Button type="button" variant="outline" size="sm" className="shadow-none">
{icon}
{label}
<Badge variant="secondary">{values.length}</Badge>
<ChevronDown />
</Button>
</PopoverTrigger>
<PopoverContent align={align} className="w-60 p-0">
<PopoverContent align={align} className="w-60 p-0 shadow-none">
<div className="flex items-center gap-2 border-b p-3">
<button
type="button"

View File

@@ -137,7 +137,7 @@ function ComboboxContent({
data-slot="combobox-content"
data-chips={!!anchor}
className={cn(
'group/combobox-content relative max-h-96 w-[var(--anchor-width)] max-w-[var(--available-width)] min-w-[calc(var(--anchor-width)+1.75rem)] origin-[var(--transform-origin)] overflow-hidden rounded-md bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-[var(--anchor-width)] 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 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
'group/combobox-content relative max-h-96 w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+1.75rem)] origin-(--transform-origin) overflow-hidden rounded-md bg-popover text-popover-foreground ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) 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 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
className,
)}
{...props}
@@ -254,7 +254,7 @@ function ComboboxChips({
<ComboboxPrimitive.Chips
data-slot="combobox-chips"
className={cn(
'flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border border-input bg-transparent bg-clip-padding px-2.5 py-1.5 text-sm shadow-xs transition-[color,box-shadow] focus-within:border-ring has-aria-invalid:border-destructive has-data-[slot=combobox-chip]:px-1.5 dark:bg-input/30 dark:has-aria-invalid:border-destructive',
'flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border border-input bg-transparent bg-clip-padding px-2.5 py-1.5 text-sm transition-[color,box-shadow] focus-within:border-ring has-aria-invalid:border-destructive has-data-[slot=combobox-chip]:px-1.5 dark:bg-input/30 dark:has-aria-invalid:border-destructive',
className,
)}
{...props}

View File

@@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
data-slot="input-group"
role="group"
className={cn(
'group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30',
'group/input-group relative flex w-full items-center rounded-md border border-input transition-[color,box-shadow] outline-none dark:bg-input/30',
'h-9 min-w-0 has-[>textarea]:h-auto',
// Variants based on alignment.

View File

@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
type={type}
data-slot="input"
className={cn(
'h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30',
'h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30',
'focus-visible:border-ring',
'aria-invalid:border-destructive',
className,

View File

@@ -37,7 +37,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"flex h-9 w-full min-w-0 items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive data-placeholder:text-muted-foreground data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
"flex h-9 w-full min-w-0 items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-1 text-sm transition-[color,box-shadow] outline-none focus-visible:border-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive data-placeholder:text-muted-foreground data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
className,
)}
{...props}
@@ -62,7 +62,7 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
'relative z-50 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
'relative z-50 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
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,